diff --git a/public/i18n/en.json b/public/i18n/en.json index bb0d2611..e24fef04 100644 --- a/public/i18n/en.json +++ b/public/i18n/en.json @@ -92,7 +92,8 @@ "redeem_bitcoin": "Redeem Bitcoin", "lnurl_amount_message": "Enter withdrawal amount between {{min}} and {{max}} sats", "lnurl_redeem_failed": "Withdrawal Failed", - "lnurl_redeem_success": "Payment Received" + "lnurl_redeem_success": "Payment Received", + "cashu_already_spent": "That token has already been spent" }, "request": { "request_bitcoin": "Request Bitcoin", @@ -781,6 +782,7 @@ "minutes_short": "{{count}}m", "nowish": "Nowish", "seconds_future": "Seconds from now", - "seconds_past": "Just now" + "seconds_past": "Just now", + "weeks_short": "{{count}}w" } } diff --git a/src/components/Activity.tsx b/src/components/Activity.tsx index 4163f768..a2fcbf04 100644 --- a/src/components/Activity.tsx +++ b/src/components/Activity.tsx @@ -1,6 +1,6 @@ import { TagItem } from "@mutinywallet/mutiny-wasm"; import { cache, createAsync, useNavigate } from "@solidjs/router"; -import { Plus, Save, Search, Shuffle, Users } from "lucide-solid"; +import { Nut, Plus, Save, Search, Shuffle, Users } from "lucide-solid"; import { createEffect, createMemo, @@ -157,12 +157,21 @@ export function UnifiedActivityItem(props: { return filtered[0]; }; - const shouldShowShuffle = () => { - return ( + const maybeIcon = () => { + if ( props.item.kind === "ChannelOpen" || props.item.kind === "ChannelClose" || (props.item.labels.length > 0 && props.item.labels[0] === "SWAP") - ); + ) { + return ; + } + + if ( + props.item.labels.length > 0 && + props.item.labels[0] === "Cashu Token Melt" + ) { + return ; + } }; const verb = () => { @@ -266,7 +275,7 @@ export function UnifiedActivityItem(props: { ? primaryContact()?.image_url : profileFromNostr()?.primal_image_url || "" } - icon={shouldShowShuffle() ? : undefined} + icon={maybeIcon()} primaryOnClick={handlePrimaryOnClick} amountOnClick={click} primaryName={ diff --git a/src/components/GenericItem.tsx b/src/components/GenericItem.tsx index 4816a99b..ae231d8f 100644 --- a/src/components/GenericItem.tsx +++ b/src/components/GenericItem.tsx @@ -114,6 +114,7 @@ export function GenericItem(props: {
+ {/* the date might include slashes so we don't want to escape those */} {i18n.t("common.expires", { time: props.due, interpolation: { escapeValue: false } diff --git a/src/logic/waila.ts b/src/logic/waila.ts index 41ee03f9..e5c944ca 100644 --- a/src/logic/waila.ts +++ b/src/logic/waila.ts @@ -17,6 +17,7 @@ export type ParsedParams = { fedimint_invite?: string; is_lnurl_auth?: boolean; contact_id?: string; + cashu_token?: string; }; export async function toParsedParams( @@ -63,7 +64,8 @@ export async function toParsedParams( lightning_address: params.lightning_address, nostr_wallet_auth: params.nostr_wallet_auth, is_lnurl_auth: params.is_lnurl_auth, - fedimint_invite: params.fedimint_invite_code + fedimint_invite: params.fedimint_invite_code, + cashu_token: params.cashu_token } }; } diff --git a/src/routes/Chat.tsx b/src/routes/Chat.tsx index de8e2a58..1aee76ee 100644 --- a/src/routes/Chat.tsx +++ b/src/routes/Chat.tsx @@ -126,6 +126,19 @@ function SingleMessage(props: { amount: result.value.amount_sats }; } + + if (result.value?.cashu_token) { + return { + type: "cashu", + message_without_invoice: props.dm.message.replace( + result.value.original, + "" + ), + from: props.dm.from, + value: result.value.cashu_token, + amount: result.value.amount_sats + }; + } }, { initialValue: undefined @@ -159,6 +172,16 @@ function SingleMessage(props: { ); } + function handleRedeem(token: string) { + actions.handleIncomingString( + token, + (error) => { + showToast(error); + }, + payContact + ); + } + return (
+ +
+ +

+ {parsed()?.message_without_invoice} +

+
+
+ + Cashu Token +
+ + + + + + +

Paid

+
+
+
+

{props.dm.message} diff --git a/src/routes/Redeem.tsx b/src/routes/Redeem.tsx index d166499b..71a3b025 100644 --- a/src/routes/Redeem.tsx +++ b/src/routes/Redeem.tsx @@ -18,6 +18,7 @@ import { BackLink, Button, DefaultMain, + Failure, InfoBox, LargeHeader, LoadingShimmer, @@ -33,7 +34,7 @@ import { useI18n } from "~/i18n/context"; import { useMegaStore } from "~/state/megaStore"; import { eify, vibrateSuccess } from "~/utils"; -type RedeemState = "edit" | "paid"; +type RedeemState = "edit" | "paid" | "already_paid"; export function Redeem() { const [state, _actions, sw] = useMegaStore(); @@ -67,12 +68,13 @@ export function Redeem() { setError(""); } + // + // Lnurl stuff + // const [decodedLnurl] = createResource(async () => { - if (state.scan_result) { - if (state.scan_result.lnurl) { - const decoded = await sw.decode_lnurl(state.scan_result.lnurl); - return decoded; - } + if (state.scan_result && state.scan_result.lnurl) { + const decoded = await sw.decode_lnurl(state.scan_result.lnurl); + return decoded; } }); @@ -107,7 +109,7 @@ export function Redeem() { } }); - const canSend = createMemo(() => { + const lnUrlCanSend = createMemo(() => { const lnurlParams = lnurlData(); if (!lnurlParams) return false; const min = mSatsToSats(lnurlParams.min); @@ -140,6 +142,50 @@ export function Redeem() { } } + // + // Cashu stuff + // + const [decodedCashuToken] = createResource(async () => { + if (state.scan_result && state.scan_result.cashu_token) { + // If it's a cashu token we already have what we need + const token = state.scan_result?.cashu_token; + const amount = state.scan_result?.amount_sats; + if (amount) { + setAmount(amount); + setFixedAmount(true); + } + + return token; + } + }); + + const cashuCanSend = createMemo(() => { + if (!decodedCashuToken()) return false; + if (amount() === 0n) return false; + return true; + }); + + async function meltCashuToken() { + try { + setError(""); + setLoading(true); + if (!state.scan_result?.cashu_token) return; + await sw.melt_cashu_token(state.scan_result?.cashu_token); + setRedeemState("paid"); + await vibrateSuccess(); + } catch (e) { + console.error("melt_cashu_token failed", e); + const err = eify(e); + if (err.message === "Token has been already spent.") { + setRedeemState("already_paid"); + } else { + showToast(err); + } + } finally { + setLoading(false); + } + } + return ( @@ -156,14 +202,24 @@ export function Redeem() {

} > - - - + + + + + + {}} + onSubmit={() => {}} + frozenAmount={fixedAmount()} + /> + + */} - + + + + + + + +
@@ -234,7 +304,23 @@ export function Redeem() {
{/* TODO: add payment details */} -
NICE
+ + + { + if (!open) clearAll(); + }} + onConfirm={() => { + clearAll(); + navigate("/"); + }} + confirmText={i18n.t("common.dangit")} + > + + diff --git a/src/state/megaStore.tsx b/src/state/megaStore.tsx index 8bd49230..1e818ae1 100644 --- a/src/state/megaStore.tsx +++ b/src/state/megaStore.tsx @@ -453,6 +453,11 @@ export const makeMegaStoreContext = () => { encodeURIComponent(result.value?.nostr_wallet_auth) ); } + if (result.value?.cashu_token) { + console.log("cashu_token", result.value?.cashu_token); + actions.setScanResult(result.value); + navigate("/redeem"); + } } }, setTestFlightPromptDismissed() { diff --git a/src/utils/prettyPrintTime.ts b/src/utils/prettyPrintTime.ts index 94c04450..8f2692a7 100644 --- a/src/utils/prettyPrintTime.ts +++ b/src/utils/prettyPrintTime.ts @@ -57,6 +57,7 @@ export function veryShortTimeStamp(ts?: number | bigint) { const elapsedMinutes = Math.floor(elapsedSeconds / 60); const elapsedHours = Math.floor(elapsedMinutes / 60); const elapsedDays = Math.floor(elapsedHours / 24); + const elapsedWeeks = Math.floor(elapsedDays / 7); if (elapsedSeconds < 60) { return i18n.t("utils.nowish"); @@ -66,6 +67,8 @@ export function veryShortTimeStamp(ts?: number | bigint) { return i18n.t("utils.hours_short", { count: elapsedHours }); } else if (elapsedDays < 7) { return i18n.t("utils.days_short", { count: elapsedDays }); + } else if (elapsedDays < 30) { + return i18n.t("utils.weeks_short", { count: elapsedWeeks }); } else { const date = new Date(timestamp); const day = String(date.getDate()).padStart(2, "0"); diff --git a/src/workers/walletWorker.ts b/src/workers/walletWorker.ts index 8f5d54b5..a2d9d1f9 100644 --- a/src/workers/walletWorker.ts +++ b/src/workers/walletWorker.ts @@ -1565,6 +1565,15 @@ export async function estimate_sweep_federation_fee( return await wallet!.estimate_sweep_federation_fee(amount); } +/** + * Calls upon a Cash mint and melts the token from it. + * @param {string} maybe_token + * @returns {Promise} + */ +export async function melt_cashu_token(maybe_token: string): Promise { + return await wallet!.melt_cashu_token(maybe_token); +} + export async function parse_params(params: string): Promise { const paramsResult = await new PaymentParams(params); // PAIN just another object rebuild