Skip to content

Commit

Permalink
feat: nip46-login
Browse files Browse the repository at this point in the history
  • Loading branch information
verbiricha committed Jan 16, 2024
1 parent c790e69 commit 6331294
Show file tree
Hide file tree
Showing 12 changed files with 221 additions and 10 deletions.
15 changes: 15 additions & 0 deletions apps/docs/stories/login.stories.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -40,3 +40,18 @@ export const Pubkey: Story = {
method: "npub",
},
};

export const Bunker: Story = {
name: "Bunker",
render(props) {
return (
<Stack spacing={5}>
<Login {...props} />
<LoginMenu />
</Stack>
);
},
args: {
method: "nip46",
},
};
2 changes: 1 addition & 1 deletion apps/emojis/ui/header.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ export default function Header() {
</Flex>
<HStack>
<ColorModeToggle />
<LoginButton size="sm" methods={["nip07", "npub"]}>
<LoginButton size="sm">
{session?.pubkey && (
<>
<MenuItem
Expand Down
2 changes: 1 addition & 1 deletion apps/relays/app/components/header.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@ export default function Header() {
</Flex>
<HStack>
<ColorModeToggle />
<LoginButton size="sm" methods={["nip07", "npub"]}>
<LoginButton size="sm">
{session?.pubkey && (
<>
<MenuItem
Expand Down
2 changes: 1 addition & 1 deletion packages/core/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@
"@emotion/react": "^11.11.1",
"@emotion/styled": "^11.11.0",
"@noble/hashes": "^1.3.3",
"@nostr-dev-kit/ndk": "^2.3.0",
"@nostr-dev-kit/ndk": "^2.3.3",
"@scure/base": "^1.1.5",
"@tanstack/react-query": "^5.13.4",
"@void-cat/api": "^1.0.10",
Expand Down
67 changes: 67 additions & 0 deletions packages/core/src/components/Login.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import {
Divider,
Stack,
HStack,
Input,
} from "@chakra-ui/react";
import { useIntl, FormattedMessage } from "react-intl";

Expand All @@ -17,6 +18,7 @@ import useFeedback from "../hooks/useFeedback";
import {
useExtensionLogin,
usePubkeyLogin,
useBunkerLogin,
useLinkComponent,
} from "../context";
import type { LoginMethod } from "../types";
Expand Down Expand Up @@ -120,6 +122,67 @@ export function LoginPubkey({ onLogin }: LoginProps) {
);
}

export function LoginBunker({ onLogin }: LoginProps) {
const toast = useFeedback();
const { formatMessage } = useIntl();
const [url, setUrl] = useState<string | undefined>();
const [isBusy, setIsBusy] = useState(false);
const bunkerLogin = useBunkerLogin();

async function loginWithBunker() {
try {
setIsBusy(true);
if (url) {
const user = await bunkerLogin(url);
if (user) {
onLogin && onLogin();
}
}
} catch (error) {
console.error(error);
const msg = formatMessage({
id: "ngine.login-error",
description: "Login failed error message",
defaultMessage: "Could not sign in",
});
toast.error((error as Error)?.message, msg);
console.error(error);
} finally {
setIsBusy(false);
}
}

return (
<Stack>
<HStack minW="20em">
<Input
isDisabled={isBusy}
placeholder={formatMessage({
id: "ngine.bunker-url-placeholder",
description: "Bunker URL placeholder",
defaultMessage: "bunker://",
})}
value={url}
onChange={(ev) => setUrl(ev.target.value)}
/>
</HStack>
<Button
isLoading={isBusy}
isDisabled={!url}
variant="solid"
colorScheme="brand"
onClick={loginWithBunker}
>
<FormattedMessage
id="ngine.bunker-login"
description="A button for logging in with a nsecbunker"
defaultMessage="Log in"
/>
</Button>
</Stack>
);
}

export default function Login({ method, onLogin }: LoginProps) {
if (method === "nip07") {
return <LoginNip07 method={method} onLogin={onLogin} />;
Expand All @@ -129,5 +192,9 @@ export default function Login({ method, onLogin }: LoginProps) {
return <LoginPubkey method={method} onLogin={onLogin} />;
}

if (method === "nip46") {
return <LoginBunker method={method} onLogin={onLogin} />;
}

return null;
}
20 changes: 19 additions & 1 deletion packages/core/src/components/LoginButton.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,24 @@ function LoginOption({ method, onLogin }: LoginProps) {
</Text>
</>
)}
{method === "nip46" && (
<>
<Heading fontSize="xl">
<FormattedMessage
id="ngine.login-nip46"
description="Title of nip46 login section"
defaultMessage="Nsecbunker"
/>
</Heading>
<Text>
<FormattedMessage
id="ngine.login-nsecbunker-descr"
description="Description of nsecbunker login description"
defaultMessage="You can log in with nsecbunker URL and sign events remotely"
/>
</Text>
</>
)}
{method === "npub" && (
<>
<Heading fontSize="xl">
Expand Down Expand Up @@ -104,7 +122,7 @@ interface LoginButtonProps extends Pick<LoginProps, "onLogin"> {
}

export default function LoginButton({
methods = ["nip07", "npub"],
methods = ["nip07", "nip46", "npub"],
children,
onLogin,
size,
Expand Down
4 changes: 2 additions & 2 deletions packages/core/src/components/ReactionPicker.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -151,9 +151,9 @@ export default function ReactionPicker({
await event.react(emoji.native);
const msg = formatMessage(
{
id: "ngine.react-success",
id: "ngine.react-success-native",
description: "Success message for reactions",
defaultMessage: "Reacted with {emoji}",
defaultMessage: "Reacted with { emoji }",
},
{ emoji: emoji.native },
);
Expand Down
63 changes: 62 additions & 1 deletion packages/core/src/context.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
import NDK, {
NDKKind,
NDKNip07Signer,
NDKNip46Signer,
NDKPrivateKeySigner,
NDKUser,
NostrEvent,
Expand All @@ -39,6 +40,7 @@ const queryClient = new QueryClient();
interface NgineContextProps {
ndk: NDK;
nip07Login: () => Promise<NDKUser | undefined>;
nip46Login: (url: string) => Promise<NDKUser | undefined>;
nsecLogin: (nsec: string) => Promise<NDKUser>;
npubLogin: (npub: string) => Promise<NDKUser>;
sign: (
Expand All @@ -54,6 +56,9 @@ const NgineContext = createContext<NgineContextProps>({
nip07Login: () => {
return Promise.reject();
},
nip46Login: () => {
return Promise.reject();
},
nsecLogin: () => {
return Promise.reject();
},
Expand Down Expand Up @@ -160,7 +165,20 @@ export const NgineProvider = ({
} else if (session?.method === "nsec") {
const signer = new NDKPrivateKeySigner(session.privkey);
ndk.signer = signer;
} else if (session?.method === "nip46" && session.bunker) {
const { privkey, pubkey, relays } = session.bunker;
const localSigner = new NDKPrivateKeySigner(privkey);
const bunkerNDK = new NDK({ explicitRelayUrls: relays });
bunkerNDK.connect().then(() => {
const signer = new NDKNip46Signer(
bunkerNDK,
session.pubkey,
localSigner,
);
ndk.signer = signer;
});
}
// todo: nip05
}, [session]);

async function nip07Login() {
Expand All @@ -176,6 +194,32 @@ export const NgineProvider = ({
return user;
}

async function nip46Login(url: string) {
const asURL = new URL(url);
const relays = asURL.searchParams.getAll("relay");
const pubkey = asURL.pathname.replace(/^\/\//, "");
const bunkerNDK = new NDK({
explicitRelayUrls: relays,
});
await bunkerNDK.connect();
const localSigner = NDKPrivateKeySigner.generate();
const signer = new NDKNip46Signer(bunkerNDK, pubkey, localSigner);
const user = await signer.blockUntilReady();
if (user) {
ndk.signer = signer;
setSession({
method: "nip46",
pubkey: user.pubkey,
bunker: {
privkey: localSigner.privateKey as string,
pubkey,
relays,
},
});
}
return user;
}

async function npubLogin(pubkey: string) {
const user = ndk.getUser({ hexpubkey: pubkey });
setSession({
Expand Down Expand Up @@ -226,7 +270,16 @@ export const NgineProvider = ({

return (
<NgineContext.Provider
value={{ ndk, nip07Login, nsecLogin, npubLogin, sign, logOut, links }}
value={{
ndk,
nip07Login,
nip46Login,
nsecLogin,
npubLogin,
sign,
logOut,
links,
}}
>
<IntlProvider
defaultLocale="en-US"
Expand Down Expand Up @@ -270,6 +323,14 @@ export const usePubkeyLogin = () => {
return context.npubLogin;
};

export const useBunkerLogin = () => {
const context = useContext(NgineContext);
if (context === undefined) {
throw new Error("Ngine context not found");
}
return context.nip46Login;
};

export const useNsecLogin = () => {
const context = useContext(NgineContext);
if (context === undefined) {
Expand Down
20 changes: 20 additions & 0 deletions packages/core/src/lang.json
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,14 @@
"defaultMessage": "Bookmarks ({count})",
"description": "Bookmarks count"
},
"ngine.bunker-login": {
"defaultMessage": "Log in",
"description": "A button for logging in with a nsecbunker"
},
"ngine.bunker-url-placeholder": {
"defaultMessage": "bunker://",
"description": "Bunker URL placeholder"
},
"ngine.comment-label": {
"defaultMessage": "Comment",
"description": "Comment field label"
Expand Down Expand Up @@ -91,6 +99,10 @@
"defaultMessage": "You can use a nostr extension to log in to the site.",
"description": "Description of extension login section"
},
"ngine.login-nip46": {
"defaultMessage": "Nsecbunker",
"description": "Title of nip46 login section"
},
"ngine.login-npub": {
"defaultMessage": "Public key",
"description": "Title of pubkey login section"
Expand All @@ -99,6 +111,10 @@
"defaultMessage": "You can log in with a pubkey or nostr address for browsing the site in read-only mode.",
"description": "Description of pubkey login section"
},
"ngine.login-nsecbunker-descr": {
"defaultMessage": "You can log in with nsecbunker URL and sign events remotely",
"description": "Description of nsecbunker login description"
},
"ngine.menu-reactions": {
"defaultMessage": "Reactions",
"description": "Reactions dialog opener"
Expand Down Expand Up @@ -195,6 +211,10 @@
"defaultMessage": "Reacted with",
"description": "Success message for reactions"
},
"ngine.react-success-native": {
"defaultMessage": "Reacted with {emoji}",
"description": "Success message for reactions"
},
"ngine.react-to": {
"defaultMessage": "Reacting to",
"description": "React modal title"
Expand Down
5 changes: 5 additions & 0 deletions packages/core/src/translations/en.json
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
{
"ngine.bookmarks-count": "Bookmarks ({count})",
"ngine.bunker-login": "Log in",
"ngine.bunker-url-placeholder": "bunker://",
"ngine.comment-label": "Comment",
"ngine.comment-placeholder": "Type your reply here",
"ngine.copy": "Copy",
Expand All @@ -22,8 +24,10 @@
"ngine.login-error": "Could not sign in",
"ngine.login-nip07": "Nostr extension",
"ngine.login-nip07-descr": "You can use a nostr extension to log in to the site.",
"ngine.login-nip46": "Nsecbunker",
"ngine.login-npub": "Public key",
"ngine.login-npub-descr": "You can log in with a pubkey or nostr address for browsing the site in read-only mode.",
"ngine.login-nsecbunker-descr": "You can log in with nsecbunker URL and sign events remotely",
"ngine.menu-reactions": "Reactions",
"ngine.npub-placeholder": "npub, nprofile or nostr address...",
"ngine.onboarding.avatar": "Add a profile image",
Expand All @@ -48,6 +52,7 @@
"ngine.pubkey-login": "Log in (read only)",
"ngine.react-error": "Couldn't react",
"ngine.react-success": "Reacted with",
"ngine.react-success-native": "Reacted with {emoji}",
"ngine.react-to": "Reacting to",
"ngine.react-with": "React with",
"ngine.reactions-count": "Reactions ({count})",
Expand Down
8 changes: 7 additions & 1 deletion packages/core/src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -39,12 +39,18 @@ export interface Links {

// Sessions

export type LoginMethod = "nip07" | "npub" | "nsec";
// todo: nip05 with nip46
export type LoginMethod = "nip07" | "nip46" | "npub" | "nsec";

export interface Session {
method: LoginMethod;
pubkey: string;
privkey?: string;
bunker?: {
privkey: string;
pubkey: string;
relays: string[];
};
}

// Components
Expand Down
Loading

0 comments on commit 6331294

Please sign in to comment.