mirror of
https://github.com/hzrd149/nostrudel.git
synced 2025-06-14 10:51:25 +02:00
Add support for nsecBunker OAuth flow
This commit is contained in:
parent
b15f9bc1a8
commit
e8e3dc0fac
5
.changeset/little-rockets-breathe.md
Normal file
5
.changeset/little-rockets-breathe.md
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
---
|
||||||
|
"nostrudel": minor
|
||||||
|
---
|
||||||
|
|
||||||
|
Add support for nsecBunker OAuth flow
|
@ -86,6 +86,8 @@ import CacheRelayView from "./views/relays/cache";
|
|||||||
import RelaySetView from "./views/relays/relay-set";
|
import RelaySetView from "./views/relays/relay-set";
|
||||||
import AppRelays from "./views/relays/app";
|
import AppRelays from "./views/relays/app";
|
||||||
import MailboxesView from "./views/relays/mailboxes";
|
import MailboxesView from "./views/relays/mailboxes";
|
||||||
|
import LoginNostrAddressView from "./views/signin/address";
|
||||||
|
import LoginNostrAddressCreate from "./views/signin/address/create";
|
||||||
const TracksView = lazy(() => import("./views/tracks"));
|
const TracksView = lazy(() => import("./views/tracks"));
|
||||||
const UserTracksTab = lazy(() => import("./views/user/tracks"));
|
const UserTracksTab = lazy(() => import("./views/user/tracks"));
|
||||||
const UserVideosTab = lazy(() => import("./views/user/videos"));
|
const UserVideosTab = lazy(() => import("./views/user/videos"));
|
||||||
@ -173,6 +175,13 @@ const router = createHashRouter([
|
|||||||
{ path: "npub", element: <LoginNpubView /> },
|
{ path: "npub", element: <LoginNpubView /> },
|
||||||
{ path: "nip05", element: <LoginNip05View /> },
|
{ path: "nip05", element: <LoginNip05View /> },
|
||||||
{ path: "nsec", element: <LoginNsecView /> },
|
{ path: "nsec", element: <LoginNsecView /> },
|
||||||
|
{
|
||||||
|
path: "address",
|
||||||
|
children: [
|
||||||
|
{ path: "", element: <LoginNostrAddressView /> },
|
||||||
|
{ path: "create", element: <LoginNostrAddressCreate /> },
|
||||||
|
],
|
||||||
|
},
|
||||||
{ path: "nostr-connect", element: <LoginNostrConnectView /> },
|
{ path: "nostr-connect", element: <LoginNostrConnectView /> },
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
|
@ -125,6 +125,9 @@ export default class NostrMultiSubscription {
|
|||||||
|
|
||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
|
waitForConnection(): Promise<void> {
|
||||||
|
return Promise.all(this.relays.map((r) => r.waitForConnection())).then((v) => void 0);
|
||||||
|
}
|
||||||
close() {
|
close() {
|
||||||
if (this.state !== NostrMultiSubscription.OPEN) return this;
|
if (this.state !== NostrMultiSubscription.OPEN) return this;
|
||||||
|
|
||||||
|
@ -2,6 +2,7 @@ import { offlineMode } from "../services/offline-mode";
|
|||||||
import relayScoreboardService from "../services/relay-scoreboard";
|
import relayScoreboardService from "../services/relay-scoreboard";
|
||||||
import { RawIncomingNostrEvent, NostrEvent, CountResponse } from "../types/nostr-event";
|
import { RawIncomingNostrEvent, NostrEvent, CountResponse } from "../types/nostr-event";
|
||||||
import { NostrOutgoingMessage } from "../types/nostr-query";
|
import { NostrOutgoingMessage } from "../types/nostr-query";
|
||||||
|
import createDefer, { Deferred } from "./deferred";
|
||||||
import { PersistentSubject, Subject } from "./subject";
|
import { PersistentSubject, Subject } from "./subject";
|
||||||
|
|
||||||
export type IncomingEvent = {
|
export type IncomingEvent = {
|
||||||
@ -54,6 +55,8 @@ export default class Relay {
|
|||||||
onCommandResult = new Subject<IncomingCommandResult>(undefined, false);
|
onCommandResult = new Subject<IncomingCommandResult>(undefined, false);
|
||||||
ws?: WebSocket;
|
ws?: WebSocket;
|
||||||
|
|
||||||
|
private connectionPromises: Deferred<void>[] = [];
|
||||||
|
|
||||||
private connectionTimer?: () => void;
|
private connectionTimer?: () => void;
|
||||||
private ejectTimer?: () => void;
|
private ejectTimer?: () => void;
|
||||||
private intentionalClose = false;
|
private intentionalClose = false;
|
||||||
@ -77,6 +80,9 @@ export default class Relay {
|
|||||||
if (this.connectionTimer) {
|
if (this.connectionTimer) {
|
||||||
this.connectionTimer();
|
this.connectionTimer();
|
||||||
this.connectionTimer = undefined;
|
this.connectionTimer = undefined;
|
||||||
|
|
||||||
|
for (const p of this.connectionPromises) p.reject();
|
||||||
|
this.connectionPromises = [];
|
||||||
}
|
}
|
||||||
// relayScoreboardService.relayTimeouts.get(this.url).addIncident();
|
// relayScoreboardService.relayTimeouts.get(this.url).addIncident();
|
||||||
}, CONNECTION_TIMEOUT);
|
}, CONNECTION_TIMEOUT);
|
||||||
@ -101,6 +107,9 @@ export default class Relay {
|
|||||||
}
|
}
|
||||||
|
|
||||||
this.sendQueued();
|
this.sendQueued();
|
||||||
|
|
||||||
|
for (const p of this.connectionPromises) p.resolve();
|
||||||
|
this.connectionPromises = [];
|
||||||
};
|
};
|
||||||
this.ws.onclose = () => {
|
this.ws.onclose = () => {
|
||||||
this.onClose.next(this);
|
this.onClose.next(this);
|
||||||
@ -129,6 +138,13 @@ export default class Relay {
|
|||||||
this.subscriptionResTimer.clear();
|
this.subscriptionResTimer.clear();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
waitForConnection(): Promise<void> {
|
||||||
|
if (this.connected) return Promise.resolve();
|
||||||
|
const p = createDefer<void>();
|
||||||
|
this.connectionPromises.push(p);
|
||||||
|
return p;
|
||||||
|
}
|
||||||
|
|
||||||
private startSubResTimer(sub: string) {
|
private startSubResTimer(sub: string) {
|
||||||
this.subscriptionResTimer.set(sub, relayScoreboardService.relayResponseTimes.get(this.url).createTimer());
|
this.subscriptionResTimer.set(sub, relayScoreboardService.relayResponseTimes.get(this.url).createTimer());
|
||||||
}
|
}
|
||||||
|
@ -5,7 +5,7 @@ import { useAsync } from "react-use";
|
|||||||
import { useUserMetadata } from "../hooks/use-user-metadata";
|
import { useUserMetadata } from "../hooks/use-user-metadata";
|
||||||
import { getIdenticon } from "../helpers/identicon";
|
import { getIdenticon } from "../helpers/identicon";
|
||||||
import { safeUrl } from "../helpers/parse";
|
import { safeUrl } from "../helpers/parse";
|
||||||
import { getUserDisplayName } from "../helpers/user-metadata";
|
import { Kind0ParsedContent, getUserDisplayName } from "../helpers/user-metadata";
|
||||||
import useAppSettings from "../hooks/use-app-settings";
|
import useAppSettings from "../hooks/use-app-settings";
|
||||||
import useCurrentAccount from "../hooks/use-current-account";
|
import useCurrentAccount from "../hooks/use-current-account";
|
||||||
import { buildImageProxyURL } from "../helpers/image";
|
import { buildImageProxyURL } from "../helpers/image";
|
||||||
@ -18,23 +18,33 @@ export const UserIdenticon = memo(({ pubkey }: { pubkey: string }) => {
|
|||||||
|
|
||||||
const RESIZE_PROFILE_SIZE = 96;
|
const RESIZE_PROFILE_SIZE = 96;
|
||||||
|
|
||||||
export type UserAvatarProps = Omit<AvatarProps, "src"> & {
|
export type UserAvatarProps = Omit<MetadataAvatarProps, "pubkey" | "metadata"> & {
|
||||||
pubkey: string;
|
pubkey: string;
|
||||||
relay?: string;
|
relay?: string;
|
||||||
noProxy?: boolean;
|
|
||||||
};
|
};
|
||||||
export const UserAvatar = forwardRef<HTMLDivElement, UserAvatarProps>(({ pubkey, noProxy, relay, ...props }, ref) => {
|
export const UserAvatar = forwardRef<HTMLDivElement, UserAvatarProps>(({ pubkey, noProxy, relay, ...props }, ref) => {
|
||||||
|
const metadata = useUserMetadata(pubkey, relay ? [relay] : undefined);
|
||||||
|
return <MetadataAvatar pubkey={pubkey} metadata={metadata} noProxy={noProxy} ref={ref} {...props} />;
|
||||||
|
});
|
||||||
|
UserAvatar.displayName = "UserAvatar";
|
||||||
|
|
||||||
|
export type MetadataAvatarProps = Omit<AvatarProps, "src"> & {
|
||||||
|
metadata?: Kind0ParsedContent;
|
||||||
|
pubkey?: string;
|
||||||
|
noProxy?: boolean;
|
||||||
|
};
|
||||||
|
export const MetadataAvatar = forwardRef<HTMLDivElement, MetadataAvatarProps>(
|
||||||
|
({ pubkey, metadata, noProxy, ...props }, ref) => {
|
||||||
const { imageProxy, proxyUserMedia, hideUsernames } = useAppSettings();
|
const { imageProxy, proxyUserMedia, hideUsernames } = useAppSettings();
|
||||||
const account = useCurrentAccount();
|
const account = useCurrentAccount();
|
||||||
const metadata = useUserMetadata(pubkey, relay ? [relay] : undefined);
|
|
||||||
const picture = useMemo(() => {
|
const picture = useMemo(() => {
|
||||||
if (hideUsernames && pubkey !== account?.pubkey) return undefined;
|
if (hideUsernames && pubkey && pubkey !== account?.pubkey) return undefined;
|
||||||
if (metadata?.picture) {
|
if (metadata?.picture) {
|
||||||
const src = safeUrl(metadata?.picture);
|
const src = safeUrl(metadata?.picture);
|
||||||
if (src) {
|
if (src) {
|
||||||
const proxyURL = buildImageProxyURL(src, RESIZE_PROFILE_SIZE);
|
const proxyURL = buildImageProxyURL(src, RESIZE_PROFILE_SIZE);
|
||||||
if (proxyURL) return proxyURL;
|
if (proxyURL) return proxyURL;
|
||||||
} else if (!noProxy && proxyUserMedia) {
|
} else if (!noProxy && proxyUserMedia && pubkey) {
|
||||||
const last4 = String(pubkey).slice(pubkey.length - 4, pubkey.length);
|
const last4 = String(pubkey).slice(pubkey.length - 4, pubkey.length);
|
||||||
return `https://media.nostr.band/thumbs/${last4}/${pubkey}-picture-64`;
|
return `https://media.nostr.band/thumbs/${last4}/${pubkey}-picture-64`;
|
||||||
}
|
}
|
||||||
@ -45,14 +55,15 @@ export const UserAvatar = forwardRef<HTMLDivElement, UserAvatarProps>(({ pubkey,
|
|||||||
return (
|
return (
|
||||||
<Avatar
|
<Avatar
|
||||||
src={picture}
|
src={picture}
|
||||||
icon={<UserIdenticon pubkey={pubkey} />}
|
icon={pubkey ? <UserIdenticon pubkey={pubkey} /> : undefined}
|
||||||
overflow="hidden"
|
overflow="hidden"
|
||||||
title={getUserDisplayName(metadata, pubkey)}
|
title={getUserDisplayName(metadata, pubkey ?? "")}
|
||||||
ref={ref}
|
ref={ref}
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
});
|
},
|
||||||
|
);
|
||||||
UserAvatar.displayName = "UserAvatar";
|
UserAvatar.displayName = "UserAvatar";
|
||||||
|
|
||||||
export default memo(UserAvatar);
|
export default memo(UserAvatar);
|
||||||
|
@ -6,6 +6,7 @@ export type Kind0ParsedContent = {
|
|||||||
pubkey?: string;
|
pubkey?: string;
|
||||||
name?: string;
|
name?: string;
|
||||||
display_name?: string;
|
display_name?: string;
|
||||||
|
displayName?: string;
|
||||||
about?: string;
|
about?: string;
|
||||||
/** @deprecated */
|
/** @deprecated */
|
||||||
image?: string;
|
image?: string;
|
||||||
@ -37,11 +38,11 @@ export function parseKind0Event(event: NostrEvent): Kind0ParsedContent {
|
|||||||
export function getSearchNames(metadata: Kind0ParsedContent) {
|
export function getSearchNames(metadata: Kind0ParsedContent) {
|
||||||
if (!metadata) return [];
|
if (!metadata) return [];
|
||||||
|
|
||||||
return [metadata.display_name, metadata.name].filter(Boolean) as string[];
|
return [metadata.displayName, metadata.display_name, metadata.name].filter(Boolean) as string[];
|
||||||
}
|
}
|
||||||
|
|
||||||
export function getUserDisplayName(metadata: Kind0ParsedContent | undefined, pubkey: string) {
|
export function getUserDisplayName(metadata: Kind0ParsedContent | undefined, pubkey: string) {
|
||||||
return metadata?.display_name || metadata?.name || truncatedId(nip19.npubEncode(pubkey));
|
return metadata?.displayName || metadata?.display_name || metadata?.name || truncatedId(nip19.npubEncode(pubkey));
|
||||||
}
|
}
|
||||||
|
|
||||||
export function fixWebsiteUrl(website: string) {
|
export function fixWebsiteUrl(website: string) {
|
||||||
|
14
src/hooks/use-nip05-providers.ts
Normal file
14
src/hooks/use-nip05-providers.ts
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
import { kinds } from "nostr-tools";
|
||||||
|
|
||||||
|
import useTimelineLoader from "./use-timeline-loader";
|
||||||
|
import { recommendedReadRelays } from "../services/client-relays";
|
||||||
|
import useSubject from "./use-subject";
|
||||||
|
|
||||||
|
export default function useNip05Providers() {
|
||||||
|
const timeline = useTimelineLoader("nip05-providers", recommendedReadRelays, {
|
||||||
|
kinds: [kinds.Handlerinformation],
|
||||||
|
"#k": [String(kinds.NostrConnect)],
|
||||||
|
});
|
||||||
|
|
||||||
|
return useSubject(timeline.timeline);
|
||||||
|
}
|
@ -5,10 +5,13 @@ import useSubject from "./use-subject";
|
|||||||
import { RequestOptions } from "../services/replaceable-event-requester";
|
import { RequestOptions } from "../services/replaceable-event-requester";
|
||||||
import { COMMON_CONTACT_RELAY } from "../const";
|
import { COMMON_CONTACT_RELAY } from "../const";
|
||||||
|
|
||||||
export function useUserMetadata(pubkey: string, additionalRelays: Iterable<string> = [], opts: RequestOptions = {}) {
|
export function useUserMetadata(pubkey?: string, additionalRelays: Iterable<string> = [], opts: RequestOptions = {}) {
|
||||||
const relays = useReadRelays([...additionalRelays, COMMON_CONTACT_RELAY]);
|
const relays = useReadRelays([...additionalRelays, COMMON_CONTACT_RELAY]);
|
||||||
|
|
||||||
const subject = useMemo(() => userMetadataService.requestMetadata(pubkey, relays, opts), [pubkey, relays]);
|
const subject = useMemo(
|
||||||
|
() => (pubkey ? userMetadataService.requestMetadata(pubkey, relays, opts) : undefined),
|
||||||
|
[pubkey, relays],
|
||||||
|
);
|
||||||
const metadata = useSubject(subject);
|
const metadata = useSubject(subject);
|
||||||
|
|
||||||
return metadata;
|
return metadata;
|
||||||
|
@ -14,12 +14,15 @@ export function parseAddress(address: string): { name?: string; domain?: string
|
|||||||
type IdentityJson = {
|
type IdentityJson = {
|
||||||
names: Record<string, string | undefined>;
|
names: Record<string, string | undefined>;
|
||||||
relays?: Record<string, string[]>;
|
relays?: Record<string, string[]>;
|
||||||
|
nip46?: Record<string, string[]>;
|
||||||
};
|
};
|
||||||
export type DnsIdentity = {
|
export type DnsIdentity = {
|
||||||
name: string;
|
name: string;
|
||||||
domain: string;
|
domain: string;
|
||||||
pubkey: string;
|
pubkey: string;
|
||||||
relays: string[];
|
relays: string[];
|
||||||
|
hasNip46?: boolean;
|
||||||
|
nip46Relays?: string[];
|
||||||
};
|
};
|
||||||
|
|
||||||
function getIdentityFromJson(name: string, domain: string, json: IdentityJson): DnsIdentity | undefined {
|
function getIdentityFromJson(name: string, domain: string, json: IdentityJson): DnsIdentity | undefined {
|
||||||
@ -27,7 +30,9 @@ function getIdentityFromJson(name: string, domain: string, json: IdentityJson):
|
|||||||
if (!pubkey) return;
|
if (!pubkey) return;
|
||||||
|
|
||||||
const relays: string[] = json.relays?.[pubkey] ?? [];
|
const relays: string[] = json.relays?.[pubkey] ?? [];
|
||||||
return { name, domain, pubkey, relays };
|
const hasNip46 = !!json.nip46;
|
||||||
|
const nip46Relays = json.nip46?.[pubkey];
|
||||||
|
return { name, domain, pubkey, relays, nip46Relays, hasNip46 };
|
||||||
}
|
}
|
||||||
|
|
||||||
class DnsIdentityService {
|
class DnsIdentityService {
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
import { finalizeEvent, generateSecretKey, getPublicKey, nip04, nip19 } from "nostr-tools";
|
import { finalizeEvent, generateSecretKey, getPublicKey, kinds, nip04, nip19 } from "nostr-tools";
|
||||||
import dayjs from "dayjs";
|
import dayjs from "dayjs";
|
||||||
import { nanoid } from "nanoid";
|
import { nanoid } from "nanoid";
|
||||||
|
|
||||||
@ -11,10 +11,16 @@ import createDefer, { Deferred } from "../classes/deferred";
|
|||||||
import { truncatedId } from "../helpers/nostr/events";
|
import { truncatedId } from "../helpers/nostr/events";
|
||||||
import { NostrConnectAccount } from "./account";
|
import { NostrConnectAccount } from "./account";
|
||||||
import { bytesToHex, hexToBytes } from "@noble/hashes/utils";
|
import { bytesToHex, hexToBytes } from "@noble/hashes/utils";
|
||||||
import { normalizeRelayURL } from "../helpers/relay";
|
import { safeRelayUrl } from "../helpers/relay";
|
||||||
|
import Subject from "../classes/subject";
|
||||||
|
|
||||||
|
export function isErrorResponse(response: any): response is NostrConnectErrorResponse {
|
||||||
|
return !!response.error;
|
||||||
|
}
|
||||||
|
|
||||||
export enum NostrConnectMethod {
|
export enum NostrConnectMethod {
|
||||||
Connect = "connect",
|
Connect = "connect",
|
||||||
|
CreateAccount = "create_account",
|
||||||
Disconnect = "disconnect",
|
Disconnect = "disconnect",
|
||||||
GetPublicKey = "get_pubic_key",
|
GetPublicKey = "get_pubic_key",
|
||||||
SignEvent = "sign_event",
|
SignEvent = "sign_event",
|
||||||
@ -23,6 +29,7 @@ export enum NostrConnectMethod {
|
|||||||
}
|
}
|
||||||
type RequestParams = {
|
type RequestParams = {
|
||||||
[NostrConnectMethod.Connect]: [string] | [string, string];
|
[NostrConnectMethod.Connect]: [string] | [string, string];
|
||||||
|
[NostrConnectMethod.CreateAccount]: [string, string] | [string, string, string];
|
||||||
[NostrConnectMethod.Disconnect]: [];
|
[NostrConnectMethod.Disconnect]: [];
|
||||||
[NostrConnectMethod.GetPublicKey]: [];
|
[NostrConnectMethod.GetPublicKey]: [];
|
||||||
[NostrConnectMethod.SignEvent]: [string];
|
[NostrConnectMethod.SignEvent]: [string];
|
||||||
@ -31,6 +38,7 @@ type RequestParams = {
|
|||||||
};
|
};
|
||||||
type ResponseResults = {
|
type ResponseResults = {
|
||||||
[NostrConnectMethod.Connect]: "ack";
|
[NostrConnectMethod.Connect]: "ack";
|
||||||
|
[NostrConnectMethod.CreateAccount]: string;
|
||||||
[NostrConnectMethod.Disconnect]: "ack";
|
[NostrConnectMethod.Disconnect]: "ack";
|
||||||
[NostrConnectMethod.GetPublicKey]: string;
|
[NostrConnectMethod.GetPublicKey]: string;
|
||||||
[NostrConnectMethod.SignEvent]: string;
|
[NostrConnectMethod.SignEvent]: string;
|
||||||
@ -43,6 +51,11 @@ export type NostrConnectResponse<N extends NostrConnectMethod> = {
|
|||||||
result: ResponseResults[N];
|
result: ResponseResults[N];
|
||||||
error?: string;
|
error?: string;
|
||||||
};
|
};
|
||||||
|
export type NostrConnectErrorResponse = {
|
||||||
|
id: string;
|
||||||
|
result: string;
|
||||||
|
error: string;
|
||||||
|
};
|
||||||
|
|
||||||
export class NostrConnectClient {
|
export class NostrConnectClient {
|
||||||
sub: NostrMultiSubscription;
|
sub: NostrMultiSubscription;
|
||||||
@ -50,27 +63,41 @@ export class NostrConnectClient {
|
|||||||
|
|
||||||
isConnected = false;
|
isConnected = false;
|
||||||
pubkey: string;
|
pubkey: string;
|
||||||
|
provider?: string;
|
||||||
relays: string[];
|
relays: string[];
|
||||||
|
|
||||||
secretKey: string;
|
secretKey: string;
|
||||||
publicKey: string;
|
publicKey: string;
|
||||||
|
|
||||||
|
onAuthURL = new Subject<string>(undefined, false);
|
||||||
|
|
||||||
supportedMethods: NostrConnectMethod[] | undefined;
|
supportedMethods: NostrConnectMethod[] | undefined;
|
||||||
|
|
||||||
constructor(pubkey: string, relays: string[], secretKey?: string) {
|
constructor(pubkey: string, relays: string[], secretKey?: string, provider?: string) {
|
||||||
this.sub = new NostrMultiSubscription(`${truncatedId(pubkey)}-nostr-connect`);
|
this.sub = new NostrMultiSubscription(`${truncatedId(pubkey)}-nostr-connect`);
|
||||||
this.pubkey = pubkey;
|
this.pubkey = pubkey;
|
||||||
this.relays = relays;
|
this.relays = relays;
|
||||||
|
this.provider = provider;
|
||||||
|
|
||||||
this.secretKey = secretKey || bytesToHex(generateSecretKey());
|
this.secretKey = secretKey || bytesToHex(generateSecretKey());
|
||||||
this.publicKey = getPublicKey(hexToBytes(this.secretKey));
|
this.publicKey = getPublicKey(hexToBytes(this.secretKey));
|
||||||
|
|
||||||
this.sub.onEvent.subscribe(this.handleEvent, this);
|
this.sub.onEvent.subscribe(this.handleEvent, this);
|
||||||
this.sub.setQueryMap(createSimpleQueryMap(this.relays, { kinds: [24133], "#p": [this.publicKey] }));
|
this.sub.setQueryMap(
|
||||||
|
createSimpleQueryMap(this.relays, {
|
||||||
|
kinds: [kinds.NostrConnect, 24134],
|
||||||
|
"#p": [this.publicKey],
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
|
this.log("Secret Key:", this.secretKey);
|
||||||
|
this.log("Public Key:", this.publicKey);
|
||||||
}
|
}
|
||||||
|
|
||||||
open() {
|
async open() {
|
||||||
this.sub.open();
|
this.sub.open();
|
||||||
|
await this.sub.waitForConnection();
|
||||||
|
this.log("Connected to relays", this.relays);
|
||||||
}
|
}
|
||||||
close() {
|
close() {
|
||||||
this.sub.close();
|
this.sub.close();
|
||||||
@ -78,34 +105,35 @@ export class NostrConnectClient {
|
|||||||
|
|
||||||
private requests = new Map<string, Deferred<any>>();
|
private requests = new Map<string, Deferred<any>>();
|
||||||
async handleEvent(event: NostrEvent) {
|
async handleEvent(event: NostrEvent) {
|
||||||
if (event.kind !== 24133) return;
|
if (this.provider && event.pubkey !== this.provider) return;
|
||||||
|
|
||||||
const to = event.tags.find(isPTag)?.[1];
|
const to = event.tags.find(isPTag)?.[1];
|
||||||
if (!to) return;
|
if (!to) return;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const responseStr = await nip04.decrypt(this.secretKey, this.pubkey, event.content);
|
const responseStr = await nip04.decrypt(this.secretKey, event.pubkey, event.content);
|
||||||
const response = JSON.parse(responseStr);
|
const response = JSON.parse(responseStr);
|
||||||
|
this.log("Got Response", response);
|
||||||
if (response.id) {
|
if (response.id) {
|
||||||
const p = this.requests.get(response.id);
|
const p = this.requests.get(response.id);
|
||||||
if (!p) return;
|
if (!p) return;
|
||||||
if (response.error) {
|
if (response.error) {
|
||||||
this.log(`ERROR: Got error for ${response.id}`, response);
|
if (response.result === "auth_url") this.onAuthURL.next(response.error);
|
||||||
p.reject(new Error(response.error));
|
else p.reject(response);
|
||||||
} else if (response.result) {
|
} else if (response.result) {
|
||||||
this.log(response.id, response);
|
this.log(response.id, response.result);
|
||||||
p.resolve(response.result);
|
p.resolve(response.result);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} catch (e) {}
|
} catch (e) {}
|
||||||
}
|
}
|
||||||
|
|
||||||
private createEvent(content: string) {
|
private createEvent(content: string, target = this.pubkey, kind = kinds.NostrConnect) {
|
||||||
return finalizeEvent(
|
return finalizeEvent(
|
||||||
{
|
{
|
||||||
kind: 24133,
|
kind,
|
||||||
created_at: dayjs().unix(),
|
created_at: dayjs().unix(),
|
||||||
tags: [["p", this.pubkey]],
|
tags: [["p", target]],
|
||||||
content,
|
content,
|
||||||
},
|
},
|
||||||
hexToBytes(this.secretKey),
|
hexToBytes(this.secretKey),
|
||||||
@ -114,22 +142,44 @@ export class NostrConnectClient {
|
|||||||
private async makeRequest<T extends NostrConnectMethod>(
|
private async makeRequest<T extends NostrConnectMethod>(
|
||||||
method: T,
|
method: T,
|
||||||
params: RequestParams[T],
|
params: RequestParams[T],
|
||||||
|
kind = kinds.NostrConnect,
|
||||||
): Promise<ResponseResults[T]> {
|
): Promise<ResponseResults[T]> {
|
||||||
const id = nanoid();
|
const id = nanoid(8);
|
||||||
const request: NostrConnectRequest<T> = { method, id, params };
|
const request: NostrConnectRequest<T> = { id, method, params };
|
||||||
const encrypted = await nip04.encrypt(this.secretKey, this.pubkey, JSON.stringify(request));
|
const encrypted = await nip04.encrypt(this.secretKey, this.pubkey, JSON.stringify(request));
|
||||||
this.log(`Sending request ${id} (${method}) ${JSON.stringify(params)}`);
|
const event = this.createEvent(encrypted, this.pubkey, kind);
|
||||||
this.sub.sendAll(this.createEvent(encrypted));
|
this.log(`Sending request ${id} (${method}) ${JSON.stringify(params)}`, event);
|
||||||
|
this.sub.sendAll(event);
|
||||||
|
|
||||||
|
const p = createDefer<ResponseResults[T]>();
|
||||||
|
this.requests.set(id, p);
|
||||||
|
return p;
|
||||||
|
}
|
||||||
|
private async makeAdminRequest<T extends NostrConnectMethod>(
|
||||||
|
method: T,
|
||||||
|
params: RequestParams[T],
|
||||||
|
kind = 24134,
|
||||||
|
): Promise<ResponseResults[T]> {
|
||||||
|
if (!this.provider) throw new Error("Missing provider");
|
||||||
|
const id = nanoid(8);
|
||||||
|
const request: NostrConnectRequest<T> = { id, method, params };
|
||||||
|
const encrypted = await nip04.encrypt(this.secretKey, this.provider, JSON.stringify(request));
|
||||||
|
const event = this.createEvent(encrypted, this.provider, kind);
|
||||||
|
this.log(`Sending admin request ${id} (${method}) ${JSON.stringify(params)}`, event);
|
||||||
|
this.sub.sendAll(event);
|
||||||
|
|
||||||
const p = createDefer<ResponseResults[T]>();
|
const p = createDefer<ResponseResults[T]>();
|
||||||
this.requests.set(id, p);
|
this.requests.set(id, p);
|
||||||
return p;
|
return p;
|
||||||
}
|
}
|
||||||
|
|
||||||
connect(token?: string) {
|
async connect(token?: string) {
|
||||||
this.open();
|
await this.open();
|
||||||
try {
|
try {
|
||||||
const result = this.makeRequest(NostrConnectMethod.Connect, token ? [this.publicKey, token] : [this.publicKey]);
|
const result = await this.makeRequest(
|
||||||
|
NostrConnectMethod.Connect,
|
||||||
|
token ? [this.publicKey, token] : [this.publicKey],
|
||||||
|
);
|
||||||
this.isConnected = true;
|
this.isConnected = true;
|
||||||
return result;
|
return result;
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
@ -138,6 +188,24 @@ export class NostrConnectClient {
|
|||||||
throw e;
|
throw e;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async createAccount(name: string, domain: string, email?: string) {
|
||||||
|
await this.open();
|
||||||
|
|
||||||
|
try {
|
||||||
|
const newPubkey = await this.makeAdminRequest(
|
||||||
|
NostrConnectMethod.CreateAccount,
|
||||||
|
email ? [name, domain, email] : [name, domain],
|
||||||
|
);
|
||||||
|
this.pubkey = newPubkey;
|
||||||
|
this.isConnected = true;
|
||||||
|
return newPubkey;
|
||||||
|
} catch (e) {
|
||||||
|
this.isConnected = false;
|
||||||
|
this.close();
|
||||||
|
throw e;
|
||||||
|
}
|
||||||
|
}
|
||||||
ensureConnected() {
|
ensureConnected() {
|
||||||
if (!this.isConnected) return this.connect();
|
if (!this.isConnected) return this.connect();
|
||||||
}
|
}
|
||||||
@ -172,21 +240,26 @@ class NostrConnectService {
|
|||||||
if (!this.clients.includes(client)) this.clients.push(client);
|
if (!this.clients.includes(client)) this.clients.push(client);
|
||||||
}
|
}
|
||||||
|
|
||||||
createClient(pubkey: string, relays: string[], secretKey?: string) {
|
createClient(pubkey: string, relays: string[], secretKey?: string, provider?: string) {
|
||||||
if (this.getClient(pubkey)) throw new Error("A client for that pubkey already exists");
|
if (this.getClient(pubkey)) throw new Error("A client for that pubkey already exists");
|
||||||
|
|
||||||
const client = new NostrConnectClient(pubkey, relays, secretKey);
|
const client = new NostrConnectClient(pubkey, relays, secretKey, provider);
|
||||||
client.log = this.log.extend(pubkey);
|
client.log = this.log.extend(pubkey);
|
||||||
|
|
||||||
this.log(`Created client for ${pubkey} using ${relays.join(", ")}`);
|
this.log(`Created client for ${pubkey} using ${relays.join(", ")}`);
|
||||||
|
|
||||||
return client;
|
return client;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fromHostedBunker(pubkey: string, relays: string[], provider?: string) {
|
||||||
|
return this.getClient(pubkey) || this.createClient(pubkey, relays, undefined, provider);
|
||||||
|
}
|
||||||
fromBunkerAddress(address: string) {
|
fromBunkerAddress(address: string) {
|
||||||
const parts = address.replace("bunker://", "").split("@");
|
const parts = address.replace("bunker://", "").split("@");
|
||||||
if (parts.length !== 2) throw new Error("Invalid bunker address");
|
if (parts.length !== 2) throw new Error("Invalid bunker address");
|
||||||
const pubkey = normalizeToHexPubkey(parts[0]);
|
const pubkey = normalizeToHexPubkey(parts[0]);
|
||||||
const pathRelay = normalizeRelayURL("wss://" + parts[1]);
|
const pathRelay = safeRelayUrl("wss://" + parts[1]);
|
||||||
|
if (!pathRelay) throw new Error("Missing relay");
|
||||||
if (!pubkey || !isHexKey(pubkey)) throw new Error("Missing pubkey");
|
if (!pubkey || !isHexKey(pubkey)) throw new Error("Missing pubkey");
|
||||||
|
|
||||||
return this.getClient(pubkey) || this.createClient(pubkey, [pathRelay]);
|
return this.getClient(pubkey) || this.createClient(pubkey, [pathRelay]);
|
||||||
@ -204,7 +277,7 @@ class NostrConnectService {
|
|||||||
|
|
||||||
return this.getClient(pubkey) || this.createClient(pubkey, relays);
|
return this.getClient(pubkey) || this.createClient(pubkey, relays);
|
||||||
}
|
}
|
||||||
fromNsecBunkerToken(token: string) {
|
fromBunkerToken(token: string) {
|
||||||
const [npub, hexToken] = token.split("#");
|
const [npub, hexToken] = token.split("#");
|
||||||
const decoded = nip19.decode(npub);
|
const decoded = nip19.decode(npub);
|
||||||
const pubkey = getPubkeyFromDecodeResult(decoded);
|
const pubkey = getPubkeyFromDecodeResult(decoded);
|
||||||
|
@ -61,7 +61,7 @@ if (import.meta.env.DEV) {
|
|||||||
// random helper for logging
|
// random helper for logging
|
||||||
export function nameOrPubkey(pubkey: string) {
|
export function nameOrPubkey(pubkey: string) {
|
||||||
const parsed = userMetadataService.getSubject(pubkey).value;
|
const parsed = userMetadataService.getSubject(pubkey).value;
|
||||||
return parsed?.name || parsed?.display_name || pubkey;
|
return parsed?.displayName || parsed?.display_name || parsed?.name || pubkey;
|
||||||
}
|
}
|
||||||
|
|
||||||
export default userMetadataService;
|
export default userMetadataService;
|
||||||
|
@ -193,7 +193,7 @@ export const ProfileEditView = () => {
|
|||||||
|
|
||||||
const defaultValues = useMemo<FormData>(
|
const defaultValues = useMemo<FormData>(
|
||||||
() => ({
|
() => ({
|
||||||
displayName: metadata?.display_name,
|
displayName: metadata?.displayName || metadata?.display_name,
|
||||||
username: metadata?.name,
|
username: metadata?.name,
|
||||||
picture: metadata?.picture,
|
picture: metadata?.picture,
|
||||||
about: metadata?.about,
|
about: metadata?.about,
|
||||||
@ -209,7 +209,7 @@ export const ProfileEditView = () => {
|
|||||||
name: data.username,
|
name: data.username,
|
||||||
picture: data.picture,
|
picture: data.picture,
|
||||||
};
|
};
|
||||||
if (data.displayName) metadata.display_name = data.displayName;
|
if (data.displayName) metadata.displayName = metadata.display_name = data.displayName;
|
||||||
if (data.about) metadata.about = data.about;
|
if (data.about) metadata.about = data.about;
|
||||||
if (data.website) metadata.website = data.website;
|
if (data.website) metadata.website = data.website;
|
||||||
if (data.nip05) metadata.nip05 = data.nip05;
|
if (data.nip05) metadata.nip05 = data.nip05;
|
||||||
|
155
src/views/signin/address/create.tsx
Normal file
155
src/views/signin/address/create.tsx
Normal file
@ -0,0 +1,155 @@
|
|||||||
|
import { useState } from "react";
|
||||||
|
import {
|
||||||
|
Button,
|
||||||
|
Card,
|
||||||
|
CardBody,
|
||||||
|
CardHeader,
|
||||||
|
Flex,
|
||||||
|
Heading,
|
||||||
|
Input,
|
||||||
|
InputGroup,
|
||||||
|
InputRightAddon,
|
||||||
|
LinkBox,
|
||||||
|
Tag,
|
||||||
|
Text,
|
||||||
|
useToast,
|
||||||
|
} from "@chakra-ui/react";
|
||||||
|
import { useNavigate } from "react-router-dom";
|
||||||
|
import { NostrEvent } from "nostr-tools";
|
||||||
|
|
||||||
|
import useNip05Providers from "../../../hooks/use-nip05-providers";
|
||||||
|
import { Kind0ParsedContent } from "../../../helpers/user-metadata";
|
||||||
|
import HoverLinkOverlay from "../../../components/hover-link-overlay";
|
||||||
|
import { getEventCoordinate } from "../../../helpers/nostr/events";
|
||||||
|
import { MetadataAvatar } from "../../../components/user-avatar";
|
||||||
|
import { ErrorBoundary } from "../../../components/error-boundary";
|
||||||
|
import dnsIdentityService from "../../../services/dns-identity";
|
||||||
|
import { useUserMetadata } from "../../../hooks/use-user-metadata";
|
||||||
|
import nostrConnectService from "../../../services/nostr-connect";
|
||||||
|
import accountService from "../../../services/account";
|
||||||
|
|
||||||
|
function ProviderCard({ onClick, provider }: { onClick: () => void; provider: NostrEvent }) {
|
||||||
|
const metadata = JSON.parse(provider.content) as Kind0ParsedContent;
|
||||||
|
const features = provider.tags.filter((t) => t[0] === "f").map((t) => t[1]);
|
||||||
|
|
||||||
|
if (!metadata.nip05) return null;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Card as={LinkBox}>
|
||||||
|
<CardHeader p="4" display="flex" gap="2" alignItems="center">
|
||||||
|
<MetadataAvatar metadata={metadata} size="sm" />
|
||||||
|
<HoverLinkOverlay onClick={onClick} cursor="pointer">
|
||||||
|
<Heading size="sm">{metadata.displayName || metadata.name}</Heading>
|
||||||
|
</HoverLinkOverlay>
|
||||||
|
</CardHeader>
|
||||||
|
<CardBody p="4" pt="0">
|
||||||
|
<Text>{metadata.about}</Text>
|
||||||
|
{features.length > 0 && (
|
||||||
|
<Flex gap="2" mt="2">
|
||||||
|
{features.map((f) => (
|
||||||
|
<Tag key={f} colorScheme="primary">
|
||||||
|
{f}
|
||||||
|
</Tag>
|
||||||
|
))}
|
||||||
|
</Flex>
|
||||||
|
)}
|
||||||
|
</CardBody>
|
||||||
|
</Card>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function LoginNostrAddressCreate() {
|
||||||
|
const navigate = useNavigate();
|
||||||
|
const toast = useToast();
|
||||||
|
|
||||||
|
const [loading, setLoading] = useState<string>();
|
||||||
|
const [name, setName] = useState("");
|
||||||
|
const providers = useNip05Providers();
|
||||||
|
const [selected, setSelected] = useState<NostrEvent>();
|
||||||
|
const metadata = useUserMetadata(selected?.pubkey);
|
||||||
|
|
||||||
|
const createAccount: React.FormEventHandler<HTMLDivElement> = async (e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
if (!selected || !name) return;
|
||||||
|
|
||||||
|
try {
|
||||||
|
setLoading("Creating...");
|
||||||
|
if (!metadata) throw new Error("Cant verify provider");
|
||||||
|
if (!metadata.nip05) throw new Error("Provider missing nip05 address");
|
||||||
|
const nip05 = await dnsIdentityService.fetchIdentity(metadata.nip05);
|
||||||
|
if (!nip05 || nip05.pubkey !== selected.pubkey) throw new Error("Invalid provider");
|
||||||
|
if (nip05.name !== "_") throw new Error("Provider dose not own the domain");
|
||||||
|
if (!nip05.hasNip46) throw new Error("Provider dose not support NIP-46");
|
||||||
|
|
||||||
|
const client = nostrConnectService.createClient("", nip05.nip46Relays || nip05.relays, undefined, nip05.pubkey);
|
||||||
|
client.onAuthURL.subscribe((url) => {
|
||||||
|
window.open(url, "auth", "width=400,height=600,resizable=no,status=no,location=no,toolbar=no,menubar=no");
|
||||||
|
});
|
||||||
|
|
||||||
|
const newPubkey = await client.createAccount(name, nip05.domain);
|
||||||
|
|
||||||
|
await client.connect();
|
||||||
|
|
||||||
|
nostrConnectService.saveClient(client);
|
||||||
|
accountService.addAccount({
|
||||||
|
type: "nostr-connect",
|
||||||
|
signerRelays: client.relays,
|
||||||
|
clientSecretKey: client.secretKey,
|
||||||
|
pubkey: client.pubkey,
|
||||||
|
readonly: false,
|
||||||
|
});
|
||||||
|
accountService.switchAccount(client.pubkey!);
|
||||||
|
} catch (e) {
|
||||||
|
if (e instanceof Error) toast({ description: e.message, status: "error" });
|
||||||
|
}
|
||||||
|
setLoading(undefined);
|
||||||
|
};
|
||||||
|
|
||||||
|
const renderContent = () => {
|
||||||
|
if (loading) return <Text fontSize="lg">{loading}</Text>;
|
||||||
|
|
||||||
|
if (selected) {
|
||||||
|
const metadata = JSON.parse(selected.content) as Kind0ParsedContent;
|
||||||
|
const [_, domain] = metadata.nip05!.split("@");
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<Flex gap="2" alignItems="center">
|
||||||
|
<MetadataAvatar metadata={metadata} size="sm" />
|
||||||
|
<Heading size="sm">{metadata.displayName || metadata.name}</Heading>
|
||||||
|
</Flex>
|
||||||
|
<InputGroup>
|
||||||
|
<Input name="name" isRequired value={name} onChange={(e) => setName(e.target.value)} />
|
||||||
|
<InputRightAddon as="button" onClick={() => setSelected(undefined)} cursor="pointer">
|
||||||
|
@{domain}
|
||||||
|
</InputRightAddon>
|
||||||
|
</InputGroup>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
{providers.map((p) => (
|
||||||
|
<ErrorBoundary key={getEventCoordinate(p)}>
|
||||||
|
<ProviderCard provider={p} onClick={() => setSelected(p)} />
|
||||||
|
</ErrorBoundary>
|
||||||
|
))}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Flex as="form" direction="column" gap="2" onSubmit={createAccount} w="full">
|
||||||
|
{renderContent()}
|
||||||
|
<Flex justifyContent="space-between" gap="2" mt="2">
|
||||||
|
<Button variant="link" onClick={() => navigate("../")}>
|
||||||
|
Back
|
||||||
|
</Button>
|
||||||
|
<Button colorScheme="primary" ml="auto" type="submit" isLoading={!!loading} isDisabled={!selected}>
|
||||||
|
Create Account
|
||||||
|
</Button>
|
||||||
|
</Flex>
|
||||||
|
</Flex>
|
||||||
|
);
|
||||||
|
}
|
157
src/views/signin/address/index.tsx
Normal file
157
src/views/signin/address/index.tsx
Normal file
@ -0,0 +1,157 @@
|
|||||||
|
import { useState } from "react";
|
||||||
|
import { Button, Card, CardProps, Flex, FormControl, FormLabel, Image, Input, Text, useToast } from "@chakra-ui/react";
|
||||||
|
import { useNavigate, Link as RouterLink } from "react-router-dom";
|
||||||
|
import { useDebounce } from "react-use";
|
||||||
|
|
||||||
|
import dnsIdentityService, { DnsIdentity } from "../../../services/dns-identity";
|
||||||
|
import { CheckIcon } from "../../../components/icons";
|
||||||
|
import nostrConnectService from "../../../services/nostr-connect";
|
||||||
|
import accountService from "../../../services/account";
|
||||||
|
import { COMMON_CONTACT_RELAY } from "../../../const";
|
||||||
|
import { safeRelayUrls } from "../../../helpers/relay";
|
||||||
|
|
||||||
|
export default function LoginNostrAddressView() {
|
||||||
|
const navigate = useNavigate();
|
||||||
|
const toast = useToast();
|
||||||
|
|
||||||
|
const [provider, setProvider] = useState("");
|
||||||
|
const [address, setAddress] = useState("");
|
||||||
|
const userSpecifiedDomain = address.includes("@");
|
||||||
|
|
||||||
|
const fullAddress = userSpecifiedDomain ? address || undefined : provider ? address + "@" + provider : undefined;
|
||||||
|
|
||||||
|
const [rootNip05, setRootNip05] = useState<DnsIdentity>();
|
||||||
|
const [nip05, setNip05] = useState<DnsIdentity>();
|
||||||
|
useDebounce(
|
||||||
|
async () => {
|
||||||
|
if (!fullAddress) return setNip05(undefined);
|
||||||
|
let [name, domain] = fullAddress.split("@");
|
||||||
|
if (!name || !domain || !domain.includes(".")) return setNip05(undefined);
|
||||||
|
setNip05(await dnsIdentityService.fetchIdentity(fullAddress));
|
||||||
|
setRootNip05(await dnsIdentityService.fetchIdentity(`_@${domain}`));
|
||||||
|
},
|
||||||
|
300,
|
||||||
|
[fullAddress],
|
||||||
|
);
|
||||||
|
|
||||||
|
const [loading, setLoading] = useState<string | undefined>();
|
||||||
|
const connect: React.FormEventHandler<HTMLDivElement> = async (e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
if (!nip05) return;
|
||||||
|
|
||||||
|
try {
|
||||||
|
if (nip05.hasNip46) {
|
||||||
|
setLoading("Connecting...");
|
||||||
|
const relays = safeRelayUrls(nip05.nip46Relays || rootNip05?.nip46Relays || rootNip05?.relays || nip05.relays);
|
||||||
|
const client = nostrConnectService.fromHostedBunker(nip05.pubkey, relays, rootNip05?.pubkey);
|
||||||
|
client.onAuthURL.subscribe((url) => {
|
||||||
|
window.open(url, "auth", "width=400,height=600,resizable=no,status=no,location=no,toolbar=no,menubar=no");
|
||||||
|
});
|
||||||
|
await client.connect();
|
||||||
|
|
||||||
|
nostrConnectService.saveClient(client);
|
||||||
|
accountService.addAccount({
|
||||||
|
type: "nostr-connect",
|
||||||
|
signerRelays: client.relays,
|
||||||
|
clientSecretKey: client.secretKey,
|
||||||
|
pubkey: client.pubkey!,
|
||||||
|
readonly: false,
|
||||||
|
});
|
||||||
|
accountService.switchAccount(client.pubkey!);
|
||||||
|
} else {
|
||||||
|
accountService.addAccount({
|
||||||
|
type: "pubkey",
|
||||||
|
pubkey: nip05.pubkey,
|
||||||
|
relays: [...nip05.relays, COMMON_CONTACT_RELAY],
|
||||||
|
readonly: true,
|
||||||
|
});
|
||||||
|
accountService.switchAccount(nip05.pubkey);
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
if (e instanceof Error) toast({ status: "error", description: e.message });
|
||||||
|
}
|
||||||
|
setLoading(undefined);
|
||||||
|
};
|
||||||
|
|
||||||
|
const renderStatus = () => {
|
||||||
|
if (!address) return null;
|
||||||
|
|
||||||
|
const cardProps: CardProps = {
|
||||||
|
variant: "outline",
|
||||||
|
p: "2",
|
||||||
|
flexDirection: "row",
|
||||||
|
gap: "2",
|
||||||
|
alignItems: "center",
|
||||||
|
flexWrap: "wrap",
|
||||||
|
};
|
||||||
|
|
||||||
|
if (nip05) {
|
||||||
|
if (nip05.hasNip46) {
|
||||||
|
return (
|
||||||
|
<Card {...cardProps}>
|
||||||
|
<Image w="7" h="7" src={`//${nip05.domain}/favicon.ico`} />
|
||||||
|
<Text fontWeight="bold">{fullAddress}</Text>
|
||||||
|
<Text color="green.500" ml="auto">
|
||||||
|
Found provider <CheckIcon boxSize={5} />
|
||||||
|
</Text>
|
||||||
|
</Card>
|
||||||
|
);
|
||||||
|
} else
|
||||||
|
return (
|
||||||
|
<Card {...cardProps}>
|
||||||
|
<Text fontWeight="bold">{fullAddress}</Text>
|
||||||
|
<Text color="yellow.500" ml="auto">
|
||||||
|
Read-only
|
||||||
|
</Text>
|
||||||
|
</Card>
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
return (
|
||||||
|
<Card {...cardProps}>
|
||||||
|
<Text fontWeight="bold">{fullAddress}</Text>
|
||||||
|
<Text color="red.500" ml="auto">
|
||||||
|
Cant find identity
|
||||||
|
</Text>
|
||||||
|
</Card>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Flex as="form" direction="column" gap="2" onSubmit={connect} w="full">
|
||||||
|
{loading && <Text fontSize="lg">{loading}</Text>}
|
||||||
|
{!loading && (
|
||||||
|
<>
|
||||||
|
<FormControl>
|
||||||
|
<FormLabel htmlFor="address">Nostr Address</FormLabel>
|
||||||
|
<Flex gap="2">
|
||||||
|
<Input
|
||||||
|
id="address"
|
||||||
|
type="email"
|
||||||
|
isRequired
|
||||||
|
value={address}
|
||||||
|
onChange={(e) => setAddress(e.target.value)}
|
||||||
|
autoComplete="off"
|
||||||
|
/>
|
||||||
|
</Flex>
|
||||||
|
</FormControl>
|
||||||
|
{renderStatus()}
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
<Flex justifyContent="space-between" gap="2" mt="2">
|
||||||
|
<Button variant="link" onClick={() => navigate("../")}>
|
||||||
|
Back
|
||||||
|
</Button>
|
||||||
|
{nip05 ? (
|
||||||
|
<Button colorScheme="primary" ml="auto" type="submit" isLoading={!!loading} isDisabled={!nip05}>
|
||||||
|
Connect
|
||||||
|
</Button>
|
||||||
|
) : (
|
||||||
|
<Button colorScheme="primary" ml="auto" as={RouterLink} to="/signin/address/create">
|
||||||
|
Find Provider
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
</Flex>
|
||||||
|
</Flex>
|
||||||
|
);
|
||||||
|
}
|
@ -1,4 +1,4 @@
|
|||||||
import { Avatar, Center, Flex, Heading } from "@chakra-ui/react";
|
import { Avatar, Flex, Heading } from "@chakra-ui/react";
|
||||||
import { Navigate, Outlet, useLocation } from "react-router-dom";
|
import { Navigate, Outlet, useLocation } from "react-router-dom";
|
||||||
import { ReloadPrompt } from "../../components/reload-prompt";
|
import { ReloadPrompt } from "../../components/reload-prompt";
|
||||||
import useSubject from "../../hooks/use-subject";
|
import useSubject from "../../hooks/use-subject";
|
||||||
@ -13,15 +13,15 @@ export default function LoginView() {
|
|||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<ReloadPrompt />
|
<ReloadPrompt />
|
||||||
<Center w="full" h="full">
|
<Flex w="full" justifyContent="center">
|
||||||
<Flex direction="column" alignItems="center" gap="2" maxW="sm" w="full" mx="4">
|
<Flex direction="column" alignItems="center" gap="2" maxW="md" w="full" px="4" py="10">
|
||||||
<Avatar src="/apple-touch-icon.png" size="lg" flexShrink={0} />
|
<Avatar src="/apple-touch-icon.png" size="lg" flexShrink={0} />
|
||||||
<Heading size="lg" mb="2">
|
<Heading size="lg" mb="2">
|
||||||
Sign in
|
Sign in
|
||||||
</Heading>
|
</Heading>
|
||||||
<Outlet />
|
<Outlet />
|
||||||
</Flex>
|
</Flex>
|
||||||
</Center>
|
</Flex>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -23,7 +23,7 @@ export default function LoginNostrConnectView() {
|
|||||||
|
|
||||||
await client.connect();
|
await client.connect();
|
||||||
} else if (uri.startsWith("npub")) {
|
} else if (uri.startsWith("npub")) {
|
||||||
client = nostrConnectService.fromNsecBunkerToken(uri);
|
client = nostrConnectService.fromBunkerToken(uri);
|
||||||
const [npub, hexToken] = uri.split("#");
|
const [npub, hexToken] = uri.split("#");
|
||||||
await client.connect(hexToken);
|
await client.connect(hexToken);
|
||||||
} else throw new Error("Unknown format");
|
} else throw new Error("Unknown format");
|
||||||
@ -44,12 +44,13 @@ export default function LoginNostrConnectView() {
|
|||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Flex as="form" direction="column" gap="4" onSubmit={handleSubmit} minWidth="350" w="full">
|
<Flex as="form" direction="column" gap="4" onSubmit={handleSubmit} w="full">
|
||||||
{loading && <Text fontSize="lg">{loading}</Text>}
|
{loading && <Text fontSize="lg">{loading}</Text>}
|
||||||
{!loading && (
|
{!loading && (
|
||||||
<FormControl>
|
<FormControl>
|
||||||
<FormLabel>Connect URI</FormLabel>
|
<FormLabel>Connect URI</FormLabel>
|
||||||
<Input
|
<Input
|
||||||
|
name="nostr-address"
|
||||||
placeholder="bunker://<pubkey>?relay=wss://relay.example.com"
|
placeholder="bunker://<pubkey>?relay=wss://relay.example.com"
|
||||||
isRequired
|
isRequired
|
||||||
value={uri}
|
value={uri}
|
||||||
|
@ -28,7 +28,7 @@ export default function LoginNpubView() {
|
|||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Flex as="form" direction="column" gap="4" onSubmit={handleSubmit} minWidth="350" w="full">
|
<Flex as="form" direction="column" gap="4" onSubmit={handleSubmit} w="full">
|
||||||
<FormControl>
|
<FormControl>
|
||||||
<FormLabel>Enter user npub</FormLabel>
|
<FormLabel>Enter user npub</FormLabel>
|
||||||
<Input type="text" placeholder="npub1" isRequired value={npub} onChange={(e) => setNpub(e.target.value)} />
|
<Input type="text" placeholder="npub1" isRequired value={npub} onChange={(e) => setNpub(e.target.value)} />
|
||||||
|
@ -85,7 +85,7 @@ export default function LoginNsecView() {
|
|||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Flex as="form" direction="column" gap="4" onSubmit={handleSubmit} minWidth="350" w="full">
|
<Flex as="form" direction="column" gap="4" onSubmit={handleSubmit} w="full">
|
||||||
<Alert status="warning" maxWidth="30rem">
|
<Alert status="warning" maxWidth="30rem">
|
||||||
<AlertIcon />
|
<AlertIcon />
|
||||||
<Box>
|
<Box>
|
||||||
|
@ -24,6 +24,7 @@ import { COMMON_CONTACT_RELAY } from "../../const";
|
|||||||
import accountService from "../../services/account";
|
import accountService from "../../services/account";
|
||||||
import serialPortService from "../../services/serial-port";
|
import serialPortService from "../../services/serial-port";
|
||||||
import amberSignerService from "../../services/amber-signer";
|
import amberSignerService from "../../services/amber-signer";
|
||||||
|
import { AtIcon } from "../../components/icons";
|
||||||
|
|
||||||
export default function LoginStartView() {
|
export default function LoginStartView() {
|
||||||
const location = useLocation();
|
const location = useLocation();
|
||||||
@ -109,14 +110,14 @@ export default function LoginStartView() {
|
|||||||
if (loading) return <Spinner />;
|
if (loading) return <Spinner />;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Flex direction="column" gap="2" flexShrink={0} alignItems="center">
|
<>
|
||||||
{window.nostr && (
|
{window.nostr && (
|
||||||
<Button onClick={signinWithExtension} leftIcon={<Key01 boxSize={6} />} w="sm" colorScheme="primary">
|
<Button onClick={signinWithExtension} leftIcon={<Key01 boxSize={6} />} w="full" colorScheme="primary">
|
||||||
Sign in with extension
|
Sign in with extension
|
||||||
</Button>
|
</Button>
|
||||||
)}
|
)}
|
||||||
<Button as={RouterLink} to="./nostr-connect" state={location.state} w="sm" colorScheme="blue">
|
<Button as={RouterLink} to="./address" state={location.state} w="full" colorScheme="blue" leftIcon={<AtIcon />}>
|
||||||
Nostr Connect (NIP-46)
|
Nostr Address
|
||||||
</Button>
|
</Button>
|
||||||
{serialPortService.supported && (
|
{serialPortService.supported && (
|
||||||
<ButtonGroup colorScheme="purple">
|
<ButtonGroup colorScheme="purple">
|
||||||
@ -151,27 +152,24 @@ export default function LoginStartView() {
|
|||||||
<Button
|
<Button
|
||||||
variant="link"
|
variant="link"
|
||||||
onClick={advanced.onToggle}
|
onClick={advanced.onToggle}
|
||||||
mt="2"
|
py="2"
|
||||||
w="sm"
|
w="full"
|
||||||
rightIcon={advanced.isOpen ? <ChevronUp /> : <ChevronDown />}
|
rightIcon={advanced.isOpen ? <ChevronUp /> : <ChevronDown />}
|
||||||
>
|
>
|
||||||
Show Advanced
|
Show Advanced
|
||||||
</Button>
|
</Button>
|
||||||
{advanced.isOpen && (
|
{advanced.isOpen && (
|
||||||
<>
|
<>
|
||||||
<Button as={RouterLink} to="./nip05" state={location.state} w="sm">
|
<Button as={RouterLink} to="./nostr-connect" state={location.state} w="full">
|
||||||
DNS ID
|
Nostr Connect / Bunker
|
||||||
<Badge ml="2" colorScheme="blue">
|
|
||||||
read-only
|
|
||||||
</Badge>
|
|
||||||
</Button>
|
</Button>
|
||||||
<Button as={RouterLink} to="./npub" state={location.state} w="sm">
|
<Button as={RouterLink} to="./npub" state={location.state} w="full">
|
||||||
Public key (npub)
|
Public key (npub)
|
||||||
<Badge ml="2" colorScheme="blue">
|
<Badge ml="2" colorScheme="blue">
|
||||||
read-only
|
read-only
|
||||||
</Badge>
|
</Badge>
|
||||||
</Button>
|
</Button>
|
||||||
<Button as={RouterLink} to="./nsec" state={location.state} w="sm">
|
<Button as={RouterLink} to="./nsec" state={location.state} w="full">
|
||||||
Secret key (nsec)
|
Secret key (nsec)
|
||||||
</Button>
|
</Button>
|
||||||
</>
|
</>
|
||||||
@ -179,9 +177,17 @@ export default function LoginStartView() {
|
|||||||
<Text fontWeight="bold" mt="4">
|
<Text fontWeight="bold" mt="4">
|
||||||
Don't have an account?
|
Don't have an account?
|
||||||
</Text>
|
</Text>
|
||||||
<Button as={RouterLink} to="/signup" state={location.state}>
|
<Button
|
||||||
|
as={RouterLink}
|
||||||
|
to="/signup"
|
||||||
|
state={location.state}
|
||||||
|
colorScheme="primary"
|
||||||
|
variant="outline"
|
||||||
|
maxW="xs"
|
||||||
|
w="full"
|
||||||
|
>
|
||||||
Sign up
|
Sign up
|
||||||
</Button>
|
</Button>
|
||||||
</Flex>
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -88,7 +88,7 @@ export default function CreateStep({
|
|||||||
<Flex gap="4" {...containerProps}>
|
<Flex gap="4" {...containerProps}>
|
||||||
<Avatar size="xl" src={preview} />
|
<Avatar size="xl" src={preview} />
|
||||||
<Flex direction="column" alignItems="center">
|
<Flex direction="column" alignItems="center">
|
||||||
<Heading size="md">{metadata.display_name}</Heading>
|
<Heading size="md">{metadata.displayName}</Heading>
|
||||||
{metadata.about && <Text>{metadata.about}</Text>}
|
{metadata.about && <Text>{metadata.about}</Text>}
|
||||||
</Flex>
|
</Flex>
|
||||||
<Button w="full" colorScheme="primary" isLoading={loading} onClick={createProfile} autoFocus>
|
<Button w="full" colorScheme="primary" isLoading={loading} onClick={createProfile} autoFocus>
|
||||||
|
@ -33,7 +33,7 @@ export default function SignupView() {
|
|||||||
case "profile":
|
case "profile":
|
||||||
return (
|
return (
|
||||||
<ProfileImageStep
|
<ProfileImageStep
|
||||||
displayName={metadata.display_name}
|
displayName={metadata.displayName}
|
||||||
onSubmit={(file) => {
|
onSubmit={(file) => {
|
||||||
setProfileImage(file);
|
setProfileImage(file);
|
||||||
navigate("/signup/relays");
|
navigate("/signup/relays");
|
||||||
|
@ -20,7 +20,7 @@ export default function NameStep({ onSubmit }: { onSubmit: (metadata: Kind0Parse
|
|||||||
|
|
||||||
onSubmit({
|
onSubmit({
|
||||||
name: username,
|
name: username,
|
||||||
display_name: displayName,
|
displayName: displayName,
|
||||||
about: values.about,
|
about: values.about,
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@ -38,7 +38,7 @@ export default function NameStep({ onSubmit }: { onSubmit: (metadata: Kind0Parse
|
|||||||
Next
|
Next
|
||||||
</Button>
|
</Button>
|
||||||
<Text fontWeight="bold">Already have an account?</Text>
|
<Text fontWeight="bold">Already have an account?</Text>
|
||||||
<Button as={RouterLink} to="/signin" state={location.state}>
|
<Button as={RouterLink} to="/signin" state={location.state} variant="outline" w="xs" colorScheme="primary">
|
||||||
Sign in
|
Sign in
|
||||||
</Button>
|
</Button>
|
||||||
</Flex>
|
</Flex>
|
||||||
|
Loading…
x
Reference in New Issue
Block a user