Mint discoverability

This commit is contained in:
benthecarman
2024-03-08 23:24:25 +00:00
committed by Tony Giorgio
parent e6352e5ae7
commit 5f3695b18e
6 changed files with 354 additions and 203 deletions

View File

@@ -540,8 +540,10 @@
"remove": "Remove",
"expires": "Expires",
"federation_id": "Federation ID",
"description": "Mutiny has experimental support for the Fedimint protocol. You'll need a federation invite code to use this feature. Store funds in a federation at your own risk!",
"learn_more": "Learn more about Fedimint."
"description": "Mutiny has experimental support for the Fedimint protocol. Store funds in a federation at your own risk!",
"learn_more": "Learn more about Fedimint.",
"discover": "Discover Federations",
"manual": "Invite Code"
},
"gift": {
"give_sats_link": "Give sats as a gift",

View File

@@ -495,7 +495,7 @@
"remove": "Eliminar",
"expires": "Expira",
"federation_id": "ID federación",
"description": "Mutiny tiene soporte experimental para el protocolo Fedimint. Necesitará un código de invitación a la federación para poder usar esta funcionalidad ¡Almacene fondos en una federación bajo su propio riesgo!",
"description": "Mutiny tiene soporte experimental para el protocolo Fedimint. ¡Almacene fondos en una federación bajo su propio riesgo!",
"learn_more": "Aprenda más sobre Fedimint."
},
"gift": {

View File

@@ -63,55 +63,46 @@ export function BalanceBox(props: { loading?: boolean; small?: boolean }) {
<VStack>
<Switch>
<Match when={state.federations && state.federations.length}>
<div>
<MediumHeader>Fedimint</MediumHeader>
<FancyCard>
<Show
when={!props.loading}
fallback={<LoadingShimmer />}
>
<div class="flex justify-between">
<div class="flex flex-col gap-1">
<div class="text-2xl">
<AmountSats
amountSats={
state.balance?.federation ||
0n
}
icon="community"
denominationSize="lg"
isFederation
/>
</div>
<div class="text-lg text-white/70">
<AmountFiat
amountSats={
state.balance?.federation ||
0n
}
denominationSize="sm"
/>
</div>
<MediumHeader>Fedimint</MediumHeader>
<FancyCard>
<Show
when={!props.loading}
fallback={<LoadingShimmer />}
>
<div class="flex justify-between">
<div class="flex flex-col gap-1">
<div class="text-2xl">
<AmountSats
amountSats={
state.balance?.federation || 0n
}
icon="community"
denominationSize="lg"
isFederation
/>
</div>
<div class="text-lg text-white/70">
<AmountFiat
amountSats={
state.balance?.federation || 0n
}
denominationSize="sm"
/>
</div>
<Show
when={
state.balance?.federation || 0n > 0n
}
>
<div class="self-end justify-self-end">
<A
href="/swaplightning"
class={STYLE}
>
<Shuffle class="h-6 w-6" />
</A>
</div>
</Show>
</div>
</Show>
</FancyCard>
</div>
<Show
when={state.balance?.federation || 0n > 0n}
>
<div class="self-end justify-self-end">
<A href="/swaplightning" class={STYLE}>
<Shuffle class="h-6 w-6" />
</A>
</div>
</Show>
</div>
</Show>
</FancyCard>
<ButtonCard
onClick={() => navigate("/settings/federations")}
>
@@ -132,80 +123,78 @@ export function BalanceBox(props: { loading?: boolean; small?: boolean }) {
</ButtonCard>
</Match>
</Switch>
<div>
<MediumHeader>{i18n.t("profile.self_custody")}</MediumHeader>
<FancyCard>
<Show when={!props.loading} fallback={<LoadingShimmer />}>
<Switch>
<Match when={state.safe_mode}>
<div class="flex flex-col gap-1">
<InfoBox accent="red">
{i18n.t("common.error_safe_mode")}
</InfoBox>
</div>
</Match>
<Match when={true}>
<div class="flex flex-col gap-1">
<div class="text-2xl">
<AmountSats
amountSats={
state.balance?.lightning || 0
}
icon="lightning"
denominationSize="lg"
/>
</div>
<div class="text-lg text-white/70">
<AmountFiat
amountSats={
state.balance?.lightning || 0
}
denominationSize="sm"
/>
</div>
</div>
</Match>
</Switch>
</Show>
<hr class="my-2 border-m-grey-750" />
<Show when={!props.loading} fallback={<LoadingShimmer />}>
<div class="flex justify-between">
<MediumHeader>{i18n.t("profile.self_custody")}</MediumHeader>
<FancyCard>
<Show when={!props.loading} fallback={<LoadingShimmer />}>
<Switch>
<Match when={state.safe_mode}>
<div class="flex flex-col gap-1">
<InfoBox accent="red">
{i18n.t("common.error_safe_mode")}
</InfoBox>
</div>
</Match>
<Match when={true}>
<div class="flex flex-col gap-1">
<div class="text-2xl">
<AmountSats
amountSats={totalOnchain()}
icon="chain"
amountSats={
state.balance?.lightning || 0
}
icon="lightning"
denominationSize="lg"
/>
</div>
<div class="text-lg text-white/70">
<AmountFiat
amountSats={totalOnchain()}
amountSats={
state.balance?.lightning || 0
}
denominationSize="sm"
/>
</div>
</div>
<div class="flex flex-col items-end justify-between gap-1">
<Show when={state.balance?.unconfirmed != 0n}>
<Indicator>
{i18n.t("common.pending")}
</Indicator>
</Show>
<Show when={state.balance?.unconfirmed === 0n}>
<div />
</Show>
<Show when={usableOnchain() > 0n}>
<div class="self-end justify-self-end">
<A href="/swap" class={STYLE}>
<Shuffle class="h-6 w-6" />
</A>
</div>
</Show>
</Match>
</Switch>
</Show>
<hr class="my-2 border-m-grey-750" />
<Show when={!props.loading} fallback={<LoadingShimmer />}>
<div class="flex justify-between">
<div class="flex flex-col gap-1">
<div class="text-2xl">
<AmountSats
amountSats={totalOnchain()}
icon="chain"
denominationSize="lg"
/>
</div>
<div class="text-lg text-white/70">
<AmountFiat
amountSats={totalOnchain()}
denominationSize="sm"
/>
</div>
</div>
</Show>
</FancyCard>
</div>
<div class="flex flex-col items-end justify-between gap-1">
<Show when={state.balance?.unconfirmed != 0n}>
<Indicator>
{i18n.t("common.pending")}
</Indicator>
</Show>
<Show when={state.balance?.unconfirmed === 0n}>
<div />
</Show>
<Show when={usableOnchain() > 0n}>
<div class="self-end justify-self-end">
<A href="/swap" class={STYLE}>
<Shuffle class="h-6 w-6" />
</A>
</div>
</Show>
</div>
</div>
</Show>
</FancyCard>
</VStack>
);
}

View File

@@ -78,9 +78,11 @@ export const FancyCard: ParentComponent<{
}> = (props) => {
return (
<VStack smallgap>
<div class="mt-2 pl-4">
<SmallHeader>{props.title}</SmallHeader>
</div>
<Show when={props.title}>
<div class="mt-2 pl-4">
<SmallHeader>{props.title}</SmallHeader>
</div>
</Show>
<div class="flex flex-col gap-2 rounded-xl border border-b-4 border-black/50 bg-m-grey-900 p-4 shadow-fancy-card">
{props.children}
</div>

View File

@@ -103,17 +103,6 @@ export function Profile() {
</FancyCard>
</Show>
</div>
{/* <div>
<MediumHeader>{i18n.t("profile.social")}</MediumHeader>
<FancyCard>
<KeyValue key="LN Address">
<MiniStringShower text={profile().lud16 || ""} />
</KeyValue>
<KeyValue key="npub">
<MiniStringShower text={npub() || ""} />
</KeyValue>
</FancyCard>
</div> */}
<ButtonCard onClick={() => navigate("/editprofile")}>
<div class="flex items-center gap-2">
{/* <Users class="inline-block text-m-red" /> */}
@@ -122,7 +111,6 @@ export function Profile() {
</div>
</ButtonCard>
<BalanceBox loading={state.wallet_loading} />
<NavBar activeTab="profile" />
</DefaultMain>
</MutinyWalletGuard>

View File

@@ -5,7 +5,7 @@ import {
setValue,
SubmitHandler
} from "@modular-forms/solid";
import { FederationBalance } from "@mutinywallet/mutiny-wasm";
import { FederationBalance, TagItem } from "@mutinywallet/mutiny-wasm";
import { A, useSearchParams } from "@solidjs/router";
import { Scan } from "lucide-solid";
import {
@@ -30,7 +30,10 @@ import {
FancyCard,
InfoBox,
KeyValue,
LabelCircle,
LargeHeader,
LoadingSpinner,
MediumHeader,
MiniStringShower,
MutinyWalletGuard,
NavBar,
@@ -54,6 +57,22 @@ export type MutinyFederationIdentity = {
invite_code: string;
};
export type Metadata = {
name: string;
picture?: string;
about?: string;
};
export type DiscoveredFederation = {
id: string;
invite_codes: string[];
pubkey: string;
created_at: number;
event_id: string;
metadata: Metadata | undefined;
recommendations: TagItem[]; // fixme, not the best type to use here
};
type RefetchType = (
info?: unknown
) =>
@@ -68,6 +87,8 @@ function AddFederationForm(props: { refetch?: RefetchType }) {
const [error, setError] = createSignal<Error>();
const [success, setSuccess] = createSignal("");
const [loadingFederation, setLoadingFederation] = createSignal("");
const [params, setParams] = useSearchParams();
onMount(() => {
@@ -88,12 +109,28 @@ function AddFederationForm(props: { refetch?: RefetchType }) {
const handleSubmit: SubmitHandler<FederationForm> = async (
f: FederationForm
) => {
const federation_code = f.federation_code.trim();
await onSelect(federation_code);
};
const [federations] = createResource(async () => {
try {
const federations: DiscoveredFederation[] =
await state.mutiny_wallet?.discover_federations();
return federations;
} catch (e) {
console.error(e);
return [];
}
});
const onSelect = async (inviteCode: string) => {
setSuccess("");
setError(undefined);
setLoadingFederation(inviteCode);
try {
const federation_code = f.federation_code.trim();
const newFederation =
await state.mutiny_wallet?.new_federation(federation_code);
await state.mutiny_wallet?.new_federation(inviteCode);
console.log("New federation added:", newFederation);
setSuccess(
i18n.t("settings.manage_federations.federation_added_success")
@@ -107,50 +144,163 @@ function AddFederationForm(props: { refetch?: RefetchType }) {
console.error("Error submitting federation:", e);
setError(eify(e));
}
setLoadingFederation("");
};
return (
<Form onSubmit={handleSubmit}>
<VStack>
<Field
name="federation_code"
validate={[
required(
i18n.t(
"settings.manage_federations.federation_code_required"
<>
<MediumHeader>
{i18n.t("settings.manage_federations.manual")}
</MediumHeader>
<Form onSubmit={handleSubmit}>
<VStack>
<Field
name="federation_code"
validate={[
required(
i18n.t(
"settings.manage_federations.federation_code_required"
)
)
)
]}
>
{(field, props) => (
<TextField
{...props}
{...field}
error={field.error}
label={i18n.t(
"settings.manage_federations.federation_code_label"
]}
>
{(field, props) => (
<TextField
{...props}
{...field}
error={field.error}
placeholder="fed11..."
required
/>
)}
</Field>
<Button
loading={feedbackForm.submitting}
disabled={feedbackForm.invalid || !feedbackForm.dirty}
intent="blue"
type="submit"
>
{i18n.t("settings.manage_federations.add")}
</Button>
<Show when={error()}>
<InfoBox accent="red">{error()?.message}</InfoBox>
</Show>
<Show when={success()}>
<InfoBox accent="green">{success()}</InfoBox>
</Show>
</VStack>
</Form>
<MediumHeader>
{i18n.t("settings.manage_federations.discover")}
</MediumHeader>
<Suspense>
<Switch>
<Match when={federations.loading}>
<div class="flex flex-col items-center justify-center">
<LoadingSpinner wide />
</div>
</Match>
<Match when={federations.latest}>
<For each={federations()}>
{(fed) => (
<FancyCard>
<VStack>
<div class="flex items-center gap-2 md:gap-4">
<LabelCircle
name={fed.metadata?.name}
image_url={
fed.metadata?.picture
}
contact={false}
label={false}
/>
<div>
<header class={`font-semibold`}>
{fed.metadata?.name}
</header>
<Show
when={fed.metadata?.about}
>
<p>{fed.metadata?.about}</p>
</Show>
</div>
</div>
<KeyValue
key={i18n.t(
"settings.manage_federations.federation_id"
)}
>
<MiniStringShower text={fed.id} />
</KeyValue>
<Show when={fed.created_at}>
<KeyValue key="created at">
{/* todo i18n */}
<time>
{timeAgo(fed.created_at)}
</time>
</KeyValue>
</Show>
<KeyValue key={"invite code"}>
{/* todo i18n, handle singular vs plural */}
<For each={fed.invite_codes}>
{(invite) => (
<MiniStringShower
text={invite}
/>
)}
</For>
</KeyValue>
<Show
when={
fed.recommendations.length > 0
}
>
<KeyValue key={"recommended by"}>
{/* todo i18n */}
<div class="flex items-center gap-2 md:gap-4">
<For
each={
fed.recommendations
}
>
{(contact) => (
<LabelCircle
name={
contact.name
}
image_url={
contact.image_url
}
contact={true}
label={false}
/>
)}
</For>
</div>
</KeyValue>
</Show>
{/* FIXME: do something smarter than just taking first code */}
<Button
intent="blue"
onClick={() =>
onSelect(fed.invite_codes[0])
}
loading={fed.invite_codes.includes(
loadingFederation()
)}
>
{i18n.t(
"settings.manage_federations.add"
)}
</Button>
</VStack>
</FancyCard>
)}
placeholder="fed11..."
required
/>
)}
</Field>
<Button
loading={feedbackForm.submitting}
disabled={feedbackForm.invalid}
intent="blue"
type="submit"
>
{i18n.t("settings.manage_federations.add")}
</Button>
<Show when={error()}>
<InfoBox accent="red">{error()?.message}</InfoBox>
</Show>
<Show when={success()}>
<InfoBox accent="green">{success()}</InfoBox>
</Show>
</VStack>
</Form>
</For>
</Match>
</Switch>
</Suspense>
</>
);
}
@@ -174,50 +324,73 @@ function FederationListItem(props: {
setConfirmLoading(false);
}
async function recommendFederation() {
setRecommendLoading(true);
try {
const event_id = await state.mutiny_wallet?.recommend_federation(
props.fed.invite_code
);
console.log("Recommended federation: ", event_id);
} catch (e) {
console.error("Error recommending federation: ", e);
}
setRecommendLoading(false);
}
async function confirmRemove() {
setConfirmOpen(true);
}
const [confirmOpen, setConfirmOpen] = createSignal(false);
const [confirmLoading, setConfirmLoading] = createSignal(false);
const [recommendLoading, setRecommendLoading] = createSignal(false);
return (
<>
<FancyCard>
<Show when={props.fed.federation_name}>
<header class={`font-semibold`}>
{props.fed.federation_name}
</header>
</Show>
<Show when={props.fed.welcome_message}>
<p>{props.fed.welcome_message}</p>
</Show>
<Show when={props.balance !== undefined}>
<KeyValue
key={i18n.t("activity.transaction_details.balance")}
>
<AmountSats
amountSats={props.balance}
denominationSize={"sm"}
isFederation
/>
</KeyValue>
</Show>
<Show when={props.fed.federation_expiry_timestamp}>
<KeyValue
key={i18n.t("settings.manage_federations.expires")}
>
<time>
{timeAgo(props.fed.federation_expiry_timestamp)}
</time>
</KeyValue>
</Show>
<KeyValue
key={i18n.t("settings.manage_federations.federation_id")}
>
<MiniStringShower text={props.fed.federation_id} />
</KeyValue>
<VStack>
<Show when={props.fed.federation_name}>
<header class={`font-semibold`}>
{props.fed.federation_name}
</header>
</Show>
<Show when={props.fed.welcome_message}>
<p>{props.fed.welcome_message}</p>
</Show>
<Show when={props.balance !== undefined}>
<KeyValue
key={i18n.t("activity.transaction_details.balance")}
>
<AmountSats
amountSats={props.balance}
denominationSize={"sm"}
isFederation
/>
</KeyValue>
</Show>
<Show when={props.fed.federation_expiry_timestamp}>
<KeyValue
key={i18n.t("settings.manage_federations.expires")}
>
<time>
{timeAgo(props.fed.federation_expiry_timestamp)}
</time>
</KeyValue>
</Show>
<KeyValue
key={i18n.t(
"settings.manage_federations.federation_id"
)}
>
<MiniStringShower text={props.fed.federation_id} />
</KeyValue>
<Button
intent="blue"
onClick={recommendFederation}
loading={recommendLoading()}
>
Recommend
</Button>
<div class="w-full rounded-xl bg-white">
<QRCodeSVG
value={props.fed.invite_code}
@@ -279,9 +452,6 @@ export function ManageFederations() {
</LargeHeader>
<NiceP>
{i18n.t("settings.manage_federations.description")}{" "}
<ExternalLink href="https://fedimint.org/">
{i18n.t("settings.manage_federations.learn_more")}
</ExternalLink>
</NiceP>
<Suspense>
<Show when={!state.federations?.length}>