@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.
- package/lib/commonjs/index.js +12 -0
- package/lib/commonjs/index.js.map +1 -1
- package/lib/commonjs/ui/components/OxyAccountDialog.js +27 -529
- package/lib/commonjs/ui/components/OxyAccountDialog.js.map +1 -1
- package/lib/commonjs/ui/components/OxyAuthChooser.js +902 -0
- package/lib/commonjs/ui/components/OxyAuthChooser.js.map +1 -0
- package/lib/commonjs/ui/components/oxyAuthChooserStyles.js +158 -0
- package/lib/commonjs/ui/components/oxyAuthChooserStyles.js.map +1 -0
- package/lib/commonjs/ui/components/passkeyHubPopup.js +33 -0
- package/lib/commonjs/ui/components/passkeyHubPopup.js.map +1 -0
- package/lib/commonjs/ui/context/OxyContext.js +35 -4
- package/lib/commonjs/ui/context/OxyContext.js.map +1 -1
- package/lib/commonjs/ui/context/useOxyAccountGraph.js +6 -2
- package/lib/commonjs/ui/context/useOxyAccountGraph.js.map +1 -1
- package/lib/commonjs/ui/hooks/useSwitchableAccounts.js +2 -1
- package/lib/commonjs/ui/hooks/useSwitchableAccounts.js.map +1 -1
- package/lib/commonjs/ui/utils/commonsStoreLinks.js +42 -0
- package/lib/commonjs/ui/utils/commonsStoreLinks.js.map +1 -0
- package/lib/module/index.js +5 -0
- package/lib/module/index.js.map +1 -1
- package/lib/module/ui/components/OxyAccountDialog.js +29 -533
- package/lib/module/ui/components/OxyAccountDialog.js.map +1 -1
- package/lib/module/ui/components/OxyAuthChooser.js +899 -0
- package/lib/module/ui/components/OxyAuthChooser.js.map +1 -0
- package/lib/module/ui/components/oxyAuthChooserStyles.js +155 -0
- package/lib/module/ui/components/oxyAuthChooserStyles.js.map +1 -0
- package/lib/module/ui/components/passkeyHubPopup.js +29 -0
- package/lib/module/ui/components/passkeyHubPopup.js.map +1 -0
- package/lib/module/ui/context/OxyContext.js +35 -4
- package/lib/module/ui/context/OxyContext.js.map +1 -1
- package/lib/module/ui/context/useOxyAccountGraph.js +6 -2
- package/lib/module/ui/context/useOxyAccountGraph.js.map +1 -1
- package/lib/module/ui/hooks/useSwitchableAccounts.js +2 -1
- package/lib/module/ui/hooks/useSwitchableAccounts.js.map +1 -1
- package/lib/module/ui/utils/commonsStoreLinks.js +37 -0
- package/lib/module/ui/utils/commonsStoreLinks.js.map +1 -0
- package/lib/typescript/commonjs/index.d.ts +2 -0
- package/lib/typescript/commonjs/index.d.ts.map +1 -1
- package/lib/typescript/commonjs/ui/components/OxyAccountDialog.d.ts +10 -20
- package/lib/typescript/commonjs/ui/components/OxyAccountDialog.d.ts.map +1 -1
- package/lib/typescript/commonjs/ui/components/OxyAuthChooser.d.ts +60 -0
- package/lib/typescript/commonjs/ui/components/OxyAuthChooser.d.ts.map +1 -0
- package/lib/typescript/commonjs/ui/components/oxyAuthChooserStyles.d.ts +148 -0
- package/lib/typescript/commonjs/ui/components/oxyAuthChooserStyles.d.ts.map +1 -0
- package/lib/typescript/commonjs/ui/components/passkeyHubPopup.d.ts +23 -0
- package/lib/typescript/commonjs/ui/components/passkeyHubPopup.d.ts.map +1 -0
- package/lib/typescript/commonjs/ui/context/OxyContext.d.ts.map +1 -1
- package/lib/typescript/commonjs/ui/context/useOxyAccountGraph.d.ts +1 -0
- package/lib/typescript/commonjs/ui/context/useOxyAccountGraph.d.ts.map +1 -1
- package/lib/typescript/commonjs/ui/hooks/useSwitchableAccounts.d.ts.map +1 -1
- package/lib/typescript/commonjs/ui/utils/commonsStoreLinks.d.ts +24 -0
- package/lib/typescript/commonjs/ui/utils/commonsStoreLinks.d.ts.map +1 -0
- package/lib/typescript/module/index.d.ts +2 -0
- package/lib/typescript/module/index.d.ts.map +1 -1
- package/lib/typescript/module/ui/components/OxyAccountDialog.d.ts +10 -20
- package/lib/typescript/module/ui/components/OxyAccountDialog.d.ts.map +1 -1
- package/lib/typescript/module/ui/components/OxyAuthChooser.d.ts +60 -0
- package/lib/typescript/module/ui/components/OxyAuthChooser.d.ts.map +1 -0
- package/lib/typescript/module/ui/components/oxyAuthChooserStyles.d.ts +148 -0
- package/lib/typescript/module/ui/components/oxyAuthChooserStyles.d.ts.map +1 -0
- package/lib/typescript/module/ui/components/passkeyHubPopup.d.ts +23 -0
- package/lib/typescript/module/ui/components/passkeyHubPopup.d.ts.map +1 -0
- package/lib/typescript/module/ui/context/OxyContext.d.ts.map +1 -1
- package/lib/typescript/module/ui/context/useOxyAccountGraph.d.ts +1 -0
- package/lib/typescript/module/ui/context/useOxyAccountGraph.d.ts.map +1 -1
- package/lib/typescript/module/ui/hooks/useSwitchableAccounts.d.ts.map +1 -1
- package/lib/typescript/module/ui/utils/commonsStoreLinks.d.ts +24 -0
- package/lib/typescript/module/ui/utils/commonsStoreLinks.d.ts.map +1 -0
- package/package.json +11 -11
- package/src/index.ts +7 -0
- package/src/ui/components/OxyAccountDialog.tsx +33 -557
- package/src/ui/components/OxyAuthChooser.tsx +907 -0
- package/src/ui/components/oxyAuthChooserStyles.ts +152 -0
- package/src/ui/components/passkeyHubPopup.ts +28 -0
- package/src/ui/context/OxyContext.tsx +37 -3
- package/src/ui/context/useOxyAccountGraph.ts +4 -2
- package/src/ui/hooks/useSwitchableAccounts.ts +1 -0
- 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
|
-
//
|
|
613
|
-
//
|
|
614
|
-
//
|
|
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
|
-
|
|
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
|
},
|
|
@@ -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
|
+
}
|