+
-
-
-
- {receiveState() === "paid" &&
- paidState() === "lightning_paid"
- ? i18n.t("receive.payment_received")
- : i18n.t("receive.payment_initiated")}
-
-
-
-
+
+
+
+
+ {/*TODO: Confirmation time estimate still not possible needs to be implemented in mutiny-node first*/}
+
+
-
-
- {/*TODO: Confirmation time estimate still not possible needs to be implemented in mutiny-node first*/}
-
-
- {i18n.t("common.view_payment_details")}
-
-
-
-
-
-
-
-
+ {i18n.t("common.view_payment_details")}
+
+
+
+
+
+
+
);
}
diff --git a/src/routes/Search.tsx b/src/routes/Search.tsx
new file mode 100644
index 00000000..8b84c612
--- /dev/null
+++ b/src/routes/Search.tsx
@@ -0,0 +1,465 @@
+import { Clipboard } from "@capacitor/clipboard";
+import { Capacitor } from "@capacitor/core";
+import { TagItem } from "@mutinywallet/mutiny-wasm";
+import { A, useNavigate } from "@solidjs/router";
+import {
+ createMemo,
+ createResource,
+ createSignal,
+ For,
+ Match,
+ onMount,
+ Show,
+ Suspense,
+ Switch
+} from "solid-js";
+
+import close from "~/assets/icons/close.svg";
+import paste from "~/assets/icons/paste.svg";
+import scan from "~/assets/icons/scan.svg";
+import {
+ ContactEditor,
+ ContactFormValues,
+ LabelCircle,
+ LoadingShimmer,
+ NavBar,
+ showToast
+} from "~/components";
+import {
+ BackLink,
+ Button,
+ DefaultMain,
+ MutinyWalletGuard,
+ SafeArea
+} from "~/components/layout";
+import { useI18n } from "~/i18n/context";
+import { useMegaStore } from "~/state/megaStore";
+import {
+ actuallyFetchNostrProfile,
+ hexpubFromNpub,
+ profileToPseudoContact,
+ PseudoContact,
+ searchProfiles
+} from "~/utils";
+
+export function Search() {
+ return (
+
+
+
+
+
+
+
+ {" "}
+
+ {/* Need to put the search view in a supsense so it loads list on first nav */}
+
+
+
+
+
+
+
+ );
+}
+
+function ActualSearch() {
+ const [searchValue, setSearchValue] = createSignal("");
+ const [state, actions] = useMegaStore();
+ const navigate = useNavigate();
+ const i18n = useI18n();
+
+ async function contactsFetcher() {
+ try {
+ const contacts: TagItem[] =
+ state.mutiny_wallet?.get_contacts_sorted();
+ return contacts || [];
+ } catch (e) {
+ console.error(e);
+ return [];
+ }
+ }
+
+ const [contacts] = createResource(contactsFetcher);
+
+ const filteredContacts = createMemo(() => {
+ const s = searchValue().toLowerCase();
+ return (
+ contacts()?.filter((c) => {
+ return (
+ c.ln_address &&
+ (c.name.toLowerCase().includes(s) ||
+ c.ln_address?.toLowerCase().includes(s) ||
+ c.npub?.includes(s))
+ );
+ }) || []
+ );
+ });
+
+ const foundNpubs = createMemo(() => {
+ return (
+ filteredContacts()
+ ?.map((c) => c.npub)
+ .filter((n) => !!n) || []
+ );
+ });
+
+ const showSendButton = createMemo(() => {
+ if (searchValue() === "") {
+ return false;
+ } else {
+ const text = searchValue().trim();
+ // Only want to check for something parseable if it's of reasonable length
+ if (text.length < 6) {
+ return false;
+ }
+ let success = false;
+ actions.handleIncomingString(
+ text,
+ (_error) => {
+ // noop
+ },
+ (_result) => {
+ success = true;
+ }
+ );
+ return success;
+ }
+ });
+
+ function handleContinue() {
+ actions.handleIncomingString(
+ searchValue().trim(),
+ (error) => {
+ showToast(error);
+ },
+ (result) => {
+ if (result) {
+ actions.setScanResult(result);
+ navigate("/send", { state: { previous: "/search" } });
+ } else {
+ showToast(new Error(i18n.t("send.error_address")));
+ }
+ }
+ );
+ }
+
+ function sendToContact(contact: TagItem) {
+ const address = contact.ln_address || contact.lnurl;
+ if (address) {
+ actions.handleIncomingString(
+ (address || "").trim(),
+ (error) => {
+ showToast(error);
+ },
+ (result) => {
+ actions.setScanResult({
+ ...result,
+ contact_id: contact.id
+ });
+ navigate("/send", { state: { previous: "/search" } });
+ }
+ );
+ } else {
+ console.error("no ln_address or lnurl");
+ }
+ }
+
+ async function createContact(contact: ContactFormValues) {
+ try {
+ const contactId = await state.mutiny_wallet?.create_new_contact(
+ contact.name,
+ contact.npub ? contact.npub.trim() : undefined,
+ contact.ln_address ? contact.ln_address.trim() : undefined,
+ undefined,
+ undefined
+ );
+
+ if (!contactId) {
+ throw new Error("no contact id returned");
+ }
+
+ const tagItem = await state.mutiny_wallet?.get_tag_item(contactId);
+
+ if (!tagItem) {
+ throw new Error("no contact returned");
+ }
+
+ sendToContact(tagItem);
+ } catch (e) {
+ console.error(e);
+ }
+ }
+
+ // Search input stuff
+ async function handlePaste() {
+ try {
+ let text;
+
+ if (Capacitor.isNativePlatform()) {
+ const { value } = await Clipboard.read();
+ text = value;
+ } else {
+ if (!navigator.clipboard.readText) {
+ return showToast(new Error(i18n.t("send.error_clipboard")));
+ }
+ text = await navigator.clipboard.readText();
+ }
+
+ const trimText = text.trim();
+ setSearchValue(trimText);
+ parsePaste(trimText);
+ } catch (e) {
+ console.error(e);
+ }
+ }
+
+ function parsePaste(text: string) {
+ actions.handleIncomingString(
+ text,
+ (error) => {
+ showToast(error);
+ },
+ (result) => {
+ actions.setScanResult(result);
+ navigate("/send", { state: { previous: "/search" } });
+ }
+ );
+ }
+
+ let searchInputRef!: HTMLInputElement;
+
+ onMount(() => {
+ searchInputRef.focus();
+ });
+
+ return (
+ <>
+
+
setSearchValue(e.currentTarget.value)}
+ placeholder="Name, address, invoice..."
+ autofocus
+ ref={(el) => (searchInputRef = el)}
+ />
+
+
+
+
+
+
+
+
+
+
+
+ >
+ );
+}
+
+function GlobalSearch(props: {
+ searchValue: string;
+ sendToContact: (contact: TagItem) => void;
+ foundNpubs: (string | undefined)[];
+}) {
+ const hexpubs = createMemo(() => {
+ const hexpubs: string[] = [];
+ for (const npub of props.foundNpubs) {
+ hexpubFromNpub(npub)
+ .then((h) => {
+ if (h) {
+ hexpubs.push(h);
+ }
+ })
+ .catch((e) => {
+ console.error(e);
+ });
+ }
+ return hexpubs;
+ });
+
+ async function searchFetcher(args: { value?: string; hexpubs?: string[] }) {
+ try {
+ // Handling case when value starts with "npub"
+ if (args.value?.startsWith("npub")) {
+ const hexpub = await hexpubFromNpub(args.value);
+ if (!hexpub) return [];
+
+ const profile = await actuallyFetchNostrProfile(hexpub);
+ if (!profile) return [];
+
+ const contact = profileToPseudoContact(profile);
+ return contact.ln_address ? [contact] : [];
+ }
+
+ // Handling case for other values (name, nip-05, whatever else primal searches)
+ const contacts = await searchProfiles(args.value!.toLowerCase());
+ return contacts.filter(
+ (c) => c.ln_address && !args.hexpubs?.includes(c.hexpub)
+ );
+ } catch (e) {
+ console.error(e);
+ return [];
+ }
+ }
+
+ const searchArgs = createMemo(() => {
+ if (props.searchValue) {
+ return {
+ value: props.searchValue,
+ hexpubs: hexpubs()
+ };
+ } else {
+ return {
+ value: "",
+ hexpubs: undefined
+ };
+ }
+ });
+
+ const [searchResults] = createResource(searchArgs, searchFetcher);
+
+ return (
+
+
+
+ No results found for "{props.searchValue}"
+
+
+
+
+ {(contact) => (
+
+ )}
+
+
+
+ );
+}
+
+function SingleContact(props: {
+ contact: PseudoContact;
+ sendToContact: (contact: TagItem) => void;
+}) {
+ const [state, _actions] = useMegaStore();
+ async function createContactFromSearchResult(contact: PseudoContact) {
+ try {
+ const contactId = await state.mutiny_wallet?.create_new_contact(
+ contact.name,
+ contact.hexpub ? contact.hexpub : undefined,
+ contact.ln_address ? contact.ln_address : undefined,
+ undefined,
+ contact.image_url ? contact.image_url : undefined
+ );
+
+ if (!contactId) {
+ throw new Error("no contact id returned");
+ }
+
+ const tagItem = await state.mutiny_wallet?.get_tag_item(contactId);
+
+ if (!tagItem) {
+ throw new Error("no contact returned");
+ }
+
+ props.sendToContact(tagItem);
+ } catch (e) {
+ console.error(e);
+ }
+ }
+
+ return (
+
+ );
+}
diff --git a/src/routes/Send.tsx b/src/routes/Send.tsx
index 0223ad45..d9a7c532 100644
--- a/src/routes/Send.tsx
+++ b/src/routes/Send.tsx
@@ -1,48 +1,46 @@
-import { Clipboard } from "@capacitor/clipboard";
-import { Capacitor } from "@capacitor/core";
-import { MutinyInvoice, TagItem } from "@mutinywallet/mutiny-wasm";
+import { MutinyInvoice } from "@mutinywallet/mutiny-wasm";
import { A, useNavigate, useSearchParams } from "@solidjs/router";
import {
createEffect,
createMemo,
+ createResource,
createSignal,
+ JSX,
Match,
onMount,
Show,
+ Suspense,
Switch
} from "solid-js";
-import { Paste } from "~/assets/svg/Paste";
-import { Scan } from "~/assets/svg/Scan";
+import bolt from "~/assets/icons/bolt.svg";
+import chain from "~/assets/icons/chain.svg";
+import close from "~/assets/icons/close.svg";
import {
ActivityDetailsModal,
- AmountCard,
+ AmountEditable,
AmountFiat,
AmountSats,
- BackButton,
- BackLink,
+ BackPop,
Button,
- ButtonLink,
- Card,
DefaultMain,
Fee,
- GiftLink,
+ FeeDisplay,
HackActivityType,
- HStack,
InfoBox,
- LargeHeader,
+ LabelCircle,
+ LoadingShimmer,
MegaCheck,
MegaClock,
MegaEx,
MutinyWalletGuard,
NavBar,
- SafeArea,
showToast,
+ SimpleInput,
SmallHeader,
StringShower,
- StyledRadioGroup,
SuccessModal,
- TagEditor,
+ UnstyledBackPop,
VStack
} from "~/components";
import { useI18n } from "~/i18n/context";
@@ -65,115 +63,6 @@ type SentDetails = {
fee_estimate?: bigint | number;
};
-export function MethodChooser(props: {
- source: SendSource;
- setSource: (source: string) => void;
- both?: boolean;
-}) {
- const [store, _actions] = useMegaStore();
-
- const methods = createMemo(() => {
- const lnBalance =
- (store.balance?.lightning || 0n) +
- (store.balance?.federation || 0n);
- const onchainBalance =
- (store.balance?.confirmed || 0n) +
- (store.balance?.unconfirmed || 0n);
- return [
- {
- value: "lightning",
- label: "Lightning Balance",
- caption:
- lnBalance > 0n
- ? `${lnBalance.toLocaleString()} SATS`
- : "No balance",
- disabled: lnBalance === 0n
- },
- {
- value: "onchain",
- label: "On-chain Balance",
- caption:
- onchainBalance > 0n
- ? `${onchainBalance.toLocaleString()} SATS`
- : "No balance",
- disabled: onchainBalance === 0n
- }
- ];
- });
- return (
-
-
-
-
-
-
-
-
-
-
-
- );
-}
-
-function DestinationInput(props: {
- fieldDestination: string;
- setFieldDestination: (destination: string) => void;
- handleDecode: () => void;
- handlePaste: () => void;
-}) {
- const i18n = useI18n();
- return (
-
- {i18n.t("send.destination")}
-
- );
-}
-
function DestinationShower(props: {
source: SendSource;
description?: string;
@@ -182,25 +71,95 @@ function DestinationShower(props: {
nodePubkey?: string;
lnurl?: string;
lightning_address?: string;
- clearAll: () => void;
+ contact_id?: string;
}) {
+ const [state, _actions] = useMegaStore();
+
+ async function getContact(id: string) {
+ console.log("fetching contact", id);
+ try {
+ const contact = state.mutiny_wallet?.get_tag_item(id);
+ console.log("fetching contact", contact);
+ // This shouldn't happen
+ if (!contact) throw new Error("Contact not found");
+ return contact;
+ } catch (e) {
+ console.error(e);
+ showToast(eify(e));
+ }
+ }
+
+ const [contact] = createResource(() => props.contact_id, getContact);
+
return (
+
+
+ }
+ />
+
-
+ }
+ icon={
+
+ }
+ />
-
-
-
-
+ }
+ icon={
+
+ }
+ />
-
- {props.lightning_address || ""}
-
+
+ }
+ />
+
+
+ }
+ icon={
+
+ }
+ />
-
+ }
+ icon={
+
+ }
+ />
);
}
+function DestinationItem(props: {
+ title: string;
+ value: JSX.Element;
+ icon: JSX.Element;
+}) {
+ return (
+
+ {props.icon}
+
+
{props.title}
+
{props.value}
+
+
+
+
![Clear]({close})
+
+
+
+ );
+}
+
function Failure(props: { reason: string }) {
const i18n = useI18n();
@@ -257,34 +247,28 @@ export function Send() {
const [params, setParams] = useSearchParams();
const i18n = useI18n();
- // These can only be set by the user
- const [fieldDestination, setFieldDestination] = createSignal("");
- const [destination, setDestination] = createSignal
();
+ const [amountInput, setAmountInput] = createSignal("");
+ const [whatForInput, setWhatForInput] = createSignal("");
- // These can be derived from the "destination" signal or set by the user
+ // These can be derived from the destination or set by the user
const [amountSats, setAmountSats] = createSignal(0n);
+
+ // These are derived from the incoming destination
const [isAmtEditable, setIsAmtEditable] = createSignal(true);
const [source, setSource] = createSignal("lightning");
-
- // These can only be derived from the "destination" signal
const [invoice, setInvoice] = createSignal();
const [nodePubkey, setNodePubkey] = createSignal();
const [lnurlp, setLnurlp] = createSignal();
const [lnAddress, setLnAddress] = createSignal();
const [address, setAddress] = createSignal();
const [description, setDescription] = createSignal();
-
+ const [contactId, setContactId] = createSignal();
const [isHodlInvoice, setIsHodlInvoice] = createSignal(false);
// Is sending / sent
const [sending, setSending] = createSignal(false);
const [sentDetails, setSentDetails] = createSignal();
- // Tagging stuff
- const [selectedContacts, setSelectedContacts] = createSignal<
- Partial[]
- >([]);
-
// Details Modal
const [detailsOpen, setDetailsOpen] = createSignal(false);
const [detailsKind, setDetailsKind] = createSignal();
@@ -293,21 +277,6 @@ export function Send() {
// Errors
const [error, setError] = createSignal();
- function clearAll() {
- setDestination(undefined);
- setAmountSats(0n);
- setIsAmtEditable(true);
- setIsHodlInvoice(false);
- setSource("lightning");
- setInvoice(undefined);
- setAddress(undefined);
- setDescription(undefined);
- setNodePubkey(undefined);
- setLnurlp(undefined);
- setLnAddress(undefined);
- setFieldDestination("");
- }
-
function openDetailsModal() {
const paymentTxId = sentDetails()?.txid
? sentDetails()
@@ -331,13 +300,19 @@ export function Send() {
setDetailsOpen(true);
}
- // If we got here from a scan result we want to set the destination and clean up that scan result
- onMount(() => {
- if (state.scan_result) {
- setDestination(state.scan_result);
- actions.setScanResult(undefined);
- }
- });
+ // TODO: can I dedupe this from the search page?
+ function parsePaste(text: string) {
+ actions.handleIncomingString(
+ text,
+ (error) => {
+ showToast(error);
+ },
+ (result) => {
+ actions.setScanResult(result);
+ navigate("/send", { state: { previous: "/search" } });
+ }
+ );
+ }
// send?invoice=... need to check for wallet because we can't parse until we have the wallet
createEffect(() => {
@@ -347,7 +322,6 @@ export function Send() {
}
});
- // Three suspiciously similar "max" values we want to compute
const maxOnchain = createMemo(() => {
return (
(state.balance?.confirmed ?? 0n) +
@@ -355,8 +329,18 @@ export function Send() {
);
});
+ const maxLightning = createMemo(() => {
+ const fed = state.balance?.federation ?? 0n;
+ const ln = state.balance?.lightning ?? 0n;
+ if (fed > ln) {
+ return fed;
+ } else {
+ return ln;
+ }
+ });
+
const maxAmountSats = createMemo(() => {
- return source() === "onchain" ? maxOnchain() : undefined;
+ return source() === "onchain" ? maxOnchain() : maxLightning();
});
const isMax = createMemo(() => {
@@ -424,60 +408,26 @@ export function Send() {
return undefined;
});
- // Rerun every time the destination changes
- createEffect(() => {
- const source = destination();
+ const [parsingDestination, setParsingDestination] = createSignal(false);
+
+ function handleDestination(source: ParsedParams | undefined) {
if (!source) return;
+ setParsingDestination(true);
+
try {
if (source.address) setAddress(source.address);
if (source.memo) setDescription(source.memo);
+ if (source.contact_id) setContactId(source.contact_id);
if (source.invoice) {
- state.mutiny_wallet
- ?.decode_invoice(source.invoice)
- .then((invoice) => {
- if (invoice?.amount_sats) {
- setAmountSats(invoice.amount_sats);
- setIsAmtEditable(false);
- }
- setInvoice(invoice);
- setIsHodlInvoice(invoice.potential_hodl_invoice);
- setSource("lightning");
- });
+ processInvoice(source as ParsedParams & { invoice: string });
} else if (source.node_pubkey) {
- setAmountSats(source.amount_sats || 0n);
- setNodePubkey(source.node_pubkey);
- setSource("lightning");
+ processNodePubkey(
+ source as ParsedParams & { node_pubkey: string }
+ );
} else if (source.lnurl) {
- state.mutiny_wallet
- ?.decode_lnurl(source.lnurl)
- .then((lnurlParams) => {
- if (lnurlParams.tag === "payRequest") {
- if (lnurlParams.min == lnurlParams.max) {
- setAmountSats(lnurlParams.min / 1000n);
- setIsAmtEditable(false);
- } else {
- setAmountSats(source.amount_sats || 0n);
- }
-
- // If it is a lightning address, set the address so we can display it
- if (source.lightning_address) {
- setLnAddress(source.lightning_address);
- // check for hodl invoices
- setIsHodlInvoice(
- source.lightning_address
- .toLowerCase()
- .includes("zeuspay.com")
- );
- }
-
- setLnurlp(source.lnurl);
- setSource("lightning");
- }
- })
- .catch((e) => {
- showToast(eify(e));
- });
+ console.log("processing lnurl");
+ processLnurl(source as ParsedParams & { lnurl: string });
} else {
setAmountSats(source.amount_sats || 0n);
setSource("onchain");
@@ -486,82 +436,81 @@ export function Send() {
return source;
} catch (e) {
console.error("error", e);
- clearAll();
+ } finally {
+ setParsingDestination(false);
}
- });
-
- function parsePaste(text: string) {
- 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() {
- const text = fieldDestination();
- parsePaste(text);
- }
-
- async function handlePaste() {
- try {
- let text;
-
- if (Capacitor.isNativePlatform()) {
- const { value } = await Clipboard.read();
- text = value;
- } else {
- if (!navigator.clipboard.readText) {
- return showToast(new Error(i18n.t("send.error_clipboard")));
+ // A ParsedParams with an invoice in it
+ function processInvoice(source: ParsedParams & { invoice: string }) {
+ state.mutiny_wallet
+ ?.decode_invoice(source.invoice!)
+ .then((invoice) => {
+ if (invoice?.amount_sats) {
+ setAmountSats(invoice.amount_sats);
+ setIsAmtEditable(false);
}
- text = await navigator.clipboard.readText();
- }
-
- const trimText = text.trim();
- setFieldDestination(trimText);
- parsePaste(trimText);
- } catch (e) {
- console.error(e);
- }
+ setInvoice(invoice);
+ setIsHodlInvoice(invoice.potential_hodl_invoice);
+ setSource("lightning");
+ })
+ .catch((e) => showToast(eify(e)));
}
- async function processContacts(
- contacts: Partial[]
- ): Promise {
- if (contacts.length) {
- const first = contacts![0];
+ // A ParsedParams with a node_pubkey in it
+ function processNodePubkey(source: ParsedParams & { node_pubkey: string }) {
+ setAmountSats(source.amount_sats || 0n);
+ setNodePubkey(source.node_pubkey);
+ setSource("lightning");
+ }
- if (!first.name) {
- return [];
- }
+ // A ParsedParams with an lnurl in it
+ function processLnurl(source: ParsedParams & { lnurl: string }) {
+ state.mutiny_wallet
+ ?.decode_lnurl(source.lnurl)
+ .then((lnurlParams) => {
+ if (lnurlParams.tag === "payRequest") {
+ if (lnurlParams.min == lnurlParams.max) {
+ setAmountSats(lnurlParams.min / 1000n);
+ setIsAmtEditable(false);
+ } else {
+ setAmountSats(source.amount_sats || 0n);
+ }
- if (!first.id && first.name) {
- try {
- const newContactId =
- await state.mutiny_wallet?.create_new_contact(
- first.name
+ if (source.lightning_address) {
+ setLnAddress(source.lightning_address);
+ setIsHodlInvoice(
+ source.lightning_address
+ .toLowerCase()
+ .includes("zeuspay.com")
);
- if (newContactId) {
- return [newContactId];
}
- } catch (e) {
- console.error(e);
+ setLnurlp(source.lnurl);
+ setSource("lightning");
}
- }
+ })
+ .catch((e) => showToast(eify(e)));
+ }
- if (first.id) {
- return [first.id];
+ createEffect(() => {
+ if (amountInput() === "") {
+ setAmountSats(0n);
+ } else {
+ const parsed = BigInt(amountInput());
+ console.log("parsed", parsed);
+ if (parsed > 0n) {
+ setAmountSats(parsed);
}
}
+ });
- return [];
- }
+ // If we got here from a scan or search
+ onMount(() => {
+ if (state.scan_result) {
+ handleDestination(state.scan_result);
+ actions.setScanResult(undefined);
+ }
+ });
async function handleSend() {
try {
@@ -569,7 +518,11 @@ export function Send() {
const bolt11 = invoice()?.bolt11;
const sentDetails: Partial = {};
- const tags = await processContacts(selectedContacts());
+ const tags = contactId() ? [contactId()!] : [];
+
+ if (whatForInput()) {
+ tags.push(whatForInput().trim());
+ }
if (source() === "lightning" && invoice() && bolt11) {
sentDetails.destination = bolt11;
@@ -580,8 +533,8 @@ export function Send() {
undefined,
tags
);
- sentDetails.amount = invoice()?.amount_sats;
- sentDetails.payment_hash = invoice()?.payment_hash;
+ sentDetails.amount = payment?.amount_sats;
+ sentDetails.payment_hash = payment?.payment_hash;
sentDetails.fee_estimate = payment?.fees_paid || 0;
} else {
const payment = await state.mutiny_wallet?.pay_invoice(
@@ -589,8 +542,8 @@ export function Send() {
amountSats(),
tags
);
- sentDetails.amount = amountSats();
- sentDetails.payment_hash = invoice()?.payment_hash;
+ sentDetails.amount = payment?.amount_sats;
+ sentDetails.payment_hash = payment?.payment_hash;
sentDetails.fee_estimate = payment?.fees_paid || 0;
}
} else if (source() === "lightning" && nodePubkey()) {
@@ -605,8 +558,8 @@ export function Send() {
if (!payment?.paid) {
throw new Error(i18n.t("send.error_keysend"));
} else {
- sentDetails.amount = amountSats();
- sentDetails.payment_hash = invoice()?.payment_hash;
+ sentDetails.amount = payment?.amount_sats;
+ sentDetails.payment_hash = payment?.payment_hash;
sentDetails.fee_estimate = payment?.fees_paid || 0;
}
} else if (source() === "lightning" && lnurlp()) {
@@ -614,15 +567,16 @@ export function Send() {
lnurlp()!,
amountSats(),
undefined, // zap_npub
- tags
+ tags,
+ undefined // comment
);
- sentDetails.payment_hash = invoice()?.payment_hash;
+ sentDetails.payment_hash = payment?.payment_hash;
if (!payment?.paid) {
throw new Error(i18n.t("send.error_LNURL"));
} else {
- sentDetails.amount = amountSats();
- sentDetails.payment_hash = invoice()?.payment_hash;
+ sentDetails.amount = payment?.amount_sats;
+ sentDetails.payment_hash = payment?.payment_hash;
sentDetails.fee_estimate = payment?.fees_paid || 0;
}
} else if (source() === "onchain" && address()) {
@@ -651,9 +605,13 @@ export function Send() {
sentDetails.fee_estimate = feeEstimate() ?? 0;
}
}
- setSentDetails(sentDetails as SentDetails);
- clearAll();
- await vibrateSuccess();
+ if (sentDetails.payment_hash || sentDetails.txid) {
+ setSentDetails(sentDetails as SentDetails);
+ await vibrateSuccess();
+ } else {
+ // TODO: what should we do here? hopefully this never happens?
+ console.error("failed to send: no payment hash or txid");
+ }
} catch (e) {
const error = eify(e);
setSentDetails({ failure_reason: error.message });
@@ -666,203 +624,173 @@ export function Send() {
}
const sendButtonDisabled = createMemo(() => {
- return !destination() || sending() || amountSats() === 0n || !!error();
- });
-
- const shouldShowGiftLink = createMemo(() => {
- // iOS users should only see gift link if they're mutiny+ subscribers
- const isIOS = Capacitor.getPlatform() === "ios";
-
- return !isIOS || state.mutiny_plus;
+ return (
+ parsingDestination() ||
+ sending() ||
+ amountSats() === 0n ||
+ !!error()
+ );
});
return (
-
-
- }
- >
- clearAll()}
- title={i18n.t("send.start_over")}
- />
-
- {i18n.t("send.send_bitcoin")}
- {
- if (!open) setSentDetails(undefined);
- }}
- onConfirm={() => {
- setSentDetails(undefined);
- navigate("/");
- }}
- >
-
-
-
+
+ {
+ if (!open) setSentDetails(undefined);
+ }}
+ onConfirm={() => {
+ setSentDetails(undefined);
+ navigate("/");
+ }}
+ >
+
+
+
+
+
+
+
-
-
-
-
+
+
+ {sentDetails()?.amount
+ ? source() === "onchain"
+ ? i18n.t("send.payment_initiated")
+ : i18n.t("send.payment_sent")
+ : sentDetails()?.failure_reason}
+
+
+
+
-
-
-
- {sentDetails()?.amount
- ? source() === "onchain"
- ? i18n.t("send.payment_initiated")
- : i18n.t("send.payment_sent")
- : sentDetails()?.failure_reason}
-
-
-
-
-
- {i18n.t("common.view_payment_details")}
-
-
-
-
-
-
-
+
+
+
+
+
+
-
-
-
-
-
- {i18n.t("common.private_tags")}
-
-
-
-
-
-
-
-
- {i18n.t(
- "send.hodl_invoice_warning"
- )}
-
-
-
-
-
- {error()}
-
-
-
-
-
-
-
-
+
+
+
+
+
}>
+
+
+
+ {/* Need both these versions so that we make sure to get the right initial amount on load */}
+
+
+ sendButtonDisabled() ? undefined : handleSend()
+ }
+ />
+
+
+
+ sendButtonDisabled() ? undefined : handleSend()
}
+ />
+
+
+
+
+
+
+ {i18n.t("send.hodl_invoice_warning")}
+
+
+
+
+ {error()}
+
+
+
+
+
+
+
-
-
-
+
+
+
);
}
diff --git a/src/routes/Swap.tsx b/src/routes/Swap.tsx
index 97b14449..f82df369 100644
--- a/src/routes/Swap.tsx
+++ b/src/routes/Swap.tsx
@@ -13,17 +13,19 @@ import {
import {
ActivityDetailsModal,
- AmountCard,
+ AmountEditable,
AmountFiat,
BackLink,
Button,
Card,
DefaultMain,
+ FeeDisplay,
HackActivityType,
InfoBox,
LargeHeader,
MegaCheck,
MegaEx,
+ MethodChooser,
MutinyWalletGuard,
NavBar,
SafeArea,
@@ -34,7 +36,7 @@ import {
} from "~/components";
import { useI18n } from "~/i18n/context";
import { Network } from "~/logic/mutinyWalletSetup";
-import { MethodChooser, SendSource } from "~/routes/Send";
+import { SendSource } from "~/routes/Send";
import { useMegaStore } from "~/state/megaStore";
import { eify, vibrateSuccess } from "~/utils";
@@ -441,13 +443,19 @@ export function Swap() {
-
+ 0n}>
+
+
0n}>
{amountWarning()}
diff --git a/src/routes/index.ts b/src/routes/index.ts
index 0e520cca..923dec31 100644
--- a/src/routes/index.ts
+++ b/src/routes/index.ts
@@ -7,3 +7,4 @@ export * from "./Receive";
export * from "./Scanner";
export * from "./Send";
export * from "./Swap";
+export * from "./Search";
diff --git a/src/routes/settings/Channels.tsx b/src/routes/settings/Channels.tsx
index e49a5e7b..63e33c1f 100644
--- a/src/routes/settings/Channels.tsx
+++ b/src/routes/settings/Channels.tsx
@@ -169,7 +169,7 @@ function SingleChannelItem(props: { channel: MutinyChannel }) {
);
}
-export function LiquidityMonitor() {
+function LiquidityMonitor() {
const i18n = useI18n();
const [state, _actions] = useMegaStore();
diff --git a/src/routes/settings/Gift.tsx b/src/routes/settings/Gift.tsx
index 0287ddde..9a9cacf2 100644
--- a/src/routes/settings/Gift.tsx
+++ b/src/routes/settings/Gift.tsx
@@ -19,7 +19,7 @@ import {
} from "solid-js";
import {
- AmountCard,
+ AmountEditable,
BackPop,
Button,
Collapser,
@@ -49,10 +49,7 @@ type CreateGiftForm = {
amount: string;
};
-export function SingleGift(props: {
- profile: NwcProfile;
- onDelete?: () => void;
-}) {
+function SingleGift(props: { profile: NwcProfile; onDelete?: () => void }) {
const i18n = useI18n();
const [state, _actions] = useMegaStore();
@@ -268,6 +265,22 @@ export function Gift() {
{i18n.t("settings.gift.send_explainer")}
+
+ {(field) => (
+
+ setValue(
+ giftForm,
+ "amount",
+ newAmount.toString()
+ )
+ }
+ />
+ )}
+
)}
-
- {(field) => (
- <>
-
- setValue(
- giftForm,
- "amount",
- newAmount.toString()
- )
- }
- />
- >
- )}
-
+
{i18n.t(
diff --git a/src/routes/settings/Restore.tsx b/src/routes/settings/Restore.tsx
index 98a8308c..371d5dfa 100644
--- a/src/routes/settings/Restore.tsx
+++ b/src/routes/settings/Restore.tsx
@@ -45,7 +45,7 @@ function validateWord(word?: string): boolean {
return WORDS_EN.includes(word?.trim() ?? "");
}
-export function SeedTextField(props: TextFieldProps) {
+function SeedTextField(props: TextFieldProps) {
const [fieldProps] = splitProps(props, [
"placeholder",
"ref",
diff --git a/src/routes/settings/Servers.tsx b/src/routes/settings/Servers.tsx
index 4abf0161..caf15673 100644
--- a/src/routes/settings/Servers.tsx
+++ b/src/routes/settings/Servers.tsx
@@ -24,7 +24,7 @@ import {
} from "~/logic/mutinyWalletSetup";
import { eify } from "~/utils";
-export function SettingsStringsEditor(props: {
+function SettingsStringsEditor(props: {
initialSettings: MutinyWalletSettingStrings;
}) {
const i18n = useI18n();
diff --git a/src/routes/settings/SyncNostrContacts.tsx b/src/routes/settings/SyncNostrContacts.tsx
index 1ca12317..c5847f02 100644
--- a/src/routes/settings/SyncNostrContacts.tsx
+++ b/src/routes/settings/SyncNostrContacts.tsx
@@ -2,7 +2,7 @@ import { createForm, required, SubmitHandler } from "@modular-forms/solid";
import { createSignal, Match, Show, Switch } from "solid-js";
import {
- BackLink,
+ BackPop,
Button,
DefaultMain,
FancyCard,
@@ -26,7 +26,7 @@ type NostrContactsForm = {
const PRIMAL_API = import.meta.env.VITE_PRIMAL;
-export function SyncContactsForm() {
+function SyncContactsForm() {
const i18n = useI18n();
const [state, actions] = useMegaStore();
const [error, setError] = createSignal();
@@ -121,7 +121,7 @@ export function SyncNostrContacts() {
-
+
Sync Nostr Contacts
diff --git a/src/state/megaStore.tsx b/src/state/megaStore.tsx
index b9bec274..f4f11e7a 100644
--- a/src/state/megaStore.tsx
+++ b/src/state/megaStore.tsx
@@ -36,14 +36,14 @@ import {
const MegaStoreContext = createContext();
-export type LoadStage =
+type LoadStage =
| "fresh"
| "checking_double_init"
| "downloading"
| "setup"
| "done";
-export type MegaStore = [
+type MegaStore = [
{
mutiny_wallet?: MutinyWallet;
deleting: boolean;
diff --git a/src/styles/solid-select.css b/src/styles/solid-select.css
deleted file mode 100644
index 31dd4403..00000000
--- a/src/styles/solid-select.css
+++ /dev/null
@@ -1,67 +0,0 @@
-.solid-select-container[data-disabled="true"] {
- @apply pointer-events-none;
-}
-.solid-select-container {
- @apply relative;
-}
-.solid-select-control[data-disabled="true"] {
-}
-.solid-select-control {
- @apply w-full rounded-lg bg-white/10 p-2 placeholder-neutral-400;
- @apply grid leading-6;
- grid-template-columns: repeat(1, minmax(0, 1fr));
-}
-.solid-select-control[data-multiple="true"][data-has-value="true"] {
- @apply flex flex-wrap items-stretch gap-1;
-}
-
-.solid-select-placeholder {
- @apply text-neutral-400;
- @apply col-start-1 row-start-1;
-}
-.solid-select-single-value {
- @apply col-start-1 row-start-1;
-}
-.solid-select-multi-value {
- @apply flex items-center rounded bg-white/20 px-1;
-}
-
-.solid-select-multi-value-remove {
- /* TODO: there's gotta be a better way to vertically center this */
- @apply -mt-2 pl-2 pr-1 text-2xl leading-3;
-}
-
-.solid-select-input {
- @apply flex-shrink flex-grow bg-transparent caret-transparent;
- outline: 2px solid transparent;
- @apply col-start-1 row-start-1;
-}
-.solid-select-input:read-only {
- @apply cursor-default;
-}
-.solid-select-input[data-multiple="true"] {
- @apply caret-current;
-}
-.solid-select-input[data-is-active="true"] {
- @apply caret-current;
-}
-
-.solid-select-list {
- @apply absolute z-10 max-h-[50vh] min-w-full overflow-y-auto whitespace-nowrap rounded-lg bg-neutral-950 p-2;
-}
-
-.solid-select-option[data-focused="true"] {
-}
-
-.solid-select-option > mark {
- @apply bg-white/10 text-white underline;
-}
-.solid-select-option {
- @apply cursor-default select-none rounded p-1 hover:bg-neutral-800;
-}
-.solid-select-option[data-disabled="true"] {
- @apply pointer-events-none opacity-50;
-}
-.solid-select-list-placeholder {
- @apply cursor-default select-none;
-}
diff --git a/src/utils/bech32.ts b/src/utils/bech32.ts
index 45983f59..10d27d40 100644
--- a/src/utils/bech32.ts
+++ b/src/utils/bech32.ts
@@ -221,12 +221,11 @@ export function bech32WordsToUrl(words: number[]) {
}
export const bech32 = getLibraryFromEncoding("bech32");
-export const bech32m = getLibraryFromEncoding("bech32m");
-export interface Decoded {
+interface Decoded {
prefix: string;
words: number[];
}
-export interface BechLib {
+interface BechLib {
decodeUnsafe: (
str: string,
LIMIT?: number | undefined
diff --git a/src/utils/fetchZaps.ts b/src/utils/fetchZaps.ts
index c20c687f..d5df0b25 100644
--- a/src/utils/fetchZaps.ts
+++ b/src/utils/fetchZaps.ts
@@ -6,7 +6,7 @@ import { ResourceFetcher } from "solid-js";
import { useMegaStore } from "~/state/megaStore";
import { hexpubFromNpub, NostrKind, NostrTag } from "~/utils/nostr";
-export type NostrEvent = {
+type NostrEvent = {
created_at: number;
content: string;
tags: NostrTag[];
@@ -16,7 +16,7 @@ export type NostrEvent = {
sig?: string;
};
-export type SimpleZapItem = {
+type SimpleZapItem = {
kind: "public" | "private" | "anonymous";
from_hexpub: string;
to_hexpub: string;
@@ -116,7 +116,7 @@ async function simpleZapFromEvent(
}
}
-export const PRIMAL_API = import.meta.env.VITE_PRIMAL;
+const PRIMAL_API = import.meta.env.VITE_PRIMAL;
async function fetchFollows(npub: string): Promise {
let pubkey = undefined;
@@ -288,6 +288,10 @@ export const fetchNostrProfile: ResourceFetcher<
string,
NostrProfile | undefined
> = async (hexpub, _info) => {
+ return await actuallyFetchNostrProfile(hexpub);
+};
+
+export async function actuallyFetchNostrProfile(hexpub: string) {
try {
if (!PRIMAL_API)
throw new Error("Missing PRIMAL_API environment variable");
@@ -315,4 +319,59 @@ export const fetchNostrProfile: ResourceFetcher<
console.error("Failed to load profile: ", e);
throw new Error("Failed to load profile");
}
+}
+
+// Search results from primal have some of the stuff we want for a TagItem contact
+export type PseudoContact = {
+ name: string;
+ hexpub: string;
+ ln_address?: string;
+ image_url?: string;
};
+
+export async function searchProfiles(query: string): Promise {
+ console.log("searching profiles...");
+ const response = await fetch(PRIMAL_API, {
+ method: "POST",
+ headers: {
+ "Content-Type": "application/json"
+ },
+ body: JSON.stringify([
+ "user_search",
+ { query: query.trim(), limit: 10 }
+ ])
+ });
+
+ if (!response.ok) {
+ throw new Error(`Failed to search`);
+ }
+
+ const data = await response.json();
+
+ const users: PseudoContact[] = [];
+
+ for (const object of data) {
+ if (object.kind === 0) {
+ try {
+ const profile = object as NostrProfile;
+ const contact = profileToPseudoContact(profile);
+ users.push(contact);
+ } catch (e) {
+ console.error("Failed to parse content: ", object.content);
+ }
+ }
+ }
+
+ return users;
+}
+
+export function profileToPseudoContact(profile: NostrProfile): PseudoContact {
+ const content = JSON.parse(profile.content);
+ const contact: Partial = {
+ hexpub: profile.pubkey
+ };
+ contact.name = content.display_name || content.name || profile.pubkey;
+ contact.ln_address = content.lud16 || undefined;
+ contact.image_url = content.picture || undefined;
+ return contact as PseudoContact;
+}
diff --git a/src/utils/getHostname.ts b/src/utils/getHostname.ts
deleted file mode 100644
index be1fc155..00000000
--- a/src/utils/getHostname.ts
+++ /dev/null
@@ -1,13 +0,0 @@
-export function getHostname(url: string): string {
- // Check if the URL begins with "ws://" or "wss://"
- if (url.startsWith("ws://")) {
- // If it does, remove "ws://" from the URL
- url = url.slice(5);
- } else if (url.startsWith("wss://")) {
- // If it begins with "wss://", remove "wss://" from the URL
- url = url.slice(6);
- }
-
- // Return the resulting URL
- return url;
-}
diff --git a/src/utils/index.ts b/src/utils/index.ts
index e372fada..4e0b2ed2 100644
--- a/src/utils/index.ts
+++ b/src/utils/index.ts
@@ -2,13 +2,11 @@ export * from "./conversions";
export * from "./deepSignal";
export * from "./download";
export * from "./eify";
-export * from "./getHostname";
export * from "./gradientHash";
export * from "./mempoolTxUrl";
export * from "./objectToSearchParams";
export * from "./prettyPrintTime";
export * from "./subscriptions";
-export * from "./tags";
export * from "./timeout";
export * from "./typescript";
export * from "./useCopy";
@@ -19,3 +17,4 @@ export * from "./openLinkProgrammatically";
export * from "./nostr";
export * from "./currencies";
export * from "./bech32";
+export * from "./keypad";
diff --git a/src/utils/keypad.ts b/src/utils/keypad.ts
new file mode 100644
index 00000000..79036d2a
--- /dev/null
+++ b/src/utils/keypad.ts
@@ -0,0 +1,121 @@
+import { Currency } from "./currencies";
+
+// Checks the users locale to determine if decimals should be a "." or a ","
+const decimalDigitDivider = Number(1.0)
+ .toLocaleString(navigator.languages[0], { minimumFractionDigits: 1 })
+ .substring(1, 2);
+
+export function toDisplayHandleNaN(
+ input?: string | bigint,
+ fiat?: Currency
+): string {
+ if (!input) {
+ return "0";
+ }
+
+ if (typeof input === "bigint") {
+ console.error("toDisplayHandleNaN: input is a bigint", input);
+ }
+
+ const inputStr = input.toString();
+
+ const parsed = Number(input);
+
+ //handle decimals so the user can always see the accurate amount
+ if (isNaN(parsed)) {
+ return "0";
+ } else if (parsed === Math.trunc(parsed) && inputStr.endsWith(".")) {
+ return (
+ parsed.toLocaleString(navigator.languages[0]) + decimalDigitDivider
+ );
+ /* To avoid having logic to handle every number up to 8 decimals
+ any custom currency pair that has more than 3 decimals will always show all decimals*/
+ } else if (fiat?.maxFractionalDigits && fiat.maxFractionalDigits > 3) {
+ return parsed.toLocaleString(navigator.languages[0], {
+ minimumFractionDigits: parsed === 0 ? 0 : fiat.maxFractionalDigits,
+ maximumFractionDigits: fiat.maxFractionalDigits
+ });
+ } else if (parsed === Math.trunc(parsed) && inputStr.endsWith(".0")) {
+ return parsed.toLocaleString(navigator.languages[0], {
+ minimumFractionDigits: 1
+ });
+ } else if (parsed === Math.trunc(parsed) && inputStr.endsWith(".00")) {
+ return parsed.toLocaleString(navigator.languages[0], {
+ minimumFractionDigits: 2
+ });
+ } else if (parsed === Math.trunc(parsed) && inputStr.endsWith(".000")) {
+ return parsed.toLocaleString(navigator.languages[0], {
+ minimumFractionDigits: 3
+ });
+ } else if (
+ parsed !== Math.trunc(parsed) &&
+ // matches strings that have 3 total digits after the decimal and ends with 0
+ inputStr.match(/\.\d{2}0$/) &&
+ inputStr.includes(".", inputStr.length - 4)
+ ) {
+ return parsed.toLocaleString(navigator.languages[0], {
+ minimumFractionDigits: 3
+ });
+ } else if (
+ parsed !== Math.trunc(parsed) &&
+ // matches strings that have 2 total digits after the decimal and ends with 0
+ inputStr.match(/\.\d{1}0$/) &&
+ inputStr.includes(".", inputStr.length - 3)
+ ) {
+ return parsed.toLocaleString(navigator.languages[0], {
+ minimumFractionDigits: 2
+ });
+ } else {
+ return parsed.toLocaleString(navigator.languages[0], {
+ maximumFractionDigits: 3
+ });
+ }
+}
+
+export function fiatInputSanitizer(input: string, maxDecimals: number): string {
+ // Make sure only numbers and a single decimal point are allowed if decimals are allowed
+ let allowDecimalRegex;
+ if (maxDecimals !== 0) {
+ allowDecimalRegex = new RegExp("[^0-9.]", "g");
+ } else {
+ allowDecimalRegex = new RegExp("[^0-9]", "g");
+ }
+ const numeric = input
+ .replace(allowDecimalRegex, "")
+ .replace(/(\..*)\./g, "$1");
+
+ // Remove leading zeros if not a decimal, add 0 if starts with a decimal
+ const cleaned = numeric.replace(/^0([^.]|$)/g, "$1").replace(/^\./g, "0.");
+
+ // If there are more characters after the decimal than allowed, shift the decimal
+ const shiftRegex = new RegExp(
+ "(\\.[0-9]{" + (maxDecimals + 1) + "}).*",
+ "g"
+ );
+ const shifted = cleaned.match(shiftRegex)
+ ? (parseFloat(cleaned) * 10).toFixed(maxDecimals)
+ : cleaned;
+
+ // Truncate any numbers past the maxDecimal for the currency
+ const decimalRegex = new RegExp("(\\.[0-9]{" + maxDecimals + "}).*", "g");
+ const decimals = shifted.replace(decimalRegex, "$1");
+
+ return decimals;
+}
+
+export function satsInputSanitizer(input: string): string {
+ // Make sure only numbers are allowed
+ const numeric = input.replace(/[^0-9]/g, "");
+ // If it starts with a 0, remove the 0
+ const noLeadingZero = numeric.replace(/^0([^.]|$)/g, "$1");
+
+ return noLeadingZero;
+}
+
+export function btcFloatRounding(localValue: string): string {
+ return (
+ (parseFloat(localValue) -
+ parseFloat(localValue.charAt(localValue.length - 1)) / 100000000) /
+ 10
+ ).toFixed(8);
+}
diff --git a/src/utils/nostr.ts b/src/utils/nostr.ts
index 3c376901..97a0aa60 100644
--- a/src/utils/nostr.ts
+++ b/src/utils/nostr.ts
@@ -45,8 +45,11 @@ export declare enum NostrKind {
}
export async function hexpubFromNpub(
- npub: string
+ npub?: string
): Promise {
+ if (!npub) {
+ return undefined;
+ }
if (!npub.toLowerCase().startsWith("npub")) {
return undefined;
}
diff --git a/src/utils/tags.ts b/src/utils/tags.ts
deleted file mode 100644
index 1ca7adaa..00000000
--- a/src/utils/tags.ts
+++ /dev/null
@@ -1,12 +0,0 @@
-import { TagItem } from "@mutinywallet/mutiny-wasm";
-
-export function tagsToIds(tags?: TagItem[]): string[] {
- if (!tags) {
- return [];
- }
- return tags.filter((tag) => tag.id !== "Unknown").map((tag) => tag.id);
-}
-
-export function sortByLastUsed(a: TagItem, b: TagItem) {
- return Number(b.last_used_time - a.last_used_time);
-}
diff --git a/src/utils/useCopy.ts b/src/utils/useCopy.ts
index ee98846c..f989b55b 100644
--- a/src/utils/useCopy.ts
+++ b/src/utils/useCopy.ts
@@ -4,7 +4,7 @@ import { Capacitor } from "@capacitor/core";
import type { Accessor } from "solid-js";
import { createSignal } from "solid-js";
-export type UseCopyProps = {
+type UseCopyProps = {
copiedTimeout?: number;
};
type CopyFn = (text: string) => Promise;
diff --git a/src/utils/vibrate.ts b/src/utils/vibrate.ts
index f53d0c04..b46e38a3 100644
--- a/src/utils/vibrate.ts
+++ b/src/utils/vibrate.ts
@@ -1,14 +1,6 @@
import { Haptics } from "@capacitor/haptics";
import { NotificationType } from "@capacitor/haptics/dist/esm/definitions";
-export const vibrate = async (millis = 250) => {
- try {
- await Haptics.vibrate({ duration: millis });
- } catch (error) {
- console.warn(error);
- }
-};
-
export const vibrateSuccess = async () => {
try {
await Haptics.notification({ type: NotificationType.Success });
diff --git a/tailwind.config.cjs b/tailwind.config.cjs
index baa9fe4f..602f1804 100644
--- a/tailwind.config.cjs
+++ b/tailwind.config.cjs
@@ -37,7 +37,7 @@ module.exports = {
"m-grey-750": "hsla(0, 0%, 17%, 1)",
"m-grey-800": "hsla(0, 0%, 12%, 1)",
"m-grey-900": "hsla(0, 0%, 9%, 1)",
- "m-grey-950": "hsla(0, 0%, 8%, 1)",
+ "m-grey-950": "hsla(0, 0%, 8%, 1)"
},
backgroundImage: {
"fade-to-blue":
@@ -58,7 +58,11 @@ module.exports = {
"fancy-card": "0px 4px 4px rgba(0, 0, 0, 0.1)",
"subtle-bevel":
"inset -4px -4px 6px 0 rgba(0, 0, 0, 0.10), inset 4px 4px 4px 0 rgba(255, 255, 255, 0.10)",
- above: "0px -4px 10px rgba(0, 0, 0, 0.25)"
+ above: "0px -4px 10px rgba(0, 0, 0, 0.25)",
+ keycap: "15px 15px 20px -5px rgba(0, 0, 0, 0.3)"
+ },
+ fontFamily: {
+ "system-mono": ["ui-monospace", "Menlo", "Monaco", "monospace"]
},
textShadow: {
button: "1px 1px 0px rgba(0, 0, 0, 0.4)"
@@ -86,7 +90,14 @@ module.exports = {
height: "calc(100vh - env(safe-area-inset-top) - env(safe-area-inset-bottom))"
},
"max-h-device": {
- maxHeight: "calc(100vh - env(safe-area-inset-top) - env(safe-area-inset-bottom))"
+ maxHeight:
+ "calc(100vh - env(safe-area-inset-top) - env(safe-area-inset-bottom))"
+ },
+ ".h-dvh": {
+ height: "calc(100dvh - env(safe-area-inset-top) - env(safe-area-inset-bottom - env(keyboard-inset-height))"
+ },
+ ".h-svh": {
+ height: "calc(100svh - env(safe-area-inset-top) - env(safe-area-inset-bottom))"
},
".disable-scrollbars": {
scrollbarWidth: "none",