@oxyhq/services 22.0.0 → 22.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 (78) hide show
  1. package/lib/commonjs/index.js +12 -0
  2. package/lib/commonjs/index.js.map +1 -1
  3. package/lib/commonjs/ui/components/OxyAccountDialog.js +27 -529
  4. package/lib/commonjs/ui/components/OxyAccountDialog.js.map +1 -1
  5. package/lib/commonjs/ui/components/OxyAuthChooser.js +902 -0
  6. package/lib/commonjs/ui/components/OxyAuthChooser.js.map +1 -0
  7. package/lib/commonjs/ui/components/oxyAuthChooserStyles.js +158 -0
  8. package/lib/commonjs/ui/components/oxyAuthChooserStyles.js.map +1 -0
  9. package/lib/commonjs/ui/components/passkeyHubPopup.js +33 -0
  10. package/lib/commonjs/ui/components/passkeyHubPopup.js.map +1 -0
  11. package/lib/commonjs/ui/context/OxyContext.js +35 -4
  12. package/lib/commonjs/ui/context/OxyContext.js.map +1 -1
  13. package/lib/commonjs/ui/context/useOxyAccountGraph.js +6 -2
  14. package/lib/commonjs/ui/context/useOxyAccountGraph.js.map +1 -1
  15. package/lib/commonjs/ui/hooks/useSwitchableAccounts.js +2 -1
  16. package/lib/commonjs/ui/hooks/useSwitchableAccounts.js.map +1 -1
  17. package/lib/commonjs/ui/utils/commonsStoreLinks.js +42 -0
  18. package/lib/commonjs/ui/utils/commonsStoreLinks.js.map +1 -0
  19. package/lib/module/index.js +5 -0
  20. package/lib/module/index.js.map +1 -1
  21. package/lib/module/ui/components/OxyAccountDialog.js +29 -533
  22. package/lib/module/ui/components/OxyAccountDialog.js.map +1 -1
  23. package/lib/module/ui/components/OxyAuthChooser.js +899 -0
  24. package/lib/module/ui/components/OxyAuthChooser.js.map +1 -0
  25. package/lib/module/ui/components/oxyAuthChooserStyles.js +155 -0
  26. package/lib/module/ui/components/oxyAuthChooserStyles.js.map +1 -0
  27. package/lib/module/ui/components/passkeyHubPopup.js +29 -0
  28. package/lib/module/ui/components/passkeyHubPopup.js.map +1 -0
  29. package/lib/module/ui/context/OxyContext.js +35 -4
  30. package/lib/module/ui/context/OxyContext.js.map +1 -1
  31. package/lib/module/ui/context/useOxyAccountGraph.js +6 -2
  32. package/lib/module/ui/context/useOxyAccountGraph.js.map +1 -1
  33. package/lib/module/ui/hooks/useSwitchableAccounts.js +2 -1
  34. package/lib/module/ui/hooks/useSwitchableAccounts.js.map +1 -1
  35. package/lib/module/ui/utils/commonsStoreLinks.js +37 -0
  36. package/lib/module/ui/utils/commonsStoreLinks.js.map +1 -0
  37. package/lib/typescript/commonjs/index.d.ts +2 -0
  38. package/lib/typescript/commonjs/index.d.ts.map +1 -1
  39. package/lib/typescript/commonjs/ui/components/OxyAccountDialog.d.ts +10 -20
  40. package/lib/typescript/commonjs/ui/components/OxyAccountDialog.d.ts.map +1 -1
  41. package/lib/typescript/commonjs/ui/components/OxyAuthChooser.d.ts +60 -0
  42. package/lib/typescript/commonjs/ui/components/OxyAuthChooser.d.ts.map +1 -0
  43. package/lib/typescript/commonjs/ui/components/oxyAuthChooserStyles.d.ts +148 -0
  44. package/lib/typescript/commonjs/ui/components/oxyAuthChooserStyles.d.ts.map +1 -0
  45. package/lib/typescript/commonjs/ui/components/passkeyHubPopup.d.ts +23 -0
  46. package/lib/typescript/commonjs/ui/components/passkeyHubPopup.d.ts.map +1 -0
  47. package/lib/typescript/commonjs/ui/context/OxyContext.d.ts.map +1 -1
  48. package/lib/typescript/commonjs/ui/context/useOxyAccountGraph.d.ts +1 -0
  49. package/lib/typescript/commonjs/ui/context/useOxyAccountGraph.d.ts.map +1 -1
  50. package/lib/typescript/commonjs/ui/hooks/useSwitchableAccounts.d.ts.map +1 -1
  51. package/lib/typescript/commonjs/ui/utils/commonsStoreLinks.d.ts +24 -0
  52. package/lib/typescript/commonjs/ui/utils/commonsStoreLinks.d.ts.map +1 -0
  53. package/lib/typescript/module/index.d.ts +2 -0
  54. package/lib/typescript/module/index.d.ts.map +1 -1
  55. package/lib/typescript/module/ui/components/OxyAccountDialog.d.ts +10 -20
  56. package/lib/typescript/module/ui/components/OxyAccountDialog.d.ts.map +1 -1
  57. package/lib/typescript/module/ui/components/OxyAuthChooser.d.ts +60 -0
  58. package/lib/typescript/module/ui/components/OxyAuthChooser.d.ts.map +1 -0
  59. package/lib/typescript/module/ui/components/oxyAuthChooserStyles.d.ts +148 -0
  60. package/lib/typescript/module/ui/components/oxyAuthChooserStyles.d.ts.map +1 -0
  61. package/lib/typescript/module/ui/components/passkeyHubPopup.d.ts +23 -0
  62. package/lib/typescript/module/ui/components/passkeyHubPopup.d.ts.map +1 -0
  63. package/lib/typescript/module/ui/context/OxyContext.d.ts.map +1 -1
  64. package/lib/typescript/module/ui/context/useOxyAccountGraph.d.ts +1 -0
  65. package/lib/typescript/module/ui/context/useOxyAccountGraph.d.ts.map +1 -1
  66. package/lib/typescript/module/ui/hooks/useSwitchableAccounts.d.ts.map +1 -1
  67. package/lib/typescript/module/ui/utils/commonsStoreLinks.d.ts +24 -0
  68. package/lib/typescript/module/ui/utils/commonsStoreLinks.d.ts.map +1 -0
  69. package/package.json +11 -11
  70. package/src/index.ts +7 -0
  71. package/src/ui/components/OxyAccountDialog.tsx +33 -557
  72. package/src/ui/components/OxyAuthChooser.tsx +907 -0
  73. package/src/ui/components/oxyAuthChooserStyles.ts +152 -0
  74. package/src/ui/components/passkeyHubPopup.ts +28 -0
  75. package/src/ui/context/OxyContext.tsx +37 -3
  76. package/src/ui/context/useOxyAccountGraph.ts +4 -2
  77. package/src/ui/hooks/useSwitchableAccounts.ts +1 -0
  78. package/src/ui/utils/commonsStoreLinks.ts +38 -0
