diff --git a/src/assets/treasure-closed.png b/src/assets/treasure-closed.png new file mode 100644 index 000000000..2f458c848 Binary files /dev/null and b/src/assets/treasure-closed.png differ diff --git a/src/assets/treasure.gif b/src/assets/treasure.gif new file mode 100644 index 000000000..ddc039c90 Binary files /dev/null and b/src/assets/treasure.gif differ diff --git a/src/components/AmountEditable.tsx b/src/components/AmountEditable.tsx index 8aca93492..fd885127f 100644 --- a/src/components/AmountEditable.tsx +++ b/src/components/AmountEditable.tsx @@ -361,7 +361,8 @@ export const AmountEditable: ParentComponent<{ } } - function handleClose() { + function handleClose(e: SubmitEvent | MouseEvent | KeyboardEvent) { + e.preventDefault(); props.setAmountSats(BigInt(props.initialAmountSats)); setIsOpen(false); setLocalSats(props.initialAmountSats); @@ -373,6 +374,7 @@ export const AmountEditable: ParentComponent<{ ) ); props.exitRoute && navigate(props.exitRoute); + return false; } // What we're all here for in the first place: returning a value @@ -381,6 +383,7 @@ export const AmountEditable: ParentComponent<{ props.setAmountSats(BigInt(localSats())); setLocalFiat(satsToUsd(state.price, Number(localSats()) || 0, false)); setIsOpen(false); + return false; } function handleSatsInput(e: InputEvent) { @@ -438,6 +441,7 @@ export const AmountEditable: ParentComponent<{ return ( + + + + + + + + + + + ); +} diff --git a/src/routes/Scanner.tsx b/src/routes/Scanner.tsx index 965d71961..a08b3d1a6 100644 --- a/src/routes/Scanner.tsx +++ b/src/routes/Scanner.tsx @@ -5,12 +5,11 @@ import { useNavigate } from "solid-start"; import { Button, Scanner as Reader, showToast } from "~/components"; import { useI18n } from "~/i18n/context"; -import { toParsedParams } from "~/logic/waila"; import { useMegaStore } from "~/state/megaStore"; export default function Scanner() { const i18n = useI18n(); - const [state, actions] = useMegaStore(); + const [_state, actions] = useMegaStore(); const [scanResult, setScanResult] = createSignal(); const navigate = useNavigate(); @@ -44,22 +43,16 @@ export default function Scanner() { // When we have a nice result we can head over to the send screen createEffect(() => { if (scanResult()) { - const network = state.mutiny_wallet?.get_network() || "signet"; - const result = toParsedParams(scanResult() || "", network); - if (!result.ok) { - showToast(result.error); - return; - } else { - if ( - result.value?.address || - result.value?.invoice || - result.value?.node_pubkey || - result.value?.lnurl - ) { - actions.setScanResult(result.value); + actions.handleIncomingString( + scanResult()!, + (error) => { + showToast(error); + }, + (result) => { + actions.setScanResult(result); navigate("/send"); } - } + ); } }); diff --git a/src/routes/Send.tsx b/src/routes/Send.tsx index 7765d815f..e50ce03f2 100644 --- a/src/routes/Send.tsx +++ b/src/routes/Send.tsx @@ -44,7 +44,7 @@ import { } from "~/components"; import { useI18n } from "~/i18n/context"; import { Network } from "~/logic/mutinyWalletSetup"; -import { ParsedParams, toParsedParams } from "~/logic/waila"; +import { ParsedParams } from "~/logic/waila"; import { useMegaStore } from "~/state/megaStore"; import eify from "~/utils/eify"; import mempoolTxUrl from "~/utils/mempoolTxUrl"; @@ -371,25 +371,17 @@ export default function Send() { }); function parsePaste(text: string) { - if (text) { - const network = state.mutiny_wallet?.get_network() || "signet"; - const result = toParsedParams(text || "", network); - if (!result.ok) { - showToast(result.error); - return; - } else { - if ( - result.value?.address || - result.value?.invoice || - result.value?.node_pubkey || - result.value?.lnurl - ) { - setDestination(result.value); - // Important! we need to clear the scan result once we've used it - actions.setScanResult(undefined); - } + actions.handleIncomingString( + text, + (error) => { + showToast(error); + }, + (result) => { + setDestination(result); + // Important! we need to clear the scan result once we've used it + actions.setScanResult(undefined); } - } + ); } function handleDecode() { diff --git a/src/routes/settings/Connections.tsx b/src/routes/settings/Connections.tsx index 250d37e51..d55ac9cec 100644 --- a/src/routes/settings/Connections.tsx +++ b/src/routes/settings/Connections.tsx @@ -162,27 +162,36 @@ function Nwc() { - + - + + + + setConfirmOpen(false)} + > + {i18n.t("settings.gift.send_delete_confirm")} + + + ); +} + +function ExistingGifts() { + const [state, _actions] = useMegaStore(); + + const [giftNWCProfiles, { refetch }] = createResource(async () => { + try { + const profiles: NwcProfile[] = + await state.mutiny_wallet?.get_nwc_profiles(); + + const filteredForGifts = profiles.filter((p) => p.tag === "Gift"); + + return filteredForGifts; + } catch (e) { + console.error(e); + } + }); + + return ( + 0}> + + + {(profile) => ( + + + + )} + + + + ); +} + +export default function GiftPage() { + const i18n = useI18n(); + const [state, _actions] = useMegaStore(); + + const [_error, setError] = createSignal(); + + const [giftResult, setGiftResult] = createSignal(); + + const [giftForm, { Form, Field }] = createForm({ + initialValues: { + name: "", + amount: "50000" + } + }); + + function resetGifting() { + reset(giftForm); + setGiftResult(undefined); + } + + const handleSubmit: SubmitHandler = async ( + f: CreateGiftForm + ) => { + const nwc_name = f.name.trim(); + const amount = Number(f.amount); + + try { + const profile = await state.mutiny_wallet?.create_single_use_nwc( + nwc_name, + BigInt(amount) + ); + + setGiftResult(profile); + } catch (e) { + console.error(e); + setError(eify(e)); + } + }; + + async function fetchProfile(gift?: NwcProfile) { + if (!gift) return; + try { + const fresh = await state.mutiny_wallet?.get_nwc_profile( + gift.index + ); + return fresh; + } catch (e) { + console.error(e); + } + } + + const [freshProfile, { refetch }] = createResource( + () => giftResult(), + fetchProfile, + { + storage: createDeepSignal + } + ); + + createEffect(() => { + // Should re-run after every sync + if (!state.is_syncing) { + refetch(); + } + }); + + const lessThan50k = () => { + return Number(getValue(giftForm, "amount")) < 50000; + }; + + return ( + + + + + + + + + + {i18n.t( + "settings.gift.send_header_claimed" + )} + + + {i18n.t("settings.gift.send_claimed")} + + + + + {i18n.t( + "settings.gift.send_sharable_header" + )} + + + {i18n.t( + "settings.gift.send_instructions" + )} + + + + + + + + + + {i18n.t("settings.gift.send_header")} + +
+ + + {i18n.t("settings.gift.send_explainer")} + + + {(field, props) => ( + + )} + + + {(field) => ( + <> + + setValue( + giftForm, + "amount", + newAmount.toString() + ) + } + /> + + )} + + + + {i18n.t( + "settings.gift.send_small_warning" + )} + + + + +
+ }> + + +
+
+ +
+
+ ); +} diff --git a/src/routes/settings/Plus.tsx b/src/routes/settings/Plus.tsx index f184d5b14..21b91e366 100644 --- a/src/routes/settings/Plus.tsx +++ b/src/routes/settings/Plus.tsx @@ -192,7 +192,7 @@ function PlusCTA() { {i18n.t("settings.plus.title")} - ?{i18n.t("settings.plus.click_confirm")} + ? {i18n.t("settings.plus.click_confirm")}

diff --git a/src/routes/settings/index.tsx b/src/routes/settings/index.tsx index 7b30cc31e..f77e8dfce 100644 --- a/src/routes/settings/index.tsx +++ b/src/routes/settings/index.tsx @@ -117,6 +117,10 @@ export default function Settings() { href: "/settings/connections", text: i18n.t("settings.connections.title") }, + { + href: "/settings/gift", + text: i18n.t("settings.gift.title") + }, { href: "/settings/lnurlauth", text: i18n.t("settings.lnurl_auth.title") diff --git a/src/state/megaStore.tsx b/src/state/megaStore.tsx index 8787b02b4..2138ef1a6 100644 --- a/src/state/megaStore.tsx +++ b/src/state/megaStore.tsx @@ -1,7 +1,11 @@ /* @refresh reload */ // Inspired by https://github.com/solidjs/solid-realworld/blob/main/src/store/index.js -import { MutinyBalance, MutinyWallet } from "@mutinywallet/mutiny-wasm"; +import { + MutinyBalance, + MutinyWallet, + NwcProfile +} from "@mutinywallet/mutiny-wasm"; import { createContext, onCleanup, @@ -10,7 +14,7 @@ import { useContext } from "solid-js"; import { createStore } from "solid-js/store"; -import { useSearchParams } from "solid-start"; +import { useNavigate, useSearchParams } from "solid-start"; import { checkBrowserCompatibility } from "~/logic/browserCompatibility"; import { @@ -20,7 +24,7 @@ import { MutinyWalletSettingStrings, setupMutinyWallet } from "~/logic/mutinyWalletSetup"; -import { ParsedParams } from "~/logic/waila"; +import { ParsedParams, toParsedParams } from "~/logic/waila"; import eify from "~/utils/eify"; import { subscriptionValid } from "~/utils/subscriptions"; import { MutinyTagItem } from "~/utils/tags"; @@ -63,11 +67,18 @@ export type MegaStore = [ setHasBackedUp(): void; listTags(): Promise; checkForSubscription(justPaid?: boolean): Promise; + handleIncomingString( + str: string, + onError: (e: Error) => void, + onSuccess: (value: ParsedParams) => void + ): Promise; + archiveNwcProfileByIndex(profile_index: number): Promise; } ]; export const Provider: ParentComponent = (props) => { const [searchParams] = useSearchParams(); + const navigate = useNavigate(); const [state, setState] = createStore({ mutiny_wallet: undefined as MutinyWallet | undefined, @@ -237,6 +248,55 @@ export const Provider: ParentComponent = (props) => { console.error(e); return []; } + }, + handleIncomingString( + str: string, + onError: (e: Error) => void, + onSuccess: (value: ParsedParams) => void + ): void { + // First we'll check for gifts + if (str.includes("/gift?amount=")) { + navigate("/gift" + str.split("/gift")[1]); + return; + } + + const network = state.mutiny_wallet?.get_network() || "signet"; + const result = toParsedParams(str || "", network); + if (!result.ok) { + if (onError) { + onError(result.error); + } + return; + } else { + if ( + result.value?.address || + result.value?.invoice || + result.value?.node_pubkey || + result.value?.lnurl + ) { + if (onSuccess) { + onSuccess(result.value); + } + } + } + }, + async archiveNwcProfileByIndex(profile_index: number): Promise { + // Get all profiles + const profiles: NwcProfile[] = + await state.mutiny_wallet?.get_nwc_profiles(); + + // Find the profile with the matching index + // (Need the real pointer, not just the js object) + const actualProfile = profiles.find( + (p) => p.index === profile_index + ); + + if (actualProfile) { + await state.mutiny_wallet?.edit_nwc_profile({ + ...actualProfile, + archived: true + }); + } } }; diff --git a/vite.config.ts b/vite.config.ts index f3a47d1ad..048d1ed1e 100644 --- a/vite.config.ts +++ b/vite.config.ts @@ -62,7 +62,8 @@ export default defineConfig({ "i18next", "i18next-browser-languagedetector", "@capacitor/clipboard", - "@capacitor/core" + "@capacitor/core", + "@mutinywallet/barcode-scanner" ], // This is necessary because otherwise `vite dev` can't find the wasm exclude: ["@mutinywallet/mutiny-wasm", "@mutinywallet/waila-wasm"]