@oxy.so/services 1.0.3 → 1.2.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 (163) hide show
  1. package/lib/commonjs/index.js.map +1 -1
  2. package/lib/commonjs/ui/components/LanguageBridge.js +51 -0
  3. package/lib/commonjs/ui/components/LanguageBridge.js.map +1 -0
  4. package/lib/commonjs/ui/components/OxyAuthChooser.js +95 -140
  5. package/lib/commonjs/ui/components/OxyAuthChooser.js.map +1 -1
  6. package/lib/commonjs/ui/components/OxyConsentScreen.js +132 -2
  7. package/lib/commonjs/ui/components/OxyConsentScreen.js.map +1 -1
  8. package/lib/commonjs/ui/components/OxyProvider.js +19 -1
  9. package/lib/commonjs/ui/components/OxyProvider.js.map +1 -1
  10. package/lib/commonjs/ui/components/authChooser/AccountsMenuView.js +8 -1
  11. package/lib/commonjs/ui/components/authChooser/AccountsMenuView.js.map +1 -1
  12. package/lib/commonjs/ui/components/authChooser/SignInEntryView.js +35 -25
  13. package/lib/commonjs/ui/components/authChooser/SignInEntryView.js.map +1 -1
  14. package/lib/commonjs/ui/components/authChooser/SignInRequestView.js +2 -3
  15. package/lib/commonjs/ui/components/authChooser/SignInRequestView.js.map +1 -1
  16. package/lib/commonjs/ui/components/authChooser/SignUpView.js +12 -127
  17. package/lib/commonjs/ui/components/authChooser/SignUpView.js.map +1 -1
  18. package/lib/commonjs/ui/components/authChooser/signInFailureMessage.js +40 -0
  19. package/lib/commonjs/ui/components/authChooser/signInFailureMessage.js.map +1 -0
  20. package/lib/commonjs/ui/components/authChooser/types.js +4 -6
  21. package/lib/commonjs/ui/components/authChooser/types.js.map +1 -1
  22. package/lib/commonjs/ui/context/hooks/useAuthOperations.js +15 -2
  23. package/lib/commonjs/ui/context/hooks/useAuthOperations.js.map +1 -1
  24. package/lib/commonjs/ui/hooks/accountDialogSnapshot.js +3 -1
  25. package/lib/commonjs/ui/hooks/accountDialogSnapshot.js.map +1 -1
  26. package/lib/commonjs/ui/hooks/useProfileEditing.js +14 -0
  27. package/lib/commonjs/ui/hooks/useProfileEditing.js.map +1 -1
  28. package/lib/commonjs/ui/screens/EditProfileFieldScreen.js +225 -9
  29. package/lib/commonjs/ui/screens/EditProfileFieldScreen.js.map +1 -1
  30. package/lib/commonjs/ui/screens/ManageAccountScreen.js +10 -0
  31. package/lib/commonjs/ui/screens/ManageAccountScreen.js.map +1 -1
  32. package/lib/module/index.js.map +1 -1
  33. package/lib/module/ui/components/LanguageBridge.js +47 -0
  34. package/lib/module/ui/components/LanguageBridge.js.map +1 -0
  35. package/lib/module/ui/components/OxyAuthChooser.js +97 -142
  36. package/lib/module/ui/components/OxyAuthChooser.js.map +1 -1
  37. package/lib/module/ui/components/OxyConsentScreen.js +132 -2
  38. package/lib/module/ui/components/OxyConsentScreen.js.map +1 -1
  39. package/lib/module/ui/components/OxyProvider.js +19 -1
  40. package/lib/module/ui/components/OxyProvider.js.map +1 -1
  41. package/lib/module/ui/components/authChooser/AccountsMenuView.js +8 -1
  42. package/lib/module/ui/components/authChooser/AccountsMenuView.js.map +1 -1
  43. package/lib/module/ui/components/authChooser/SignInEntryView.js +35 -25
  44. package/lib/module/ui/components/authChooser/SignInEntryView.js.map +1 -1
  45. package/lib/module/ui/components/authChooser/SignInRequestView.js +2 -3
  46. package/lib/module/ui/components/authChooser/SignInRequestView.js.map +1 -1
  47. package/lib/module/ui/components/authChooser/SignUpView.js +14 -129
  48. package/lib/module/ui/components/authChooser/SignUpView.js.map +1 -1
  49. package/lib/module/ui/components/authChooser/signInFailureMessage.js +36 -0
  50. package/lib/module/ui/components/authChooser/signInFailureMessage.js.map +1 -0
  51. package/lib/module/ui/components/authChooser/types.js +4 -6
  52. package/lib/module/ui/components/authChooser/types.js.map +1 -1
  53. package/lib/module/ui/context/hooks/useAuthOperations.js +15 -2
  54. package/lib/module/ui/context/hooks/useAuthOperations.js.map +1 -1
  55. package/lib/module/ui/hooks/accountDialogSnapshot.js +3 -1
  56. package/lib/module/ui/hooks/accountDialogSnapshot.js.map +1 -1
  57. package/lib/module/ui/hooks/useProfileEditing.js +14 -0
  58. package/lib/module/ui/hooks/useProfileEditing.js.map +1 -1
  59. package/lib/module/ui/screens/EditProfileFieldScreen.js +225 -9
  60. package/lib/module/ui/screens/EditProfileFieldScreen.js.map +1 -1
  61. package/lib/module/ui/screens/ManageAccountScreen.js +12 -2
  62. package/lib/module/ui/screens/ManageAccountScreen.js.map +1 -1
  63. package/lib/typescript/commonjs/index.d.ts +2 -2
  64. package/lib/typescript/commonjs/index.d.ts.map +1 -1
  65. package/lib/typescript/commonjs/ui/components/LanguageBridge.d.ts +19 -0
  66. package/lib/typescript/commonjs/ui/components/LanguageBridge.d.ts.map +1 -0
  67. package/lib/typescript/commonjs/ui/components/OxyAuthChooser.d.ts +3 -4
  68. package/lib/typescript/commonjs/ui/components/OxyAuthChooser.d.ts.map +1 -1
  69. package/lib/typescript/commonjs/ui/components/OxyConsentScreen.d.ts +17 -1
  70. package/lib/typescript/commonjs/ui/components/OxyConsentScreen.d.ts.map +1 -1
  71. package/lib/typescript/commonjs/ui/components/OxyProvider.d.ts +14 -0
  72. package/lib/typescript/commonjs/ui/components/OxyProvider.d.ts.map +1 -1
  73. package/lib/typescript/commonjs/ui/components/authChooser/AccountsMenuView.d.ts.map +1 -1
  74. package/lib/typescript/commonjs/ui/components/authChooser/SignInEntryView.d.ts +11 -12
  75. package/lib/typescript/commonjs/ui/components/authChooser/SignInEntryView.d.ts.map +1 -1
  76. package/lib/typescript/commonjs/ui/components/authChooser/SignInRequestView.d.ts.map +1 -1
  77. package/lib/typescript/commonjs/ui/components/authChooser/SignUpView.d.ts +10 -13
  78. package/lib/typescript/commonjs/ui/components/authChooser/SignUpView.d.ts.map +1 -1
  79. package/lib/typescript/commonjs/ui/components/authChooser/signInFailureMessage.d.ts +16 -0
  80. package/lib/typescript/commonjs/ui/components/authChooser/signInFailureMessage.d.ts.map +1 -0
  81. package/lib/typescript/commonjs/ui/components/authChooser/types.d.ts +11 -13
  82. package/lib/typescript/commonjs/ui/components/authChooser/types.d.ts.map +1 -1
  83. package/lib/typescript/commonjs/ui/context/OxyContext.d.ts +1 -1
  84. package/lib/typescript/commonjs/ui/context/OxyContext.d.ts.map +1 -1
  85. package/lib/typescript/commonjs/ui/context/hooks/useAuthOperations.d.ts +3 -2
  86. package/lib/typescript/commonjs/ui/context/hooks/useAuthOperations.d.ts.map +1 -1
  87. package/lib/typescript/commonjs/ui/context/oxyContextTypes.d.ts +20 -2
  88. package/lib/typescript/commonjs/ui/context/oxyContextTypes.d.ts.map +1 -1
  89. package/lib/typescript/commonjs/ui/hooks/accountDialogSnapshot.d.ts.map +1 -1
  90. package/lib/typescript/commonjs/ui/hooks/mutations/useAccountMutations.d.ts +3 -0
  91. package/lib/typescript/commonjs/ui/hooks/mutations/useAccountMutations.d.ts.map +1 -1
  92. package/lib/typescript/commonjs/ui/hooks/useProfileEditing.d.ts +1 -0
  93. package/lib/typescript/commonjs/ui/hooks/useProfileEditing.d.ts.map +1 -1
  94. package/lib/typescript/commonjs/ui/screens/EditProfileFieldScreen.d.ts.map +1 -1
  95. package/lib/typescript/commonjs/ui/screens/ManageAccountScreen.d.ts.map +1 -1
  96. package/lib/typescript/commonjs/ui/types/navigation.d.ts +29 -0
  97. package/lib/typescript/commonjs/ui/types/navigation.d.ts.map +1 -1
  98. package/lib/typescript/module/index.d.ts +2 -2
  99. package/lib/typescript/module/index.d.ts.map +1 -1
  100. package/lib/typescript/module/ui/components/LanguageBridge.d.ts +19 -0
  101. package/lib/typescript/module/ui/components/LanguageBridge.d.ts.map +1 -0
  102. package/lib/typescript/module/ui/components/OxyAuthChooser.d.ts +3 -4
  103. package/lib/typescript/module/ui/components/OxyAuthChooser.d.ts.map +1 -1
  104. package/lib/typescript/module/ui/components/OxyConsentScreen.d.ts +17 -1
  105. package/lib/typescript/module/ui/components/OxyConsentScreen.d.ts.map +1 -1
  106. package/lib/typescript/module/ui/components/OxyProvider.d.ts +14 -0
  107. package/lib/typescript/module/ui/components/OxyProvider.d.ts.map +1 -1
  108. package/lib/typescript/module/ui/components/authChooser/AccountsMenuView.d.ts.map +1 -1
  109. package/lib/typescript/module/ui/components/authChooser/SignInEntryView.d.ts +11 -12
  110. package/lib/typescript/module/ui/components/authChooser/SignInEntryView.d.ts.map +1 -1
  111. package/lib/typescript/module/ui/components/authChooser/SignInRequestView.d.ts.map +1 -1
  112. package/lib/typescript/module/ui/components/authChooser/SignUpView.d.ts +10 -13
  113. package/lib/typescript/module/ui/components/authChooser/SignUpView.d.ts.map +1 -1
  114. package/lib/typescript/module/ui/components/authChooser/signInFailureMessage.d.ts +16 -0
  115. package/lib/typescript/module/ui/components/authChooser/signInFailureMessage.d.ts.map +1 -0
  116. package/lib/typescript/module/ui/components/authChooser/types.d.ts +11 -13
  117. package/lib/typescript/module/ui/components/authChooser/types.d.ts.map +1 -1
  118. package/lib/typescript/module/ui/context/OxyContext.d.ts +1 -1
  119. package/lib/typescript/module/ui/context/OxyContext.d.ts.map +1 -1
  120. package/lib/typescript/module/ui/context/hooks/useAuthOperations.d.ts +3 -2
  121. package/lib/typescript/module/ui/context/hooks/useAuthOperations.d.ts.map +1 -1
  122. package/lib/typescript/module/ui/context/oxyContextTypes.d.ts +20 -2
  123. package/lib/typescript/module/ui/context/oxyContextTypes.d.ts.map +1 -1
  124. package/lib/typescript/module/ui/hooks/accountDialogSnapshot.d.ts.map +1 -1
  125. package/lib/typescript/module/ui/hooks/mutations/useAccountMutations.d.ts +3 -0
  126. package/lib/typescript/module/ui/hooks/mutations/useAccountMutations.d.ts.map +1 -1
  127. package/lib/typescript/module/ui/hooks/useProfileEditing.d.ts +1 -0
  128. package/lib/typescript/module/ui/hooks/useProfileEditing.d.ts.map +1 -1
  129. package/lib/typescript/module/ui/screens/EditProfileFieldScreen.d.ts.map +1 -1
  130. package/lib/typescript/module/ui/screens/ManageAccountScreen.d.ts.map +1 -1
  131. package/lib/typescript/module/ui/types/navigation.d.ts +29 -0
  132. package/lib/typescript/module/ui/types/navigation.d.ts.map +1 -1
  133. package/package.json +5 -4
  134. package/src/index.ts +2 -1
  135. package/src/ui/components/LanguageBridge.tsx +44 -0
  136. package/src/ui/components/OxyAuthChooser.tsx +106 -156
  137. package/src/ui/components/OxyConsentScreen.tsx +127 -0
  138. package/src/ui/components/OxyProvider.tsx +17 -0
  139. package/src/ui/components/__tests__/LanguageBridge.test.tsx +81 -0
  140. package/src/ui/components/__tests__/OxyConsentScreen.test.tsx +32 -0
  141. package/src/ui/components/authChooser/AccountsMenuView.tsx +7 -0
  142. package/src/ui/components/authChooser/SignInEntryView.tsx +41 -44
  143. package/src/ui/components/authChooser/SignInRequestView.tsx +1 -4
  144. package/src/ui/components/authChooser/SignUpView.tsx +16 -114
  145. package/src/ui/components/authChooser/signInFailureMessage.ts +39 -0
  146. package/src/ui/components/authChooser/types.ts +11 -13
  147. package/src/ui/context/OxyContext.tsx +1 -1
  148. package/src/ui/context/hooks/useAuthOperations.ts +10 -5
  149. package/src/ui/context/oxyContextTypes.ts +16 -2
  150. package/src/ui/hooks/accountDialogSnapshot.ts +2 -0
  151. package/src/ui/hooks/useProfileEditing.ts +15 -0
  152. package/src/ui/screens/EditProfileFieldScreen.tsx +202 -7
  153. package/src/ui/screens/ManageAccountScreen.tsx +12 -0
  154. package/src/ui/types/navigation.ts +30 -0
  155. package/lib/commonjs/ui/components/authChooser/useUsernameAvailability.js +0 -50
  156. package/lib/commonjs/ui/components/authChooser/useUsernameAvailability.js.map +0 -1
  157. package/lib/module/ui/components/authChooser/useUsernameAvailability.js +0 -46
  158. package/lib/module/ui/components/authChooser/useUsernameAvailability.js.map +0 -1
  159. package/lib/typescript/commonjs/ui/components/authChooser/useUsernameAvailability.d.ts +0 -16
  160. package/lib/typescript/commonjs/ui/components/authChooser/useUsernameAvailability.d.ts.map +0 -1
  161. package/lib/typescript/module/ui/components/authChooser/useUsernameAvailability.d.ts +0 -16
  162. package/lib/typescript/module/ui/components/authChooser/useUsernameAvailability.d.ts.map +0 -1
  163. package/src/ui/components/authChooser/useUsernameAvailability.ts +0 -60
