show federation popups

This commit is contained in:
Paul Miller
2024-05-08 16:48:23 -05:00
parent 87d8964b86
commit f27e87178a
11 changed files with 131 additions and 118 deletions

View File

@@ -568,7 +568,8 @@
"join_me": "Join me", "join_me": "Join me",
"recommend": "Recommend federation", "recommend": "Recommend federation",
"recommended_by_you": "Recommended by you", "recommended_by_you": "Recommended by you",
"transfer_funds": "Transfer funds" "transfer_funds": "Transfer funds",
"transfer_funds_message": "Add a second federation to enable transfers."
}, },
"gift": { "gift": {
"give_sats_link": "Give sats as a gift", "give_sats_link": "Give sats as a gift",

View File

@@ -8,7 +8,6 @@ import {
createSignal, createSignal,
For, For,
Match, Match,
onMount,
Show, Show,
Suspense, Suspense,
Switch Switch
@@ -19,6 +18,7 @@ import {
Button, Button,
ButtonCard, ButtonCard,
ContactButton, ContactButton,
FederationPopup,
LoadingShimmer, LoadingShimmer,
NiceP, NiceP,
SimpleDialog SimpleDialog
@@ -365,7 +365,7 @@ function NewContactModal(props: { profile: PseudoContact; close: () => void }) {
} }
export function CombinedActivity() { export function CombinedActivity() {
const [state, actions, sw] = useMegaStore(); const [state, _actions, sw] = useMegaStore();
const i18n = useI18n(); const i18n = useI18n();
const [detailsOpen, setDetailsOpen] = createSignal(false); const [detailsOpen, setDetailsOpen] = createSignal(false);
@@ -408,17 +408,6 @@ export function CombinedActivity() {
const [newContact, setNewContact] = createSignal<PseudoContact>(); const [newContact, setNewContact] = createSignal<PseudoContact>();
const [
showFederationExpirationWarning,
setShowFederationExpirationWarning
] = createSignal(false);
onMount(() => {
if (state.expiration_warning) {
setShowFederationExpirationWarning(true);
}
});
return ( return (
<> <>
<Show when={detailsId() && detailsKind()}> <Show when={detailsId() && detailsKind()}>
@@ -437,30 +426,7 @@ export function CombinedActivity() {
</Show> </Show>
<Suspense fallback={<LoadingShimmer />}> <Suspense fallback={<LoadingShimmer />}>
<Show when={state.expiration_warning}> <Show when={state.expiration_warning}>
<SimpleDialog <FederationPopup />
title={i18n.t("activity.federation_message")}
open={showFederationExpirationWarning()}
setOpen={(open: boolean) => {
if (!open) {
setShowFederationExpirationWarning(false);
actions.clearExpirationWarning();
}
}}
>
<NiceP>
{state.expiration_warning?.expiresMessage}
</NiceP>
<ButtonCard
onClick={() => navigate("/settings/federations")}
>
<div class="flex items-center gap-2">
<Users class="inline-block text-m-red" />
<NiceP>
{i18n.t("profile.manage_federation")}
</NiceP>
</div>
</ButtonCard>
</SimpleDialog>
</Show> </Show>
<Show when={!state.has_backed_up}> <Show when={!state.has_backed_up}>
<ButtonCard <ButtonCard

View File

@@ -23,14 +23,14 @@ export type MethodChoice = {
maxAmountSats?: bigint; maxAmountSats?: bigint;
}; };
// Make sure to update this when we get the fedi option in here!
function methodToIcon(method: MethodChoice["method"]) { function methodToIcon(method: MethodChoice["method"]) {
if (method === "lightning") { switch (method) {
return "lightning"; case "lightning":
} else if (method === "onchain") { return "lightning";
return "chain"; case "onchain":
} else if (method === "fedimint") { return "chain";
return "community"; case "fedimint":
return "community";
} }
} }

View File

@@ -0,0 +1,45 @@
import { useNavigate } from "@solidjs/router";
import { Users } from "lucide-solid";
import { createSignal } from "solid-js";
import { ButtonCard, NiceP, SimpleDialog } from "~/components/layout";
import { useI18n } from "~/i18n/context";
import { useMegaStore } from "~/state/megaStore";
export function FederationPopup() {
const [state, actions, _sw] = useMegaStore();
const [
showFederationExpirationWarning,
setShowFederationExpirationWarning
] = createSignal(!state.expiration_warning_seen);
const i18n = useI18n();
const navigate = useNavigate();
return (
<SimpleDialog
title={i18n.t("activity.federation_message")}
open={showFederationExpirationWarning()}
setOpen={(open: boolean) => {
if (!open) {
setShowFederationExpirationWarning(false);
actions.clearExpirationWarning();
}
}}
>
<NiceP>{state.expiration_warning?.expiresMessage}</NiceP>
<ButtonCard
onClick={() => {
actions.clearExpirationWarning();
setShowFederationExpirationWarning(false);
navigate("/settings/federations");
}}
>
<div class="flex items-center gap-2">
<Users class="inline-block text-m-red" />
<NiceP>{i18n.t("profile.manage_federation")}</NiceP>
</div>
</ButtonCard>
</SimpleDialog>
);
}

View File

@@ -56,3 +56,4 @@ export * from "./EditProfileForm";
export * from "./ImportNsecForm"; export * from "./ImportNsecForm";
export * from "./LightningAddressShower"; export * from "./LightningAddressShower";
export * from "./FederationInviteShower"; export * from "./FederationInviteShower";
export * from "./FederationPopup";

View File

@@ -1,10 +1,5 @@
import { MutinyInvoice, TagItem } from "@mutinywallet/mutiny-wasm"; import { MutinyInvoice, TagItem } from "@mutinywallet/mutiny-wasm";
import { import { useLocation, useNavigate, useSearchParams } from "@solidjs/router";
createAsync,
useLocation,
useNavigate,
useSearchParams
} from "@solidjs/router";
import { Eye, EyeOff, Link, X, Zap } from "lucide-solid"; import { Eye, EyeOff, Link, X, Zap } from "lucide-solid";
import { import {
createEffect, createEffect,

View File

@@ -1,6 +1,6 @@
import { createForm, required } from "@modular-forms/solid"; import { createForm, required } from "@modular-forms/solid";
import { MutinyChannel } from "@mutinywallet/mutiny-wasm"; import { MutinyChannel } from "@mutinywallet/mutiny-wasm";
import { createAsync, useNavigate } from "@solidjs/router"; import { useNavigate } from "@solidjs/router";
import { import {
createEffect, createEffect,
createMemo, createMemo,

View File

@@ -36,10 +36,6 @@ export function Transfer() {
const [loading, setLoading] = createSignal(false); const [loading, setLoading] = createSignal(false);
const [params] = useSearchParams(); const [params] = useSearchParams();
const canTransfer = createMemo(() => {
return true;
});
const [transferResult, setTransferResult] = const [transferResult, setTransferResult] =
createSignal<TransferResultDetails>(); createSignal<TransferResultDetails>();
@@ -62,9 +58,10 @@ export function Transfer() {
}); });
const calculateMaxFederation = createAsync(async () => { const calculateMaxFederation = createAsync(async () => {
return federationBalances()?.find( const balance = federationBalances()?.find(
(f) => f.identity_federation_id === fromFed()?.federation_id (f) => f.identity_federation_id === fromFed()?.federation_id
)?.balance; )?.balance;
return balance || 0n;
}); });
const toBalance = createAsync(async () => { const toBalance = createAsync(async () => {
@@ -77,6 +74,11 @@ export function Transfer() {
return amountSats() === calculateMaxFederation(); return amountSats() === calculateMaxFederation();
}); });
const canTransfer = createMemo(() => {
if (!calculateMaxFederation()) return false;
return amountSats() > 0n && amountSats() <= calculateMaxFederation()!;
});
async function handleTransfer() { async function handleTransfer() {
try { try {
setLoading(true); setLoading(true);
@@ -111,8 +113,6 @@ export function Transfer() {
} }
} }
// const fromFederatationId = params.from;
return ( return (
<MutinyWalletGuard> <MutinyWalletGuard>
<DefaultMain> <DefaultMain>
@@ -171,7 +171,11 @@ export function Transfer() {
</Match> </Match>
</Switch> </Switch>
</SuccessModal> </SuccessModal>
<BackLink href="/settings/federations" /> <BackLink
title={i18n.t("common.back")}
href="/settings/federations"
showOnDesktop
/>
<LargeHeader>{i18n.t("transfer.title")}</LargeHeader> <LargeHeader>{i18n.t("transfer.title")}</LargeHeader>
<div class="flex flex-1 flex-col justify-between gap-2"> <div class="flex flex-1 flex-col justify-between gap-2">
<div class="flex-1" /> <div class="flex-1" />

View File

@@ -29,6 +29,7 @@ import {
ExternalLink, ExternalLink,
FancyCard, FancyCard,
FederationInviteShower, FederationInviteShower,
FederationPopup,
InfoBox, InfoBox,
KeyValue, KeyValue,
LabelCircle, LabelCircle,
@@ -40,6 +41,7 @@ import {
NavBar, NavBar,
NiceP, NiceP,
showToast, showToast,
SimpleDialog,
SubtleButton, SubtleButton,
TextField, TextField,
VStack VStack
@@ -59,6 +61,8 @@ export type MutinyFederationIdentity = {
federation_expiry_timestamp: number; federation_expiry_timestamp: number;
invite_code: string; invite_code: string;
meta_external_url?: string; meta_external_url?: string;
popup_end_timestamp?: number;
popup_countdown_message?: string;
}; };
export type Metadata = { export type Metadata = {
@@ -91,7 +95,7 @@ export function AddFederationForm(props: {
browseOnly?: boolean; browseOnly?: boolean;
}) { }) {
const i18n = useI18n(); const i18n = useI18n();
const [_state, actions, sw] = useMegaStore(); const [state, actions, sw] = useMegaStore();
const navigate = useNavigate(); const navigate = useNavigate();
const [error, setError] = createSignal<Error>(); const [error, setError] = createSignal<Error>();
const [success, setSuccess] = createSignal(""); const [success, setSuccess] = createSignal("");
@@ -181,6 +185,9 @@ export function AddFederationForm(props: {
return ( return (
<div class="flex w-full flex-col gap-4"> <div class="flex w-full flex-col gap-4">
<Show when={state.expiration_warning}>
<FederationPopup />
</Show>
<Show when={!props.setup && !props.browseOnly}> <Show when={!props.setup && !props.browseOnly}>
<MediumHeader> <MediumHeader>
{i18n.t("settings.manage_federations.manual")} {i18n.t("settings.manage_federations.manual")}
@@ -446,8 +453,15 @@ function FederationListItem(props: {
setConfirmOpen(true); setConfirmOpen(true);
} }
const [transferDialogOpen, setTransferDialogOpen] = createSignal(false);
async function transferFunds() { async function transferFunds() {
navigate("/transfer?from=" + props.fed.federation_id); // If there's only one federation we need to let them know to add another
if (state.federations?.length && state.federations.length < 2) {
setTransferDialogOpen(true);
} else {
navigate("/transfer?from=" + props.fed.federation_id);
}
} }
const [confirmOpen, setConfirmOpen] = createSignal(false); const [confirmOpen, setConfirmOpen] = createSignal(false);
@@ -457,7 +471,6 @@ function FederationListItem(props: {
<> <>
<FancyCard> <FancyCard>
<VStack> <VStack>
{/* <pre>{JSON.stringify(props.fed, null, 2)}</pre> */}
<Show when={props.fed.federation_name}> <Show when={props.fed.federation_name}>
<header class={`font-semibold`}> <header class={`font-semibold`}>
{props.fed.federation_name} {props.fed.federation_name}
@@ -466,6 +479,19 @@ function FederationListItem(props: {
<Show when={props.fed.welcome_message}> <Show when={props.fed.welcome_message}>
<p>{props.fed.welcome_message}</p> <p>{props.fed.welcome_message}</p>
</Show> </Show>
<SimpleDialog
title={i18n.t(
"settings.manage_federations.transfer_funds"
)}
open={transferDialogOpen()}
setOpen={setTransferDialogOpen}
>
<NiceP>
{i18n.t(
"settings.manage_federations.transfer_funds_message"
)}
</NiceP>
</SimpleDialog>
<Show when={props.balance !== undefined}> <Show when={props.balance !== undefined}>
<KeyValue <KeyValue
key={i18n.t("activity.transaction_details.balance")} key={i18n.t("activity.transaction_details.balance")}
@@ -499,19 +525,10 @@ function FederationListItem(props: {
inviteCode={props.fed.invite_code} inviteCode={props.fed.invite_code}
/> />
</KeyValue> </KeyValue>
<Show <SubtleButton onClick={transferFunds}>
when={ <ArrowLeftRight class="h-4 w-4" />
state.federations?.length && {i18n.t("settings.manage_federations.transfer_funds")}
state.federations.length === 2 </SubtleButton>
}
>
<SubtleButton onClick={transferFunds}>
<ArrowLeftRight class="h-4 w-4" />
{i18n.t(
"settings.manage_federations.transfer_funds"
)}
</SubtleButton>
</Show>
<Suspense> <Suspense>
<RecommendButton fed={props.fed} /> <RecommendButton fed={props.fed} />
</Suspense> </Suspense>

View File

@@ -90,7 +90,8 @@ export const makeMegaStoreContext = () => {
balanceView: localStorage.getItem("balanceView") || "sats", balanceView: localStorage.getItem("balanceView") || "sats",
expiration_warning: undefined as expiration_warning: undefined as
| { expiresTimestamp: number; expiresMessage: string } | { expiresTimestamp: number; expiresMessage: string }
| undefined | undefined,
expiration_warning_seen: false
}); });
const actions = { const actions = {
@@ -235,45 +236,14 @@ export const makeMegaStoreContext = () => {
| { expiresTimestamp: number; expiresMessage: string } | { expiresTimestamp: number; expiresMessage: string }
| undefined = undefined; | undefined = undefined;
try { federations.forEach((f) => {
if (federations.length) { if (f.popup_countdown_message && f.popup_end_timestamp) {
const activeFederation = federations[0]; expiration_warning = {
const metadataUrl = activeFederation.meta_external_url; expiresTimestamp: f.popup_end_timestamp,
console.log("federation metadata url", metadataUrl); expiresMessage: f.popup_countdown_message
if (metadataUrl) { };
const response = await fetch(metadataUrl);
if (response.ok) {
const metadata = await response.json();
console.log(
"all federation metadata",
metadata
);
const specificFederation =
metadata[activeFederation.federation_id];
console.log(
"specific federation metadata",
specificFederation
);
const expiresTimestamp =
specificFederation.popup_end_timestamp;
console.log(
"federation expires",
expiresTimestamp
);
const expiresMessage =
specificFederation.popup_countdown_message;
expiration_warning = {
expiresTimestamp,
expiresMessage
};
}
}
} }
} catch (e) { });
console.error("Error getting federation metadata", e);
}
console.log("expiration_warning", expiration_warning);
setState({ setState({
wallet_loading: false, wallet_loading: false,
@@ -513,7 +483,21 @@ export const makeMegaStoreContext = () => {
}, },
async refreshFederations() { async refreshFederations() {
const federations = await sw.list_federations(); const federations = await sw.list_federations();
setState({ federations });
let expiration_warning:
| { expiresTimestamp: number; expiresMessage: string }
| undefined = undefined;
federations.forEach((f) => {
if (f.popup_countdown_message && f.popup_end_timestamp) {
expiration_warning = {
expiresTimestamp: f.popup_end_timestamp,
expiresMessage: f.popup_countdown_message
};
}
});
setState({ federations, expiration_warning });
}, },
cycleBalanceView() { cycleBalanceView() {
if (state.balanceView === "sats") { if (state.balanceView === "sats") {
@@ -556,7 +540,7 @@ export const makeMegaStoreContext = () => {
}, },
// Only show the expiration warning once per session // Only show the expiration warning once per session
clearExpirationWarning() { clearExpirationWarning() {
setState({ expiration_warning: undefined }); setState({ expiration_warning_seen: true });
} }
}; };

View File

@@ -217,7 +217,6 @@ export async function get_balance(): Promise<MutinyBalance> {
*/ */
export async function list_federations(): Promise<MutinyFederationIdentity[]> { export async function list_federations(): Promise<MutinyFederationIdentity[]> {
const federations = await wallet!.list_federations(); const federations = await wallet!.list_federations();
console.log("list_federations", federations);
return federations as MutinyFederationIdentity[]; return federations as MutinyFederationIdentity[];
} }
@@ -1382,6 +1381,7 @@ export async function delete_federation_recommendation(
*/ */
export async function get_federation_balances(): Promise<FederationBalances> { export async function get_federation_balances(): Promise<FederationBalances> {
const balances = await wallet!.get_federation_balances(); const balances = await wallet!.get_federation_balances();
if (!balances) return { balances: [] } as unknown as FederationBalances;
// PAIN // PAIN
// Have to rebuild the balances from the raw data, which is a bit of a pain // Have to rebuild the balances from the raw data, which is a bit of a pain
const newBalances: FederationBalance[] = []; const newBalances: FederationBalance[] = [];