@oxyhq/services 14.1.0 → 16.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.
- package/lib/commonjs/index.js +4 -21
- package/lib/commonjs/index.js.map +1 -1
- package/lib/commonjs/ui/components/SignInAccountChooser.js +189 -0
- package/lib/commonjs/ui/components/SignInAccountChooser.js.map +1 -0
- package/lib/commonjs/ui/components/SignInModal.js +350 -131
- package/lib/commonjs/ui/components/SignInModal.js.map +1 -1
- package/lib/commonjs/ui/context/OxyContext.js +340 -1547
- package/lib/commonjs/ui/context/OxyContext.js.map +1 -1
- package/lib/commonjs/ui/context/hooks/useAuthOperations.js +61 -64
- package/lib/commonjs/ui/context/hooks/useAuthOperations.js.map +1 -1
- package/lib/commonjs/ui/hooks/mutations/mutationKeys.js +1 -1
- package/lib/commonjs/ui/hooks/mutations/useAccountMutations.js +9 -9
- package/lib/commonjs/ui/hooks/mutations/useAccountMutations.js.map +1 -1
- package/lib/commonjs/ui/hooks/queries/queryKeys.js +1 -1
- package/lib/commonjs/ui/hooks/queries/useAccountQueries.js +7 -8
- package/lib/commonjs/ui/hooks/queries/useAccountQueries.js.map +1 -1
- package/lib/commonjs/ui/hooks/useAuth.js +14 -33
- package/lib/commonjs/ui/hooks/useAuth.js.map +1 -1
- package/lib/commonjs/ui/hooks/usePasswordSignIn.js +170 -0
- package/lib/commonjs/ui/hooks/usePasswordSignIn.js.map +1 -0
- package/lib/commonjs/ui/hooks/useProfileEditing.js +2 -5
- package/lib/commonjs/ui/hooks/useProfileEditing.js.map +1 -1
- package/lib/commonjs/ui/hooks/useSessionManagement.js +2 -2
- package/lib/commonjs/ui/hooks/useSessionManagement.js.map +1 -1
- package/lib/commonjs/ui/navigation/routes.js +1 -0
- package/lib/commonjs/ui/navigation/routes.js.map +1 -1
- package/lib/commonjs/ui/screens/ConnectedAppsScreen.js +17 -18
- package/lib/commonjs/ui/screens/ConnectedAppsScreen.js.map +1 -1
- package/lib/commonjs/ui/screens/EditProfileFieldScreen.js +93 -68
- package/lib/commonjs/ui/screens/EditProfileFieldScreen.js.map +1 -1
- package/lib/commonjs/ui/screens/EditProfileScreen.js +187 -0
- package/lib/commonjs/ui/screens/EditProfileScreen.js.map +1 -0
- package/lib/commonjs/ui/screens/ManageAccountScreen.js +3 -7
- package/lib/commonjs/ui/screens/ManageAccountScreen.js.map +1 -1
- package/lib/commonjs/ui/screens/OxyAuthScreen.js +231 -95
- package/lib/commonjs/ui/screens/OxyAuthScreen.js.map +1 -1
- package/lib/commonjs/ui/screens/ProfileScreen.js +15 -2
- package/lib/commonjs/ui/screens/ProfileScreen.js.map +1 -1
- package/lib/commonjs/ui/screens/linkFormat.js +38 -0
- package/lib/commonjs/ui/screens/linkFormat.js.map +1 -0
- package/lib/commonjs/ui/session/authStore.js +146 -0
- package/lib/commonjs/ui/session/authStore.js.map +1 -0
- package/lib/commonjs/ui/session/createSessionClient.js +23 -17
- package/lib/commonjs/ui/session/createSessionClient.js.map +1 -1
- package/lib/commonjs/ui/session/index.js +13 -7
- package/lib/commonjs/ui/session/index.js.map +1 -1
- package/lib/commonjs/ui/session/tokenTransport.js +19 -31
- package/lib/commonjs/ui/session/tokenTransport.js.map +1 -1
- package/lib/commonjs/ui/utils/isWebBrowser.js +13 -0
- package/lib/commonjs/ui/utils/isWebBrowser.js.map +1 -0
- package/lib/commonjs/utils/deviceFlowSignIn.js +13 -16
- package/lib/commonjs/utils/deviceFlowSignIn.js.map +1 -1
- package/lib/module/index.js +2 -7
- package/lib/module/index.js.map +1 -1
- package/lib/module/ui/components/SignInAccountChooser.js +183 -0
- package/lib/module/ui/components/SignInAccountChooser.js.map +1 -0
- package/lib/module/ui/components/SignInModal.js +352 -134
- package/lib/module/ui/components/SignInModal.js.map +1 -1
- package/lib/module/ui/context/OxyContext.js +342 -1551
- package/lib/module/ui/context/OxyContext.js.map +1 -1
- package/lib/module/ui/context/hooks/useAuthOperations.js +60 -63
- package/lib/module/ui/context/hooks/useAuthOperations.js.map +1 -1
- package/lib/module/ui/hooks/mutations/mutationKeys.js +1 -1
- package/lib/module/ui/hooks/mutations/useAccountMutations.js +7 -7
- package/lib/module/ui/hooks/mutations/useAccountMutations.js.map +1 -1
- package/lib/module/ui/hooks/queries/queryKeys.js +1 -1
- package/lib/module/ui/hooks/queries/useAccountQueries.js +5 -6
- package/lib/module/ui/hooks/queries/useAccountQueries.js.map +1 -1
- package/lib/module/ui/hooks/useAuth.js +14 -33
- package/lib/module/ui/hooks/useAuth.js.map +1 -1
- package/lib/module/ui/hooks/usePasswordSignIn.js +165 -0
- package/lib/module/ui/hooks/usePasswordSignIn.js.map +1 -0
- package/lib/module/ui/hooks/useProfileEditing.js +2 -5
- package/lib/module/ui/hooks/useProfileEditing.js.map +1 -1
- package/lib/module/ui/hooks/useSessionManagement.js +1 -1
- package/lib/module/ui/hooks/useSessionManagement.js.map +1 -1
- package/lib/module/ui/navigation/routes.js +1 -0
- package/lib/module/ui/navigation/routes.js.map +1 -1
- package/lib/module/ui/screens/ConnectedAppsScreen.js +19 -20
- package/lib/module/ui/screens/ConnectedAppsScreen.js.map +1 -1
- package/lib/module/ui/screens/EditProfileFieldScreen.js +91 -66
- package/lib/module/ui/screens/EditProfileFieldScreen.js.map +1 -1
- package/lib/module/ui/screens/EditProfileScreen.js +182 -0
- package/lib/module/ui/screens/EditProfileScreen.js.map +1 -0
- package/lib/module/ui/screens/ManageAccountScreen.js +3 -7
- package/lib/module/ui/screens/ManageAccountScreen.js.map +1 -1
- package/lib/module/ui/screens/OxyAuthScreen.js +233 -97
- package/lib/module/ui/screens/OxyAuthScreen.js.map +1 -1
- package/lib/module/ui/screens/ProfileScreen.js +15 -2
- package/lib/module/ui/screens/ProfileScreen.js.map +1 -1
- package/lib/module/ui/screens/linkFormat.js +31 -0
- package/lib/module/ui/screens/linkFormat.js.map +1 -0
- package/lib/module/ui/session/authStore.js +143 -0
- package/lib/module/ui/session/authStore.js.map +1 -0
- package/lib/module/ui/session/createSessionClient.js +22 -16
- package/lib/module/ui/session/createSessionClient.js.map +1 -1
- package/lib/module/ui/session/index.js +9 -9
- package/lib/module/ui/session/index.js.map +1 -1
- package/lib/module/ui/session/tokenTransport.js +20 -33
- package/lib/module/ui/session/tokenTransport.js.map +1 -1
- package/lib/module/ui/utils/isWebBrowser.js +11 -0
- package/lib/module/ui/utils/isWebBrowser.js.map +1 -0
- package/lib/module/utils/deviceFlowSignIn.js +14 -17
- package/lib/module/utils/deviceFlowSignIn.js.map +1 -1
- package/lib/typescript/commonjs/index.d.ts +2 -3
- package/lib/typescript/commonjs/index.d.ts.map +1 -1
- package/lib/typescript/commonjs/ui/components/SignInAccountChooser.d.ts +29 -0
- package/lib/typescript/commonjs/ui/components/SignInAccountChooser.d.ts.map +1 -0
- package/lib/typescript/commonjs/ui/components/SignInModal.d.ts +15 -10
- package/lib/typescript/commonjs/ui/components/SignInModal.d.ts.map +1 -1
- package/lib/typescript/commonjs/ui/context/OxyContext.d.ts +36 -42
- package/lib/typescript/commonjs/ui/context/OxyContext.d.ts.map +1 -1
- package/lib/typescript/commonjs/ui/context/hooks/useAuthOperations.d.ts +18 -23
- package/lib/typescript/commonjs/ui/context/hooks/useAuthOperations.d.ts.map +1 -1
- package/lib/typescript/commonjs/ui/hooks/mutations/useAccountMutations.d.ts +4 -6
- package/lib/typescript/commonjs/ui/hooks/mutations/useAccountMutations.d.ts.map +1 -1
- package/lib/typescript/commonjs/ui/hooks/queries/useAccountQueries.d.ts +6 -7
- package/lib/typescript/commonjs/ui/hooks/queries/useAccountQueries.d.ts.map +1 -1
- package/lib/typescript/commonjs/ui/hooks/useAuth.d.ts.map +1 -1
- package/lib/typescript/commonjs/ui/hooks/usePasswordSignIn.d.ts +31 -0
- package/lib/typescript/commonjs/ui/hooks/usePasswordSignIn.d.ts.map +1 -0
- package/lib/typescript/commonjs/ui/hooks/useProfileEditing.d.ts +0 -1
- package/lib/typescript/commonjs/ui/hooks/useProfileEditing.d.ts.map +1 -1
- package/lib/typescript/commonjs/ui/navigation/routes.d.ts +1 -1
- package/lib/typescript/commonjs/ui/navigation/routes.d.ts.map +1 -1
- package/lib/typescript/commonjs/ui/screens/ConnectedAppsScreen.d.ts.map +1 -1
- package/lib/typescript/commonjs/ui/screens/EditProfileFieldScreen.d.ts.map +1 -1
- package/lib/typescript/commonjs/ui/screens/EditProfileScreen.d.ts +14 -0
- package/lib/typescript/commonjs/ui/screens/EditProfileScreen.d.ts.map +1 -0
- package/lib/typescript/commonjs/ui/screens/ManageAccountScreen.d.ts +1 -1
- package/lib/typescript/commonjs/ui/screens/ManageAccountScreen.d.ts.map +1 -1
- package/lib/typescript/commonjs/ui/screens/OxyAuthScreen.d.ts +13 -12
- package/lib/typescript/commonjs/ui/screens/OxyAuthScreen.d.ts.map +1 -1
- package/lib/typescript/commonjs/ui/screens/ProfileScreen.d.ts.map +1 -1
- package/lib/typescript/commonjs/ui/screens/linkFormat.d.ts +23 -0
- package/lib/typescript/commonjs/ui/screens/linkFormat.d.ts.map +1 -0
- package/lib/typescript/commonjs/ui/session/authStore.d.ts +33 -0
- package/lib/typescript/commonjs/ui/session/authStore.d.ts.map +1 -0
- package/lib/typescript/commonjs/ui/session/createSessionClient.d.ts +19 -15
- package/lib/typescript/commonjs/ui/session/createSessionClient.d.ts.map +1 -1
- package/lib/typescript/commonjs/ui/session/index.d.ts +9 -9
- package/lib/typescript/commonjs/ui/session/index.d.ts.map +1 -1
- package/lib/typescript/commonjs/ui/session/tokenTransport.d.ts +13 -19
- package/lib/typescript/commonjs/ui/session/tokenTransport.d.ts.map +1 -1
- package/lib/typescript/commonjs/ui/utils/isWebBrowser.d.ts +9 -0
- package/lib/typescript/commonjs/ui/utils/isWebBrowser.d.ts.map +1 -0
- package/lib/typescript/commonjs/ui/utils/storageHelpers.d.ts +7 -7
- package/lib/typescript/commonjs/utils/deviceFlowSignIn.d.ts +15 -13
- package/lib/typescript/commonjs/utils/deviceFlowSignIn.d.ts.map +1 -1
- package/lib/typescript/module/index.d.ts +2 -3
- package/lib/typescript/module/index.d.ts.map +1 -1
- package/lib/typescript/module/ui/components/SignInAccountChooser.d.ts +29 -0
- package/lib/typescript/module/ui/components/SignInAccountChooser.d.ts.map +1 -0
- package/lib/typescript/module/ui/components/SignInModal.d.ts +15 -10
- package/lib/typescript/module/ui/components/SignInModal.d.ts.map +1 -1
- package/lib/typescript/module/ui/context/OxyContext.d.ts +36 -42
- package/lib/typescript/module/ui/context/OxyContext.d.ts.map +1 -1
- package/lib/typescript/module/ui/context/hooks/useAuthOperations.d.ts +18 -23
- package/lib/typescript/module/ui/context/hooks/useAuthOperations.d.ts.map +1 -1
- package/lib/typescript/module/ui/hooks/mutations/useAccountMutations.d.ts +4 -6
- package/lib/typescript/module/ui/hooks/mutations/useAccountMutations.d.ts.map +1 -1
- package/lib/typescript/module/ui/hooks/queries/useAccountQueries.d.ts +6 -7
- package/lib/typescript/module/ui/hooks/queries/useAccountQueries.d.ts.map +1 -1
- package/lib/typescript/module/ui/hooks/useAuth.d.ts.map +1 -1
- package/lib/typescript/module/ui/hooks/usePasswordSignIn.d.ts +31 -0
- package/lib/typescript/module/ui/hooks/usePasswordSignIn.d.ts.map +1 -0
- package/lib/typescript/module/ui/hooks/useProfileEditing.d.ts +0 -1
- package/lib/typescript/module/ui/hooks/useProfileEditing.d.ts.map +1 -1
- package/lib/typescript/module/ui/navigation/routes.d.ts +1 -1
- package/lib/typescript/module/ui/navigation/routes.d.ts.map +1 -1
- package/lib/typescript/module/ui/screens/ConnectedAppsScreen.d.ts.map +1 -1
- package/lib/typescript/module/ui/screens/EditProfileFieldScreen.d.ts.map +1 -1
- package/lib/typescript/module/ui/screens/EditProfileScreen.d.ts +14 -0
- package/lib/typescript/module/ui/screens/EditProfileScreen.d.ts.map +1 -0
- package/lib/typescript/module/ui/screens/ManageAccountScreen.d.ts +1 -1
- package/lib/typescript/module/ui/screens/ManageAccountScreen.d.ts.map +1 -1
- package/lib/typescript/module/ui/screens/OxyAuthScreen.d.ts +13 -12
- package/lib/typescript/module/ui/screens/OxyAuthScreen.d.ts.map +1 -1
- package/lib/typescript/module/ui/screens/ProfileScreen.d.ts.map +1 -1
- package/lib/typescript/module/ui/screens/linkFormat.d.ts +23 -0
- package/lib/typescript/module/ui/screens/linkFormat.d.ts.map +1 -0
- package/lib/typescript/module/ui/session/authStore.d.ts +33 -0
- package/lib/typescript/module/ui/session/authStore.d.ts.map +1 -0
- package/lib/typescript/module/ui/session/createSessionClient.d.ts +19 -15
- package/lib/typescript/module/ui/session/createSessionClient.d.ts.map +1 -1
- package/lib/typescript/module/ui/session/index.d.ts +9 -9
- package/lib/typescript/module/ui/session/index.d.ts.map +1 -1
- package/lib/typescript/module/ui/session/tokenTransport.d.ts +13 -19
- package/lib/typescript/module/ui/session/tokenTransport.d.ts.map +1 -1
- package/lib/typescript/module/ui/utils/isWebBrowser.d.ts +9 -0
- package/lib/typescript/module/ui/utils/isWebBrowser.d.ts.map +1 -0
- package/lib/typescript/module/ui/utils/storageHelpers.d.ts +7 -7
- package/lib/typescript/module/utils/deviceFlowSignIn.d.ts +15 -13
- package/lib/typescript/module/utils/deviceFlowSignIn.d.ts.map +1 -1
- package/package.json +4 -4
- package/src/index.ts +2 -6
- package/src/ui/components/SignInAccountChooser.tsx +162 -0
- package/src/ui/components/SignInModal.tsx +309 -139
- package/src/ui/context/OxyContext.tsx +532 -1785
- package/src/ui/context/hooks/useAuthOperations.ts +65 -76
- package/src/ui/hooks/mutations/mutationKeys.ts +1 -1
- package/src/ui/hooks/mutations/useAccountMutations.ts +7 -7
- package/src/ui/hooks/queries/queryKeys.ts +1 -1
- package/src/ui/hooks/queries/useAccountQueries.ts +8 -9
- package/src/ui/hooks/useAuth.ts +14 -35
- package/src/ui/hooks/usePasswordSignIn.ts +207 -0
- package/src/ui/hooks/useProfileEditing.ts +2 -8
- package/src/ui/hooks/useSessionManagement.ts +1 -1
- package/src/ui/navigation/routes.ts +3 -1
- package/src/ui/screens/ConnectedAppsScreen.tsx +23 -24
- package/src/ui/screens/EditProfileFieldScreen.tsx +102 -66
- package/src/ui/screens/EditProfileScreen.tsx +155 -0
- package/src/ui/screens/ManageAccountScreen.tsx +3 -7
- package/src/ui/screens/OxyAuthScreen.tsx +259 -112
- package/src/ui/screens/ProfileScreen.tsx +15 -2
- package/src/ui/screens/__tests__/linkFormat.test.ts +73 -0
- package/src/ui/screens/linkFormat.ts +37 -0
- package/src/ui/session/__tests__/createSessionClient.test.ts +22 -7
- package/src/ui/session/__tests__/tokenTransport.test.ts +37 -49
- package/src/ui/session/authStore.ts +164 -0
- package/src/ui/session/createSessionClient.ts +30 -17
- package/src/ui/session/index.ts +10 -9
- package/src/ui/session/tokenTransport.ts +26 -36
- package/src/ui/utils/isWebBrowser.ts +8 -0
- package/src/ui/utils/storageHelpers.ts +7 -7
- package/src/utils/__tests__/deviceFlowSignIn.test.ts +50 -131
- package/src/utils/deviceFlowSignIn.ts +19 -26
- package/lib/commonjs/ui/context/inSessionTokenRefresh.js +0 -243
- package/lib/commonjs/ui/context/inSessionTokenRefresh.js.map +0 -1
- package/lib/commonjs/ui/context/silentSessionRestore.js +0 -56
- package/lib/commonjs/ui/context/silentSessionRestore.js.map +0 -1
- package/lib/commonjs/ui/hooks/useWebSSO.js +0 -28
- package/lib/commonjs/ui/hooks/useWebSSO.js.map +0 -1
- package/lib/commonjs/ui/session/projectSessionState.js +0 -86
- package/lib/commonjs/ui/session/projectSessionState.js.map +0 -1
- package/lib/commonjs/ui/session/sessionClientHost.js +0 -30
- package/lib/commonjs/ui/session/sessionClientHost.js.map +0 -1
- package/lib/commonjs/ui/utils/activeAuthuser.js +0 -142
- package/lib/commonjs/ui/utils/activeAuthuser.js.map +0 -1
- package/lib/commonjs/utils/crossApex.js +0 -74
- package/lib/commonjs/utils/crossApex.js.map +0 -1
- package/lib/module/ui/context/inSessionTokenRefresh.js +0 -238
- package/lib/module/ui/context/inSessionTokenRefresh.js.map +0 -1
- package/lib/module/ui/context/silentSessionRestore.js +0 -53
- package/lib/module/ui/context/silentSessionRestore.js.map +0 -1
- package/lib/module/ui/hooks/useWebSSO.js +0 -24
- package/lib/module/ui/hooks/useWebSSO.js.map +0 -1
- package/lib/module/ui/session/projectSessionState.js +0 -79
- package/lib/module/ui/session/projectSessionState.js.map +0 -1
- package/lib/module/ui/session/sessionClientHost.js +0 -26
- package/lib/module/ui/session/sessionClientHost.js.map +0 -1
- package/lib/module/ui/utils/activeAuthuser.js +0 -134
- package/lib/module/ui/utils/activeAuthuser.js.map +0 -1
- package/lib/module/utils/crossApex.js +0 -69
- package/lib/module/utils/crossApex.js.map +0 -1
- package/lib/typescript/commonjs/ui/context/inSessionTokenRefresh.d.ts +0 -106
- package/lib/typescript/commonjs/ui/context/inSessionTokenRefresh.d.ts.map +0 -1
- package/lib/typescript/commonjs/ui/context/silentSessionRestore.d.ts +0 -36
- package/lib/typescript/commonjs/ui/context/silentSessionRestore.d.ts.map +0 -1
- package/lib/typescript/commonjs/ui/hooks/useWebSSO.d.ts +0 -19
- package/lib/typescript/commonjs/ui/hooks/useWebSSO.d.ts.map +0 -1
- package/lib/typescript/commonjs/ui/session/projectSessionState.d.ts +0 -45
- package/lib/typescript/commonjs/ui/session/projectSessionState.d.ts.map +0 -1
- package/lib/typescript/commonjs/ui/session/sessionClientHost.d.ts +0 -14
- package/lib/typescript/commonjs/ui/session/sessionClientHost.d.ts.map +0 -1
- package/lib/typescript/commonjs/ui/utils/activeAuthuser.d.ts +0 -67
- package/lib/typescript/commonjs/ui/utils/activeAuthuser.d.ts.map +0 -1
- package/lib/typescript/commonjs/utils/crossApex.d.ts +0 -55
- package/lib/typescript/commonjs/utils/crossApex.d.ts.map +0 -1
- package/lib/typescript/module/ui/context/inSessionTokenRefresh.d.ts +0 -106
- package/lib/typescript/module/ui/context/inSessionTokenRefresh.d.ts.map +0 -1
- package/lib/typescript/module/ui/context/silentSessionRestore.d.ts +0 -36
- package/lib/typescript/module/ui/context/silentSessionRestore.d.ts.map +0 -1
- package/lib/typescript/module/ui/hooks/useWebSSO.d.ts +0 -19
- package/lib/typescript/module/ui/hooks/useWebSSO.d.ts.map +0 -1
- package/lib/typescript/module/ui/session/projectSessionState.d.ts +0 -45
- package/lib/typescript/module/ui/session/projectSessionState.d.ts.map +0 -1
- package/lib/typescript/module/ui/session/sessionClientHost.d.ts +0 -14
- package/lib/typescript/module/ui/session/sessionClientHost.d.ts.map +0 -1
- package/lib/typescript/module/ui/utils/activeAuthuser.d.ts +0 -67
- package/lib/typescript/module/ui/utils/activeAuthuser.d.ts.map +0 -1
- package/lib/typescript/module/utils/crossApex.d.ts +0 -55
- package/lib/typescript/module/utils/crossApex.d.ts.map +0 -1
- package/src/ui/context/inSessionTokenRefresh.ts +0 -264
- package/src/ui/context/silentSessionRestore.ts +0 -53
- package/src/ui/hooks/useWebSSO.ts +0 -23
- package/src/ui/session/__tests__/projectSessionState.test.ts +0 -114
- package/src/ui/session/__tests__/sessionClientHost.test.ts +0 -43
- package/src/ui/session/projectSessionState.ts +0 -85
- package/src/ui/session/sessionClientHost.ts +0 -27
- package/src/ui/utils/activeAuthuser.ts +0 -142
- package/src/utils/crossApex.ts +0 -75
|
@@ -1,22 +1,24 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* OxyAuthScreen
|
|
2
|
+
* OxyAuthScreen — Sign in with Oxy (native bottom-sheet container).
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
4
|
+
* Two phases, Google-style:
|
|
5
|
+
* 1. Account chooser (FRONT screen, shown when the device/user already has
|
|
6
|
+
* accounts): pick an account to continue as — one tap switches through the
|
|
7
|
+
* SAME `switchToAccount` path the account switcher uses — or "Use another
|
|
8
|
+
* account" to reveal the sign-in options.
|
|
9
|
+
* 2. Sign-in options: the first-party password flow (identifier → password →
|
|
10
|
+
* optional 2FA, `usePasswordSignIn`) as the PRIMARY action, with the
|
|
11
|
+
* cross-app device flow (same-device deep-link + "sign in on another device"
|
|
12
|
+
* QR) as a SECONDARY option below an "or" divider.
|
|
9
13
|
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
* which the web `SignInModal` also consumes — neither container re-implements
|
|
14
|
-
* the transport. The Oxy Accounts app is where users manage their cryptographic
|
|
15
|
-
* identity; this screen should NOT be used within the Accounts app itself.
|
|
14
|
+
* The device-flow machinery lives in the shared `useOxyAuthSession` hook (the
|
|
15
|
+
* web `SignInModal` consumes it too). This screen should NOT be used within the
|
|
16
|
+
* Oxy Accounts app itself.
|
|
16
17
|
*/
|
|
17
18
|
|
|
18
19
|
import type React from 'react';
|
|
19
|
-
import {
|
|
20
|
+
import { useCallback, useState } from 'react';
|
|
21
|
+
import { View, TextInput, Linking, type TextStyle } from 'react-native';
|
|
20
22
|
import type { BaseScreenProps } from '../types/navigation';
|
|
21
23
|
import { useTheme } from '@oxyhq/bloom/theme';
|
|
22
24
|
import { Button } from '@oxyhq/bloom/button';
|
|
@@ -24,129 +26,274 @@ import { Loading } from '@oxyhq/bloom/loading';
|
|
|
24
26
|
import { H4, Text } from '@oxyhq/bloom/typography';
|
|
25
27
|
import { IconCircle } from '@oxyhq/bloom/icon-circle';
|
|
26
28
|
import * as Icons from '@oxyhq/bloom/icons';
|
|
29
|
+
import { toast } from '@oxyhq/bloom';
|
|
30
|
+
import { isDev, logger as loggerUtil } from '@oxyhq/core';
|
|
27
31
|
import { useOxy } from '../context/OxyContext';
|
|
28
32
|
import OxyLogo from '../components/OxyLogo';
|
|
29
33
|
import AnotherDeviceQR from '../components/AnotherDeviceQR';
|
|
30
34
|
import LoadingState from '../components/LoadingState';
|
|
35
|
+
import SignInAccountChooser from '../components/SignInAccountChooser';
|
|
36
|
+
import { useSwitchableAccounts, type SwitchableAccount } from '../hooks/useSwitchableAccounts';
|
|
37
|
+
import { useI18n } from '../hooks/useI18n';
|
|
31
38
|
import { useOxyAuthSession, OXY_ACCOUNTS_WEB_URL } from '../hooks/useOxyAuthSession';
|
|
32
|
-
import {
|
|
39
|
+
import { usePasswordSignIn } from '../hooks/usePasswordSignIn';
|
|
33
40
|
|
|
34
41
|
const OxyAuthScreen: React.FC<BaseScreenProps> = ({ goBack, onAuthenticated }) => {
|
|
35
42
|
const bloomTheme = useTheme();
|
|
36
|
-
const { oxyServices, handleWebSession, clientId } = useOxy();
|
|
43
|
+
const { oxyServices, handleWebSession, clientId, switchToAccount } = useOxy();
|
|
44
|
+
const { t } = useI18n();
|
|
45
|
+
const { accounts } = useSwitchableAccounts();
|
|
37
46
|
|
|
38
|
-
const
|
|
47
|
+
const [useAnother, setUseAnother] = useState(false);
|
|
48
|
+
const [switchingId, setSwitchingId] = useState<string | null>(null);
|
|
49
|
+
const showChooser = !useAnother && accounts.length > 0;
|
|
50
|
+
|
|
51
|
+
const { qrData, qrPayload, isLoading, error, isWaiting, openSameDeviceApproval, retry } = useOxyAuthSession(
|
|
39
52
|
oxyServices,
|
|
40
53
|
clientId,
|
|
41
54
|
handleWebSession,
|
|
42
55
|
{ onSignedIn: onAuthenticated },
|
|
43
56
|
);
|
|
44
57
|
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
);
|
|
85
|
-
}
|
|
58
|
+
const pw = usePasswordSignIn({ onSignedIn: onAuthenticated });
|
|
59
|
+
|
|
60
|
+
const handleSelectAccount = useCallback(async (account: SwitchableAccount) => {
|
|
61
|
+
if (account.isCurrent) {
|
|
62
|
+
onAuthenticated?.();
|
|
63
|
+
return;
|
|
64
|
+
}
|
|
65
|
+
if (switchingId) return;
|
|
66
|
+
setSwitchingId(account.accountId);
|
|
67
|
+
try {
|
|
68
|
+
await switchToAccount(account.accountId);
|
|
69
|
+
onAuthenticated?.();
|
|
70
|
+
} catch (switchError) {
|
|
71
|
+
if (isDev()) {
|
|
72
|
+
loggerUtil.warn('OxyAuthScreen: switch account failed', { component: 'OxyAuthScreen' }, switchError as unknown);
|
|
73
|
+
}
|
|
74
|
+
toast.error(t('accountSwitcher.toasts.switchFailed') || 'Failed to switch account');
|
|
75
|
+
} finally {
|
|
76
|
+
setSwitchingId(null);
|
|
77
|
+
}
|
|
78
|
+
}, [switchingId, switchToAccount, onAuthenticated, t]);
|
|
79
|
+
|
|
80
|
+
const inputStyle: TextStyle = {
|
|
81
|
+
width: '100%',
|
|
82
|
+
borderWidth: 1,
|
|
83
|
+
borderRadius: 12,
|
|
84
|
+
paddingHorizontal: 16,
|
|
85
|
+
paddingVertical: 13,
|
|
86
|
+
fontSize: 15,
|
|
87
|
+
marginBottom: 12,
|
|
88
|
+
borderColor: bloomTheme.colors.border,
|
|
89
|
+
color: bloomTheme.colors.text,
|
|
90
|
+
backgroundColor: bloomTheme.colors.backgroundSecondary,
|
|
91
|
+
};
|
|
92
|
+
|
|
93
|
+
const title = showChooser ? t('signin.chooser.title') : 'Sign in to Oxy';
|
|
94
|
+
const subtitle = showChooser
|
|
95
|
+
? t('signin.chooser.subtitle')
|
|
96
|
+
: 'Continue with your Oxy identity to sign in securely';
|
|
86
97
|
|
|
87
98
|
return (
|
|
88
99
|
<View className="flex-1 items-center justify-center bg-bg px-screen-margin">
|
|
89
100
|
{/* Branded header */}
|
|
90
101
|
<View className="items-center mb-space-24 gap-space-12">
|
|
91
|
-
<OxyLogo variant="icon" size={
|
|
92
|
-
<H4 className="text-headerBold font-headerBold text-text text-center">
|
|
93
|
-
|
|
94
|
-
</H4>
|
|
95
|
-
<Text className="font-sans text-body text-text-secondary text-center">
|
|
96
|
-
Continue with your Oxy identity to sign in securely
|
|
97
|
-
</Text>
|
|
102
|
+
<OxyLogo variant="icon" size={52} />
|
|
103
|
+
<H4 className="text-headerBold font-headerBold text-text text-center">{title}</H4>
|
|
104
|
+
<Text className="font-sans text-body text-text-secondary text-center">{subtitle}</Text>
|
|
98
105
|
</View>
|
|
99
106
|
|
|
100
|
-
{
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
className="w-full mt-space-12"
|
|
124
|
-
onPress={openSameDeviceApproval}
|
|
125
|
-
icon={
|
|
126
|
-
<OxyLogo variant="icon" size={20} fillColor={bloomTheme.colors.text} />
|
|
127
|
-
}
|
|
128
|
-
>
|
|
129
|
-
Sign in with the Oxy app
|
|
130
|
-
</Button>
|
|
131
|
-
)}
|
|
107
|
+
{showChooser ? (
|
|
108
|
+
<SignInAccountChooser
|
|
109
|
+
accounts={accounts}
|
|
110
|
+
onSelectAccount={handleSelectAccount}
|
|
111
|
+
onUseAnother={() => setUseAnother(true)}
|
|
112
|
+
pendingAccountId={switchingId}
|
|
113
|
+
disabled={switchingId !== null}
|
|
114
|
+
/>
|
|
115
|
+
) : (
|
|
116
|
+
<>
|
|
117
|
+
{/* Back to the chooser when accounts exist. */}
|
|
118
|
+
{accounts.length > 0 && (
|
|
119
|
+
<Button
|
|
120
|
+
variant="text"
|
|
121
|
+
size="small"
|
|
122
|
+
className="self-start mb-space-8"
|
|
123
|
+
onPress={() => setUseAnother(false)}
|
|
124
|
+
accessibilityLabel="Choose an account"
|
|
125
|
+
icon={<Icons.ArrowLeft_Stroke2_Corner0_Rounded size="sm" style={{ color: bloomTheme.colors.textSecondary }} />}
|
|
126
|
+
>
|
|
127
|
+
Choose an account
|
|
128
|
+
</Button>
|
|
129
|
+
)}
|
|
132
130
|
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
131
|
+
{/* PRIMARY — first-party password sign-in. Always usable; the device-flow
|
|
132
|
+
loading/error state below never gates it. */}
|
|
133
|
+
<View className="w-full">
|
|
134
|
+
{pw.step === 'identifier' && (
|
|
135
|
+
<TextInput
|
|
136
|
+
style={inputStyle}
|
|
137
|
+
value={pw.identifier}
|
|
138
|
+
onChangeText={pw.setIdentifier}
|
|
139
|
+
onSubmitEditing={pw.submitIdentifier}
|
|
140
|
+
placeholder="Username or email"
|
|
141
|
+
placeholderTextColor={bloomTheme.colors.textSecondary}
|
|
142
|
+
autoCapitalize="none"
|
|
143
|
+
autoCorrect={false}
|
|
144
|
+
keyboardType="email-address"
|
|
145
|
+
returnKeyType="next"
|
|
146
|
+
accessibilityLabel="Username or email"
|
|
147
|
+
/>
|
|
148
|
+
)}
|
|
149
|
+
|
|
150
|
+
{pw.step === 'password' && (
|
|
151
|
+
<>
|
|
152
|
+
<Text className="font-sans text-body text-text-secondary text-center mb-space-12">
|
|
153
|
+
{pw.identifier}
|
|
154
|
+
</Text>
|
|
155
|
+
<TextInput
|
|
156
|
+
style={inputStyle}
|
|
157
|
+
value={pw.password}
|
|
158
|
+
onChangeText={pw.setPassword}
|
|
159
|
+
onSubmitEditing={pw.submitPassword}
|
|
160
|
+
placeholder="Password"
|
|
161
|
+
placeholderTextColor={bloomTheme.colors.textSecondary}
|
|
162
|
+
secureTextEntry
|
|
163
|
+
autoCapitalize="none"
|
|
164
|
+
autoCorrect={false}
|
|
165
|
+
returnKeyType="go"
|
|
166
|
+
accessibilityLabel="Password"
|
|
167
|
+
/>
|
|
168
|
+
</>
|
|
169
|
+
)}
|
|
170
|
+
|
|
171
|
+
{pw.step === 'twoFactor' && (
|
|
172
|
+
<TextInput
|
|
173
|
+
style={inputStyle}
|
|
174
|
+
value={pw.code}
|
|
175
|
+
onChangeText={pw.setCode}
|
|
176
|
+
onSubmitEditing={pw.submitTwoFactor}
|
|
177
|
+
placeholder={pw.useBackupCode ? 'Backup code' : '6-digit code'}
|
|
178
|
+
placeholderTextColor={bloomTheme.colors.textSecondary}
|
|
179
|
+
autoCapitalize="none"
|
|
180
|
+
autoCorrect={false}
|
|
181
|
+
keyboardType={pw.useBackupCode ? 'default' : 'number-pad'}
|
|
182
|
+
returnKeyType="go"
|
|
183
|
+
accessibilityLabel={pw.useBackupCode ? 'Backup code' : 'Two-factor code'}
|
|
184
|
+
/>
|
|
185
|
+
)}
|
|
186
|
+
|
|
187
|
+
<Button
|
|
188
|
+
variant="primary"
|
|
189
|
+
size="large"
|
|
190
|
+
fullWidth
|
|
191
|
+
className="w-full"
|
|
192
|
+
onPress={
|
|
193
|
+
pw.step === 'identifier' ? pw.submitIdentifier
|
|
194
|
+
: pw.step === 'password' ? pw.submitPassword
|
|
195
|
+
: pw.submitTwoFactor
|
|
196
|
+
}
|
|
197
|
+
loading={pw.isSubmitting}
|
|
198
|
+
disabled={pw.isSubmitting}
|
|
199
|
+
>
|
|
200
|
+
{pw.step === 'identifier' ? 'Continue' : pw.step === 'password' ? 'Sign in' : 'Verify'}
|
|
201
|
+
</Button>
|
|
202
|
+
|
|
203
|
+
{pw.error && (
|
|
204
|
+
<Text className="font-sans text-body text-destructive text-center mt-space-12">
|
|
205
|
+
{pw.error}
|
|
206
|
+
</Text>
|
|
207
|
+
)}
|
|
208
|
+
|
|
209
|
+
{pw.step === 'twoFactor' && (
|
|
210
|
+
<Button
|
|
211
|
+
variant="text"
|
|
212
|
+
size="small"
|
|
213
|
+
className="mt-space-8"
|
|
214
|
+
onPress={() => pw.setUseBackupCode(!pw.useBackupCode)}
|
|
215
|
+
>
|
|
216
|
+
{pw.useBackupCode ? 'Use authenticator code' : 'Use a backup code'}
|
|
217
|
+
</Button>
|
|
218
|
+
)}
|
|
219
|
+
|
|
220
|
+
{pw.step !== 'identifier' && (
|
|
221
|
+
<Button
|
|
222
|
+
variant="text"
|
|
223
|
+
size="small"
|
|
224
|
+
className="mt-space-8"
|
|
225
|
+
onPress={pw.back}
|
|
226
|
+
accessibilityLabel="Back"
|
|
227
|
+
>
|
|
228
|
+
Back
|
|
229
|
+
</Button>
|
|
230
|
+
)}
|
|
231
|
+
</View>
|
|
232
|
+
|
|
233
|
+
{/* "or" divider — separates the password form from the SECONDARY device flow. */}
|
|
234
|
+
<View className="flex-row items-center w-full my-space-16 gap-space-8">
|
|
235
|
+
<View className="flex-1" style={{ height: 1, backgroundColor: bloomTheme.colors.border }} />
|
|
236
|
+
<Text className="font-sans text-caption text-text-tertiary">or</Text>
|
|
237
|
+
<View className="flex-1" style={{ height: 1, backgroundColor: bloomTheme.colors.border }} />
|
|
238
|
+
</View>
|
|
239
|
+
|
|
240
|
+
{/* SECONDARY — cross-app device flow. Its loading/error state gates ONLY
|
|
241
|
+
this section; the password form above is always usable. */}
|
|
242
|
+
{isLoading ? (
|
|
243
|
+
<LoadingState
|
|
244
|
+
size="small"
|
|
245
|
+
color={bloomTheme.colors.primary}
|
|
246
|
+
message="Preparing sign in…"
|
|
247
|
+
/>
|
|
248
|
+
) : error ? (
|
|
249
|
+
<View className="items-center gap-space-16 w-full">
|
|
250
|
+
<IconCircle icon={Icons.Warning_Stroke2_Corner0_Rounded} />
|
|
251
|
+
<Text className="font-sans text-body text-text-secondary text-center">{error}</Text>
|
|
252
|
+
<Button
|
|
253
|
+
variant="secondary"
|
|
254
|
+
fullWidth
|
|
255
|
+
className="w-full"
|
|
256
|
+
onPress={retry}
|
|
257
|
+
icon={
|
|
258
|
+
<Icons.ArrowRotateClockwise_Stroke2_Corner0_Rounded
|
|
259
|
+
size="sm"
|
|
260
|
+
style={{ color: bloomTheme.colors.text }}
|
|
261
|
+
/>
|
|
262
|
+
}
|
|
263
|
+
>
|
|
264
|
+
Try Again
|
|
265
|
+
</Button>
|
|
266
|
+
</View>
|
|
267
|
+
) : (
|
|
268
|
+
<>
|
|
269
|
+
{qrPayload && (
|
|
270
|
+
<Button
|
|
271
|
+
variant="secondary"
|
|
272
|
+
size="large"
|
|
273
|
+
fullWidth
|
|
274
|
+
className="w-full"
|
|
275
|
+
onPress={openSameDeviceApproval}
|
|
276
|
+
icon={<OxyLogo variant="icon" size={20} fillColor={bloomTheme.colors.text} />}
|
|
277
|
+
>
|
|
278
|
+
Sign in with the Oxy app
|
|
279
|
+
</Button>
|
|
280
|
+
)}
|
|
281
|
+
|
|
282
|
+
{isWaiting && (
|
|
283
|
+
<View className="flex-row items-center mt-space-16 gap-space-8">
|
|
284
|
+
<Loading size="small" />
|
|
285
|
+
<Text className="font-sans text-body text-text-secondary">
|
|
286
|
+
Waiting for authorization…
|
|
287
|
+
</Text>
|
|
288
|
+
</View>
|
|
289
|
+
)}
|
|
142
290
|
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
</View>
|
|
291
|
+
<View className="w-full mt-space-24">
|
|
292
|
+
<AnotherDeviceQR qrData={qrData} qrPayload={qrPayload} />
|
|
293
|
+
</View>
|
|
294
|
+
</>
|
|
295
|
+
)}
|
|
296
|
+
</>
|
|
150
297
|
)}
|
|
151
298
|
|
|
152
299
|
{/* Footer — create an account */}
|
|
@@ -178,6 +178,19 @@ const ProfileScreen: React.FC<ProfileScreenProps> = ({ userId, username, theme,
|
|
|
178
178
|
|
|
179
179
|
const displayName = profile ? getAccountDisplayName(profile, locale) : username || '';
|
|
180
180
|
|
|
181
|
+
// The singular `location` field was removed from the User contract; derive
|
|
182
|
+
// the primary place from the `locations` list instead. `locations` is only
|
|
183
|
+
// reachable through the User index signature (typed `unknown`), so narrow it
|
|
184
|
+
// defensively before rendering the chip.
|
|
185
|
+
const primaryLocation = ((): string | undefined => {
|
|
186
|
+
const locations = profile?.locations;
|
|
187
|
+
const first: unknown = Array.isArray(locations) ? locations[0] : undefined;
|
|
188
|
+
if (first && typeof first === 'object' && 'name' in first && typeof first.name === 'string') {
|
|
189
|
+
return first.name || undefined;
|
|
190
|
+
}
|
|
191
|
+
return undefined;
|
|
192
|
+
})();
|
|
193
|
+
|
|
181
194
|
return (
|
|
182
195
|
<View style={styles.container} className="bg-bg">
|
|
183
196
|
<ScrollView style={styles.flex} contentContainerStyle={styles.scrollContainer}>
|
|
@@ -238,10 +251,10 @@ const ProfileScreen: React.FC<ProfileScreenProps> = ({ userId, username, theme,
|
|
|
238
251
|
showChevron={false}
|
|
239
252
|
/>
|
|
240
253
|
)}
|
|
241
|
-
{
|
|
254
|
+
{primaryLocation && (
|
|
242
255
|
<SettingsListItem
|
|
243
256
|
icon={<Ionicons name="location-outline" size={INFO_ICON_SIZE} color={bloomTheme.colors.textSecondary} />}
|
|
244
|
-
title={
|
|
257
|
+
title={primaryLocation}
|
|
245
258
|
showChevron={false}
|
|
246
259
|
/>
|
|
247
260
|
)}
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import {
|
|
2
|
+
getLinkTitle,
|
|
3
|
+
getLinkDescription,
|
|
4
|
+
linksToListItems,
|
|
5
|
+
} from '../linkFormat';
|
|
6
|
+
|
|
7
|
+
describe('linkFormat', () => {
|
|
8
|
+
describe('getLinkTitle', () => {
|
|
9
|
+
it.each([
|
|
10
|
+
['https://example.com', 'example.com'],
|
|
11
|
+
['http://example.com/', 'example.com'],
|
|
12
|
+
['https://example.com/path/', 'example.com/path'],
|
|
13
|
+
['example.com', 'example.com'],
|
|
14
|
+
])('strips protocol and trailing slash: %p → %p', (input, expected) => {
|
|
15
|
+
expect(getLinkTitle(input)).toBe(expected);
|
|
16
|
+
});
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
describe('getLinkDescription', () => {
|
|
20
|
+
it('prefixes the url with "Link to "', () => {
|
|
21
|
+
expect(getLinkDescription('https://example.com')).toBe('Link to https://example.com');
|
|
22
|
+
});
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
describe('linksToListItems', () => {
|
|
26
|
+
it('maps a normal string array, preserving valid-link rendering', () => {
|
|
27
|
+
const result = linksToListItems(['https://example.com/', 'http://oxy.so']);
|
|
28
|
+
expect(result).toEqual([
|
|
29
|
+
{
|
|
30
|
+
id: 'link-0',
|
|
31
|
+
url: 'https://example.com/',
|
|
32
|
+
title: 'example.com',
|
|
33
|
+
description: 'Link to https://example.com/',
|
|
34
|
+
},
|
|
35
|
+
{
|
|
36
|
+
id: 'link-1',
|
|
37
|
+
url: 'http://oxy.so',
|
|
38
|
+
title: 'oxy.so',
|
|
39
|
+
description: 'Link to http://oxy.so',
|
|
40
|
+
},
|
|
41
|
+
]);
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
it.each([
|
|
45
|
+
[null, 'null'],
|
|
46
|
+
[undefined, 'undefined'],
|
|
47
|
+
[42, 'a number'],
|
|
48
|
+
[{ href: 'x' }, 'an object'],
|
|
49
|
+
[['nested'], 'an array'],
|
|
50
|
+
])('does not throw on a non-string element (%s)', (bad) => {
|
|
51
|
+
// Regression: `links.map(getLinkTitle)` called `.replace` on a
|
|
52
|
+
// non-string element and crashed the editor. The coercion guard must
|
|
53
|
+
// yield a string url/title/description instead.
|
|
54
|
+
const run = () => linksToListItems([bad]);
|
|
55
|
+
expect(run).not.toThrow();
|
|
56
|
+
const [item] = run();
|
|
57
|
+
expect(typeof item.url).toBe('string');
|
|
58
|
+
expect(typeof item.title).toBe('string');
|
|
59
|
+
expect(typeof item.description).toBe('string');
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
it('coerces null / undefined to an empty-string url', () => {
|
|
63
|
+
const result = linksToListItems([null, undefined]);
|
|
64
|
+
expect(result[0].url).toBe('');
|
|
65
|
+
expect(result[0].title).toBe('');
|
|
66
|
+
expect(result[1].url).toBe('');
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
it('returns an empty array for an empty input', () => {
|
|
70
|
+
expect(linksToListItems([])).toEqual([]);
|
|
71
|
+
});
|
|
72
|
+
});
|
|
73
|
+
});
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pure link-display helpers for the profile "links" editor. Kept out of the
|
|
3
|
+
* screen component so the string-coercion guard can be unit-tested without
|
|
4
|
+
* rendering the whole React Native screen.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
/** Strip the protocol and any trailing slash from a link URL for display. */
|
|
8
|
+
export const getLinkTitle = (url: string): string =>
|
|
9
|
+
url.replace(/^https?:\/\//, '').replace(/\/$/, '');
|
|
10
|
+
|
|
11
|
+
/** Human-readable description for a link URL. */
|
|
12
|
+
export const getLinkDescription = (url: string): string => `Link to ${url}`;
|
|
13
|
+
|
|
14
|
+
export interface LinkListItem {
|
|
15
|
+
id: string;
|
|
16
|
+
url: string;
|
|
17
|
+
title: string;
|
|
18
|
+
description: string;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Build editable list items from a raw links array. The array can come from
|
|
23
|
+
* legacy or untrusted profile data whose elements are not guaranteed to be
|
|
24
|
+
* strings, so each entry is coerced with `String(item ?? '')` before formatting
|
|
25
|
+
* — a non-string element must never crash the editor.
|
|
26
|
+
*/
|
|
27
|
+
export function linksToListItems(links: readonly unknown[]): LinkListItem[] {
|
|
28
|
+
return links.map((item, i) => {
|
|
29
|
+
const url = String(item ?? '');
|
|
30
|
+
return {
|
|
31
|
+
id: `link-${i}`,
|
|
32
|
+
url,
|
|
33
|
+
title: getLinkTitle(url),
|
|
34
|
+
description: getLinkDescription(url),
|
|
35
|
+
};
|
|
36
|
+
});
|
|
37
|
+
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { SessionClient } from '@oxyhq/core';
|
|
1
|
+
import { SessionClient, createMemoryAuthStateStore } from '@oxyhq/core';
|
|
2
2
|
|
|
3
3
|
type Handler = (...args: unknown[]) => void;
|
|
4
4
|
class FakeSocket {
|
|
@@ -27,16 +27,14 @@ function fakeOxy() {
|
|
|
27
27
|
listeners.add(l);
|
|
28
28
|
return () => listeners.delete(l);
|
|
29
29
|
}),
|
|
30
|
-
silentSignIn: jest.fn().mockResolvedValue(null),
|
|
31
|
-
signInWithSharedIdentity: jest.fn().mockResolvedValue(null),
|
|
32
30
|
};
|
|
33
31
|
}
|
|
34
32
|
|
|
35
33
|
describe('createSessionClient', () => {
|
|
36
|
-
test('wires a SessionClient instance backed by the host + token transport', () => {
|
|
34
|
+
test('wires a SessionClient instance backed by the host + device-first token transport', () => {
|
|
37
35
|
const oxy = fakeOxy();
|
|
38
36
|
|
|
39
|
-
const { client, host } = createSessionClient(oxy as never);
|
|
37
|
+
const { client, host } = createSessionClient(oxy as never, createMemoryAuthStateStore());
|
|
40
38
|
|
|
41
39
|
expect(client).toBeInstanceOf(SessionClient);
|
|
42
40
|
expect(typeof client.bootstrap).toBe('function');
|
|
@@ -47,7 +45,7 @@ describe('createSessionClient', () => {
|
|
|
47
45
|
test('the returned host reflects setCurrentAccountId', () => {
|
|
48
46
|
const oxy = fakeOxy();
|
|
49
47
|
|
|
50
|
-
const { host } = createSessionClient(oxy as never);
|
|
48
|
+
const { host } = createSessionClient(oxy as never, createMemoryAuthStateStore());
|
|
51
49
|
|
|
52
50
|
expect(host.getCurrentAccountId()).toBeNull();
|
|
53
51
|
host.setCurrentAccountId('u1');
|
|
@@ -58,7 +56,7 @@ describe('createSessionClient', () => {
|
|
|
58
56
|
ioMock.mockClear();
|
|
59
57
|
const oxy = fakeOxy();
|
|
60
58
|
|
|
61
|
-
const { client } = createSessionClient(oxy as never);
|
|
59
|
+
const { client } = createSessionClient(oxy as never, createMemoryAuthStateStore());
|
|
62
60
|
await client.start();
|
|
63
61
|
|
|
64
62
|
// The injected `io` was used to open the session socket at the base URL.
|
|
@@ -66,4 +64,21 @@ describe('createSessionClient', () => {
|
|
|
66
64
|
expect(ioMock).toHaveBeenCalledWith('https://api.oxy.so', expect.objectContaining({ transports: ['websocket'] }));
|
|
67
65
|
client.stop();
|
|
68
66
|
});
|
|
67
|
+
|
|
68
|
+
test('passes onUnauthenticated through to the SessionClient', async () => {
|
|
69
|
+
ioMock.mockClear();
|
|
70
|
+
const oxy = fakeOxy();
|
|
71
|
+
const onUnauthenticated = jest.fn();
|
|
72
|
+
|
|
73
|
+
const { client } = createSessionClient(oxy as never, createMemoryAuthStateStore(), onUnauthenticated);
|
|
74
|
+
|
|
75
|
+
// The client was constructed with the callback (no direct getter is exposed;
|
|
76
|
+
// constructing without throwing + wiring the socket factory is the contract
|
|
77
|
+
// this factory owns — the callback firing on a zero-account applied state is
|
|
78
|
+
// covered by @oxyhq/core's SessionClient tests).
|
|
79
|
+
expect(client).toBeInstanceOf(SessionClient);
|
|
80
|
+
await client.start();
|
|
81
|
+
expect(ioMock).toHaveBeenCalledTimes(1);
|
|
82
|
+
client.stop();
|
|
83
|
+
});
|
|
69
84
|
});
|