@@ -0,0 +1,152 @@
1
+ import { StyleSheet } from 'react-native';
2
+
3
+ /** High-contrast QR plate background — kept in sync with `OxyAuthChooser`'s QR colors. */
4
+ const QR_PLATE_BG = '#FFFFFF';
5
+
6
+ /**
7
+ * Shared styles for `OxyAuthChooser`'s views. Extracted from `OxyAccountDialog`
8
+ * so both the Dialog-wrapped host and any bare host (e.g. a future
9
+ * auth.oxy.so hub page) render identically without duplicating a StyleSheet.
10
+ */
11
+ export const authChooserStyles = StyleSheet.create({
12
+ rows: {
13
+ width: '100%',
14
+ gap: 8,
15
+ },
16
+ accountRow: {
17
+ flexDirection: 'row',
18
+ alignItems: 'center',
19
+ gap: 12,
20
+ borderWidth: StyleSheet.hairlineWidth,
21
+ borderRadius: 16,
22
+ paddingVertical: 10,
23
+ paddingHorizontal: 12,
24
+ },
25
+ rowDisabled: {
26
+ opacity: 0.6,
27
+ },
28
+ avatarRing: {
29
+ borderRadius: 9999,
30
+ borderWidth: 2,
31
+ padding: 1,
32
+ },
33
+ rowMeta: {
34
+ flex: 1,
35
+ minWidth: 0,
36
+ },
37
+ rowName: {
38
+ fontSize: 15,
39
+ fontWeight: '600',
40
+ },
41
+ rowHandle: {
42
+ fontSize: 12.5,
43
+ marginTop: 1,
44
+ },
45
+ addRow: {
46
+ flexDirection: 'row',
47
+ alignItems: 'center',
48
+ gap: 12,
49
+ borderWidth: StyleSheet.hairlineWidth,
50
+ borderStyle: 'dashed',
51
+ borderRadius: 16,
52
+ paddingVertical: 10,
53
+ paddingHorizontal: 12,
54
+ marginTop: 8,
55
+ },
56
+ addBadge: {
57
+ width: 40,
58
+ height: 40,
59
+ borderRadius: 9999,
60
+ borderWidth: StyleSheet.hairlineWidth,
61
+ alignItems: 'center',
62
+ justifyContent: 'center',
63
+ },
64
+ footerLinks: {
65
+ alignItems: 'center',
66
+ marginTop: 16,
67
+ },
68
+ footerLink: {
69
+ alignSelf: 'center',
70
+ paddingVertical: 10,
71
+ marginTop: 12,
72
+ },
73
+ linkText: {
74
+ fontSize: 14,
75
+ fontWeight: '600',
76
+ },
77
+ signInBlock: {
78
+ width: '100%',
79
+ },
80
+ primaryButton: {
81
+ width: '100%',
82
+ borderRadius: 14,
83
+ marginTop: 8,
84
+ },
85
+ secondaryButton: {
86
+ width: '100%',
87
+ borderRadius: 14,
88
+ marginTop: 10,
89
+ },
90
+ usernameInput: {
91
+ width: '100%',
92
+ borderWidth: StyleSheet.hairlineWidth,
93
+ borderRadius: 14,
94
+ paddingVertical: 12,
95
+ paddingHorizontal: 14,
96
+ fontSize: 16,
97
+ marginTop: 8,
98
+ },
99
+ dividerRow: {
100
+ flexDirection: 'row',
101
+ alignItems: 'center',
102
+ width: '100%',
103
+ marginVertical: 14,
104
+ },
105
+ dividerLine: {
106
+ flex: 1,
107
+ height: StyleSheet.hairlineWidth,
108
+ },
109
+ dividerText: {
110
+ marginHorizontal: 12,
111
+ fontSize: 13,
112
+ },
113
+ centeredBlock: {
114
+ alignItems: 'center',
115
+ justifyContent: 'center',
116
+ gap: 12,
117
+ paddingVertical: 24,
118
+ },
119
+ mutedText: {
120
+ fontSize: 14,
121
+ textAlign: 'center',
122
+ },
123
+ qrHeadline: {
124
+ fontSize: 16,
125
+ fontWeight: '700',
126
+ textAlign: 'center',
127
+ },
128
+ errorText: {
129
+ fontSize: 14,
130
+ textAlign: 'center',
131
+ },
132
+ errorBanner: {
133
+ flexDirection: 'row',
134
+ alignItems: 'center',
135
+ gap: 8,
136
+ width: '100%',
137
+ borderRadius: 12,
138
+ paddingVertical: 10,
139
+ paddingHorizontal: 12,
140
+ marginBottom: 12,
141
+ },
142
+ errorBannerText: {
143
+ flex: 1,
144
+ fontSize: 13.5,
145
+ lineHeight: 18,
146
+ },
147
+ qrPlate: {
148
+ padding: 16,
149
+ borderRadius: 16,
150
+ backgroundColor: QR_PLATE_BG,
151
+ },
152
+ });
@@ -0,0 +1,28 @@
1
+ /**
2
+ * Popup opener for the cross-origin passkey hub (b2).
3
+ *
4
+ * On a non-Oxy web origin (e.g. mention.earth) a WebAuthn ceremony can't run
5
+ * locally — the credential's RP ID is bound to `oxy.so`. Instead,
6
+ * `AccountDialogController.startPasskeyHubSignIn` opens a popup at the
7
+ * auth.oxy.so passkey hub, where the SAME ceremony IS a first-party operation.
8
+ *
9
+ * Kept in its own module (mirroring `oauthNavigation.ts`'s
10
+ * `redirectToAuthorize`) so `window.open` sits behind a plain function the
11
+ * controller wiring in `OxyContext` can inject without touching `window`
12
+ * itself, and so a test can substitute a fake opener.
13
+ */
14
+
15
+ import type { PopupWindowHandle } from '@oxyhq/core';
16
+
17
+ /**
18
+ * Open a new, EMPTY popup window. Must be called SYNCHRONOUSLY from within a
19
+ * user-gesture handler (before any `await`) — opening it after an async call
20
+ * loses the gesture attribution and the browser silently blocks it. Returns
21
+ * `null` when blocked, or when there is no DOM `window` (native/SSR — the
22
+ * hub-popup flow does not apply there).
23
+ */
24
+ export function openPasskeyHubPopup(): PopupWindowHandle | null {
25
+ const win = (globalThis as { window?: Window }).window;
26
+ if (!win) return null;
27
+ return win.open('', '_blank', 'width=420,height=640');
28
+ }
@@ -32,6 +32,7 @@ import {
32
32
  notifyAccountDialogVisibility,
33
33
  } from '../navigation/accountDialogManager';
