add redeem route for lnulrwithdrawal

Co-authored-by: tompro <office@protom.eu>
This commit is contained in:
Paul Miller
2024-02-26 16:51:45 +00:00
parent 7b1d293656
commit c486fcdd37
6 changed files with 267 additions and 0 deletions

View File

@@ -132,6 +132,7 @@ just i18n $lang
### Adding new languages or keys
1. In `src/i18n/` locate your desired language folder or create one if one does not exist
- When creating a new language dir ensure it follows the ISO 639 2-letter standard
2. In this folder create a file called `translations.ts`, this is where the translation keys for your desired language will be located
@@ -139,6 +140,7 @@ just i18n $lang
3. Populate your translation file with a translation object where all of the keys will be located
If you want to add Japanese you will create a file `/src/i18n/jp/translations.ts` and populate it with keys like so:
```
export default {
Common: {
@@ -147,6 +149,7 @@ export default {
}
}
```
(You should compare your translations against the English language as all other languages are not the master and are likely deprecated)
4. Add your new translation file to the `/src/i18n/config.ts` so you can begin to see them in the app
@@ -163,6 +166,7 @@ export const resources: {
```
5. Add your language to the `Language` object in `/src/utils/languages.ts`. This will allow you to select the language via the language selector in the UI. If your desired language is set as your primary language in your browser it will be selected automatically
```
export const LANGUAGE_OPTIONS: Language[] = [
{

View File

@@ -53,6 +53,13 @@ export default {
npub: "Nostr Npub",
link_to_nostr_sync: "Import Nostr Contacts"
},
redeem: {
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"
},
receive: {
receive_bitcoin: "Receive Bitcoin",
edit: "Edit",

View File

@@ -12,6 +12,7 @@ import {
Main,
NotFound,
Receive,
Redeem,
Scanner,
Search,
Send,
@@ -100,6 +101,7 @@ export function Router() {
<Route path="/feedback" component={Feedback} />
<Route path="/gift" component={GiftReceive} />
<Route path="/receive" component={Receive} />
<Route path="/redeem" component={Redeem} />
<Route path="/scanner" component={Scanner} />
<Route path="/send" component={Send} />
<Route path="/swap" component={Swap} />

248
src/routes/Redeem.tsx Normal file
View File

@@ -0,0 +1,248 @@
import { LnUrlParams } from "@mutinywallet/mutiny-wasm";
import { useNavigate } from "@solidjs/router";
import {
createEffect,
createMemo,
createResource,
createSignal,
Match,
Show,
Suspense,
Switch
} from "solid-js";
import {
AmountEditable,
AmountFiat,
AmountSats,
BackLink,
Button,
DefaultMain,
InfoBox,
LargeHeader,
LoadingShimmer,
MegaCheck,
MutinyWalletGuard,
NavBar,
ReceiveWarnings,
showToast,
SuccessModal,
VStack
} from "~/components";
import { useI18n } from "~/i18n/context";
import { useMegaStore } from "~/state/megaStore";
import { eify, vibrateSuccess } from "~/utils";
type RedeemState = "edit" | "paid";
export function Redeem() {
const [state, _actions] = useMegaStore();
const navigate = useNavigate();
const i18n = useI18n();
const [amount, setAmount] = createSignal<bigint>(0n);
// const [whatForInput, setWhatForInput] = createSignal("");
const [lnurlData, setLnUrlData] = createSignal<LnUrlParams>();
const [lnurlString, setLnUrlString] = createSignal("");
const [fixedAmount, setFixedAmount] = createSignal(false);
const [redeemState, setRedeemState] = createSignal<RedeemState>("edit");
// loading state for the continue button
const [loading, setLoading] = createSignal(false);
const [error, setError] = createSignal<string>("");
function mSatsToSats(mSats: bigint) {
return mSats / 1000n;
}
function clearAll() {
setAmount(0n);
setLnUrlData(undefined);
setLnUrlString("");
setFixedAmount(false);
setRedeemState("edit");
setLoading(false);
setError("");
}
const [decodedLnurl] = createResource(async () => {
if (state.scan_result) {
if (state.scan_result.lnurl) {
const decoded = await state.mutiny_wallet?.decode_lnurl(
state.scan_result.lnurl
);
return decoded;
}
}
});
createEffect(() => {
if (decodedLnurl()) {
processLnurl(decodedLnurl()!);
}
});
// A ParsedParams with an lnurl in it
async function processLnurl(decoded: LnUrlParams) {
if (decoded.tag === "withdrawRequest") {
if (decoded.min === decoded.max) {
console.log("fixed amount", decoded.max.toString());
setAmount(mSatsToSats(decoded.max));
setFixedAmount(true);
} else {
setAmount(mSatsToSats(decoded.min));
setFixedAmount(false);
}
setLnUrlData(decoded);
setLnUrlString(state.scan_result?.lnurl || "");
}
}
const lnurlAmountText = createMemo(() => {
if (lnurlData()) {
return i18n.t("redeem.lnurl_amount_message", {
min: mSatsToSats(lnurlData()!.min).toLocaleString(),
max: mSatsToSats(lnurlData()!.max).toLocaleString()
});
}
});
const canSend = createMemo(() => {
const lnurlParams = lnurlData();
if (!lnurlParams) return false;
const min = mSatsToSats(lnurlParams.min);
const max = mSatsToSats(lnurlParams.max);
if (amount() === 0n || amount() < min || amount() > max) return false;
return true;
});
async function handleLnUrlWithdrawal() {
const lnurlParams = lnurlData();
if (!lnurlParams) return;
setError("");
setLoading(true);
try {
const success = await state.mutiny_wallet?.lnurl_withdraw(
lnurlString(),
amount()
);
if (!success) {
setError(i18n.t("redeem.lnurl_redeem_failed"));
} else {
setRedeemState("paid");
await vibrateSuccess();
}
} catch (e) {
console.error("lnurl_withdraw failed", e);
showToast(eify(e));
} finally {
setLoading(false);
}
}
return (
<MutinyWalletGuard>
<DefaultMain>
<BackLink />
<LargeHeader>{i18n.t("redeem.redeem_bitcoin")}</LargeHeader>
<Switch>
<Match when={redeemState() === "edit"}>
<div class="flex-1" />
<VStack>
<Suspense
fallback={
<div class="self-center">
<LoadingShimmer />
</div>
}
>
<Show when={decodedLnurl() && lnurlData()}>
<AmountEditable
initialAmountSats={amount() || "0"}
setAmountSats={setAmount}
onSubmit={handleLnUrlWithdrawal}
frozenAmount={fixedAmount()}
/>
</Show>
</Suspense>
<ReceiveWarnings
amountSats={amount() || "0"}
from_fedi_to_ln={false}
/>
<Show when={lnurlAmountText() && !fixedAmount()}>
<InfoBox accent="white">
<p>{lnurlAmountText()}</p>
</InfoBox>
</Show>
<Show when={error()}>
<InfoBox accent="red">
<p>{error()}</p>
</InfoBox>
</Show>
</VStack>
<div class="flex-1" />
<VStack>
{/* TODO: add tagging to lnurlwithdrawal and all the redeem flows */}
{/* <form onSubmit={handleLnUrlWithdrawal}>
<SimpleInput
type="text"
value={whatForInput()}
placeholder={i18n.t("receive.what_for")}
onInput={(e) =>
setWhatForInput(e.currentTarget.value)
}
/>
</form> */}
<Button
disabled={!amount() || !canSend()}
intent="green"
onClick={handleLnUrlWithdrawal}
loading={loading()}
>
{i18n.t("common.continue")}
</Button>
</VStack>
</Match>
<Match when={redeemState() === "paid"}>
<SuccessModal
open={true}
setOpen={(open: boolean) => {
if (!open) clearAll();
}}
onConfirm={() => {
clearAll();
navigate("/");
}}
>
<MegaCheck />
<h1 class="mb-2 mt-4 w-full text-center text-2xl font-semibold md:text-3xl">
{i18n.t("redeem.lnurl_redeem_success")}
</h1>
<div class="flex flex-col items-center gap-1">
<div class="text-xl">
<AmountSats
amountSats={amount()}
icon="plus"
/>
</div>
<div class="text-white/70">
<AmountFiat
amountSats={amount()}
denominationSize="sm"
/>
</div>
</div>
{/* TODO: add payment details */}
</SuccessModal>
<pre>NICE</pre>
</Match>
</Switch>
</DefaultMain>
<NavBar activeTab="receive" />
</MutinyWalletGuard>
);
}

View File

@@ -447,6 +447,11 @@ export function Send() {
setLnurlp(source.lnurl);
setSource("lightning");
}
// TODO: this is a bit of a hack, ideally we do more nav from the megastore
if (lnurlParams.tag === "withdrawRequest") {
actions.setScanResult(source);
navigate("/redeem");
}
})
.catch((e) => showToast(eify(e)));
}

View File

@@ -9,3 +9,4 @@ export * from "./Send";
export * from "./Swap";
export * from "./SwapLightning";
export * from "./Search";
export * from "./Redeem";