show existing accounts on sign in view

This commit is contained in:
hzrd149 2025-03-23 17:06:09 +00:00
parent 073cc52ea1
commit 3b8c4f2134
16 changed files with 126 additions and 92 deletions

View File

@ -9,7 +9,7 @@ import { removeCoordinateTag, addCoordinateTag } from "applesauce-factory/operat
import useFavoriteFeeds, { FAVORITE_FEEDS_IDENTIFIER } from "../../hooks/use-favorite-feeds";
import { usePublishEvent } from "../../providers/global/publish-provider";
import { StarEmptyIcon, StarFullIcon } from "../icons";
import useAsyncErrorHandler from "../../hooks/use-async-error-handler";
import useAsyncAction from "../../hooks/use-async-error-handler";
export default function DVMFeedFavoriteButton({
pointer,
@ -20,7 +20,7 @@ export default function DVMFeedFavoriteButton({
const { favorites } = useFavoriteFeeds();
const isFavorite = !!favorites && isAddressPointerInList(favorites, pointer);
const toggle = useAsyncErrorHandler(async () => {
const toggle = useAsyncAction(async () => {
const prev = favorites || {
kind: kinds.Application,
tags: [["d", FAVORITE_FEEDS_IDENTIFIER]],

View File

@ -21,7 +21,7 @@ import useUserSets from "../../hooks/use-user-lists";
import { getListName } from "../../helpers/nostr/lists";
import { getEventCoordinate } from "../../helpers/nostr/event";
import useUserContactList from "../../hooks/use-user-contact-list";
import useAsyncErrorHandler from "../../hooks/use-async-error-handler";
import useAsyncAction from "../../hooks/use-async-error-handler";
import NewSetModal from "../../views/lists/components/new-set-modal";
import useUserMuteActions from "../../hooks/use-user-mute-actions";
import { useMuteModalContext } from "../../providers/route/mute-modal-provider";
@ -37,7 +37,7 @@ function UsersLists({ pubkey }: { pubkey: string }) {
const inLists = lists.filter((list) => isProfilePointerInList(list, { pubkey }));
const handleChange = useAsyncErrorHandler(
const handleChange = useAsyncAction(
async (cords: string | string[]) => {
if (!Array.isArray(cords)) return;
@ -98,7 +98,7 @@ export function UserFollowButton({ pubkey, showLists, ...props }: UserFollowButt
const isFollowing = !!contacts && isProfilePointerInList(contacts, pubkey);
const toggleFollow = useAsyncErrorHandler(async () => {
const toggleFollow = useAsyncAction(async () => {
if (isFollowing) {
await actions.exec(UnfollowUser, pubkey).forEach((e) => publish("Unfollow user", e));
} else {

View File

@ -1,17 +1,19 @@
import { useToast } from "@chakra-ui/react";
import { DependencyList, useCallback, useState } from "react";
import { DependencyList, useCallback, useRef, useState } from "react";
export default function useAsyncErrorHandler<Args extends Array<any>, T = any>(
export default function useAsyncAction<Args extends Array<any>, T = any>(
fn: (...args: Args) => Promise<T>,
deps: DependencyList,
deps: DependencyList = [],
): { loading: boolean; run: (...args: Args) => Promise<T | undefined> } {
const toast = useToast();
const ref = useRef(fn);
ref.current = fn;
const toast = useToast();
const [loading, setLoading] = useState(false);
const run = useCallback<(...args: Args) => Promise<T | undefined>>(async (...args: Args) => {
setLoading(true);
try {
return await fn(...args);
return await ref.current(...args);
} catch (e) {
if (e instanceof Error) toast({ description: e.message, status: "error" });
}

View File

@ -8,7 +8,7 @@ import {
pruneExpiredPubkeys,
} from "../helpers/nostr/mute-list";
import { usePublishEvent } from "../providers/global/publish-provider";
import useAsyncErrorHandler from "./use-async-error-handler";
import useAsyncAction from "./use-async-error-handler";
import useUserMuteList from "./use-user-mute-list";
export default function useUserMuteActions(pubkey: string) {
@ -19,12 +19,12 @@ export default function useUserMuteActions(pubkey: string) {
const isMuted = isPubkeyInList(muteList, pubkey);
const expiration = muteList ? getPubkeyExpiration(muteList, pubkey) : 0;
const { run: mute } = useAsyncErrorHandler(async () => {
const { run: mute } = useAsyncAction(async () => {
let draft = muteListAddPubkey(muteList || createEmptyMuteList(), pubkey);
draft = pruneExpiredPubkeys(draft);
await publish("Mute", draft, undefined, false);
}, [publish, muteList]);
const { run: unmute } = useAsyncErrorHandler(async () => {
const { run: unmute } = useAsyncAction(async () => {
let draft = muteListRemovePubkey(muteList || createEmptyMuteList(), pubkey);
draft = pruneExpiredPubkeys(draft);
await publish("Unmute", draft, undefined, false);

View File

@ -6,7 +6,7 @@ import { nip19 } from "nostr-tools";
import UserAvatar from "../../../components/user/user-avatar";
import UserDnsIdentity from "../../../components/user/user-dns-identity";
import { NostrEvent } from "../../../types/nostr-event";
import useAsyncErrorHandler from "../../../hooks/use-async-error-handler";
import useAsyncAction from "../../../hooks/use-async-error-handler";
import { useActiveAccount, useEventFactory } from "applesauce-react/hooks";
import { UserFollowButton } from "../../../components/user/user-follow-button";
import { usePublishEvent } from "../../../providers/global/publish-provider";
@ -19,7 +19,7 @@ export default function UserCard({ pubkey, relay, list, ...props }: UserCardProp
const publish = usePublishEvent();
const factory = useEventFactory();
const remove = useAsyncErrorHandler(async () => {
const remove = useAsyncAction(async () => {
const draft = await factory.modifyTags(list, removePubkeyTag(pubkey));
const signed = await factory.sign(draft);
await publish("Remove from list", signed);

View File

@ -2,7 +2,7 @@ import { Button, Flex, Heading, Link, useToast } from "@chakra-ui/react";
import { PasswordSigner, SerialPortSigner, SimpleSigner } from "applesauce-signers";
import { useState } from "react";
import useAsyncErrorHandler from "../../../../hooks/use-async-error-handler";
import useAsyncAction from "../../../../hooks/use-async-error-handler";
import { useAccountManager, useActiveAccount } from "applesauce-react/hooks";
import accountService from "../../../../services/accounts";
import { SerialPortAccount } from "applesauce-accounts/accounts";
@ -15,7 +15,7 @@ export default function MigrateAccountToDevice() {
const [loading, setLoading] = useState(false);
const manager = useAccountManager();
const { run: migrate } = useAsyncErrorHandler(async () => {
const { run: migrate } = useAsyncAction(async () => {
try {
setLoading(true);
if (!current?.signer) throw new Error("Account missing signer");

View File

@ -3,7 +3,7 @@ import { Button, Flex, Heading, IconButton, Input, Link, Select, Switch, Text }
import { useForm } from "react-hook-form";
import { useObservable } from "applesauce-react/hooks";
import useAsyncErrorHandler from "../../../../hooks/use-async-error-handler";
import useAsyncAction from "../../../../hooks/use-async-error-handler";
import { controlApi$ } from "../../../../services/bakery";
import RelayFavicon from "../../../../components/relay-favicon";
import { isSafeRelayURL, normalizeURL } from "applesauce-core/helpers";
@ -11,7 +11,7 @@ import { isSafeRelayURL, normalizeURL } from "applesauce-core/helpers";
function BroadcastRelay({ relay }: { relay: string }) {
const controlApi = useObservable(controlApi$);
const config = useObservable(controlApi?.config);
const remove = useAsyncErrorHandler(async () => {
const remove = useAsyncAction(async () => {
if (!config) return;
await controlApi?.setConfigField(

View File

@ -10,7 +10,7 @@ import { useActiveAccount } from "applesauce-react/hooks";
import { InboxIcon, OutboxIcon } from "../../../components/icons";
import MediaServerFavicon from "../../../components/favicon/media-server-favicon";
import { NostrEvent } from "../../../types/nostr-event";
import useAsyncErrorHandler from "../../../hooks/use-async-error-handler";
import useAsyncAction from "../../../hooks/use-async-error-handler";
import { usePublishEvent } from "../../../providers/global/publish-provider";
import { addRelayModeToMailbox, removeRelayModeFromMailbox } from "../../../helpers/nostr/mailbox";
import AddRelayForm from "../relays/add-relay-form";
@ -22,7 +22,7 @@ import { RelayMode } from "../../../services/app-relays";
function RelayLine({ relay, mode, list }: { relay: string; mode: RelayMode; list?: NostrEvent }) {
const publish = usePublishEvent();
const remove = useAsyncErrorHandler(async () => {
const remove = useAsyncAction(async () => {
const draft = removeRelayModeFromMailbox(list, relay, mode);
await publish("Remove relay", draft, COMMON_CONTACT_RELAYS);
}, [relay, mode, list, publish]);

View File

@ -25,7 +25,7 @@ import useUsersMediaServers from "../../../hooks/use-user-media-servers";
import DebugEventButton from "../../../components/debug-modal/debug-event-button";
import { cloneEvent } from "../../../helpers/nostr/event";
import useAppSettings from "../../../hooks/use-user-app-settings";
import useAsyncErrorHandler from "../../../hooks/use-async-error-handler";
import useAsyncAction from "../../../hooks/use-async-error-handler";
import { isServerTag } from "../../../helpers/nostr/blossom";
import { USER_BLOSSOM_SERVER_LIST_KIND, areServersEqual } from "blossom-client-sdk";
import SimpleView from "../../../components/layout/presets/simple-view";
@ -64,7 +64,7 @@ function MediaServersPage() {
await publish("Remove media server", draft);
};
const { run: switchToBlossom } = useAsyncErrorHandler(async () => {
const { run: switchToBlossom } = useAsyncAction(async () => {
await updateSettings({ mediaUploadService: "blossom" });
}, [updateSettings]);

View File

@ -1,9 +1,21 @@
import { lazy, useState } from "react";
import { Button, ButtonGroup, Divider, Flex, IconButton, Link, Spinner, Text, useToast } from "@chakra-ui/react";
import {
Box,
Button,
ButtonGroup,
Card,
Divider,
Flex,
IconButton,
Link,
Spinner,
Text,
useToast,
} from "@chakra-ui/react";
import { Link as RouterLink, useLocation } from "react-router-dom";
import { AmberClipboardAccount, ExtensionAccount, SerialPortAccount } from "applesauce-accounts/accounts";
import { AmberClipboardSigner, ExtensionSigner, SerialPortSigner } from "applesauce-signers";
import { useAccountManager } from "applesauce-react/hooks";
import { useAccountManager, useAccounts } from "applesauce-react/hooks";
import Key01 from "../../components/icons/key-01";
import Diamond01 from "../../components/icons/diamond-01";
@ -14,6 +26,12 @@ import { CAP_IS_ANDROID, CAP_IS_WEB } from "../../env";
import { AtIcon } from "../../components/icons";
import Package from "../../components/icons/package";
import Eye from "../../components/icons/eye";
import useAsyncAction from "../../hooks/use-async-error-handler";
import UserAvatar from "../../components/user/user-avatar";
import UserName from "../../components/user/user-name";
import UserDnsIdentity from "../../components/user/user-dns-identity";
import { CloseIcon } from "@chakra-ui/icons";
import AccountTypeBadge from "../../components/accounts/account-info-badge";
const AndroidNativeSigners = lazy(() => import("./native"));
export default function LoginStartView() {
@ -21,64 +39,47 @@ export default function LoginStartView() {
const toast = useToast();
const [loading, setLoading] = useState(false);
const manager = useAccountManager();
const accounts = useAccounts();
const signinWithExtension = async () => {
if (window.nostr) {
try {
setLoading(true);
const extension = useAsyncAction(async () => {
if (!window.nostr) throw new Error("Missing NIP-07 signer extension");
const signer = new ExtensionSigner();
const pubkey = await signer.getPublicKey();
const signer = new ExtensionSigner();
const pubkey = await signer.getPublicKey();
const account = new ExtensionAccount(pubkey, signer);
manager.addAccount(account);
manager.setActive(account);
} catch (e) {
if (e instanceof Error) toast({ description: e.message, status: "error" });
}
setLoading(false);
} else {
toast({ status: "warning", title: "Cant find extension" });
}
};
const account = new ExtensionAccount(pubkey, signer);
manager.addAccount(account);
manager.setActive(account);
});
const signinWithSerial = async () => {
if (SerialPortSigner.SUPPORTED) {
try {
setLoading(true);
const serial = useAsyncAction(async () => {
if (!SerialPortSigner.SUPPORTED) throw new Error("Serial is not supported");
const signer = new SerialPortSigner();
const pubkey = await signer.getPublicKey();
const account = new SerialPortAccount(pubkey, signer);
manager.addAccount(account);
manager.setActive(account);
} catch (e) {
if (e instanceof Error) toast({ description: e.message, status: "error" });
}
setLoading(false);
} else {
toast({ status: "warning", title: "Serial is not supported" });
}
};
const signer = new SerialPortSigner();
const pubkey = await signer.getPublicKey();
const account = new SerialPortAccount(pubkey, signer);
manager.addAccount(account);
manager.setActive(account);
});
const signinWithAmber = async () => {
try {
const signer = new AmberClipboardSigner();
const pubkey = await signer.getPublicKey();
const account = new AmberClipboardAccount(pubkey, signer);
manager.addAccount(account);
manager.setActive(account);
} catch (e) {
if (e instanceof Error) toast({ description: e.message, status: "error" });
}
};
if (loading) return <Spinner />;
const amber = useAsyncAction(async () => {
const signer = new AmberClipboardSigner();
const pubkey = await signer.getPublicKey();
const account = new AmberClipboardAccount(pubkey, signer);
manager.addAccount(account);
manager.setActive(account);
});
return (
<>
{window.nostr && (
<Button onClick={signinWithExtension} leftIcon={<Key01 boxSize={6} />} w="full" colorScheme="primary">
<Button
onClick={extension.run}
isLoading={extension.loading}
leftIcon={<Key01 boxSize={6} />}
w="full"
colorScheme="primary"
>
Sign in with extension
</Button>
)}
@ -94,7 +95,7 @@ export default function LoginStartView() {
</Button>
{SerialPortSigner.SUPPORTED && (
<ButtonGroup colorScheme="purple">
<Button onClick={signinWithSerial} leftIcon={<UsbFlashDrive boxSize={6} />} w="xs">
<Button onClick={serial.run} isLoading={serial.loading} leftIcon={<UsbFlashDrive boxSize={6} />} w="xs">
Use Signing Device
</Button>
<IconButton
@ -109,7 +110,7 @@ export default function LoginStartView() {
)}
{CAP_IS_WEB && AmberClipboardSigner.SUPPORTED && (
<ButtonGroup colorScheme="orange" w="full">
<Button onClick={signinWithAmber} leftIcon={<Diamond01 boxSize={6} />} flex={1}>
<Button onClick={amber.run} isLoading={amber.loading} leftIcon={<Diamond01 boxSize={6} />} flex={1}>
Use Amber
</Button>
<IconButton
@ -166,6 +167,37 @@ export default function LoginStartView() {
Public key
</Button>
</Flex>
{accounts.length > 0 && (
<>
<Text fontWeight="bold" mt="4">
Existing accounts
</Text>
{accounts.map((account) => (
<Card key={account.id} p="2" display="flex" direction="row" gap="2" alignItems="center" w="full" maxW="md">
<UserAvatar pubkey={account.pubkey} size="md" />
<Box>
<UserName pubkey={account.pubkey} />
<br />
<AccountTypeBadge account={account} />
</Box>
<ButtonGroup ms="auto">
<Button variant="ghost" onClick={() => manager.setActive(account)}>
Sign in
</Button>
<IconButton
aria-label="Delete account"
icon={<CloseIcon />}
variant="ghost"
colorScheme="red"
onClick={() => manager.removeAccount(account)}
/>
</ButtonGroup>
</Card>
))}
</>
)}
<Text fontWeight="bold" mt="4">
Don't have an account?
</Text>

View File

@ -5,7 +5,7 @@ import { NostrEvent } from "nostr-tools";
import { ExternalLinkIcon } from "../../../components/icons";
import useShareableEventAddress from "../../../hooks/use-shareable-event-address";
import { AppHandlerContext } from "../../../providers/route/app-handler-provider";
import useAsyncErrorHandler from "../../../hooks/use-async-error-handler";
import useAsyncAction from "../../../hooks/use-async-error-handler";
export type StreamOpenButtonProps = Omit<IconButtonProps, "onClick" | "aria-label"> & {
stream: NostrEvent;
@ -21,7 +21,7 @@ export default function StreamOpenButton({
const { openAddress } = useContext(AppHandlerContext);
const address = useShareableEventAddress(stream);
const { run: handleClick } = useAsyncErrorHandler(async () => {
const { run: handleClick } = useAsyncAction(async () => {
if (!address) throw new Error("Failed to get address");
openAddress(address);
}, [address]);

View File

@ -3,14 +3,14 @@ import { ConsolidateTokens } from "applesauce-wallet/actions";
import { useActionHub, useActiveAccount } from "applesauce-react/hooks";
import useUserWallet from "../../../hooks/use-user-wallet";
import useAsyncErrorHandler from "../../../hooks/use-async-error-handler";
import useAsyncAction from "../../../hooks/use-async-error-handler";
export default function ConsolidateTokensButton({ children, ...props }: Omit<ButtonProps, "onClick" | "isLoading">) {
const account = useActiveAccount()!;
const wallet = useUserWallet(account.pubkey);
const actions = useActionHub();
const consolidate = useAsyncErrorHandler(async () => {
const consolidate = useAsyncAction(async () => {
if (!wallet) throw new Error("Missing wallet");
await actions.run(ConsolidateTokens);
}, [wallet, actions]);

View File

@ -3,14 +3,14 @@ import { useActionHub, useActiveAccount } from "applesauce-react/hooks";
import { UnlockWallet } from "applesauce-wallet/actions";
import useUserWallet from "../../../hooks/use-user-wallet";
import useAsyncErrorHandler from "../../../hooks/use-async-error-handler";
import useAsyncAction from "../../../hooks/use-async-error-handler";
export default function WalletUnlockButton({ children, ...props }: Omit<ButtonProps, "onClick" | "isLoading">) {
const account = useActiveAccount()!;
const wallet = useUserWallet(account.pubkey);
const actions = useActionHub();
const unlock = useAsyncErrorHandler(async () => {
const unlock = useAsyncAction(async () => {
if (!wallet) throw new Error("Missing wallet");
if (wallet.locked === false) return;

View File

@ -8,7 +8,7 @@ import SimpleView from "../../../components/layout/presets/simple-view";
import RouterLink from "../../../components/router-link";
import CashuMintFavicon from "../../../components/cashu/cashu-mint-favicon";
import CashuMintName from "../../../components/cashu/cashu-mint-name";
import useAsyncErrorHandler from "../../../hooks/use-async-error-handler";
import useAsyncAction from "../../../hooks/use-async-error-handler";
import { getCashuWallet } from "../../../services/cashu-mints";
export default function WalletReceiveTokenView() {
@ -24,7 +24,7 @@ export default function WalletReceiveTokenView() {
const decoded = getDecodedToken(token);
const originalAmount = decoded.proofs.reduce((t, p) => t + p.amount, 0);
const receive = useAsyncErrorHandler(async () => {
const receive = useAsyncAction(async () => {
try {
// swap tokens
const wallet = await getCashuWallet(decoded.mint);
@ -46,7 +46,7 @@ export default function WalletReceiveTokenView() {
}
}, [decoded, originalAmount, actions, navigate, toast]);
const swap = useAsyncErrorHandler(async () => {}, [decoded, originalAmount, actions, navigate, toast]);
const swap = useAsyncAction(async () => {}, [decoded, originalAmount, actions, navigate, toast]);
return (
<SimpleView title="Receive Token" maxW="xl" center>

View File

@ -27,7 +27,7 @@ import DebugEventButton from "../../../components/debug-modal/debug-event-button
import ArrowBlockUp from "../../../components/icons/arrow-block-up";
import ArrowBlockDown from "../../../components/icons/arrow-block-down";
import useEventIntersectionRef from "../../../hooks/use-event-intersection-ref";
import useAsyncErrorHandler from "../../../hooks/use-async-error-handler";
import useAsyncAction from "../../../hooks/use-async-error-handler";
import { useDeleteEventContext } from "../../../providers/route/delete-event-provider";
import { ChevronDownIcon, ChevronUpIcon, TrashIcon } from "../../../components/icons";
import useEventUpdate from "../../../hooks/use-event-update";
@ -53,7 +53,7 @@ function HistoryEntry({ entry }: { entry: NostrEvent }) {
const redeemedIds = getHistoryRedeemed(entry);
const redeemed = useSingleEvents(redeemedIds);
const { run: unlock } = useAsyncErrorHandler(async () => {
const { run: unlock } = useAsyncAction(async () => {
await unlockHistoryContent(entry, account);
eventStore.update(entry);
}, [entry, account, eventStore]);
@ -136,7 +136,7 @@ export default function WalletHistoryTab() {
const history = useStoreQuery(WalletHistoryQuery, [account.pubkey]) ?? [];
const locked = useStoreQuery(WalletHistoryQuery, [account.pubkey, true]) ?? [];
const { run: unlock } = useAsyncErrorHandler(async () => {
const { run: unlock } = useAsyncAction(async () => {
for (const entry of locked) {
if (!isHistoryContentLocked(entry)) continue;
await unlockHistoryContent(entry, account);
@ -144,7 +144,7 @@ export default function WalletHistoryTab() {
}
}, [locked, account, eventStore]);
const clear = useAsyncErrorHandler(async () => {
const clear = useAsyncAction(async () => {
if (confirm("Are you sure you want to clear history?") !== true) return;
const draft = await factory.delete(history);
await publish("Clear history", draft);

View File

@ -19,7 +19,7 @@ import { getTokenContent, isTokenContentLocked, unlockTokenContent } from "apple
import { NostrEvent } from "nostr-tools";
import { getEncodedToken, ProofState } from "@cashu/cashu-ts";
import useAsyncErrorHandler from "../../../hooks/use-async-error-handler";
import useAsyncAction from "../../../hooks/use-async-error-handler";
import useEventUpdate from "../../../hooks/use-event-update";
import useEventIntersectionRef from "../../../hooks/use-event-intersection-ref";
import {
@ -52,7 +52,7 @@ function TokenEvent({ token }: { token: NostrEvent }) {
const amount = details?.proofs.reduce((t, p) => t + p.amount, 0);
const [spentState, setSpentState] = useState<ProofState[]>();
const { run: check } = useAsyncErrorHandler(async () => {
const { run: check } = useAsyncAction(async () => {
if (!details) return;
const wallet = await getCashuWallet(details.mint);
const state = await wallet.checkProofsStates(details.proofs);
@ -62,7 +62,7 @@ function TokenEvent({ token }: { token: NostrEvent }) {
const { deleteEvent } = useDeleteEventContext();
const { run: unlock } = useAsyncErrorHandler(async () => {
const { run: unlock } = useAsyncAction(async () => {
if (!account) return;
await unlockTokenContent(token, account);
eventStore.update(token);
@ -153,7 +153,7 @@ export default function WalletTokensTab({ ...props }: Omit<FlexProps, "children"
const tokens = useStoreQuery(WalletTokensQuery, [account.pubkey]) ?? [];
const locked = useStoreQuery(WalletTokensQuery, [account.pubkey, true]) ?? [];
const { run: unlock } = useAsyncErrorHandler(async () => {
const { run: unlock } = useAsyncAction(async () => {
if (!locked) return;
for (const token of locked) {
await unlockTokenContent(token, account);