34
34
  import { redirectToAuthorize } from '../components/oauthNavigation';
35
+ import { openPasskeyHubPopup } from '../components/passkeyHubPopup';
35
36
  import { isWebBrowser } from '../utils/isWebBrowser';
36
37
  import { runProviderColdBoot } from '../boot/runProviderColdBoot';
37
38
  import { loadPersistedDeviceCredential } from '../utils/deviceCredential';
@@ -606,14 +607,41 @@ export const OxyProvider: React.FC<OxyContextProviderProps> = ({
606
607
  [commitSession],
607
608
  );
608
609
 
610
+ // Commit a minted graph-SWITCH session. Same funnel as `handleWebSession` but
611
+ // IN-PLACE: `hubSync: false` so switching accounts never triggers the
612
+ // cross-origin hub-sync full-page redirect. The switch already propagates
613
+ // across tabs/apps via the server's `session_state` socket broadcast.
614
+ const commitSwitchedSession = useCallback(
615
+ async (session: SessionLoginResponse): Promise<void> => {
616
+ if (!session?.user || !session?.sessionId || !session.accessToken) {
617
+ throw new Error('Session response did not include a usable session');
618
+ }
619
+ await commitSession(
620
+ {
621
+ sessionId: session.sessionId,
622
+ accessToken: session.accessToken,
623
+ deviceSecret: session.deviceSecret,
624
+ deviceId: session.deviceId,
625
+ expiresAt: session.expiresAt,
626
+ userId: session.user.id,
627
+ user: session.user,
628
+ },
629
+ { activate: true, hubSync: false },
630
+ );
631
+ },
632
+ [commitSession],
633
+ );
634
+
609
635
  // ── Unified account dialog ─────────────────────────────────────────────────
610
636
  // The single account-chooser + sign-in surface. Built ONCE per provider mount
611
637
  // and bound to the live `oxyServices` + `sessionClient` + this provider's
612
- // `handleWebSession` commit funnel. `handleWebSession` is threaded through a ref
613
- // so the controller keeps a STABLE `commitSession` (rebuilding the controller
614
- // on every commit-identity change would drop its subscription + state).
638
+ // commit funnels. Both funnels are threaded through refs so the controller
639
+ // keeps STABLE commit callbacks (rebuilding the controller on every
640
+ // commit-identity change would drop its subscription + state).
615
641
  const handleWebSessionRef = useRef(handleWebSession);
616
642
  handleWebSessionRef.current = handleWebSession;
643
+ const commitSwitchedSessionRef = useRef(commitSwitchedSession);
644
+ commitSwitchedSessionRef.current = commitSwitchedSession;
617
645
 
618
646
  const accountDialogControllerRef = useRef<AccountDialogController | null>(null);
619
647
  if (!accountDialogControllerRef.current) {
@@ -626,6 +654,7 @@ export const OxyProvider: React.FC<OxyContextProviderProps> = ({
626
654
  // instant `/auth-session` `auth_update` wake instead of a slow poll.
627
655
  socketFactory: io,
628
656
  commitSession: (session) => handleWebSessionRef.current(session),
657
+ commitSwitchedSession: (session) => commitSwitchedSessionRef.current(session),
629
658
  onSignedIn: () => setAccountDialogOpen(false),
630
659
  openUrl: (url) => {
631
660
  if (isWebBrowser()) {
@@ -640,6 +669,11 @@ export const OxyProvider: React.FC<OxyContextProviderProps> = ({
640
669
  // controller short-circuits, so `redirectToAuthorize` never runs for the
641
670
  // `oxycommons://` payload.
642
671
  canOpenApp: isWebBrowser() ? undefined : (url) => Linking.canOpenURL(url),
672
+ // Web-only: lets `startPasskeyHubSignIn` open the auth.oxy.so passkey hub
673
+ // popup (b2) for a non-Oxy origin. `undefined` on native — there is no
674
+ // popup concept there, and off-origin passkey sign-in isn't reachable
675
+ // (Commons owns the native flow).
676
+ openPopup: isWebBrowser() ? openPasskeyHubPopup : undefined,
643
677
  });
644
678
  }
645
679
  const accountDialogController = accountDialogControllerRef.current;
@@ -13,7 +13,7 @@ interface UseOxyAccountGraphParams {
13
13
  oxyServices: OxyServices;
14
14
  sessionClient: SessionClient;
15
15
  syncFromClient: () => Promise<void>;
16
- commitSession: (input: CommitInput, options: { activate: boolean }) => Promise<void>;
16
+ commitSession: (input: CommitInput, options: { activate: boolean; hubSync?: boolean }) => Promise<void>;
17
17
  queryClient: QueryClient;
18
18
  accountDialogControllerRef: RefObject<AccountDialogController | null>;
19
19
  clearSessionStateRef: RefObject<() => Promise<void>>;
@@ -89,7 +89,9 @@ export function useOxyAccountGraph({
89
89
  userId: result.user.id,
90
90
  user: result.user,
91
91
  },
92
- { activate: true },
92
+ // A switch is IN-PLACE: never run the cross-origin hub-sync full-page
93
+ // redirect. Cross-tab/app propagation rides the `session_state` socket.
94
+ { activate: true, hubSync: false },
93
95
  );
94
96
  await runPostAccountSwitchSideEffects();
95
97
  },
@@ -23,6 +23,7 @@ const EMPTY_SNAPSHOT: AccountDialogSnapshot = {
23
23
  error: null,
24
24
  switchingAccountId: null,
25
25
  signIn: { phase: 'idle', authorizeCode: null, qrPayload: null, expiresAt: null, error: null },
26
+ commonsAvailability: 'unknown',
26
27
  };
27
28
 
28
29
  /**
@@ -0,0 +1,38 @@
1
+ /**
2
+ * Where to send a user who doesn't have Commons installed yet.
3
+ *
4
+ * Commons is not published to either app store as of this writing (no submit
5
+ * profile in its `eas.json`, no store listing) — so these are ALWAYS
6
+ * env-overridable and NEVER hardcoded, and the landing-page fallback exists
7
+ * precisely so a "Get Commons" affordance is never a dead link in the
8
+ * meantime. Once Commons ships to TestFlight/Play, set the two store env vars
9
+ * and every consumer picks them up with no code change.
10
+ */
11
+
12
+ /** Real per-platform store URLs, or `undefined` where not yet configured. */
13
+ export interface CommonsStoreLinks {
14
+ ios: string | undefined;
15
+ android: string | undefined;
16
+ }
17
+
18
+ /** Per-platform Commons store URLs, from env — `undefined` when unset. */
19
+ export function getCommonsStoreLinks(): CommonsStoreLinks {
20
+ return {
21
+ ios: process.env.EXPO_PUBLIC_COMMONS_APP_STORE_URL || undefined,
22
+ android: process.env.EXPO_PUBLIC_COMMONS_PLAY_STORE_URL || undefined,
23
+ };
24
+ }
25
+
26
+ /** Placeholder landing page — the fallback while Commons has no store listing. */
27
+ const DEFAULT_COMMONS_LANDING_URL = 'https://oxy.so/commons';
28
+
29
+ /**
30
+ * Resolve the "Get Commons" href for the current platform. Never `undefined`
31
+ * — falls back to a landing page so the affordance is always a working link,
32
+ * store listing or not.
33
+ */
34
+ export function getCommonsAcquisitionUrl(platformOS: 'ios' | 'android' | string): string {
35
+ const links = getCommonsStoreLinks();
36
+ const storeUrl = platformOS === 'ios' ? links.ios : platformOS === 'android' ? links.android : undefined;
37
+ return storeUrl ?? process.env.EXPO_PUBLIC_COMMONS_LANDING_URL ?? DEFAULT_COMMONS_LANDING_URL;
38
+ }