@@ -14,6 +14,7 @@ import { createMemoryStorage, createPlatformStorage, type StorageInterface } fro
14
14
  import { isNetConnectivityOnline } from '../utils/netConnectivity';
15
15
  import { KeyboardBoundary } from './KeyboardBoundary';
16
16
  import { ProductAnalyticsObserver } from '../analytics/productAnalytics';
17
+ import { LanguageBridge } from './LanguageBridge';
17
18
 
18
19
  const bootStyles = StyleSheet.create({
19
20
  providerRoot: {
@@ -58,6 +59,20 @@ const isWeb = Platform.OS === 'web';
58
59
  * return <Text>Welcome, {user?.username}!</Text>;
59
60
  * }
60
61
  * ```
62
+ *
63
+ * An app with its own translated UI passes a language config to follow the account's
64
+ * (or, signed out, the device/guest) resolved locale automatically — see ADR
65
+ * 0022 (`docs/adr/0022-app-i18n-follows-oxy-language.md`):
66
+ * ```tsx
67
+ * <OxyProvider
68
+ * baseURL="https://api.oxy.so"
69
+ * language={{
70
+ * supportedLocales: SUPPORTED_LANGUAGES, // this app's own catalog
71
+ * fallbackLocale: DEFAULT_LANGUAGE,
72
+ * onChange: (locale) => i18n.changeLanguage(locale),
73
+ * }}
74
+ * >
75
+ * ```
61
76
  */
62
77
  const OxyProvider: FC<OxyProviderProps> = ({
63
78
  oxyServices,
@@ -76,6 +91,7 @@ const OxyProvider: FC<OxyProviderProps> = ({
76
91
  requireAuth = 'off',
77
92
  backgroundSession = false,
78
93
  deviceCredentialStorage = 'persistent',
94
+ language,
79
95
  }) => {
80
96
 
81
97
  // Storage + persistence wiring.
@@ -229,6 +245,7 @@ const OxyProvider: FC<OxyProviderProps> = ({
229
245
  onAuthStateChange={onAuthStateChange as OxyRuntimeProviderProps['onAuthStateChange']}
230
246
  >
231
247
  {productAnalytics ? <ProductAnalyticsObserver analytics={productAnalytics} /> : null}
248
+ {language ? <LanguageBridge {...language} /> : null}
232
249
  <SurfaceProvider>
233
250
  {requireAuth === 'off' ? (
234
251
  children
@@ -0,0 +1,81 @@
1
+ import React from 'react';
2
+ import { render } from '@testing-library/react';
3
+ import { LanguageBridge } from '../LanguageBridge';
4
+
5
+ /**
6
+ * `OxyProvider`'s language config prop, realized: the one place Oxy's
7
+ * resolved language reaches a host app's own i18n library. `useOxy` is mocked here —
8
+ * it is the SDK's own well-tested context — so these tests exercise exactly
9
+ * what this component adds: coercion to the caller's catalog, and calling
10
+ * back only when the RESOLVED locale actually changes.
11
+ */
12
+
13
+ let mockCurrentLanguage = 'en-US';
14
+ jest.mock('../../context/OxyContext', () => ({
15
+ useOxy: () => ({ currentLanguage: mockCurrentLanguage }),
16
+ }));
17
+
18
+ const HOST_CATALOG = ['en-US', 'es-ES', 'fr-FR'] as const;
19
+
20
+ beforeEach(() => {
21
+ mockCurrentLanguage = 'en-US';
22
+ });
23
+
24
+ it('resolves and reports the account language on mount', () => {
25
+ mockCurrentLanguage = 'es-ES';
26
+ const onChange = jest.fn();
27
+ render(<LanguageBridge supportedLocales={HOST_CATALOG} fallbackLocale="en-US" onChange={onChange} />);
28
+
29
+ expect(onChange).toHaveBeenCalledWith('es-ES');
30
+ });
31
+
32
+ it('coerces a locale the host never shipped a catalog for, by base language', () => {
33
+ mockCurrentLanguage = 'es-MX';
34
+ const onChange = jest.fn();
35
+ render(<LanguageBridge supportedLocales={HOST_CATALOG} fallbackLocale="en-US" onChange={onChange} />);
36
+
37
+ expect(onChange).toHaveBeenCalledWith('es-ES');
38
+ });
39
+
40
+ it('falls back to the caller default when nothing matches, even by base', () => {
41
+ mockCurrentLanguage = 'ja-JP';
42
+ const onChange = jest.fn();
43
+ render(<LanguageBridge supportedLocales={HOST_CATALOG} fallbackLocale="en-US" onChange={onChange} />);
44
+
45
+ expect(onChange).toHaveBeenCalledWith('en-US');
46
+ });
47
+
48
+ it('calls back again only when the RESOLVED locale changes', () => {
49
+ const onChange = jest.fn();
50
+ const { rerender } = render(
51
+ <LanguageBridge supportedLocales={HOST_CATALOG} fallbackLocale="en-US" onChange={onChange} />,
52
+ );
53
+ expect(onChange).toHaveBeenCalledTimes(1);
54
+
55
+ // Same resolved locale, brand-new callback identity (the common case for an
56
+ // inline arrow function) — must not re-fire.
57
+ rerender(<LanguageBridge supportedLocales={HOST_CATALOG} fallbackLocale="en-US" onChange={() => {}} />);
58
+ expect(onChange).toHaveBeenCalledTimes(1);
59
+
60
+ mockCurrentLanguage = 'fr-FR';
61
+ rerender(<LanguageBridge supportedLocales={HOST_CATALOG} fallbackLocale="en-US" onChange={onChange} />);
62
+ expect(onChange).toHaveBeenCalledTimes(2);
63
+ expect(onChange).toHaveBeenLastCalledWith('fr-FR');
64
+ });
65
+
66
+ it('reports a rejected onChange to onError instead of throwing', async () => {
67
+ const failure = new Error('catalog failed to load');
68
+ const onChange = jest.fn().mockRejectedValue(failure);
69
+ const onError = jest.fn();
70
+ render(
71
+ <LanguageBridge
72
+ supportedLocales={HOST_CATALOG}
73
+ fallbackLocale="en-US"
74
+ onChange={onChange}
75
+ onError={onError}
76
+ />,
77
+ );
78
+
79
+ await new Promise((resolve) => setTimeout(resolve, 0));
80
+ expect(onError).toHaveBeenCalledWith(failure, 'en-US');
81
+ });
@@ -24,6 +24,7 @@ function renderScreen(props: Partial<OxyConsentScreenProps> = {}) {
24
24
  application={props.application ?? baseApp}
25
25
  scopes={props.scopes ?? ['openid', 'profile']}
26
26
  user={props.user}
27
+ resource={props.resource}
27
28
  onAllow={props.onAllow ?? onAllow}
28
29
  onDeny={props.onDeny ?? onDeny}
29
30
  busy={props.busy}
@@ -138,6 +139,37 @@ describe('OxyConsentScreen', () => {
138
139
  expect(getByTestId('consent-account-name').textContent).toBe('Alice A');
139
140
  });
140
141
 
142
+ it('renders the exact MCP app, protected resource, account id and catalog-derived write actions', () => {
143
+ const { getByTestId } = renderScreen({
144
+ scopes: ['email.read', 'email.send'],
145
+ user: {
146
+ accountId: 'account-mail-1',
147
+ displayName: 'Oxy Mail',
148
+ handle: 'oxy-mail-account',
149
+ },
150
+ resource: {
151
+ application: { name: 'Inbox', iconUrl: 'inbox-icon' },
152
+ uri: 'https://mcp.inbox.oxy.so',
153
+ writeActions: [{
154
+ name: 'sendEmail',
155
+ version: '1.0.0',
156
+ description: 'Send an email from the selected mailbox.',
157
+ requiredCapabilities: ['email.send'],
158
+ effect: 'external',
159
+ }],
160
+ },
161
+ });
162
+
163
+ expect(getByTestId('consent-resource-app').textContent).toBe('Inbox');
164
+ expect(getByTestId('consent-resource-uri').textContent).toBe('https://mcp.inbox.oxy.so');
165
+ expect(getByTestId('consent-account-id').textContent).toContain('account-mail-1');
166
+ expect(getByTestId('consent-scope-email.send').textContent).toContain('email.send');
167
+ expect(getByTestId('consent-write-action-sendEmail').textContent).toContain(
168
+ 'Send an email from the selected mailbox.',
169
+ );
170
+ expect(getByTestId('consent-write-action-sendEmail').textContent).toContain('External');
171
+ });
172
+
141
173
  it('falls back to the handle when the account has no display name (D5)', () => {
142
174
  const { getByTestId } = renderScreen({ user: { handle: 'bob' } });
143
175
  expect(getByTestId('consent-account-name').textContent).toBe('bob');
@@ -477,6 +477,13 @@ const AccountsMenuView: React.FC<AccountsMenuViewProps> = ({
477
477
  title={t('accountMenu.settings')}
478
478
  onPress={menu.onOpenSettings}
479
479
  />
480
+ {menu.onOpenIdentity ? (
481
+ <SettingsListItem
482
+ icon={<MenuIcon name="shield-account-outline" theme={theme} />}
483
+ title={t('accountMenu.identity')}
484
+ onPress={menu.onOpenIdentity}
485
+ />
486
+ ) : null}
480
487
  <SettingsListItem
481
488
  icon={<MenuIcon name="help-circle-outline" theme={theme} />}
482
489
  title={t('accountMenu.help')}
@@ -1,24 +1,24 @@
1
1
  /**
2
2
  * The sign-in ENTRY (`signin` / `add` views) — ONE primary action.
3
3
  *
4
- * Issue #691, Phase 5: the normal experience presents no menu of authentication
5
- * methods. The user sees "Continue with Oxy"; Oxy — not the user — then picks
6
- * how the request reaches their Commons identity, and the request surface shows
7
- * honest progress. Everything else (a QR from another device, a passkey on this
8
- * device, getting Commons, creating an account) lives behind "Having trouble?".
4
+ * Web: "Continue" opens the identity origin (`id.oxy.so`) in a popup, where the
5
+ * person signs in — or creates an account — with a passkey, and the account's
6
+ * self-custody identity is created or unsealed (one identity, two carriers). No
7
+ * philosophy up front: fingerprint, face or device PIN, and they are in. An
8
+ * account that lives on another device is one subordinate link away (Commons
9
+ * QR). Nothing auto-starts: a browser only opens a popup from a user gesture.
9
10
  *
10
- * Existing accounts still render ABOVE the CTA: "continue as one of these" is a
11
- * choice of WHO, not of HOW, so it is not one of the competing method buttons
12
- * the issue removes.
11
+ * Native: "Continue with Oxy" — the Commons flow (shared keychain, else the
12
+ * request Oxy routes to the person's Commons identity).
13
13
  *
14
- * On web this view is transient — the container auto-starts the flow the instant
15
- * the view is reached, because the request surface (QR, or "Check Commons on
16
- * your phone") IS the primary route there, never something behind a tap.
14
+ * Existing accounts still render ABOVE the CTA: "continue as one of these" is a
15
+ * choice of WHO, not of HOW.
17
16
  */
18
17
 
19
18
  import type React from 'react';
20
19
  import { View } from 'react-native';
21
20
  import { Button } from '@oxy.so/bloom/button';
21
+ import { Text } from '@oxy.so/bloom/typography';
22
22
  import {
23
23
  showsPrincipalHeaders,
24
24
  type AccountDialogSnapshot,
@@ -42,7 +42,7 @@ interface SignInEntryViewProps {
42
42
  theme: Theme;
43
43
  t: Translate;
44
44
  handlers: OxyAuthChooserHandlers;
45
- /** The ONE primary action. Starts the flow; Oxy chooses the route from there. */
45
+ /** Native's primary action. Starts the Commons flow; Oxy chooses the route from there. */
46
46
  onContinueWithOxy: () => void;
47
47
  alternatives: SignInAlternatives;
48
48
  }
@@ -56,37 +56,20 @@ const SignInEntryView: React.FC<SignInEntryViewProps> = ({
56
56
  onContinueWithOxy,
57
57
  alternatives,
58
58
  }) => {
59
- // The entry has no chosen route yet, so nothing here can have failed: the
60
- // alternatives stay behind the disclosure unconditionally.
61
- const troubleActions: OxySignInSurfaceAction[] = [
62
- {
63
- key: 'scan-qr-link',
64
- label: t('accountSwitcher.scanQr'),
65
- onPress: alternatives.onShowQr,
66
- },
67
- ...(alternatives.passkeyAvailable
68
- ? [
69
- {
70
- key: 'passkey-signin-link',
71
- label: alternatives.passkeyPending
72
- ? t('accountSwitcher.passkeySigningIn')
73
- : t('accountSwitcher.useIdentityOnDevice'),
74
- onPress: alternatives.onSignInWithPasskey,
75
- disabled: alternatives.passkeyPending,
76
- },
77
- ]
78
- : []),
79
- {
80
- key: 'get-commons-link',
81
- label: t('accountSwitcher.getCommons'),
82
- onPress: alternatives.onGetCommons,
83
- },
84
- ];
59
+ const web = alternatives.passkeyAvailable;
60
+
61
+ // Web keeps Commons one link away; native keeps its QR/Commons alternatives
62
+ // behind the disclosure, as before.
63
+ const troubleActions: OxySignInSurfaceAction[] = web
64
+ ? [{ key: 'get-commons-link', label: t('accountSwitcher.getCommons'), onPress: alternatives.onGetCommons }]
65
+ : [
66
+ { key: 'scan-qr-link', label: t('accountSwitcher.scanQr'), onPress: alternatives.onShowQr },
67
+ { key: 'get-commons-link', label: t('accountSwitcher.getCommons'), onPress: alternatives.onGetCommons },
68
+ ];
85
69
 
86
70
  // Whose route this is only needs naming when the list holds more than one
87
71
  // person, or somebody with more than one account — the same rule the account
88
- // menu's group headers follow, so the two surfaces agree on when the actor is
89
- // worth stating.
72
+ // menu's group headers follow.
90
73
  const namesTheOperator = showsPrincipalHeaders(principals);
91
74
  const rows = principals.flatMap((principal) =>
92
75
  principal.contexts.map((context) => ({
@@ -117,18 +100,23 @@ const SignInEntryView: React.FC<SignInEntryViewProps> = ({
117
100
  </View>
118
101
  ) : null}
119
102
 
103
+ {web ? (
104
+ <Text style={[styles.mutedText, { color: theme.colors.textSecondary }]}>
105
+ {t('accountSwitcher.passkeyHint')}
106
+ </Text>
107
+ ) : null}
108
+
120
109
  <Button
121
110
  variant="primary"
122
- onPress={onContinueWithOxy}
111
+ onPress={web ? alternatives.onSignInWithPasskey : onContinueWithOxy}
123
112
  style={styles.primaryButton}
124
113
  testID="continue-with-oxy"
125
114
  >
126
- {t('accountSwitcher.continueWithOxy')}
115
+ {web ? t('accountSwitcher.continueWithPasskey') : t('accountSwitcher.continueWithOxy')}
127
116
  </Button>
128
117
 
129
118
  {/* Account CREATION is not an authentication method — it is the way in for
130
- someone who has no Oxy ID yet — so it keeps its own subordinate link
131
- rather than hiding behind a troubleshooting affordance. */}
119
+ someone who has no account yet — so it keeps its own subordinate link. */}
132
120
  <SubtleLink
133
121
  label={t('signin.createAccountLink')}
134
122
  theme={theme}
@@ -136,6 +124,15 @@ const SignInEntryView: React.FC<SignInEntryViewProps> = ({
136
124
  testID="create-account-link"
137
125
  />
138
126
 
127
+ {web ? (
128
+ <SubtleLink
129
+ label={t('accountSwitcher.otherDeviceCommons')}
130
+ theme={theme}
131
+ onPress={alternatives.onShowQr}
132
+ testID="scan-qr-link"
133
+ />
134
+ ) : null}
135
+
139
136
  <TroubleDisclosure actions={troubleActions} revealed={false} theme={theme} t={t} />
140
137
  </View>
141
138
  );
@@ -58,11 +58,8 @@ const SignInRequestView: React.FC<SignInRequestViewProps> = ({
58
58
  ? [
59
59
  {
60
60
  key: 'passkey-signin-link',
61
- label: alternatives.passkeyPending
62
- ? t('accountSwitcher.passkeySigningIn')
63
- : t('accountSwitcher.useIdentityOnDevice'),
61
+ label: t('accountSwitcher.useIdentityOnDevice'),
64
62
  onPress: alternatives.onSignInWithPasskey,
65
- disabled: alternatives.passkeyPending,
66
63
  },
67
64
  ]
68
65
  : [];
@@ -1,37 +1,31 @@
1
1
  /**
2
2
  * Account creation (`signup` view).
3
3
  *
4
- * Commons is ALWAYS the priority path (owner mandate) and leads everywhere it
5
- * appears: native shows only "Create your identity in Commons" (or "Get Commons"
6
- * first, when it is not installed); web on a first-party Oxy origin leads with
7
- * the SAME "Get Commons" CTA, with inline passkey creation offered UNDERNEATH as
8
- * the de-emphasized "don't want to install anything" alternative — never
9
- * co-equal, never a competing button. Anywhere else (a non-Oxy web origin,
10
- * before the b2 hub-relay ships) shows an honest "not available here yet"
11
- * handoff instead of a broken button.
4
+ * Web: the account AND its self-custody identity are created at the identity
5
+ * origin (`id.oxy.so`), in the same window a passkey sign-in uses — never on
6
+ * this page, whatever its origin (one identity, two carriers). The sign-in
7
+ * entry's "create an account" link opens that window directly; this view is
8
+ * what `openAccountDialog('signup')` lands on, and offers the same one action.
9
+ *
10
+ * Native: Commons owns identity creation. Deep-link straight in when Commons is
11
+ * installed, else lead with the "Get Commons" acquisition CTA.
12
12
  */
13
13
 
14
14
  import type React from 'react';
15
- import { useState } from 'react';
16
- import { TextInput, View } from 'react-native';
15
+ import { View } from 'react-native';
17
16
  import { Button } from '@oxy.so/bloom/button';
18
17
  import { Text } from '@oxy.so/bloom/typography';
19
- import type { AccountDialogSnapshot, OxyServices } from '@oxy.so/core';
20
- import { isWebBrowser } from '../../utils/isWebBrowser';
21
- import { Dividerish, SubtleLink } from './primitives';
18
+ import type { AccountDialogSnapshot } from '@oxy.so/core';
19
+ import { SubtleLink } from './primitives';
22
20
  import { authChooserStyles as styles } from './styles';
23
21
  import type { PasskeyMode, Theme, Translate } from './types';
24
- import { useUsernameAvailability, type UsernameStatus } from './useUsernameAvailability';
25
22
 
26
23
  interface SignUpViewProps {
27
24
  snapshot: AccountDialogSnapshot;
28
25
  theme: Theme;
29
26
  t: Translate;
30
- oxyServices: OxyServices;
31
27
  passkeyMode: PasskeyMode;
32
- onCreateWithPasskey: (username: string) => void;
33
- createPending: boolean;
34
- /** Open the auth.oxy.so hub popup (b2) — the 'hub' mode's only path. */
28
+ /** Open the identity-origin window — web's only creation path. */
35
29
  onOpenHub: () => void;
36
30
  /** Deep-link into Commons' own identity-creation screen (installed only). */
37
31
  onCreateIdentityInCommons: () => void;
@@ -44,22 +38,13 @@ const SignUpView: React.FC<SignUpViewProps> = ({
44
38
  snapshot,
45
39
  theme,
46
40
  t,
47
- oxyServices,
48
41
  passkeyMode,
49
- onCreateWithPasskey,
50
- createPending,
51
42
  onOpenHub,
52
43
  onCreateIdentityInCommons,
53
44
  onGetCommons,
54
45
  onBackToSignIn,
55
46
  }) => {
56
- const [username, setUsername] = useState('');
57
- const { status: usernameStatus, check: checkUsername } = useUsernameAvailability(oxyServices, t);
58
- const canSubmit = username.trim().length >= 3 && usernameStatus === 'available' && !createPending;
59
-
60
- // Native — Commons owns identity creation. Deep-link straight in when it is
61
- // installed, else lead with the same "Get Commons" acquisition CTA.
62
- if (!isWebBrowser()) {
47
+ if (passkeyMode === 'none') {
63
48
  const commonsInstalled = snapshot.commonsAvailability === 'available';
64
49
  return (
65
50
  <View style={styles.centeredBlock}>
@@ -75,100 +60,17 @@ const SignUpView: React.FC<SignUpViewProps> = ({
75
60
  );
76
61
  }
77
62
 
78
- // Web, first-party Oxy origin. Commons leads with a "Get Commons" CTA; passkey
79
- // creation is the de-emphasized alternative underneath for someone who doesn't
80
- // want to install anything — a secondary button introduced by its own "No
81
- // Commons?" framing, the same subordination the sign-in surfaces use.
82
- if (passkeyMode === 'direct') {
83
- return (
84
- <View style={styles.signInBlock}>
85
- <Text style={[styles.qrHeadline, { color: theme.colors.text }]}>
86
- {t('signup.commonsHeadline')}
87
- </Text>
88
- <Text style={[styles.mutedText, { color: theme.colors.textSecondary }]}>
89
- {t('signup.commonsExplainer')}
90
- </Text>
91
- <Button variant="primary" onPress={onGetCommons} style={styles.primaryButton}>
92
- {t('accountSwitcher.getCommons')}
93
- </Button>
94
-
95
- <Dividerish theme={theme} label={t('signin.or')} />
96
-
97
- <Text style={[styles.mutedText, { color: theme.colors.textSecondary }]}>
98
- {t('signup.passkeyAlternative')}
99
- </Text>
100
- <TextInput
101
- style={[styles.usernameInput, { borderColor: theme.colors.border, color: theme.colors.text }]}
102
- placeholder={t('signup.usernamePlaceholder')}
103
- placeholderTextColor={theme.colors.textSecondary}
104
- value={username}
105
- autoCapitalize="none"
106
- autoCorrect={false}
107
- autoComplete="username"
108
- onChangeText={(value) => {
109
- setUsername(value);
110
- checkUsername(value.trim());
111
- }}
112
- testID="signup-username-input"
113
- />
114
- <UsernameStatusText status={usernameStatus} theme={theme} t={t} />
115
- <Button
116
- variant="secondary"
117
- onPress={() => onCreateWithPasskey(username.trim())}
118
- disabled={!canSubmit}
119
- style={styles.secondaryButton}
120
- testID="signup-create-button"
121
- >
122
- {createPending ? t('signup.creating') : t('signup.createAccount')}
123
- </Button>
124
- <SubtleLink label={t('signup.backToSignInLink')} theme={theme} onPress={onBackToSignIn} />
125
- </View>
126
- );
127
- }
128
-
129
- // Web, non-Oxy origin (b2): a passkey ceremony cannot run locally here — the
130
- // same WebAuthn RP-ID boundary the sign-in surfaces hit — so this opens the
131
- // auth.oxy.so hub popup. Completing EITHER a sign-in OR a fresh sign-up there
132
- // relays the resulting session back through the SAME poll/socket engine.
133
63
  return (
134
64
  <View style={styles.centeredBlock}>
135
65
  <Text style={[styles.mutedText, { color: theme.colors.textSecondary }]}>
136
- {t('signup.hubExplainer')}
66
+ {t('accountSwitcher.passkeyHint')}
137
67
  </Text>
138
- <Button variant="primary" onPress={onOpenHub} style={styles.primaryButton}>
139
- {t('signup.continueInWindow')}
68
+ <Button variant="primary" onPress={onOpenHub} style={styles.primaryButton} testID="signup-open-identity">
69
+ {t('signup.createAccount')}
140
70
  </Button>
141
71
  <SubtleLink label={t('signup.backToSignInLink')} theme={theme} onPress={onBackToSignIn} />
142
72
  </View>
143
73
  );
144
74
  };
145
75
 
146
- /** The inline availability verdict. Network FAILURES are toasted, never shown here. */
147
- const UsernameStatusText: React.FC<{ status: UsernameStatus; theme: Theme; t: Translate }> = ({
148
- status,
149
- theme,
150
- t,
151
- }) => {
152
- if (status === 'idle') return null;
153
- if (status === 'checking') {
154
- return (
155
- <Text style={[styles.mutedText, { color: theme.colors.textSecondary }]}>
156
- {t('signup.usernameChecking')}
157
- </Text>
158
- );
159
- }
160
- if (status === 'available') {
161
- return (
162
- <Text style={[styles.mutedText, { color: theme.colors.success }]}>
163
- {t('signup.usernameAvailable')}
164
- </Text>
165
- );
166
- }
167
- return (
168
- <Text style={[styles.mutedText, { color: theme.colors.error }]}>
169
- {t('signup.usernameTaken')}
170
- </Text>
171
- );
172
- };
173
-
174
76
  export default SignUpView;
@@ -0,0 +1,39 @@
1
+ /**
2
+ * The localized toast for a failed sign-in attempt.
3
+ *
4
+ * One responsibility: turn the controller's machine-readable
5
+ * `signIn.failure` into copy. The controller ships no user-facing prose — its
6
+ * raw `signIn.error` is English or a server string, kept for diagnostics only.
7
+ */
8
+
9
+ import type { SignInFailureReason } from '@oxy.so/core';
10
+ import type { Translate } from './types';
11
+
12
+ /**
13
+ * The toast for `failure`, or `null` when there is nothing to report: a
14
+ * `'cancelled'` attempt is the user's own choice (they closed the window), and
15
+ * the surface already offers "Try again" without scolding them for it.
16
+ */
17
+ export function signInFailureMessage(
18
+ failure: SignInFailureReason | null,
19
+ t: Translate,
20
+ ): string | null {
21
+ switch (failure) {
22
+ case 'cancelled':
23
+ return null;
24
+ case 'denied':
25
+ return t('accountSwitcher.signInFailures.denied');
26
+ case 'expired':
27
+ return t('accountSwitcher.signInFailures.expired');
28
+ case 'network':
29
+ return t('accountSwitcher.signInFailures.network');
30
+ case 'not-configured':
31
+ return t('accountSwitcher.signInFailures.notConfigured');
32
+ case 'unsupported-flow':
33
+ return t('accountSwitcher.signInFailures.unsupportedFlow');
34
+ case 'claim-failed':
35
+ return t('accountSwitcher.signInFailures.claimFailed');
36
+ default:
37
+ return t('accountSwitcher.signInFailures.generic');
38
+ }
39
+ }
@@ -95,6 +95,11 @@ export interface AccountsMenuActions {
95
95
  onPrivacy: () => void;
96
96
  onTerms: () => void;
97
97
  onSignOut: () => void;
98
+ /**
99
+ * Open the person's identity at the identity origin (recovery phrase,
100
+ * recovery, account deletion). Web only — `undefined` hides the row.
101
+ */
102
+ onOpenIdentity?: () => void;
98
103
  customItems: readonly {
99
104
  key: string;
100
105
  label: string;
@@ -104,15 +109,13 @@ export interface AccountsMenuActions {
104
109
  }
105
110
 
106
111
  /**
107
- * Where a WebAuthn ceremony can run from the current origin.
112
+ * Where a passkey sign-in or sign-up can run from the current surface.
108
113
  *
109
- * A credential minted for `oxy.so` can only be ASSERTED there (or a loopback
110
- * dev host) — a hard, browser-enforced RP-ID boundary, not feature detection.
111
- * `'direct'` = a first-party Oxy web origin, the ceremony runs here.
112
- * `'hub'` = any other web origin; it runs in the auth.oxy.so popup instead.
113
- * `'none'` = native, where Commons owns identity.
114
+ * `'hub'` = web: it runs at the identity origin (`id.oxy.so`) in a popup, on
115
+ * every web origin, first-party or not.
116
+ * `'none'` = native, where Commons owns identity.
114
117
  */
115
- export type PasskeyMode = 'direct' | 'hub' | 'none';
118
+ export type PasskeyMode = 'hub' | 'none';
116
119
 
117
120
  /**
118
121
  * Everything that is NOT the sign-in surface's one primary action.
@@ -125,17 +128,12 @@ export type PasskeyMode = 'direct' | 'hub' | 'none';
125
128
  export interface SignInAlternatives {
126
129
  /** Whether a WebAuthn ceremony is reachable from this origin at all. */
127
130
  passkeyAvailable: boolean;
128
- /**
129
- * `true` while a DIRECT ceremony is in flight. The hub-popup variant reports
130
- * its progress through `snapshot.signIn` instead, so it never sets this.
131
- */
132
- passkeyPending: boolean;
133
131
  onSignInWithPasskey: () => void;
134
132
  /** Fall back to the cross-device QR handoff (restarts the request). */
135
133
  onShowQr: () => void;
136
134
  /** Open the Commons store listing / landing page for this platform. */
137
135
  onGetCommons: () => void;
138
- /** Enter the account-creation view. */
136
+ /** Create an account: the identity-origin window on web, the Commons flow on native. */
139
137
  onCreateAccount: () => void;
140
138
  }
141
139
 
@@ -104,7 +104,7 @@ import {
104
104
  import { queryKeys } from '../hooks/queries/queryKeys';
105
105
  import { useOxyAccountGraph } from './useOxyAccountGraph';
106
106
 
107
- export type { OxyContextState, OxyRuntimeProviderProps } from './oxyContextTypes';
107
+ export type { LogoutResult, OxyContextState, OxyRuntimeProviderProps } from './oxyContextTypes';
108
108
 
109
109
  const OxyRuntimeContext = createContext<OxyContextState | null>(null);
110
110