@@ -29,6 +29,13 @@ function KindIndicator(props: { kind: ReceiveFlavor }) {
+
+
+ {i18n.t("receive.integrated_qr.gift")}
+
+
+
+
{i18n.t("receive.integrated_qr.unified")}
@@ -62,7 +69,7 @@ async function share(receiveString: string) {
export function IntegratedQr(props: {
value: string;
amountSats: string;
- kind: ReceiveFlavor;
+ kind: ReceiveFlavor | "gift";
}) {
const i18n = useI18n();
const [copy, copied] = useCopy({ copiedTimeout: 1000 });
diff --git a/src/components/Logo.tsx b/src/components/Logo.tsx
new file mode 100644
index 00000000..9745cad5
--- /dev/null
+++ b/src/components/Logo.tsx
@@ -0,0 +1,29 @@
+import { Match, Switch } from "solid-js";
+
+import pixelLogo from "~/assets/mutiny-pixel-logo.png";
+import plusLogo from "~/assets/mutiny-plus-logo.png";
+import { useMegaStore } from "~/state/megaStore";
+
+export function Logo() {
+ const [state, _actions] = useMegaStore();
+ return (
+
+
+
+
+
+
+
+
+ );
+}
diff --git a/src/components/MutinyPlusCta.tsx b/src/components/MutinyPlusCta.tsx
new file mode 100644
index 00000000..95983c41
--- /dev/null
+++ b/src/components/MutinyPlusCta.tsx
@@ -0,0 +1,40 @@
+import { ParentComponent } from "solid-js";
+import { A } from "solid-start";
+
+import forward from "~/assets/icons/forward.svg";
+import { useI18n } from "~/i18n/context";
+
+export const CtaCard: ParentComponent = (props) => {
+ return (
+
+
+
+
+ {props.children}{" "}
+
+
+
+ );
+};
+
+export function MutinyPlusCta() {
+ const i18n = useI18n();
+ return (
+
+
+
+
+ Mutiny+
+
+
+
+
+ {i18n.t("settings.plus.cta_description")}
+
+
+
+ );
+}
diff --git a/src/components/NWCBudgetEditor.tsx b/src/components/NWCBudgetEditor.tsx
index 058be053..555068de 100644
--- a/src/components/NWCBudgetEditor.tsx
+++ b/src/components/NWCBudgetEditor.tsx
@@ -104,16 +104,16 @@ export function NWCBudgetEditor(props: {
{i18n.t("settings.connections.careful")}
-
+
{(field, _fieldProps) => (
{
setValue(
budgetForm,
@@ -129,7 +129,9 @@ export function NWCBudgetEditor(props: {
)}
-
+
{(field, fieldProps) => (
+
(state?.previous ? state?.previous : "/");
+ // If there's no previous state want to just go back one level, basically ../
+ const newBackPath = location.pathname.split("/").slice(0, -1).join("/");
+
+ const backPath = () => (state?.previous ? state?.previous : newBackPath);
return (
{
+ try {
+ const channels = await state.mutiny_wallet?.list_channels();
+ let inbound = 0n;
+
+ for (const channel of channels) {
+ inbound =
+ inbound +
+ BigInt(channel.size) -
+ BigInt(channel.balance + channel.reserve);
+ }
+
+ return inbound;
+ } catch (e) {
+ console.error(e);
+ return 0n;
+ }
+ });
+
+ const warningText = createMemo(() => {
+ if (isNaN(Number(searchParams.amount))) {
+ return undefined;
+ }
+
+ const amount = BigInt(searchParams.amount);
+
+ const network = state.mutiny_wallet?.get_network() as Network;
+
+ const threshold = network === "bitcoin" ? 50000 : 10000;
+ const balance = state.balance?.lightning || 0n;
+
+ if (balance === 0n && amount < threshold) {
+ return i18n.t("settings.gift.receive_too_small", {
+ amount: network === "bitcoin" ? "50,000" : "10,000"
+ });
+ }
+
+ if (inboundCapacity() && inboundCapacity()! > amount) {
+ return undefined;
+ } else {
+ return i18n.t("settings.gift.setup_fee_lightning");
+ }
+ });
+
+ return (
+
+
+ {warningText()}
+
+
+ );
+}
+
+export default function GiftPage() {
+ const [state, _] = useMegaStore();
+ const i18n = useI18n();
+
+ const [claimSuccess, setClaimSuccess] = createSignal(false);
+ const [error, setError] = createSignal();
+ const [loading, setLoading] = createSignal(false);
+
+ const [searchParams] = useSearchParams();
+
+ async function claim() {
+ const amount = Number(searchParams.amount);
+ const nwc = searchParams.nwc_uri;
+ setLoading(true);
+ try {
+ const claimResult = await state.mutiny_wallet?.claim_single_use_nwc(
+ BigInt(amount),
+ nwc
+ );
+ if (claimResult === "Already Claimed") {
+ throw new Error(i18n.t("settings.gift.already_claimed"));
+ }
+ if (
+ claimResult ===
+ "Failed to pay invoice: We do not have enough balance to pay the given amount."
+ ) {
+ throw new Error(i18n.t("settings.gift.sender_is_poor"));
+ }
+ // Fallback for any other errors
+ if (claimResult) {
+ throw new Error(
+ i18n.t("settings.gift.sender_generic_error", {
+ error: claimResult
+ })
+ );
+ }
+ setClaimSuccess(true);
+ } catch (e) {
+ console.error(e);
+ const err = eify(e);
+ if (err.message === "Payment timed out.") {
+ setError(new Error(i18n.t("settings.gift.sender_timed_out")));
+ } else {
+ setError(err);
+ }
+ } finally {
+ setLoading(false);
+ }
+ }
+
+ async function tryAgain() {
+ setError(undefined);
+ await claim();
+ }
+
+ return (
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {i18n.t("settings.gift.receive_header")}
+
+
+ {i18n.t(
+ "settings.gift.receive_description"
+ )}
+
+
+
+
+
+
+
+
+
+ {error()?.message}
+
+
+ {i18n.t("common.dangit")}
+
+
+
+
+
+ {i18n.t(
+ "settings.gift.receive_claimed"
+ )}
+
+
+ {i18n.t("common.nice")}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ );
+}
diff --git a/src/routes/Receive.tsx b/src/routes/Receive.tsx
index 685907d3..1ae30dc4 100644
--- a/src/routes/Receive.tsx
+++ b/src/routes/Receive.tsx
@@ -386,6 +386,7 @@ export default function Receive() {
setAmountSats={setAmount}
isAmountEditable
exitRoute={amount() ? "/receive" : "/"}
+ showWarnings
/>
diff --git a/src/routes/Scanner.tsx b/src/routes/Scanner.tsx
index 965d7196..a08b3d1a 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 9bc96a59..98ae8c98 100644
--- a/src/routes/Send.tsx
+++ b/src/routes/Send.tsx
@@ -26,6 +26,7 @@ import {
DefaultMain,
ExternalLink,
Fee,
+ GiftLink,
HStack,
InfoBox,
LargeHeader,
@@ -44,12 +45,10 @@ 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, mempoolTxUrl, MutinyTagItem } from "~/utils";
-import { FeedbackLink } from "./Feedback";
-
export type SendSource = "lightning" | "onchain";
// const TEST_DEST = "bitcoin:tb1pdh43en28jmhnsrhxkusja46aufdlae5qnfrhucw5jvefw9flce3sdxfcwe?amount=0.00001&label=heyo&lightning=lntbs10u1pjrwrdedq8dpjhjmcnp4qd60w268ve0jencwzhz048ruprkxefhj0va2uspgj4q42azdg89uupp5gngy2pqte5q5uvnwcxwl2t8fsdlla5s6xl8aar4xcsvxeus2w2pqsp5n5jp3pz3vpu92p3uswttxmw79a5lc566herwh3f2amwz2sp6f9tq9qyysgqcqpcxqrpwugv5m534ww5ukcf6sdw2m75f2ntjfh3gzeqay649256yvtecgnhjyugf74zakaf56sdh66ec9fqep2kvu6xv09gcwkv36rrkm38ylqsgpw3yfjl"
@@ -379,25 +378,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() {
@@ -709,7 +700,6 @@ export default function Send() {
fee={feeEstimate()?.toString()}
isAmountEditable={isAmtEditable()}
maxAmountSats={maxAmountSats()}
- skipWarnings={true}
/>
@@ -735,7 +725,9 @@ export default function Send() {
-
+
+
+
diff --git a/src/routes/Swap.tsx b/src/routes/Swap.tsx
index 7367cf4b..b26d5c89 100644
--- a/src/routes/Swap.tsx
+++ b/src/routes/Swap.tsx
@@ -435,7 +435,6 @@ export default function Swap() {
setAmountSats={setAmountSats}
fee={feeEstimate()?.toString()}
isAmountEditable={true}
- skipWarnings={true}
maxAmountSats={maxOnchain()}
/>
0n}>
diff --git a/src/routes/index.tsx b/src/routes/index.tsx
index 735fa45a..ddcb63ed 100644
--- a/src/routes/index.tsx
+++ b/src/routes/index.tsx
@@ -1,9 +1,8 @@
-import { Match, Show, Suspense, Switch } from "solid-js";
+import { Show, Suspense } from "solid-js";
import { A } from "solid-start";
+import scan from "~/assets/icons/scan.svg";
import settings from "~/assets/icons/settings.svg";
-import pixelLogo from "~/assets/mutiny-pixel-logo.png";
-import plusLogo from "~/assets/mutiny-plus-logo.png";
import {
BalanceBox,
BetaWarningModal,
@@ -13,6 +12,7 @@ import {
DefaultMain,
LoadingIndicator,
LoadingShimmer,
+ Logo,
NavBar,
OnboardWarning,
PendingNwc,
@@ -34,24 +34,7 @@ export default function App() {
-
-
-
-
-
-
-
-
+
-
-
-
+
diff --git a/src/routes/settings/Connections.tsx b/src/routes/settings/Connections.tsx
index a51c479a..b2e7690c 100644
--- a/src/routes/settings/Connections.tsx
+++ b/src/routes/settings/Connections.tsx
@@ -139,25 +139,35 @@ function NwcDetails(props: {
)}
remaining={Number(props.profile.budget_remaining || 0)}
/>
-
+
-
- {props.profile.budget_period}
-
+ {/* No interval for gifts */}
+
+
+ {props.profile.budget_period}
+
+
-
+
+
-
+
+
diff --git a/src/routes/settings/index.tsx b/src/routes/settings/index.tsx
index 6b803cc8..0d543416 100644
--- a/src/routes/settings/index.tsx
+++ b/src/routes/settings/index.tsx
@@ -7,6 +7,7 @@ import {
DefaultMain,
ExternalLink,
LargeHeader,
+ MutinyPlusCta,
NavBar,
SafeArea,
SettingsCard,
@@ -71,22 +72,16 @@ export default function Settings() {
// @ts-ignore
const COMMIT_HASH = import.meta.env.__COMMIT_HASH__;
+ const selfHosted = state.settings?.selfhosted === "true";
+
return (
{i18n.t("settings.header")}
-
-
+
+
();
@@ -71,11 +71,17 @@ export type MegaStore = [
setPreferredInvoiceType(
type: "unified" | "lightning" | "onchain"
): void;
+ handleIncomingString(
+ str: string,
+ onError: (e: Error) => void,
+ onSuccess: (value: ParsedParams) => void
+ ): void;
}
];
export const Provider: ParentComponent = (props) => {
const [searchParams] = useSearchParams();
+ const navigate = useNavigate();
const [state, setState] = createStore({
mutiny_wallet: undefined as MutinyWallet | undefined,
@@ -314,6 +320,41 @@ export const Provider: ParentComponent = (props) => {
},
setPreferredInvoiceType(type: "unified" | "lightning" | "onchain") {
setState({ preferredInvoiceType: type });
+ },
+ handleIncomingString(
+ str: string,
+ onError: (e: Error) => void,
+ onSuccess: (value: ParsedParams) => void
+ ): void {
+ try {
+ const url = new URL(str);
+ if (url && url.pathname.startsWith("/gift")) {
+ navigate(url.pathname + url.search);
+ return;
+ }
+ } catch (e) {
+ // If it's not a URL, we'll just continue with normal parsing
+ }
+
+ 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);
+ }
+ }
+ }
}
};
diff --git a/src/utils/baseUrl.ts b/src/utils/baseUrl.ts
new file mode 100644
index 00000000..3f31bab1
--- /dev/null
+++ b/src/utils/baseUrl.ts
@@ -0,0 +1,12 @@
+import { Capacitor } from "@capacitor/core";
+
+// On mobile the origin URL is localhost, so we hardcode the base URL
+export function baseUrlAccountingForNative(network?: string) {
+ if (Capacitor.isNativePlatform()) {
+ return network === "bitcoin"
+ ? "https://app.mutinywallet.com"
+ : "https://signet-app.mutinywallet.com";
+ } else {
+ return window.location.origin;
+ }
+}