@nostrify/react 0.2.8
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/.turbo/turbo-build.log +5 -0
- package/LICENSE +21 -0
- package/NostrContext.ts +17 -0
- package/README.md +82 -0
- package/dist/NostrContext.d.ts +13 -0
- package/dist/NostrContext.d.ts.map +1 -0
- package/dist/NostrContext.js +10 -0
- package/dist/NostrContext.js.map +1 -0
- package/dist/example/test-helpers.d.ts +2 -0
- package/dist/example/test-helpers.d.ts.map +1 -0
- package/dist/example/test-helpers.js +34 -0
- package/dist/example/test-helpers.js.map +1 -0
- package/dist/example/useAuthor.d.ts +5 -0
- package/dist/example/useAuthor.d.ts.map +1 -0
- package/dist/example/useAuthor.js +30 -0
- package/dist/example/useAuthor.js.map +1 -0
- package/dist/example/useCurrentUser.d.ts +9 -0
- package/dist/example/useCurrentUser.d.ts.map +1 -0
- package/dist/example/useCurrentUser.js +45 -0
- package/dist/example/useCurrentUser.js.map +1 -0
- package/dist/example/useLoginActions.d.ts +6 -0
- package/dist/example/useLoginActions.d.ts.map +1 -0
- package/dist/example/useLoginActions.js +24 -0
- package/dist/example/useLoginActions.js.map +1 -0
- package/dist/example/useSocialFeed.d.ts +4 -0
- package/dist/example/useSocialFeed.d.ts.map +1 -0
- package/dist/example/useSocialFeed.js +13 -0
- package/dist/example/useSocialFeed.js.map +1 -0
- package/dist/login/NLogin.d.ts +46 -0
- package/dist/login/NLogin.d.ts.map +1 -0
- package/dist/login/NLogin.js +76 -0
- package/dist/login/NLogin.js.map +1 -0
- package/dist/login/NUser.d.ts +22 -0
- package/dist/login/NUser.d.ts.map +1 -0
- package/dist/login/NUser.js +41 -0
- package/dist/login/NUser.js.map +1 -0
- package/dist/login/NostrLoginContext.d.ts +24 -0
- package/dist/login/NostrLoginContext.d.ts.map +1 -0
- package/dist/login/NostrLoginContext.js +10 -0
- package/dist/login/NostrLoginContext.js.map +1 -0
- package/dist/login/NostrLoginProvider.d.ts +15 -0
- package/dist/login/NostrLoginProvider.d.ts.map +1 -0
- package/dist/login/NostrLoginProvider.js +23 -0
- package/dist/login/NostrLoginProvider.js.map +1 -0
- package/dist/login/mod.d.ts +5 -0
- package/dist/login/mod.d.ts.map +1 -0
- package/dist/login/mod.js +12 -0
- package/dist/login/mod.js.map +1 -0
- package/dist/login/nostrLoginReducer.d.ts +16 -0
- package/dist/login/nostrLoginReducer.d.ts.map +1 -0
- package/dist/login/nostrLoginReducer.js +29 -0
- package/dist/login/nostrLoginReducer.js.map +1 -0
- package/dist/login/useNostrLogin.d.ts +3 -0
- package/dist/login/useNostrLogin.d.ts.map +1 -0
- package/dist/login/useNostrLogin.js +13 -0
- package/dist/login/useNostrLogin.js.map +1 -0
- package/dist/login/useNostrLoginReducer.d.ts +4 -0
- package/dist/login/useNostrLoginReducer.d.ts.map +1 -0
- package/dist/login/useNostrLoginReducer.js +16 -0
- package/dist/login/useNostrLoginReducer.js.map +1 -0
- package/dist/mod.d.ts +3 -0
- package/dist/mod.d.ts.map +1 -0
- package/dist/mod.js +8 -0
- package/dist/mod.js.map +1 -0
- package/dist/tsconfig.tsbuildinfo +1 -0
- package/dist/useNostr.d.ts +3 -0
- package/dist/useNostr.d.ts.map +1 -0
- package/dist/useNostr.js +13 -0
- package/dist/useNostr.js.map +1 -0
- package/example/App.tsx +71 -0
- package/example/NostrProvider.tsx +36 -0
- package/example/index.html +12 -0
- package/example/main.tsx +31 -0
- package/example/test-helpers.ts +33 -0
- package/example/useAuthor.ts +37 -0
- package/example/useCurrentUser.ts +48 -0
- package/example/useLoginActions.ts +22 -0
- package/example/useSocialFeed.ts +18 -0
- package/example/vite-env.d.ts +1 -0
- package/example/vite.config.mts +8 -0
- package/login/NLogin.ts +120 -0
- package/login/NUser.ts +56 -0
- package/login/NostrLoginContext.ts +28 -0
- package/login/NostrLoginProvider.ts +34 -0
- package/login/mod.ts +4 -0
- package/login/nostrLoginReducer.ts +42 -0
- package/login/useNostrLogin.ts +13 -0
- package/login/useNostrLoginReducer.ts +20 -0
- package/mod.ts +2 -0
- package/package.json +27 -0
- package/tsconfig.json +14 -0
- package/useNostr.ts +13 -0
package/example/App.tsx
ADDED
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import { Suspense } from 'react';
|
|
2
|
+
|
|
3
|
+
import { useAuthor } from './useAuthor';
|
|
4
|
+
import { useCurrentUser } from './useCurrentUser';
|
|
5
|
+
import { useLoginActions } from './useLoginActions';
|
|
6
|
+
import { useSocialFeed } from './useSocialFeed';
|
|
7
|
+
|
|
8
|
+
import type { NostrEvent } from '@nostrify/nostrify';
|
|
9
|
+
|
|
10
|
+
function App() {
|
|
11
|
+
const { user, metadata } = useCurrentUser();
|
|
12
|
+
|
|
13
|
+
const login = useLoginActions();
|
|
14
|
+
|
|
15
|
+
function renderLogin() {
|
|
16
|
+
if (user) {
|
|
17
|
+
if (metadata.name) {
|
|
18
|
+
return <div>Welcome back, {metadata.name}!</div>;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
return <div>You: {user.pubkey}</div>;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
return (
|
|
25
|
+
<button
|
|
26
|
+
type='button'
|
|
27
|
+
onClick={() => login.extension()}
|
|
28
|
+
>
|
|
29
|
+
Log in
|
|
30
|
+
</button>
|
|
31
|
+
);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
return (
|
|
35
|
+
<div>
|
|
36
|
+
<h2>Login</h2>
|
|
37
|
+
{renderLogin()}
|
|
38
|
+
|
|
39
|
+
<h2>Notes</h2>
|
|
40
|
+
<Suspense fallback={<div>Loading...</div>}>
|
|
41
|
+
<Feed />
|
|
42
|
+
</Suspense>
|
|
43
|
+
</div>
|
|
44
|
+
);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function Feed() {
|
|
48
|
+
const feed = useSocialFeed();
|
|
49
|
+
|
|
50
|
+
return (
|
|
51
|
+
<>
|
|
52
|
+
{feed.data?.map((note) => <FeedPost key={note.id} event={note} />)}
|
|
53
|
+
</>
|
|
54
|
+
);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function FeedPost({ event }: { event: NostrEvent }) {
|
|
58
|
+
const author = useAuthor(event.pubkey);
|
|
59
|
+
|
|
60
|
+
return (
|
|
61
|
+
<div
|
|
62
|
+
key={event.id}
|
|
63
|
+
style={{ border: '1px solid gray', padding: '10px', margin: '20px 0' }}
|
|
64
|
+
>
|
|
65
|
+
<div>{author.name ?? event.pubkey}</div>
|
|
66
|
+
<div>{event.content}</div>
|
|
67
|
+
</div>
|
|
68
|
+
);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
export default App;
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import { NostrEvent, NPool, NRelay1 } from '@nostrify/nostrify';
|
|
2
|
+
import { NostrContext } from '@nostrify/react';
|
|
3
|
+
import { type FC, type ReactNode, useRef } from 'react';
|
|
4
|
+
|
|
5
|
+
interface NostrProviderProps {
|
|
6
|
+
children: ReactNode;
|
|
7
|
+
relays: `wss://${string}`[];
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
const NostrProvider: FC<NostrProviderProps> = (props) => {
|
|
11
|
+
const { children, relays } = props;
|
|
12
|
+
|
|
13
|
+
const pool = useRef<NPool>(undefined);
|
|
14
|
+
|
|
15
|
+
if (!pool.current) {
|
|
16
|
+
pool.current = new NPool({
|
|
17
|
+
open(url: string) {
|
|
18
|
+
return new NRelay1(url);
|
|
19
|
+
},
|
|
20
|
+
reqRouter(filters) {
|
|
21
|
+
return new Map(relays.map((url) => [url, filters]));
|
|
22
|
+
},
|
|
23
|
+
eventRouter(_event: NostrEvent) {
|
|
24
|
+
return relays;
|
|
25
|
+
},
|
|
26
|
+
});
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
return (
|
|
30
|
+
<NostrContext.Provider value={{ nostr: pool.current }}>
|
|
31
|
+
{children}
|
|
32
|
+
</NostrContext.Provider>
|
|
33
|
+
);
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
export default NostrProvider;
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
<!DOCTYPE html>
|
|
2
|
+
<html lang="en">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="UTF-8" />
|
|
5
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
6
|
+
<title>Nostrify React</title>
|
|
7
|
+
</head>
|
|
8
|
+
<body>
|
|
9
|
+
<div id="root"></div>
|
|
10
|
+
<script type="module" src="./main.tsx"></script>
|
|
11
|
+
</body>
|
|
12
|
+
</html>
|
package/example/main.tsx
ADDED
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { NostrLoginProvider } from '@nostrify/react/login';
|
|
2
|
+
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
|
|
3
|
+
import { StrictMode, Suspense } from 'react';
|
|
4
|
+
import ReactDOM from 'react-dom/client';
|
|
5
|
+
|
|
6
|
+
import App from './Appx';
|
|
7
|
+
import NostrProvider from './NostrProviderx';
|
|
8
|
+
|
|
9
|
+
const queryClient = new QueryClient({
|
|
10
|
+
defaultOptions: {
|
|
11
|
+
queries: {
|
|
12
|
+
refetchOnWindowFocus: false,
|
|
13
|
+
staleTime: 60000,
|
|
14
|
+
gcTime: Infinity,
|
|
15
|
+
},
|
|
16
|
+
},
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
ReactDOM.createRoot(document.getElementById('root') as HTMLElement).render(
|
|
20
|
+
<StrictMode>
|
|
21
|
+
<QueryClientProvider client={queryClient}>
|
|
22
|
+
<NostrLoginProvider storageKey='myapp'>
|
|
23
|
+
<NostrProvider relays={['wss://ditto.pub/relay']}>
|
|
24
|
+
<Suspense>
|
|
25
|
+
<App />
|
|
26
|
+
</Suspense>
|
|
27
|
+
</NostrProvider>
|
|
28
|
+
</NostrLoginProvider>
|
|
29
|
+
</QueryClientProvider>
|
|
30
|
+
</StrictMode>,
|
|
31
|
+
);
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { PropertySymbol, Window } from 'happy-dom';
|
|
2
|
+
|
|
3
|
+
export function polyfillDOM(): void {
|
|
4
|
+
const window = new Window();
|
|
5
|
+
const document = window.document;
|
|
6
|
+
const browserWindow = document[PropertySymbol.window];
|
|
7
|
+
|
|
8
|
+
const setInnerHTML = (html: string) => document.documentElement.innerHTML = html;
|
|
9
|
+
const cancelAsync = () => window.happyDOM.abort();
|
|
10
|
+
|
|
11
|
+
Object.assign(globalThis, {
|
|
12
|
+
window,
|
|
13
|
+
document,
|
|
14
|
+
HTMLElement: browserWindow.HTMLElement,
|
|
15
|
+
Element: browserWindow.Element,
|
|
16
|
+
Node: browserWindow.Node,
|
|
17
|
+
navigator: browserWindow.navigator,
|
|
18
|
+
DocumentFragment: browserWindow.DocumentFragment,
|
|
19
|
+
DocumentType: browserWindow.DocumentType,
|
|
20
|
+
SVGElement: browserWindow.SVGElement,
|
|
21
|
+
Text: browserWindow.Text,
|
|
22
|
+
requestAnimationFrame: browserWindow.requestAnimationFrame,
|
|
23
|
+
cancelAnimationFrame: browserWindow.cancelAnimationFrame,
|
|
24
|
+
setTimeout: browserWindow.setTimeout,
|
|
25
|
+
clearTimeout: browserWindow.clearTimeout,
|
|
26
|
+
setInterval: browserWindow.setInterval,
|
|
27
|
+
clearInterval: browserWindow.clearInterval,
|
|
28
|
+
queueMicrotask: browserWindow.queueMicrotask,
|
|
29
|
+
AbortController: browserWindow.AbortController,
|
|
30
|
+
cancelAsync,
|
|
31
|
+
setInnerHTML,
|
|
32
|
+
});
|
|
33
|
+
}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import { type NostrEvent, type NostrMetadata, NSchema as n } from '@nostrify/nostrify';
|
|
2
|
+
import { useSuspenseQuery } from '@tanstack/react-query';
|
|
3
|
+
|
|
4
|
+
import { useNostr } from '../useNostr';
|
|
5
|
+
|
|
6
|
+
export function useAuthor(
|
|
7
|
+
pubkey: string | undefined,
|
|
8
|
+
): NostrMetadata & { event?: NostrEvent } {
|
|
9
|
+
const { nostr } = useNostr();
|
|
10
|
+
|
|
11
|
+
const { data } = useSuspenseQuery<NostrMetadata & { event?: NostrEvent }>({
|
|
12
|
+
queryKey: ['author', pubkey ?? ''],
|
|
13
|
+
queryFn: async ({ signal }) => {
|
|
14
|
+
if (!pubkey) {
|
|
15
|
+
return {};
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
const [event] = await nostr.query(
|
|
19
|
+
[{ kinds: [0], authors: [pubkey!], limit: 1 }],
|
|
20
|
+
{ signal: AbortSignal.any([signal, AbortSignal.timeout(500)]) },
|
|
21
|
+
);
|
|
22
|
+
|
|
23
|
+
if (!event) {
|
|
24
|
+
return {};
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
try {
|
|
28
|
+
const metadata = n.json().pipe(n.metadata()).parse(event.content);
|
|
29
|
+
return { ...metadata, event };
|
|
30
|
+
} catch {
|
|
31
|
+
return { event };
|
|
32
|
+
}
|
|
33
|
+
},
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
return data;
|
|
37
|
+
}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import { type NLoginType, NUser, useNostrLogin } from '@nostrify/react/login';
|
|
2
|
+
import { useMemo } from 'react';
|
|
3
|
+
|
|
4
|
+
import { useAuthor } from './useAuthor';
|
|
5
|
+
import { useNostr } from '../useNostr';
|
|
6
|
+
|
|
7
|
+
export function useCurrentUser() {
|
|
8
|
+
const { nostr } = useNostr();
|
|
9
|
+
const { logins } = useNostrLogin();
|
|
10
|
+
|
|
11
|
+
function loginToUser(login: NLoginType): NUser {
|
|
12
|
+
switch (login.type) {
|
|
13
|
+
case 'nsec':
|
|
14
|
+
return NUser.fromNsecLogin(login);
|
|
15
|
+
case 'bunker':
|
|
16
|
+
return NUser.fromBunkerLogin(login, nostr);
|
|
17
|
+
case 'extension':
|
|
18
|
+
return NUser.fromExtensionLogin(login);
|
|
19
|
+
default:
|
|
20
|
+
// Learn how to define other login types: https://nostrify.dev/react/logins#custom-login-types
|
|
21
|
+
throw new Error(`Unsupported login type: ${login.type}`);
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
const users = useMemo(() => {
|
|
26
|
+
const users: NUser[] = [];
|
|
27
|
+
|
|
28
|
+
for (const login of logins) {
|
|
29
|
+
try {
|
|
30
|
+
const user = loginToUser(login);
|
|
31
|
+
users.push(user);
|
|
32
|
+
} catch (error) {
|
|
33
|
+
console.warn('Skipped invalid login', login.id, error);
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
return users;
|
|
38
|
+
}, [logins, nostr]);
|
|
39
|
+
|
|
40
|
+
const user: NUser | undefined = users[0];
|
|
41
|
+
const metadata = useAuthor(user?.pubkey);
|
|
42
|
+
|
|
43
|
+
return {
|
|
44
|
+
user,
|
|
45
|
+
users,
|
|
46
|
+
metadata,
|
|
47
|
+
};
|
|
48
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { useNostr } from '@nostrify/react';
|
|
2
|
+
import { NLogin, useNostrLogin } from '@nostrify/react/login';
|
|
3
|
+
|
|
4
|
+
export function useLoginActions() {
|
|
5
|
+
const { nostr } = useNostr();
|
|
6
|
+
const { addLogin } = useNostrLogin();
|
|
7
|
+
|
|
8
|
+
return {
|
|
9
|
+
nsec(nsec: string): void {
|
|
10
|
+
const login = NLogin.fromNsec(nsec);
|
|
11
|
+
addLogin(login);
|
|
12
|
+
},
|
|
13
|
+
async bunker(uri: string): Promise<void> {
|
|
14
|
+
const login = await NLogin.fromBunker(uri, nostr);
|
|
15
|
+
addLogin(login);
|
|
16
|
+
},
|
|
17
|
+
async extension(): Promise<void> {
|
|
18
|
+
const login = await NLogin.fromExtension();
|
|
19
|
+
addLogin(login);
|
|
20
|
+
},
|
|
21
|
+
};
|
|
22
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { useSuspenseQuery, type UseSuspenseQueryResult } from '@tanstack/react-query';
|
|
2
|
+
|
|
3
|
+
import { useNostr } from '../useNostr';
|
|
4
|
+
|
|
5
|
+
import type { NostrEvent } from '@nostrify/nostrify';
|
|
6
|
+
|
|
7
|
+
export function useSocialFeed(): UseSuspenseQueryResult<NostrEvent[]> {
|
|
8
|
+
const { nostr } = useNostr();
|
|
9
|
+
|
|
10
|
+
return useSuspenseQuery({
|
|
11
|
+
queryKey: ['social-feed'],
|
|
12
|
+
queryFn: () =>
|
|
13
|
+
nostr.query(
|
|
14
|
+
[{ kinds: [1], limit: 5 }],
|
|
15
|
+
{ signal: AbortSignal.timeout(5000) },
|
|
16
|
+
),
|
|
17
|
+
});
|
|
18
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
/// <reference types="vite/client" />
|
package/login/NLogin.ts
ADDED
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
import { BunkerURI, NConnectSigner, type NostrSigner, type NPool, NSecSigner } from '@nostrify/nostrify';
|
|
2
|
+
import { generateSecretKey, getPublicKey, nip19 } from 'nostr-tools';
|
|
3
|
+
|
|
4
|
+
/** An object represeting any supported Nostr login credentials. */
|
|
5
|
+
export type NLoginType = NLoginNsec | NLoginBunker | NLoginExtension | NLoginOther;
|
|
6
|
+
|
|
7
|
+
/** Nostr login with nsec. */
|
|
8
|
+
export type NLoginNsec = NLoginBase<'nsec', {
|
|
9
|
+
nsec: `nsec1${string}`;
|
|
10
|
+
}>;
|
|
11
|
+
|
|
12
|
+
/** NIP-46 (aka remote signer) login. */
|
|
13
|
+
export type NLoginBunker = NLoginBase<'bunker', {
|
|
14
|
+
bunkerPubkey: string;
|
|
15
|
+
clientNsec: `nsec1${string}`;
|
|
16
|
+
relays: string[];
|
|
17
|
+
}>;
|
|
18
|
+
|
|
19
|
+
/** NIP-07 (browser extension) login. */
|
|
20
|
+
export type NLoginExtension = NLoginBase<'extension', null>;
|
|
21
|
+
|
|
22
|
+
/** Additional login types created by the library user. */
|
|
23
|
+
export type NLoginOther = NLoginBase<`x-${string}`, {
|
|
24
|
+
[key: string]: unknown;
|
|
25
|
+
}>;
|
|
26
|
+
|
|
27
|
+
/** Base properties shared by Nostr login objects. */
|
|
28
|
+
interface NLoginBase<T extends string, D> {
|
|
29
|
+
id: string;
|
|
30
|
+
type: T;
|
|
31
|
+
pubkey: string;
|
|
32
|
+
createdAt: string;
|
|
33
|
+
data: D;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/** Class representing Nostr login credentials. */
|
|
37
|
+
export class NLogin<T extends string, D> implements NLoginBase<T, D> {
|
|
38
|
+
public id: string;
|
|
39
|
+
public type: T;
|
|
40
|
+
public pubkey: string;
|
|
41
|
+
public createdAt: string;
|
|
42
|
+
public data: D;
|
|
43
|
+
|
|
44
|
+
constructor(type: T, pubkey: string, data: D) {
|
|
45
|
+
this.id = `${type}:${pubkey}`;
|
|
46
|
+
this.type = type;
|
|
47
|
+
this.pubkey = pubkey;
|
|
48
|
+
this.createdAt = new Date().toISOString();
|
|
49
|
+
this.data = data;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/** Create a login object from an nsec. */
|
|
53
|
+
static fromNsec(nsec: string): NLoginNsec {
|
|
54
|
+
const decoded = nip19.decode(nsec);
|
|
55
|
+
|
|
56
|
+
if (decoded.type !== 'nsec') {
|
|
57
|
+
throw new Error('Invalid nsec');
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
const sk = decoded.data;
|
|
61
|
+
const pubkey = getPublicKey(sk);
|
|
62
|
+
|
|
63
|
+
return new NLogin('nsec', pubkey, {
|
|
64
|
+
nsec: nip19.nsecEncode(sk),
|
|
65
|
+
});
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/** Create a login object from a bunker URI. */
|
|
69
|
+
static async fromBunker(uri: string, pool: NPool): Promise<NLoginBunker> {
|
|
70
|
+
const { pubkey: bunkerPubkey, secret, relays } = new BunkerURI(uri);
|
|
71
|
+
|
|
72
|
+
if (!relays.length) {
|
|
73
|
+
throw new Error('No relay provided');
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
const sk = generateSecretKey();
|
|
77
|
+
const nsec = nip19.nsecEncode(sk);
|
|
78
|
+
const clientSigner = new NSecSigner(sk);
|
|
79
|
+
|
|
80
|
+
const signer = new NConnectSigner({
|
|
81
|
+
relay: pool.group(relays),
|
|
82
|
+
pubkey: bunkerPubkey,
|
|
83
|
+
signer: clientSigner,
|
|
84
|
+
timeout: 60_000,
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
await signer.connect(secret);
|
|
88
|
+
const pubkey = await signer.getPublicKey();
|
|
89
|
+
|
|
90
|
+
return new NLogin('bunker', pubkey, {
|
|
91
|
+
bunkerPubkey,
|
|
92
|
+
clientNsec: nsec,
|
|
93
|
+
relays,
|
|
94
|
+
});
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/** Create a login object from a browser extension. */
|
|
98
|
+
static async fromExtension(): Promise<NLoginExtension> {
|
|
99
|
+
const windowSigner = (globalThis as unknown as { nostr?: NostrSigner }).nostr;
|
|
100
|
+
|
|
101
|
+
if (!windowSigner) {
|
|
102
|
+
throw new Error('Nostr extension is not available');
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
const pubkey = await windowSigner.getPublicKey();
|
|
106
|
+
|
|
107
|
+
return new NLogin('extension', pubkey, null);
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/** Convert to a JSON-serializable object. */
|
|
111
|
+
toJSON(): NLoginBase<T, D> {
|
|
112
|
+
return {
|
|
113
|
+
id: this.id,
|
|
114
|
+
type: this.type,
|
|
115
|
+
pubkey: this.pubkey,
|
|
116
|
+
createdAt: this.createdAt,
|
|
117
|
+
data: this.data,
|
|
118
|
+
};
|
|
119
|
+
}
|
|
120
|
+
}
|
package/login/NUser.ts
ADDED
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import { NBrowserSigner, NConnectSigner, type NostrSigner, type NPool, NSecSigner } from '@nostrify/nostrify';
|
|
2
|
+
import { nip19 } from 'nostr-tools';
|
|
3
|
+
|
|
4
|
+
import type { NLoginBunker, NLoginExtension, NLoginNsec } from './NLogin';
|
|
5
|
+
|
|
6
|
+
/** Represents a Nostr user with authentication credentials. */
|
|
7
|
+
export class NUser {
|
|
8
|
+
constructor(
|
|
9
|
+
/** The authentication method used for this user */
|
|
10
|
+
readonly method: 'nsec' | 'bunker' | 'extension' | `x-${string}`,
|
|
11
|
+
/** The public key of the user in hex format. */
|
|
12
|
+
readonly pubkey: string,
|
|
13
|
+
/** The signer that can sign events on behalf of this user. */
|
|
14
|
+
readonly signer: NostrSigner,
|
|
15
|
+
) {}
|
|
16
|
+
|
|
17
|
+
static fromNsecLogin(login: NLoginNsec): NUser {
|
|
18
|
+
const sk = nip19.decode(login.data.nsec) as {
|
|
19
|
+
type: 'nsec';
|
|
20
|
+
data: Uint8Array;
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
return new NUser(
|
|
24
|
+
login.type,
|
|
25
|
+
login.pubkey,
|
|
26
|
+
new NSecSigner(sk.data),
|
|
27
|
+
);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
static fromBunkerLogin(login: NLoginBunker, pool: NPool): NUser {
|
|
31
|
+
const clientSk = nip19.decode(login.data.clientNsec) as {
|
|
32
|
+
type: 'nsec';
|
|
33
|
+
data: Uint8Array;
|
|
34
|
+
};
|
|
35
|
+
const clientSigner = new NSecSigner(clientSk.data);
|
|
36
|
+
|
|
37
|
+
return new NUser(
|
|
38
|
+
login.type,
|
|
39
|
+
login.pubkey,
|
|
40
|
+
new NConnectSigner({
|
|
41
|
+
relay: pool.group(login.data.relays),
|
|
42
|
+
pubkey: login.pubkey,
|
|
43
|
+
signer: clientSigner,
|
|
44
|
+
timeout: 60_000,
|
|
45
|
+
}),
|
|
46
|
+
);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
static fromExtensionLogin(login: NLoginExtension): NUser {
|
|
50
|
+
return new NUser(
|
|
51
|
+
login.type,
|
|
52
|
+
login.pubkey,
|
|
53
|
+
new NBrowserSigner(),
|
|
54
|
+
);
|
|
55
|
+
}
|
|
56
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { type Context, createContext } from 'react';
|
|
2
|
+
|
|
3
|
+
import type { NLoginType } from './NLogin';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* NostrLoginContextType defines the shape of the context that will be provided
|
|
7
|
+
* to components that need access to the Nostr login state.
|
|
8
|
+
*/
|
|
9
|
+
export type NostrLoginContextType = {
|
|
10
|
+
/** The list of Nostr logins. */
|
|
11
|
+
logins: readonly NLoginType[];
|
|
12
|
+
/** Dispatch an action to add a login to the state. */
|
|
13
|
+
addLogin: (login: NLoginType) => void;
|
|
14
|
+
/** Dispatch an action to remove a login from the state. */
|
|
15
|
+
removeLogin: (loginId: string) => void;
|
|
16
|
+
/** Dispatch an action to set the user's current login (by moving it to the top of the state). */
|
|
17
|
+
setLogin: (loginId: string) => void;
|
|
18
|
+
/** Dispatch an action to clear the login state. */
|
|
19
|
+
clearLogins: () => void;
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* NostrLoginContext is a React context that provides access to the Nostr login state and
|
|
24
|
+
* a dispatch function to update the state.
|
|
25
|
+
*/
|
|
26
|
+
export const NostrLoginContext: Context<NostrLoginContextType | undefined> = createContext<
|
|
27
|
+
NostrLoginContextType | undefined
|
|
28
|
+
>(undefined);
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { jsx } from 'react/jsx-runtime';
|
|
2
|
+
|
|
3
|
+
import { NostrLoginContext, NostrLoginContextType } from './NostrLoginContext';
|
|
4
|
+
import { useNostrLoginReducer } from './useNostrLoginReducer';
|
|
5
|
+
|
|
6
|
+
import type { FC, ReactNode } from 'react';
|
|
7
|
+
|
|
8
|
+
/** Props for `NostrLoginProvider`. */
|
|
9
|
+
interface NostrLoginProviderProps {
|
|
10
|
+
/** The child components that will have access to the context. */
|
|
11
|
+
children: ReactNode;
|
|
12
|
+
/** The key used to store (and revive) the logins in localStorage. */
|
|
13
|
+
storageKey: string;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* NostrLoginProvider is a React component that provides a context for managing Nostr logins.
|
|
18
|
+
* It uses a reducer to handle the state of logins and stores them in localStorage.
|
|
19
|
+
*/
|
|
20
|
+
export const NostrLoginProvider: FC<NostrLoginProviderProps> = (
|
|
21
|
+
{ children, storageKey }: NostrLoginProviderProps,
|
|
22
|
+
) => {
|
|
23
|
+
const [logins, dispatch] = useNostrLoginReducer(storageKey);
|
|
24
|
+
|
|
25
|
+
const value: NostrLoginContextType = {
|
|
26
|
+
logins,
|
|
27
|
+
addLogin: (login) => dispatch({ type: 'login.add', login }),
|
|
28
|
+
removeLogin: (id) => dispatch({ type: 'login.remove', id }),
|
|
29
|
+
setLogin: (id) => dispatch({ type: 'login.set', id }),
|
|
30
|
+
clearLogins: () => dispatch({ type: 'login.clear' }),
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
return jsx(NostrLoginContext.Provider, { value, children });
|
|
34
|
+
};
|
package/login/mod.ts
ADDED
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import type { NLoginType } from './NLogin';
|
|
2
|
+
|
|
3
|
+
export type NLoginAction =
|
|
4
|
+
| { type: 'login.add'; login: NLoginType; set?: boolean }
|
|
5
|
+
| { type: 'login.remove'; id: string }
|
|
6
|
+
| { type: 'login.set'; id: string }
|
|
7
|
+
| { type: 'login.clear' };
|
|
8
|
+
|
|
9
|
+
export function nostrLoginReducer(
|
|
10
|
+
state: NLoginType[],
|
|
11
|
+
action: NLoginAction,
|
|
12
|
+
): NLoginType[] {
|
|
13
|
+
switch (action.type) {
|
|
14
|
+
case 'login.add': {
|
|
15
|
+
const filtered = state.filter((login) => login.id !== action.login.id);
|
|
16
|
+
return action.set ? [action.login, ...filtered] : [...filtered, action.login];
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
case 'login.remove': {
|
|
20
|
+
return state.filter((login) => login.id !== action.id);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
case 'login.set': {
|
|
24
|
+
const login = state.find((login) => login.id === action.id);
|
|
25
|
+
|
|
26
|
+
if (!login) {
|
|
27
|
+
return state;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
const filtered = state.filter((login) => login.id !== action.id);
|
|
31
|
+
return [login, ...filtered];
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
case 'login.clear': {
|
|
35
|
+
return [];
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
default: {
|
|
39
|
+
return state;
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { useContext } from 'react';
|
|
2
|
+
|
|
3
|
+
import { NostrLoginContext, type NostrLoginContextType } from './NostrLoginContext';
|
|
4
|
+
|
|
5
|
+
export function useNostrLogin(): NostrLoginContextType {
|
|
6
|
+
const context = useContext(NostrLoginContext);
|
|
7
|
+
|
|
8
|
+
if (!context) {
|
|
9
|
+
throw new Error('useNostrLogin must be used within a NostrLoginProvider');
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
return context;
|
|
13
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { useEffect, useReducer } from 'react';
|
|
2
|
+
|
|
3
|
+
import { type NLoginAction, nostrLoginReducer } from './nostrLoginReducer';
|
|
4
|
+
|
|
5
|
+
import type { NLoginType } from './NLogin';
|
|
6
|
+
|
|
7
|
+
export function useNostrLoginReducer(
|
|
8
|
+
storageKey: string,
|
|
9
|
+
): [state: NLoginType[], dispatch: (action: NLoginAction) => void] {
|
|
10
|
+
const [state, dispatch] = useReducer(nostrLoginReducer, [], () => {
|
|
11
|
+
const stored = localStorage.getItem(storageKey);
|
|
12
|
+
return stored ? JSON.parse(stored) : [];
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
useEffect(() => {
|
|
16
|
+
localStorage.setItem(storageKey, JSON.stringify(state));
|
|
17
|
+
}, [state]);
|
|
18
|
+
|
|
19
|
+
return [state, dispatch];
|
|
20
|
+
}
|
package/mod.ts
ADDED
package/package.json
ADDED
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@nostrify/react",
|
|
3
|
+
"version": "0.2.8",
|
|
4
|
+
"exports": {
|
|
5
|
+
".": "./mod.ts",
|
|
6
|
+
"./login": "./login/mod.ts"
|
|
7
|
+
},
|
|
8
|
+
"main": "dist/index.js",
|
|
9
|
+
"types": "dist/index.d.ts",
|
|
10
|
+
"dependencies": {
|
|
11
|
+
"@deno/vite-plugin": "^1.0.4",
|
|
12
|
+
"@tanstack/react-query": "^5.69.0",
|
|
13
|
+
"@types/react": "^18.0.0",
|
|
14
|
+
"@vitejs/plugin-react": "^4.3.4",
|
|
15
|
+
"happy-dom": "^17.4.4",
|
|
16
|
+
"react": "^18.0.0",
|
|
17
|
+
"react-dom": "^18.0.0",
|
|
18
|
+
"vite": "^6.2.2",
|
|
19
|
+
"@nostrify/nostrify": "0.46.4"
|
|
20
|
+
},
|
|
21
|
+
"publishConfig": {
|
|
22
|
+
"access": "public"
|
|
23
|
+
},
|
|
24
|
+
"scripts": {
|
|
25
|
+
"build": "tsc -p tsconfig.json"
|
|
26
|
+
}
|
|
27
|
+
}
|