mirror of
https://github.com/aljazceru/mutiny-web.git
synced 2025-12-18 23:04:25 +01:00
feat: third pass at translations
This commit is contained in:
@@ -177,7 +177,9 @@ export function ActivityItem(props: {
|
|||||||
</Switch>
|
</Switch>
|
||||||
<Switch>
|
<Switch>
|
||||||
<Match when={props.date && props.date > 2147483647}>
|
<Match when={props.date && props.date > 2147483647}>
|
||||||
<time class="text-sm text-neutral-500">Pending</time>
|
<time class="text-sm text-neutral-500">
|
||||||
|
{i18n.t("common.pending")}
|
||||||
|
</time>
|
||||||
</Match>
|
</Match>
|
||||||
<Match when={true}>
|
<Match when={true}>
|
||||||
<time class="text-sm text-neutral-500">
|
<time class="text-sm text-neutral-500">
|
||||||
|
|||||||
@@ -81,6 +81,7 @@ export function AmountCard(props: {
|
|||||||
exitRoute?: string;
|
exitRoute?: string;
|
||||||
maxAmountSats?: bigint;
|
maxAmountSats?: bigint;
|
||||||
}) {
|
}) {
|
||||||
|
const i18n = useI18n();
|
||||||
// Normally we want to add the fee to the amount, but for max amount we just show the max
|
// Normally we want to add the fee to the amount, but for max amount we just show the max
|
||||||
const totalOrTotalLessFee = () => {
|
const totalOrTotalLessFee = () => {
|
||||||
if (
|
if (
|
||||||
@@ -99,7 +100,7 @@ export function AmountCard(props: {
|
|||||||
<Switch>
|
<Switch>
|
||||||
<Match when={props.fee}>
|
<Match when={props.fee}>
|
||||||
<div class="flex flex-col gap-1">
|
<div class="flex flex-col gap-1">
|
||||||
<KeyValue key="Amount">
|
<KeyValue key={i18n.t("receive.amount")}>
|
||||||
<Show
|
<Show
|
||||||
when={props.isAmountEditable}
|
when={props.isAmountEditable}
|
||||||
fallback={
|
fallback={
|
||||||
@@ -123,13 +124,13 @@ export function AmountCard(props: {
|
|||||||
/>
|
/>
|
||||||
</Show>
|
</Show>
|
||||||
</KeyValue>
|
</KeyValue>
|
||||||
<KeyValue gray key="+ Fee">
|
<KeyValue gray key={i18n.t("receive.fee")}>
|
||||||
<InlineAmount amount={props.fee || "0"} />
|
<InlineAmount amount={props.fee || "0"} />
|
||||||
</KeyValue>
|
</KeyValue>
|
||||||
</div>
|
</div>
|
||||||
<hr class="border-white/20" />
|
<hr class="border-white/20" />
|
||||||
<div class="flex flex-col gap-1">
|
<div class="flex flex-col gap-1">
|
||||||
<KeyValue key="Total">
|
<KeyValue key={i18n.t("receive.total")}>
|
||||||
<InlineAmount amount={totalOrTotalLessFee()} />
|
<InlineAmount amount={totalOrTotalLessFee()} />
|
||||||
</KeyValue>
|
</KeyValue>
|
||||||
<USDShower
|
<USDShower
|
||||||
@@ -140,7 +141,7 @@ export function AmountCard(props: {
|
|||||||
</Match>
|
</Match>
|
||||||
<Match when={props.reserve}>
|
<Match when={props.reserve}>
|
||||||
<div class="flex flex-col gap-1">
|
<div class="flex flex-col gap-1">
|
||||||
<KeyValue key="Channel size">
|
<KeyValue key={i18n.t("receive.channel_size")}>
|
||||||
<InlineAmount
|
<InlineAmount
|
||||||
amount={add(
|
amount={add(
|
||||||
props.amountSats,
|
props.amountSats,
|
||||||
@@ -148,13 +149,16 @@ export function AmountCard(props: {
|
|||||||
).toString()}
|
).toString()}
|
||||||
/>
|
/>
|
||||||
</KeyValue>
|
</KeyValue>
|
||||||
<KeyValue gray key="- Channel Reserve">
|
<KeyValue
|
||||||
|
gray
|
||||||
|
key={i18n.t("receive.channel_reserve")}
|
||||||
|
>
|
||||||
<InlineAmount amount={props.reserve || "0"} />
|
<InlineAmount amount={props.reserve || "0"} />
|
||||||
</KeyValue>
|
</KeyValue>
|
||||||
</div>
|
</div>
|
||||||
<hr class="border-white/20" />
|
<hr class="border-white/20" />
|
||||||
<div class="flex flex-col gap-1">
|
<div class="flex flex-col gap-1">
|
||||||
<KeyValue key="Spendable">
|
<KeyValue key={i18n.t("receive.spendable")}>
|
||||||
<InlineAmount amount={props.amountSats} />
|
<InlineAmount amount={props.amountSats} />
|
||||||
</KeyValue>
|
</KeyValue>
|
||||||
<USDShower
|
<USDShower
|
||||||
@@ -165,7 +169,7 @@ export function AmountCard(props: {
|
|||||||
</Match>
|
</Match>
|
||||||
<Match when={!props.fee && !props.reserve}>
|
<Match when={!props.fee && !props.reserve}>
|
||||||
<div class="flex flex-col gap-1">
|
<div class="flex flex-col gap-1">
|
||||||
<KeyValue key="Amount">
|
<KeyValue key={i18n.t("receive.amount")}>
|
||||||
<Show
|
<Show
|
||||||
when={props.isAmountEditable}
|
when={props.isAmountEditable}
|
||||||
fallback={
|
fallback={
|
||||||
|
|||||||
@@ -24,18 +24,6 @@ import { FeesModal } from "./MoreInfoModal";
|
|||||||
import { useI18n } from "~/i18n/context";
|
import { useI18n } from "~/i18n/context";
|
||||||
import { useNavigate } from "solid-start";
|
import { useNavigate } from "solid-start";
|
||||||
|
|
||||||
const FIXED_AMOUNTS_SATS = [
|
|
||||||
{ label: "10k", amount: "10000" },
|
|
||||||
{ label: "100k", amount: "100000" },
|
|
||||||
{ label: "1m", amount: "1000000" }
|
|
||||||
];
|
|
||||||
|
|
||||||
const FIXED_AMOUNTS_USD = [
|
|
||||||
{ label: "$1", amount: "1" },
|
|
||||||
{ label: "$10", amount: "10" },
|
|
||||||
{ label: "$100", amount: "100" }
|
|
||||||
];
|
|
||||||
|
|
||||||
function fiatInputSanitizer(input: string): string {
|
function fiatInputSanitizer(input: string): string {
|
||||||
// Make sure only numbers and a single decimal point are allowed
|
// Make sure only numbers and a single decimal point are allowed
|
||||||
const numeric = input.replace(/[^0-9.]/g, "").replace(/(\..*)\./g, "$1");
|
const numeric = input.replace(/[^0-9.]/g, "").replace(/(\..*)\./g, "$1");
|
||||||
@@ -76,7 +64,7 @@ function SingleDigitButton(props: {
|
|||||||
function onHold() {
|
function onHold() {
|
||||||
if (
|
if (
|
||||||
props.character === "DEL" ||
|
props.character === "DEL" ||
|
||||||
props.character === i18n.t("char.del")
|
props.character === i18n.t("receive.amount_editable.del")
|
||||||
) {
|
) {
|
||||||
holdTimer = setTimeout(() => {
|
holdTimer = setTimeout(() => {
|
||||||
props.onClear();
|
props.onClear();
|
||||||
@@ -148,7 +136,7 @@ function SmallSubtleAmount(props: { text: string; fiat: boolean }) {
|
|||||||
<h2 class="flex flex-row items-end text-xl font-light text-neutral-400">
|
<h2 class="flex flex-row items-end text-xl font-light text-neutral-400">
|
||||||
~{props.text}
|
~{props.text}
|
||||||
<span class="text-base">
|
<span class="text-base">
|
||||||
{props.fiat ? i18n.t("common.usd") : `${i18n.t("common.sats")}`}
|
{props.fiat ? i18n.t("common.usd") : i18n.t("common.sats")}
|
||||||
</span>
|
</span>
|
||||||
<img
|
<img
|
||||||
class={"pl-[4px] pb-[4px] hover:cursor-pointer"}
|
class={"pl-[4px] pb-[4px] hover:cursor-pointer"}
|
||||||
@@ -208,6 +196,28 @@ export const AmountEditable: ParentComponent<{
|
|||||||
false
|
false
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const FIXED_AMOUNTS_SATS = [
|
||||||
|
{
|
||||||
|
label: i18n.t("receive.amount_editable.fix_amounts.ten_k"),
|
||||||
|
amount: "10000"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: i18n.t("receive.amount_editable.fix_amounts.one_hundred_k"),
|
||||||
|
amount: "100000"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: i18n.t("receive.amount_editable.fix_amounts.one_million"),
|
||||||
|
amount: "1000000"
|
||||||
|
}
|
||||||
|
];
|
||||||
|
|
||||||
|
const FIXED_AMOUNTS_USD = [
|
||||||
|
{ label: "$1", amount: "1" },
|
||||||
|
{ label: "$10", amount: "10" },
|
||||||
|
{ label: "$100", amount: "100" }
|
||||||
|
];
|
||||||
|
|
||||||
const CHARACTERS = [
|
const CHARACTERS = [
|
||||||
"1",
|
"1",
|
||||||
"2",
|
"2",
|
||||||
@@ -220,7 +230,7 @@ export const AmountEditable: ParentComponent<{
|
|||||||
"9",
|
"9",
|
||||||
".",
|
".",
|
||||||
"0",
|
"0",
|
||||||
i18n.t("char.del")
|
i18n.t("receive.amount_editable.del")
|
||||||
];
|
];
|
||||||
|
|
||||||
const displaySats = () => toDisplayHandleNaN(localSats(), false);
|
const displaySats = () => toDisplayHandleNaN(localSats(), false);
|
||||||
@@ -295,7 +305,10 @@ export const AmountEditable: ParentComponent<{
|
|||||||
|
|
||||||
let sane;
|
let sane;
|
||||||
|
|
||||||
if (character === "DEL" || character === i18n.t("char.del")) {
|
if (
|
||||||
|
character === "DEL" ||
|
||||||
|
character === i18n.t("receive.amount_editable.del")
|
||||||
|
) {
|
||||||
if (localValue().length <= 1) {
|
if (localValue().length <= 1) {
|
||||||
sane = "0";
|
sane = "0";
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
@@ -15,8 +15,10 @@ import { PendingNwc } from "./PendingNwc";
|
|||||||
import { DecryptDialog } from "./DecryptDialog";
|
import { DecryptDialog } from "./DecryptDialog";
|
||||||
import { LoadingIndicator } from "./LoadingIndicator";
|
import { LoadingIndicator } from "./LoadingIndicator";
|
||||||
import { FeedbackLink } from "~/routes/Feedback";
|
import { FeedbackLink } from "~/routes/Feedback";
|
||||||
|
import { useI18n } from "~/i18n/context";
|
||||||
|
|
||||||
export default function App() {
|
export default function App() {
|
||||||
|
const i18n = useI18n();
|
||||||
const [state, _actions] = useMegaStore();
|
const [state, _actions] = useMegaStore();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -71,7 +73,7 @@ export default function App() {
|
|||||||
<PendingNwc />
|
<PendingNwc />
|
||||||
</Show>
|
</Show>
|
||||||
</Suspense>
|
</Suspense>
|
||||||
<Card title="Activity">
|
<Card title={i18n.t("activity.title")}>
|
||||||
<div class="p-1" />
|
<div class="p-1" />
|
||||||
<VStack>
|
<VStack>
|
||||||
<Suspense>
|
<Suspense>
|
||||||
|
|||||||
@@ -79,7 +79,9 @@ export default function BalanceBox(props: { loading?: boolean }) {
|
|||||||
</div>
|
</div>
|
||||||
<div class="flex flex-col items-end gap-1 justify-between">
|
<div class="flex flex-col items-end gap-1 justify-between">
|
||||||
<Show when={state.balance?.unconfirmed != 0n}>
|
<Show when={state.balance?.unconfirmed != 0n}>
|
||||||
<Indicator>Pending</Indicator>
|
<Indicator>
|
||||||
|
{i18n.t("common.pending")}
|
||||||
|
</Indicator>
|
||||||
</Show>
|
</Show>
|
||||||
<Show when={state.balance?.unconfirmed === 0n}>
|
<Show when={state.balance?.unconfirmed === 0n}>
|
||||||
<div />
|
<div />
|
||||||
|
|||||||
@@ -4,28 +4,26 @@ import { DIALOG_CONTENT, DIALOG_POSITIONER, OVERLAY } from "./DetailsModal";
|
|||||||
import { ModalCloseButton, SmallHeader } from "./layout";
|
import { ModalCloseButton, SmallHeader } from "./layout";
|
||||||
import { ExternalLink } from "./layout/ExternalLink";
|
import { ExternalLink } from "./layout/ExternalLink";
|
||||||
import { getExistingSettings } from "~/logic/mutinyWalletSetup";
|
import { getExistingSettings } from "~/logic/mutinyWalletSetup";
|
||||||
|
import { useI18n } from "~/i18n/context";
|
||||||
|
|
||||||
export function BetaWarningModal() {
|
export function BetaWarningModal() {
|
||||||
|
const i18n = useI18n();
|
||||||
return (
|
return (
|
||||||
<WarningModal title="Warning: beta software" linkText="Why?">
|
<WarningModal
|
||||||
<p>
|
title={i18n.t("modals.beta_warning.title")}
|
||||||
We're so glad you're here. But we do want to warn you: Mutiny
|
linkText={i18n.t("common.why")}
|
||||||
Wallet is in beta, and there are still bugs and rough edges.
|
>
|
||||||
</p>
|
<p>{i18n.t("translations:modals.beta_warning.beta_warning")}</p>
|
||||||
<p>
|
<p>{i18n.t("modals.beta_warning.be_careful")}</p>
|
||||||
Please be careful and don't put more money into Mutiny than
|
|
||||||
you're willing to lose.
|
|
||||||
</p>
|
|
||||||
<p>
|
<p>
|
||||||
<ExternalLink href="https://github.com/MutinyWallet/mutiny-web/wiki/Mutiny-Beta-Readme">
|
<ExternalLink href="https://github.com/MutinyWallet/mutiny-web/wiki/Mutiny-Beta-Readme">
|
||||||
Learn more about the beta
|
{i18n.t("modals.beta_warning.beta_link")}
|
||||||
</ExternalLink>
|
</ExternalLink>
|
||||||
</p>
|
</p>
|
||||||
<p class="small text-neutral-400">
|
<p class="small text-neutral-400">
|
||||||
If you want to use pretend money to test out Mutiny without
|
{i18n.t("modals.beta_warning.pretend_money")}{" "}
|
||||||
risk,{" "}
|
|
||||||
<ExternalLink href="https://blog.mutinywallet.com/mutiny-wallet-signet-release/">
|
<ExternalLink href="https://blog.mutinywallet.com/mutiny-wallet-signet-release/">
|
||||||
check out our Signet version.
|
{i18n.t("modals.beta_warning.signet_link")}
|
||||||
</ExternalLink>
|
</ExternalLink>
|
||||||
</p>
|
</p>
|
||||||
</WarningModal>
|
</WarningModal>
|
||||||
|
|||||||
@@ -6,11 +6,13 @@ import { SubmitHandler } from "@modular-forms/solid";
|
|||||||
import { ContactForm } from "./ContactForm";
|
import { ContactForm } from "./ContactForm";
|
||||||
import { ContactFormValues } from "./ContactViewer";
|
import { ContactFormValues } from "./ContactViewer";
|
||||||
import { DIALOG_CONTENT, DIALOG_POSITIONER } from "~/styles/dialogs";
|
import { DIALOG_CONTENT, DIALOG_POSITIONER } from "~/styles/dialogs";
|
||||||
|
import { useI18n } from "~/i18n/context";
|
||||||
|
|
||||||
export function ContactEditor(props: {
|
export function ContactEditor(props: {
|
||||||
createContact: (contact: ContactFormValues) => void;
|
createContact: (contact: ContactFormValues) => void;
|
||||||
list?: boolean;
|
list?: boolean;
|
||||||
}) {
|
}) {
|
||||||
|
const i18n = useI18n();
|
||||||
const [isOpen, setIsOpen] = createSignal(false);
|
const [isOpen, setIsOpen] = createSignal(false);
|
||||||
|
|
||||||
// What we're all here for in the first place: returning a value
|
// What we're all here for in the first place: returning a value
|
||||||
@@ -32,12 +34,14 @@ export function ContactEditor(props: {
|
|||||||
<div class="bg-neutral-500 flex-none h-16 w-16 rounded-full flex items-center justify-center text-4xl uppercase ">
|
<div class="bg-neutral-500 flex-none h-16 w-16 rounded-full flex items-center justify-center text-4xl uppercase ">
|
||||||
<span class="leading-[4rem]">+</span>
|
<span class="leading-[4rem]">+</span>
|
||||||
</div>
|
</div>
|
||||||
<SmallHeader class="overflow-ellipsis">new</SmallHeader>
|
<SmallHeader class="overflow-ellipsis">
|
||||||
|
{i18n.t("contacts.new")}
|
||||||
|
</SmallHeader>
|
||||||
</button>
|
</button>
|
||||||
</Match>
|
</Match>
|
||||||
<Match when={!props.list}>
|
<Match when={!props.list}>
|
||||||
<TinyButton onClick={() => setIsOpen(true)}>
|
<TinyButton onClick={() => setIsOpen(true)}>
|
||||||
+ Add Contact
|
+ {i18n.t("contacts.add_contact")}
|
||||||
</TinyButton>
|
</TinyButton>
|
||||||
</Match>
|
</Match>
|
||||||
</Switch>
|
</Switch>
|
||||||
@@ -57,8 +61,8 @@ export function ContactEditor(props: {
|
|||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<ContactForm
|
<ContactForm
|
||||||
title="New contact"
|
title={i18n.t("contacts.new_contact")}
|
||||||
cta="Create contact"
|
cta={i18n.t("contacts.create_contact")}
|
||||||
handleSubmit={handleSubmit}
|
handleSubmit={handleSubmit}
|
||||||
/>
|
/>
|
||||||
</Dialog.Content>
|
</Dialog.Content>
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import { SubmitHandler, createForm, required } from "@modular-forms/solid";
|
|||||||
import { Button, LargeHeader, VStack } from "~/components/layout";
|
import { Button, LargeHeader, VStack } from "~/components/layout";
|
||||||
import { TextField } from "~/components/layout/TextField";
|
import { TextField } from "~/components/layout/TextField";
|
||||||
import { ContactFormValues } from "./ContactViewer";
|
import { ContactFormValues } from "./ContactViewer";
|
||||||
|
import { useI18n } from "~/i18n/context";
|
||||||
|
|
||||||
export function ContactForm(props: {
|
export function ContactForm(props: {
|
||||||
handleSubmit: SubmitHandler<ContactFormValues>;
|
handleSubmit: SubmitHandler<ContactFormValues>;
|
||||||
@@ -9,6 +10,7 @@ export function ContactForm(props: {
|
|||||||
title: string;
|
title: string;
|
||||||
cta: string;
|
cta: string;
|
||||||
}) {
|
}) {
|
||||||
|
const i18n = useI18n();
|
||||||
const [_contactForm, { Form, Field }] = createForm<ContactFormValues>({
|
const [_contactForm, { Form, Field }] = createForm<ContactFormValues>({
|
||||||
initialValues: props.initialValues
|
initialValues: props.initialValues
|
||||||
});
|
});
|
||||||
@@ -23,15 +25,15 @@ export function ContactForm(props: {
|
|||||||
<VStack>
|
<VStack>
|
||||||
<Field
|
<Field
|
||||||
name="name"
|
name="name"
|
||||||
validate={[required("We at least need a name")]}
|
validate={[required(i18n.t("contacts.error_name"))]}
|
||||||
>
|
>
|
||||||
{(field, props) => (
|
{(field, props) => (
|
||||||
<TextField
|
<TextField
|
||||||
{...props}
|
{...props}
|
||||||
placeholder="Satoshi"
|
placeholder={i18n.t("contacts.placeholder")}
|
||||||
value={field.value}
|
value={field.value}
|
||||||
error={field.error}
|
error={field.error}
|
||||||
label="Name"
|
label={i18n.t("contacts.name")}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</Field>
|
</Field>
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import { ContactForm } from "./ContactForm";
|
|||||||
import { showToast } from "./Toaster";
|
import { showToast } from "./Toaster";
|
||||||
import { Contact } from "@mutinywallet/mutiny-wasm";
|
import { Contact } from "@mutinywallet/mutiny-wasm";
|
||||||
import { DIALOG_CONTENT, DIALOG_POSITIONER } from "~/styles/dialogs";
|
import { DIALOG_CONTENT, DIALOG_POSITIONER } from "~/styles/dialogs";
|
||||||
|
import { useI18n } from "~/i18n/context";
|
||||||
|
|
||||||
export type ContactFormValues = {
|
export type ContactFormValues = {
|
||||||
name: string;
|
name: string;
|
||||||
@@ -18,6 +19,7 @@ export function ContactViewer(props: {
|
|||||||
gradient: string;
|
gradient: string;
|
||||||
saveContact: (contact: Contact) => void;
|
saveContact: (contact: Contact) => void;
|
||||||
}) {
|
}) {
|
||||||
|
const i18n = useI18n();
|
||||||
const [isOpen, setIsOpen] = createSignal(false);
|
const [isOpen, setIsOpen] = createSignal(false);
|
||||||
const [isEditing, setIsEditing] = createSignal(false);
|
const [isEditing, setIsEditing] = createSignal(false);
|
||||||
|
|
||||||
@@ -71,8 +73,8 @@ export function ContactViewer(props: {
|
|||||||
<Switch>
|
<Switch>
|
||||||
<Match when={isEditing()}>
|
<Match when={isEditing()}>
|
||||||
<ContactForm
|
<ContactForm
|
||||||
title="Edit contact"
|
title={i18n.t("contacts.edit_contact")}
|
||||||
cta="Save contact"
|
cta={i18n.t("contacts.save_contact")}
|
||||||
handleSubmit={handleSubmit}
|
handleSubmit={handleSubmit}
|
||||||
initialValues={props.contact}
|
initialValues={props.contact}
|
||||||
/>
|
/>
|
||||||
@@ -91,9 +93,13 @@ export function ContactViewer(props: {
|
|||||||
<h1 class="text-2xl font-semibold uppercase mt-2 mb-4">
|
<h1 class="text-2xl font-semibold uppercase mt-2 mb-4">
|
||||||
{props.contact.name}
|
{props.contact.name}
|
||||||
</h1>
|
</h1>
|
||||||
<Card title="Payment history">
|
<Card
|
||||||
|
title={i18n.t(
|
||||||
|
"contacts.payment_history"
|
||||||
|
)}
|
||||||
|
>
|
||||||
<NiceP>
|
<NiceP>
|
||||||
No payments yet with{" "}
|
{i18n.t("contacts.no_payments")}{" "}
|
||||||
<span class="font-semibold">
|
<span class="font-semibold">
|
||||||
{props.contact.name}
|
{props.contact.name}
|
||||||
</span>
|
</span>
|
||||||
@@ -107,19 +113,22 @@ export function ContactViewer(props: {
|
|||||||
intent="green"
|
intent="green"
|
||||||
onClick={() => setIsEditing(true)}
|
onClick={() => setIsEditing(true)}
|
||||||
>
|
>
|
||||||
Edit
|
{i18n.t("contacts.edit")}
|
||||||
</Button>
|
</Button>
|
||||||
<Button
|
<Button
|
||||||
intent="blue"
|
intent="blue"
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
showToast({
|
showToast({
|
||||||
title: "Unimplemented",
|
title: i18n.t(
|
||||||
description:
|
"contacts.unimplemented"
|
||||||
"We don't do that yet"
|
),
|
||||||
|
description: i18n.t(
|
||||||
|
"contacts.not_available"
|
||||||
|
)
|
||||||
});
|
});
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
Pay
|
{i18n.t("contacts.pay")}
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,8 +1,10 @@
|
|||||||
import { Show } from "solid-js";
|
import { Show } from "solid-js";
|
||||||
import { QRCodeSVG } from "solid-qr-code";
|
import { QRCodeSVG } from "solid-qr-code";
|
||||||
|
import { useI18n } from "~/i18n/context";
|
||||||
import { useCopy } from "~/utils/useCopy";
|
import { useCopy } from "~/utils/useCopy";
|
||||||
|
|
||||||
export function CopyableQR(props: { value: string }) {
|
export function CopyableQR(props: { value: string }) {
|
||||||
|
const i18n = useI18n();
|
||||||
const [copy, copied] = useCopy({ copiedTimeout: 1000 });
|
const [copy, copied] = useCopy({ copiedTimeout: 1000 });
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
@@ -12,7 +14,7 @@ export function CopyableQR(props: { value: string }) {
|
|||||||
>
|
>
|
||||||
<Show when={copied()}>
|
<Show when={copied()}>
|
||||||
<div class="absolute w-full h-full bg-neutral-900/60 z-50 rounded-xl flex flex-col items-center justify-center transition-all">
|
<div class="absolute w-full h-full bg-neutral-900/60 z-50 rounded-xl flex flex-col items-center justify-center transition-all">
|
||||||
<p class="text-xl font-bold">Copied</p>
|
<p class="text-xl font-bold">{i18n.t("common.copied")}</p>
|
||||||
</div>
|
</div>
|
||||||
</Show>
|
</Show>
|
||||||
<QRCodeSVG
|
<QRCodeSVG
|
||||||
|
|||||||
@@ -5,8 +5,10 @@ import { InfoBox } from "~/components/InfoBox";
|
|||||||
import { useMegaStore } from "~/state/megaStore";
|
import { useMegaStore } from "~/state/megaStore";
|
||||||
import eify from "~/utils/eify";
|
import eify from "~/utils/eify";
|
||||||
import { A } from "solid-start";
|
import { A } from "solid-start";
|
||||||
|
import { useI18n } from "~/i18n/context";
|
||||||
|
|
||||||
export function DecryptDialog() {
|
export function DecryptDialog() {
|
||||||
|
const i18n = useI18n();
|
||||||
const [state, actions] = useMegaStore();
|
const [state, actions] = useMegaStore();
|
||||||
|
|
||||||
const [password, setPassword] = createSignal("");
|
const [password, setPassword] = createSignal("");
|
||||||
@@ -27,7 +29,7 @@ export function DecryptDialog() {
|
|||||||
const err = eify(e);
|
const err = eify(e);
|
||||||
console.error(e);
|
console.error(e);
|
||||||
if (err.message === "wrong") {
|
if (err.message === "wrong") {
|
||||||
setError("Invalid password");
|
setError(i18n.t("settings.decrypt.error_wrong_password"));
|
||||||
} else {
|
} else {
|
||||||
throw e;
|
throw e;
|
||||||
}
|
}
|
||||||
@@ -42,7 +44,7 @@ export function DecryptDialog() {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<SimpleDialog
|
<SimpleDialog
|
||||||
title="Enter your password"
|
title={i18n.t("settings.decrypt.title")}
|
||||||
// Only show the dialog if we need a password and there's no setup error
|
// Only show the dialog if we need a password and there's no setup error
|
||||||
open={state.needs_password && !state.setup_error}
|
open={state.needs_password && !state.setup_error}
|
||||||
>
|
>
|
||||||
@@ -62,12 +64,12 @@ export function DecryptDialog() {
|
|||||||
<InfoBox accent="red">{error()}</InfoBox>
|
<InfoBox accent="red">{error()}</InfoBox>
|
||||||
</Show>
|
</Show>
|
||||||
<Button intent="blue" loading={loading()} onClick={decrypt}>
|
<Button intent="blue" loading={loading()} onClick={decrypt}>
|
||||||
Decrypt Wallet
|
{i18n.t("settings.decrypt.decrypt_wallet")}
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
<A class="self-end text-m-grey-400" href="/settings/restore">
|
<A class="self-end text-m-grey-400" href="/settings/restore">
|
||||||
Forgot Password?
|
{i18n.t("settings.decrypt.forgot_password_link")}
|
||||||
</A>
|
</A>
|
||||||
</SimpleDialog>
|
</SimpleDialog>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -30,6 +30,7 @@ import { Network } from "~/logic/mutinyWalletSetup";
|
|||||||
import { AmountSmall } from "./Amount";
|
import { AmountSmall } from "./Amount";
|
||||||
import { ExternalLink } from "./layout/ExternalLink";
|
import { ExternalLink } from "./layout/ExternalLink";
|
||||||
import { InfoBox } from "./InfoBox";
|
import { InfoBox } from "./InfoBox";
|
||||||
|
import { useI18n } from "~/i18n/context";
|
||||||
|
|
||||||
type ChannelClosure = {
|
type ChannelClosure = {
|
||||||
channel_id: string;
|
channel_id: string;
|
||||||
@@ -48,6 +49,7 @@ function LightningHeader(props: {
|
|||||||
info: MutinyInvoice;
|
info: MutinyInvoice;
|
||||||
tags: MutinyTagItem[];
|
tags: MutinyTagItem[];
|
||||||
}) {
|
}) {
|
||||||
|
const i18n = useI18n();
|
||||||
const [state, _actions] = useMegaStore();
|
const [state, _actions] = useMegaStore();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -56,7 +58,9 @@ function LightningHeader(props: {
|
|||||||
<img src={bolt} alt="lightning bolt" class="w-8 h-8" />
|
<img src={bolt} alt="lightning bolt" class="w-8 h-8" />
|
||||||
</div>
|
</div>
|
||||||
<h1 class="uppercase font-semibold">
|
<h1 class="uppercase font-semibold">
|
||||||
{props.info.inbound ? "Lightning receive" : "Lightning send"}
|
{props.info.inbound
|
||||||
|
? i18n.t("modals.transaction_details.lightning_receive")
|
||||||
|
: i18n.t("modals.transaction_details.lightning_send")}
|
||||||
</h1>
|
</h1>
|
||||||
<ActivityAmount
|
<ActivityAmount
|
||||||
center
|
center
|
||||||
@@ -85,6 +89,7 @@ function OnchainHeader(props: {
|
|||||||
tags: MutinyTagItem[];
|
tags: MutinyTagItem[];
|
||||||
kind?: HackActivityType;
|
kind?: HackActivityType;
|
||||||
}) {
|
}) {
|
||||||
|
const i18n = useI18n();
|
||||||
const [state, _actions] = useMegaStore();
|
const [state, _actions] = useMegaStore();
|
||||||
|
|
||||||
const isSend = () => {
|
const isSend = () => {
|
||||||
@@ -118,12 +123,12 @@ function OnchainHeader(props: {
|
|||||||
</div>
|
</div>
|
||||||
<h1 class="uppercase font-semibold">
|
<h1 class="uppercase font-semibold">
|
||||||
{props.kind === "ChannelOpen"
|
{props.kind === "ChannelOpen"
|
||||||
? "Channel Open"
|
? i18n.t("modals.transaction_details.channel_open")
|
||||||
: props.kind === "ChannelClose"
|
: props.kind === "ChannelClose"
|
||||||
? "Channel Close"
|
? i18n.t("modals.transaction_details.channel_close")
|
||||||
: isSend()
|
: isSend()
|
||||||
? "On-chain send"
|
? i18n.t("modals.transaction_details.onchain_send")
|
||||||
: "On-chain receive"}
|
: i18n.t("modals.transaction_details.onchain_receive")}
|
||||||
</h1>
|
</h1>
|
||||||
<Show when={props.kind !== "ChannelClose"}>
|
<Show when={props.kind !== "ChannelClose"}>
|
||||||
<ActivityAmount
|
<ActivityAmount
|
||||||
@@ -179,38 +184,45 @@ export function MiniStringShower(props: { text: string }) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function LightningDetails(props: { info: MutinyInvoice }) {
|
function LightningDetails(props: { info: MutinyInvoice }) {
|
||||||
|
const i18n = useI18n();
|
||||||
return (
|
return (
|
||||||
<VStack>
|
<VStack>
|
||||||
<ul class="flex flex-col gap-4">
|
<ul class="flex flex-col gap-4">
|
||||||
<KeyValue key="Status">
|
<KeyValue key={i18n.t("modals.transaction_details.status")}>
|
||||||
<span class="text-neutral-300">
|
<span class="text-neutral-300">
|
||||||
{props.info.paid ? "Paid" : "Unpaid"}
|
{props.info.paid
|
||||||
|
? i18n.t("modals.transaction_details.paid")
|
||||||
|
: i18n.t("modals.transaction_details.unpaid")}
|
||||||
</span>
|
</span>
|
||||||
</KeyValue>
|
</KeyValue>
|
||||||
<KeyValue key="When">
|
<KeyValue key={i18n.t("modals.transaction_details.when")}>
|
||||||
<span class="text-neutral-300">
|
<span class="text-neutral-300">
|
||||||
{prettyPrintTime(Number(props.info.last_updated))}
|
{prettyPrintTime(Number(props.info.last_updated))}
|
||||||
</span>
|
</span>
|
||||||
</KeyValue>
|
</KeyValue>
|
||||||
<Show when={props.info.description}>
|
<Show when={props.info.description}>
|
||||||
<KeyValue key="Description">
|
<KeyValue
|
||||||
|
key={i18n.t("modals.transaction_details.description")}
|
||||||
|
>
|
||||||
<span class="text-neutral-300 truncate">
|
<span class="text-neutral-300 truncate">
|
||||||
{props.info.description}
|
{props.info.description}
|
||||||
</span>
|
</span>
|
||||||
</KeyValue>
|
</KeyValue>
|
||||||
</Show>
|
</Show>
|
||||||
<KeyValue key="Fees">
|
<KeyValue key={i18n.t("modals.transaction_details.fees")}>
|
||||||
<span class="text-neutral-300">
|
<span class="text-neutral-300">
|
||||||
<AmountSmall amountSats={props.info.fees_paid} />
|
<AmountSmall amountSats={props.info.fees_paid} />
|
||||||
</span>
|
</span>
|
||||||
</KeyValue>
|
</KeyValue>
|
||||||
<KeyValue key="Bolt11">
|
<KeyValue key={i18n.t("modals.transaction_details.bolt11")}>
|
||||||
<MiniStringShower text={props.info.bolt11 ?? ""} />
|
<MiniStringShower text={props.info.bolt11 ?? ""} />
|
||||||
</KeyValue>
|
</KeyValue>
|
||||||
<KeyValue key="Payment Hash">
|
<KeyValue
|
||||||
|
key={i18n.t("modals.transaction_details.payment_hash")}
|
||||||
|
>
|
||||||
<MiniStringShower text={props.info.payment_hash ?? ""} />
|
<MiniStringShower text={props.info.payment_hash ?? ""} />
|
||||||
</KeyValue>
|
</KeyValue>
|
||||||
<KeyValue key="Preimage">
|
<KeyValue key={i18n.t("modals.transaction_details.preimage")}>
|
||||||
<MiniStringShower text={props.info.preimage ?? ""} />
|
<MiniStringShower text={props.info.preimage ?? ""} />
|
||||||
</KeyValue>
|
</KeyValue>
|
||||||
</ul>
|
</ul>
|
||||||
@@ -219,6 +231,7 @@ function LightningDetails(props: { info: MutinyInvoice }) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function OnchainDetails(props: { info: OnChainTx; kind?: HackActivityType }) {
|
function OnchainDetails(props: { info: OnChainTx; kind?: HackActivityType }) {
|
||||||
|
const i18n = useI18n();
|
||||||
const [state, _actions] = useMegaStore();
|
const [state, _actions] = useMegaStore();
|
||||||
|
|
||||||
const confirmationTime = () => {
|
const confirmationTime = () => {
|
||||||
@@ -251,13 +264,15 @@ function OnchainDetails(props: { info: OnChainTx; kind?: HackActivityType }) {
|
|||||||
<VStack>
|
<VStack>
|
||||||
{/* <pre>{JSON.stringify(channelInfo() || "", null, 2)}</pre> */}
|
{/* <pre>{JSON.stringify(channelInfo() || "", null, 2)}</pre> */}
|
||||||
<ul class="flex flex-col gap-4">
|
<ul class="flex flex-col gap-4">
|
||||||
<KeyValue key="Status">
|
<KeyValue key={i18n.t("modals.transaction_details.status")}>
|
||||||
<span class="text-neutral-300">
|
<span class="text-neutral-300">
|
||||||
{confirmationTime() ? "Confirmed" : "Unconfirmed"}
|
{confirmationTime()
|
||||||
|
? i18n.t("modals.transaction_details.confirmed")
|
||||||
|
: i18n.t("modals.transaction_details.unconfirmed")}
|
||||||
</span>
|
</span>
|
||||||
</KeyValue>
|
</KeyValue>
|
||||||
<Show when={confirmationTime()}>
|
<Show when={confirmationTime()}>
|
||||||
<KeyValue key="When">
|
<KeyValue key={i18n.t("modals.transaction_details.when")}>
|
||||||
<span class="text-neutral-300">
|
<span class="text-neutral-300">
|
||||||
{confirmationTime()
|
{confirmationTime()
|
||||||
? prettyPrintTime(Number(confirmationTime()))
|
? prettyPrintTime(Number(confirmationTime()))
|
||||||
@@ -266,32 +281,38 @@ function OnchainDetails(props: { info: OnChainTx; kind?: HackActivityType }) {
|
|||||||
</KeyValue>
|
</KeyValue>
|
||||||
</Show>
|
</Show>
|
||||||
<Show when={props.info.fee && props.info.fee > 0}>
|
<Show when={props.info.fee && props.info.fee > 0}>
|
||||||
<KeyValue key="Fee">
|
<KeyValue key={i18n.t("modals.transaction_details.fee")}>
|
||||||
<span class="text-neutral-300">
|
<span class="text-neutral-300">
|
||||||
<AmountSmall amountSats={props.info.fee} />
|
<AmountSmall amountSats={props.info.fee} />
|
||||||
</span>
|
</span>
|
||||||
</KeyValue>
|
</KeyValue>
|
||||||
</Show>
|
</Show>
|
||||||
<KeyValue key="Txid">
|
<KeyValue key={i18n.t("modals.transaction_details.txid")}>
|
||||||
<MiniStringShower text={props.info.txid ?? ""} />
|
<MiniStringShower text={props.info.txid ?? ""} />
|
||||||
</KeyValue>
|
</KeyValue>
|
||||||
<Switch>
|
<Switch>
|
||||||
<Match when={props.kind === "ChannelOpen" && channelInfo()}>
|
<Match when={props.kind === "ChannelOpen" && channelInfo()}>
|
||||||
<KeyValue key="Balance">
|
<KeyValue
|
||||||
|
key={i18n.t("modals.transaction_details.balance")}
|
||||||
|
>
|
||||||
<span class="text-neutral-300">
|
<span class="text-neutral-300">
|
||||||
<AmountSmall
|
<AmountSmall
|
||||||
amountSats={channelInfo()?.balance}
|
amountSats={channelInfo()?.balance}
|
||||||
/>
|
/>
|
||||||
</span>
|
</span>
|
||||||
</KeyValue>
|
</KeyValue>
|
||||||
<KeyValue key="Reserve">
|
<KeyValue
|
||||||
|
key={i18n.t("modals.transaction_details.reserve")}
|
||||||
|
>
|
||||||
<span class="text-neutral-300">
|
<span class="text-neutral-300">
|
||||||
<AmountSmall
|
<AmountSmall
|
||||||
amountSats={channelInfo()?.reserve}
|
amountSats={channelInfo()?.reserve}
|
||||||
/>
|
/>
|
||||||
</span>
|
</span>
|
||||||
</KeyValue>
|
</KeyValue>
|
||||||
<KeyValue key="Peer">
|
<KeyValue
|
||||||
|
key={i18n.t("modals.transaction_details.peer")}
|
||||||
|
>
|
||||||
<span class="text-neutral-300">
|
<span class="text-neutral-300">
|
||||||
<MiniStringShower
|
<MiniStringShower
|
||||||
text={channelInfo()?.peer ?? ""}
|
text={channelInfo()?.peer ?? ""}
|
||||||
@@ -301,15 +322,14 @@ function OnchainDetails(props: { info: OnChainTx; kind?: HackActivityType }) {
|
|||||||
</Match>
|
</Match>
|
||||||
<Match when={props.kind === "ChannelOpen"}>
|
<Match when={props.kind === "ChannelOpen"}>
|
||||||
<InfoBox accent="blue">
|
<InfoBox accent="blue">
|
||||||
No channel details found, which means this channel
|
{i18n.t("modals.transaction_details.no_details")}
|
||||||
has likely been closed.
|
|
||||||
</InfoBox>
|
</InfoBox>
|
||||||
</Match>
|
</Match>
|
||||||
</Switch>
|
</Switch>
|
||||||
</ul>
|
</ul>
|
||||||
<div class="text-center">
|
<div class="text-center">
|
||||||
<ExternalLink href={mempoolTxUrl(props.info.txid, network)}>
|
<ExternalLink href={mempoolTxUrl(props.info.txid, network)}>
|
||||||
View Transaction
|
{i18n.t("common.view_transaction")}
|
||||||
</ExternalLink>
|
</ExternalLink>
|
||||||
</div>
|
</div>
|
||||||
</VStack>
|
</VStack>
|
||||||
@@ -317,23 +337,24 @@ function OnchainDetails(props: { info: OnChainTx; kind?: HackActivityType }) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function ChannelCloseDetails(props: { info: ChannelClosure }) {
|
function ChannelCloseDetails(props: { info: ChannelClosure }) {
|
||||||
|
const i18n = useI18n();
|
||||||
return (
|
return (
|
||||||
<VStack>
|
<VStack>
|
||||||
{/* <pre>{JSON.stringify(props.info.value, null, 2)}</pre> */}
|
{/* <pre>{JSON.stringify(props.info.value, null, 2)}</pre> */}
|
||||||
<ul class="flex flex-col gap-4">
|
<ul class="flex flex-col gap-4">
|
||||||
<KeyValue key="Channel ID">
|
<KeyValue key={i18n.t("modals.transaction_details.channel_id")}>
|
||||||
<MiniStringShower text={props.info.channel_id ?? ""} />
|
<MiniStringShower text={props.info.channel_id ?? ""} />
|
||||||
</KeyValue>
|
</KeyValue>
|
||||||
<Show when={props.info.timestamp}>
|
<Show when={props.info.timestamp}>
|
||||||
<KeyValue key="When">
|
<KeyValue key={i18n.t("modals.transaction_details.when")}>
|
||||||
<span class="text-neutral-300">
|
<span class="text-neutral-300">
|
||||||
{props.info.timestamp
|
{props.info.timestamp
|
||||||
? prettyPrintTime(Number(props.info.timestamp))
|
? prettyPrintTime(Number(props.info.timestamp))
|
||||||
: "Pending"}
|
: i18n.t("common.pending")}
|
||||||
</span>
|
</span>
|
||||||
</KeyValue>
|
</KeyValue>
|
||||||
</Show>
|
</Show>
|
||||||
<KeyValue key="Reason">
|
<KeyValue key={i18n.t("modals.transaction_details.reason")}>
|
||||||
<p class="text-neutral-300 text-right">
|
<p class="text-neutral-300 text-right">
|
||||||
{props.info.reason ?? ""}
|
{props.info.reason ?? ""}
|
||||||
</p>
|
</p>
|
||||||
@@ -349,6 +370,7 @@ export function DetailsIdModal(props: {
|
|||||||
id: string;
|
id: string;
|
||||||
setOpen: (open: boolean) => void;
|
setOpen: (open: boolean) => void;
|
||||||
}) {
|
}) {
|
||||||
|
const i18n = useI18n();
|
||||||
const [state, _actions] = useMegaStore();
|
const [state, _actions] = useMegaStore();
|
||||||
|
|
||||||
const id = () => props.id;
|
const id = () => props.id;
|
||||||
@@ -473,7 +495,7 @@ export function DetailsIdModal(props: {
|
|||||||
<Show when={props.kind !== "ChannelClose"}>
|
<Show when={props.kind !== "ChannelClose"}>
|
||||||
<div class="flex justify-center">
|
<div class="flex justify-center">
|
||||||
<CopyButton
|
<CopyButton
|
||||||
title="Copy"
|
title={i18n.t("common.copy")}
|
||||||
text={json()}
|
text={json()}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import { Dialog } from "@kobalte/core";
|
import { Dialog } from "@kobalte/core";
|
||||||
import { ParentComponent } from "solid-js";
|
import { ParentComponent } from "solid-js";
|
||||||
import { Button, SmallHeader } from "./layout";
|
import { Button, SmallHeader } from "./layout";
|
||||||
|
import { useI18n } from "~/i18n/context";
|
||||||
|
|
||||||
const OVERLAY = "fixed inset-0 z-50 bg-black/50 backdrop-blur-sm";
|
const OVERLAY = "fixed inset-0 z-50 bg-black/50 backdrop-blur-sm";
|
||||||
const DIALOG_POSITIONER = "fixed inset-0 z-50 flex items-center justify-center";
|
const DIALOG_POSITIONER = "fixed inset-0 z-50 flex items-center justify-center";
|
||||||
@@ -14,6 +15,7 @@ export const ConfirmDialog: ParentComponent<{
|
|||||||
onCancel: () => void;
|
onCancel: () => void;
|
||||||
onConfirm: () => void;
|
onConfirm: () => void;
|
||||||
}> = (props) => {
|
}> = (props) => {
|
||||||
|
const i18n = useI18n();
|
||||||
return (
|
return (
|
||||||
<Dialog.Root open={props.open} onOpenChange={props.onCancel}>
|
<Dialog.Root open={props.open} onOpenChange={props.onCancel}>
|
||||||
<Dialog.Portal>
|
<Dialog.Portal>
|
||||||
@@ -22,20 +24,26 @@ export const ConfirmDialog: ParentComponent<{
|
|||||||
<Dialog.Content class={DIALOG_CONTENT}>
|
<Dialog.Content class={DIALOG_CONTENT}>
|
||||||
<div class="flex justify-between mb-2">
|
<div class="flex justify-between mb-2">
|
||||||
<Dialog.Title>
|
<Dialog.Title>
|
||||||
<SmallHeader>Are you sure?</SmallHeader>
|
<SmallHeader>
|
||||||
|
{i18n.t(
|
||||||
|
"modals.confirm_dialog.are_you_sure"
|
||||||
|
)}
|
||||||
|
</SmallHeader>
|
||||||
</Dialog.Title>
|
</Dialog.Title>
|
||||||
</div>
|
</div>
|
||||||
<Dialog.Description class="flex flex-col gap-4">
|
<Dialog.Description class="flex flex-col gap-4">
|
||||||
{props.children}
|
{props.children}
|
||||||
<div class="flex gap-4 w-full justify-end">
|
<div class="flex gap-4 w-full justify-end">
|
||||||
<Button onClick={props.onCancel}>Cancel</Button>
|
<Button onClick={props.onCancel}>
|
||||||
|
{i18n.t("modals.confirm_dialog.cancel")}
|
||||||
|
</Button>
|
||||||
<Button
|
<Button
|
||||||
intent="red"
|
intent="red"
|
||||||
onClick={props.onConfirm}
|
onClick={props.onConfirm}
|
||||||
loading={props.loading}
|
loading={props.loading}
|
||||||
disabled={props.loading}
|
disabled={props.loading}
|
||||||
>
|
>
|
||||||
Confirm
|
{i18n.t("modals.confirm_dialog.confirm")}
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</Dialog.Description>
|
</Dialog.Description>
|
||||||
|
|||||||
@@ -8,35 +8,38 @@ import {
|
|||||||
SmallHeader
|
SmallHeader
|
||||||
} from "~/components/layout";
|
} from "~/components/layout";
|
||||||
import { ExternalLink } from "./layout/ExternalLink";
|
import { ExternalLink } from "./layout/ExternalLink";
|
||||||
|
import { useI18n } from "~/i18n/context";
|
||||||
|
|
||||||
export default function ErrorDisplay(props: { error: Error }) {
|
export default function ErrorDisplay(props: { error: Error }) {
|
||||||
|
const i18n = useI18n();
|
||||||
return (
|
return (
|
||||||
<SafeArea>
|
<SafeArea>
|
||||||
<Title>Oh no!</Title>
|
<Title>{i18n.t("error.general.oh_no")}</Title>
|
||||||
<DefaultMain>
|
<DefaultMain>
|
||||||
<LargeHeader>Error</LargeHeader>
|
<LargeHeader>{i18n.t("error.title")}</LargeHeader>
|
||||||
<SmallHeader>This never should've happened</SmallHeader>
|
<SmallHeader>
|
||||||
|
{i18n.t("error.general.never_should_happen")}
|
||||||
|
</SmallHeader>
|
||||||
<p class="bg-white/10 rounded-xl p-4 font-mono">
|
<p class="bg-white/10 rounded-xl p-4 font-mono">
|
||||||
<span class="font-bold">{props.error.name}</span>:{" "}
|
<span class="font-bold">{props.error.name}</span>:{" "}
|
||||||
{props.error.message}
|
{props.error.message}
|
||||||
</p>
|
</p>
|
||||||
<NiceP>
|
<NiceP>
|
||||||
Try reloading this page or clicking the "Dangit" button. If
|
{i18n.t("error.general.try_reloading")}{" "}
|
||||||
you keep having problems,{" "}
|
|
||||||
<ExternalLink href="https://matrix.to/#/#mutiny-community:lightninghackers.com">
|
<ExternalLink href="https://matrix.to/#/#mutiny-community:lightninghackers.com">
|
||||||
reach out to us for support.
|
{i18n.t("error.general.support_link")}
|
||||||
</ExternalLink>
|
</ExternalLink>
|
||||||
</NiceP>
|
</NiceP>
|
||||||
<NiceP>
|
<NiceP>
|
||||||
Getting desperate? Try the{" "}
|
{i18n.t("error.general.getting_desperate")}{" "}
|
||||||
<A href="/emergencykit">emergency kit.</A>
|
<A href="/emergencykit">{i18n.t("error.emergency_link")}</A>
|
||||||
</NiceP>
|
</NiceP>
|
||||||
<div class="h-full" />
|
<div class="h-full" />
|
||||||
<Button
|
<Button
|
||||||
onClick={() => (window.location.href = "/")}
|
onClick={() => (window.location.href = "/")}
|
||||||
intent="red"
|
intent="red"
|
||||||
>
|
>
|
||||||
Dangit
|
{i18n.t("common.dangit")}
|
||||||
</Button>
|
</Button>
|
||||||
</DefaultMain>
|
</DefaultMain>
|
||||||
</SafeArea>
|
</SafeArea>
|
||||||
|
|||||||
@@ -8,23 +8,31 @@ import copyBlack from "~/assets/icons/copy-black.svg";
|
|||||||
import shareBlack from "~/assets/icons/share-black.svg";
|
import shareBlack from "~/assets/icons/share-black.svg";
|
||||||
import chainBlack from "~/assets/icons/chain-black.svg";
|
import chainBlack from "~/assets/icons/chain-black.svg";
|
||||||
import boltBlack from "~/assets/icons/bolt-black.svg";
|
import boltBlack from "~/assets/icons/bolt-black.svg";
|
||||||
|
import { useI18n } from "~/i18n/context";
|
||||||
|
|
||||||
function KindIndicator(props: { kind: ReceiveFlavor }) {
|
function KindIndicator(props: { kind: ReceiveFlavor }) {
|
||||||
|
const i18n = useI18n();
|
||||||
return (
|
return (
|
||||||
<div class="text-black flex flex-col items-end">
|
<div class="text-black flex flex-col items-end">
|
||||||
<Switch>
|
<Switch>
|
||||||
<Match when={props.kind === "onchain"}>
|
<Match when={props.kind === "onchain"}>
|
||||||
<h3 class="font-semibold">On-chain</h3>
|
<h3 class="font-semibold">
|
||||||
|
{i18n.t("receive.integrated_qr.onchain")}
|
||||||
|
</h3>
|
||||||
<img src={chainBlack} alt="chain" />
|
<img src={chainBlack} alt="chain" />
|
||||||
</Match>
|
</Match>
|
||||||
|
|
||||||
<Match when={props.kind === "lightning"}>
|
<Match when={props.kind === "lightning"}>
|
||||||
<h3 class="font-semibold">Lightning</h3>
|
<h3 class="font-semibold">
|
||||||
|
{i18n.t("receive.integrated_qr.lightning")}
|
||||||
|
</h3>
|
||||||
<img src={boltBlack} alt="bolt" />
|
<img src={boltBlack} alt="bolt" />
|
||||||
</Match>
|
</Match>
|
||||||
|
|
||||||
<Match when={props.kind === "unified"}>
|
<Match when={props.kind === "unified"}>
|
||||||
<h3 class="font-semibold">Unified</h3>
|
<h3 class="font-semibold">
|
||||||
|
{i18n.t("receive.integrated_qr.unified")}
|
||||||
|
</h3>
|
||||||
<div class="flex gap-1">
|
<div class="flex gap-1">
|
||||||
<img src={chainBlack} alt="chain" />
|
<img src={chainBlack} alt="chain" />
|
||||||
<img src={boltBlack} alt="bolt" />
|
<img src={boltBlack} alt="bolt" />
|
||||||
@@ -56,6 +64,7 @@ export function IntegratedQr(props: {
|
|||||||
amountSats: string;
|
amountSats: string;
|
||||||
kind: ReceiveFlavor;
|
kind: ReceiveFlavor;
|
||||||
}) {
|
}) {
|
||||||
|
const i18n = useI18n();
|
||||||
const [copy, copied] = useCopy({ copiedTimeout: 1000 });
|
const [copy, copied] = useCopy({ copiedTimeout: 1000 });
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
@@ -65,7 +74,7 @@ export function IntegratedQr(props: {
|
|||||||
>
|
>
|
||||||
<Show when={copied()}>
|
<Show when={copied()}>
|
||||||
<div class="absolute w-full h-full bg-neutral-900/60 z-50 rounded-xl flex flex-col items-center justify-center transition-all">
|
<div class="absolute w-full h-full bg-neutral-900/60 z-50 rounded-xl flex flex-col items-center justify-center transition-all">
|
||||||
<p class="text-xl font-bold">Copied</p>
|
<p class="text-xl font-bold">{i18n.t("common.copied")}</p>
|
||||||
</div>
|
</div>
|
||||||
</Show>
|
</Show>
|
||||||
<div
|
<div
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import {
|
|||||||
OVERLAY
|
OVERLAY
|
||||||
} from "~/components/DetailsModal";
|
} from "~/components/DetailsModal";
|
||||||
import { CopyButton } from "./ShareCard";
|
import { CopyButton } from "./ShareCard";
|
||||||
|
import { useI18n } from "~/i18n/context";
|
||||||
|
|
||||||
export function JsonModal(props: {
|
export function JsonModal(props: {
|
||||||
title: string;
|
title: string;
|
||||||
@@ -16,6 +17,7 @@ export function JsonModal(props: {
|
|||||||
setOpen: (open: boolean) => void;
|
setOpen: (open: boolean) => void;
|
||||||
children?: JSX.Element;
|
children?: JSX.Element;
|
||||||
}) {
|
}) {
|
||||||
|
const i18n = useI18n();
|
||||||
const json = createMemo(() =>
|
const json = createMemo(() =>
|
||||||
props.plaintext ? props.plaintext : JSON.stringify(props.data, null, 2)
|
props.plaintext ? props.plaintext : JSON.stringify(props.data, null, 2)
|
||||||
);
|
);
|
||||||
@@ -41,7 +43,10 @@ export function JsonModal(props: {
|
|||||||
</pre>
|
</pre>
|
||||||
</div>
|
</div>
|
||||||
{props.children}
|
{props.children}
|
||||||
<CopyButton title="Copy" text={json()} />
|
<CopyButton
|
||||||
|
title={i18n.t("common.copy")}
|
||||||
|
text={json()}
|
||||||
|
/>
|
||||||
</Dialog.Description>
|
</Dialog.Description>
|
||||||
</Dialog.Content>
|
</Dialog.Content>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -21,6 +21,7 @@ import { Restart } from "./Restart";
|
|||||||
import { ResyncOnchain } from "./ResyncOnchain";
|
import { ResyncOnchain } from "./ResyncOnchain";
|
||||||
import { ResetRouter } from "./ResetRouter";
|
import { ResetRouter } from "./ResetRouter";
|
||||||
import { MiniStringShower } from "./DetailsModal";
|
import { MiniStringShower } from "./DetailsModal";
|
||||||
|
import { useI18n } from "~/i18n/context";
|
||||||
|
|
||||||
// TODO: hopefully I don't have to maintain this type forever but I don't know how to pass it around otherwise
|
// TODO: hopefully I don't have to maintain this type forever but I don't know how to pass it around otherwise
|
||||||
type RefetchPeersType = (
|
type RefetchPeersType = (
|
||||||
@@ -28,6 +29,7 @@ type RefetchPeersType = (
|
|||||||
) => MutinyPeer[] | Promise<MutinyPeer[] | undefined> | null | undefined;
|
) => MutinyPeer[] | Promise<MutinyPeer[] | undefined> | null | undefined;
|
||||||
|
|
||||||
function PeerItem(props: { peer: MutinyPeer }) {
|
function PeerItem(props: { peer: MutinyPeer }) {
|
||||||
|
const i18n = useI18n();
|
||||||
const [state, _] = useMegaStore();
|
const [state, _] = useMegaStore();
|
||||||
|
|
||||||
const handleDisconnectPeer = async () => {
|
const handleDisconnectPeer = async () => {
|
||||||
@@ -65,7 +67,7 @@ function PeerItem(props: { peer: MutinyPeer }) {
|
|||||||
layout="xs"
|
layout="xs"
|
||||||
onClick={handleDisconnectPeer}
|
onClick={handleDisconnectPeer}
|
||||||
>
|
>
|
||||||
Disconnect
|
{i18n.t("settings.admin.kitchen_sink.disconnect")}
|
||||||
</Button>
|
</Button>
|
||||||
</VStack>
|
</VStack>
|
||||||
</Collapsible.Content>
|
</Collapsible.Content>
|
||||||
@@ -74,6 +76,7 @@ function PeerItem(props: { peer: MutinyPeer }) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function PeersList() {
|
function PeersList() {
|
||||||
|
const i18n = useI18n();
|
||||||
const [state, _] = useMegaStore();
|
const [state, _] = useMegaStore();
|
||||||
|
|
||||||
const getPeers = async () => {
|
const getPeers = async () => {
|
||||||
@@ -86,20 +89,26 @@ function PeersList() {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<InnerCard title="Peers">
|
<InnerCard title={i18n.t("settings.admin.kitchen_sink.peers")}>
|
||||||
{/* By wrapping this in a suspense I don't cause the page to jump to the top */}
|
{/* By wrapping this in a suspense I don't cause the page to jump to the top */}
|
||||||
<Suspense>
|
<Suspense>
|
||||||
<VStack>
|
<VStack>
|
||||||
<For
|
<For
|
||||||
each={peers.latest}
|
each={peers.latest}
|
||||||
fallback={<code>No peers</code>}
|
fallback={
|
||||||
|
<code>
|
||||||
|
{i18n.t(
|
||||||
|
"settings.admin.kitchen_sink.no_peers"
|
||||||
|
)}
|
||||||
|
</code>
|
||||||
|
}
|
||||||
>
|
>
|
||||||
{(peer) => <PeerItem peer={peer} />}
|
{(peer) => <PeerItem peer={peer} />}
|
||||||
</For>
|
</For>
|
||||||
</VStack>
|
</VStack>
|
||||||
</Suspense>
|
</Suspense>
|
||||||
<Button layout="small" onClick={refetch}>
|
<Button layout="small" onClick={refetch}>
|
||||||
Refresh Peers
|
{i18n.t("settings.admin.kitchen_sink.refresh_peers")}
|
||||||
</Button>
|
</Button>
|
||||||
</InnerCard>
|
</InnerCard>
|
||||||
<ConnectPeer refetchPeers={refetch} />
|
<ConnectPeer refetchPeers={refetch} />
|
||||||
@@ -108,6 +117,7 @@ function PeersList() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function ConnectPeer(props: { refetchPeers: RefetchPeersType }) {
|
function ConnectPeer(props: { refetchPeers: RefetchPeersType }) {
|
||||||
|
const i18n = useI18n();
|
||||||
const [state, _] = useMegaStore();
|
const [state, _] = useMegaStore();
|
||||||
|
|
||||||
const [value, setValue] = createSignal("");
|
const [value, setValue] = createSignal("");
|
||||||
@@ -139,18 +149,18 @@ function ConnectPeer(props: { refetchPeers: RefetchPeersType }) {
|
|||||||
class="flex flex-col gap-4"
|
class="flex flex-col gap-4"
|
||||||
>
|
>
|
||||||
<TextField.Label class="text-sm font-semibold uppercase">
|
<TextField.Label class="text-sm font-semibold uppercase">
|
||||||
Connect Peer
|
{i18n.t("settings.admin.kitchen_sink.connect_peer")}
|
||||||
</TextField.Label>
|
</TextField.Label>
|
||||||
<TextField.Input
|
<TextField.Input
|
||||||
class="w-full p-2 rounded-lg text-black"
|
class="w-full p-2 rounded-lg text-black"
|
||||||
placeholder="028241..."
|
placeholder="028241..."
|
||||||
/>
|
/>
|
||||||
<TextField.ErrorMessage class="text-red-500">
|
<TextField.ErrorMessage class="text-red-500">
|
||||||
Expecting a value...
|
{i18n.t("settings.admin.kitchen_sink.expect_a_value")}
|
||||||
</TextField.ErrorMessage>
|
</TextField.ErrorMessage>
|
||||||
</TextField.Root>
|
</TextField.Root>
|
||||||
<Button layout="small" type="submit">
|
<Button layout="small" type="submit">
|
||||||
Connect
|
{i18n.t("settings.admin.kitchen_sink.connect")}
|
||||||
</Button>
|
</Button>
|
||||||
</form>
|
</form>
|
||||||
</InnerCard>
|
</InnerCard>
|
||||||
@@ -164,6 +174,7 @@ type RefetchChannelsListType = (
|
|||||||
type PendingChannelAction = "close" | "force_close" | "abandon";
|
type PendingChannelAction = "close" | "force_close" | "abandon";
|
||||||
|
|
||||||
function ChannelItem(props: { channel: MutinyChannel; network?: Network }) {
|
function ChannelItem(props: { channel: MutinyChannel; network?: Network }) {
|
||||||
|
const i18n = useI18n();
|
||||||
const [state, _] = useMegaStore();
|
const [state, _] = useMegaStore();
|
||||||
|
|
||||||
const [pendingChannelAction, setPendingChannelAction] =
|
const [pendingChannelAction, setPendingChannelAction] =
|
||||||
@@ -206,28 +217,28 @@ function ChannelItem(props: { channel: MutinyChannel; network?: Network }) {
|
|||||||
props.network
|
props.network
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
View Transaction
|
{i18n.t("common.view_transaction")}
|
||||||
</ExternalLink>
|
</ExternalLink>
|
||||||
<Button
|
<Button
|
||||||
intent="glowy"
|
intent="glowy"
|
||||||
layout="xs"
|
layout="xs"
|
||||||
onClick={() => setPendingChannelAction("close")}
|
onClick={() => setPendingChannelAction("close")}
|
||||||
>
|
>
|
||||||
Close Channel
|
{i18n.t("settings.admin.kitchen_sink.close_channel")}
|
||||||
</Button>
|
</Button>
|
||||||
<Button
|
<Button
|
||||||
intent="glowy"
|
intent="glowy"
|
||||||
layout="xs"
|
layout="xs"
|
||||||
onClick={() => setPendingChannelAction("force_close")}
|
onClick={() => setPendingChannelAction("force_close")}
|
||||||
>
|
>
|
||||||
Force close Channel
|
{i18n.t("settings.admin.kitchen_sink.force_close")}
|
||||||
</Button>
|
</Button>
|
||||||
<Button
|
<Button
|
||||||
intent="glowy"
|
intent="glowy"
|
||||||
layout="xs"
|
layout="xs"
|
||||||
onClick={() => setPendingChannelAction("abandon")}
|
onClick={() => setPendingChannelAction("abandon")}
|
||||||
>
|
>
|
||||||
Abandon Channel
|
{i18n.t("settings.admin.kitchen_sink.abandon_channel")}
|
||||||
</Button>
|
</Button>
|
||||||
</VStack>
|
</VStack>
|
||||||
<ConfirmDialog
|
<ConfirmDialog
|
||||||
@@ -238,21 +249,24 @@ function ChannelItem(props: { channel: MutinyChannel; network?: Network }) {
|
|||||||
>
|
>
|
||||||
<Switch>
|
<Switch>
|
||||||
<Match when={pendingChannelAction() === "close"}>
|
<Match when={pendingChannelAction() === "close"}>
|
||||||
<p>Are you sure you want to close this channel?</p>
|
<p>
|
||||||
|
{i18n.t(
|
||||||
|
"settings.admin.kitchen_sink.confirm_close_channel"
|
||||||
|
)}
|
||||||
|
</p>
|
||||||
</Match>
|
</Match>
|
||||||
<Match when={pendingChannelAction() === "force_close"}>
|
<Match when={pendingChannelAction() === "force_close"}>
|
||||||
<p>
|
<p>
|
||||||
Are you sure you want to force close this
|
{i18n.t(
|
||||||
channel? Your funds will take a few days to
|
"settings.admin.kitchen_sink.confirm_force_close"
|
||||||
redeem on chain.
|
)}
|
||||||
</p>
|
</p>
|
||||||
</Match>
|
</Match>
|
||||||
<Match when={pendingChannelAction() === "abandon"}>
|
<Match when={pendingChannelAction() === "abandon"}>
|
||||||
<p>
|
<p>
|
||||||
Are you sure you want to abandon this channel?
|
{i18n.t(
|
||||||
Typically only do this if the opening
|
"settings.admin.kitchen_sink.confirm_abandon_channel"
|
||||||
transaction will never confirm. Otherwise, you
|
)}
|
||||||
will lose funds.
|
|
||||||
</p>
|
</p>
|
||||||
</Match>
|
</Match>
|
||||||
</Switch>
|
</Switch>
|
||||||
@@ -263,6 +277,7 @@ function ChannelItem(props: { channel: MutinyChannel; network?: Network }) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function ChannelsList() {
|
function ChannelsList() {
|
||||||
|
const i18n = useI18n();
|
||||||
const [state, _] = useMegaStore();
|
const [state, _] = useMegaStore();
|
||||||
|
|
||||||
const getChannels = async () => {
|
const getChannels = async () => {
|
||||||
@@ -277,10 +292,19 @@ function ChannelsList() {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<InnerCard title="Channels">
|
<InnerCard title={i18n.t("settings.admin.kitchen_sink.channels")}>
|
||||||
{/* By wrapping this in a suspense I don't cause the page to jump to the top */}
|
{/* By wrapping this in a suspense I don't cause the page to jump to the top */}
|
||||||
<Suspense>
|
<Suspense>
|
||||||
<For each={channels()} fallback={<code>No channels</code>}>
|
<For
|
||||||
|
each={channels()}
|
||||||
|
fallback={
|
||||||
|
<code>
|
||||||
|
{i18n.t(
|
||||||
|
"settings.admin.kitchen_sink.no_channels"
|
||||||
|
)}
|
||||||
|
</code>
|
||||||
|
}
|
||||||
|
>
|
||||||
{(channel) => (
|
{(channel) => (
|
||||||
<ChannelItem channel={channel} network={network} />
|
<ChannelItem channel={channel} network={network} />
|
||||||
)}
|
)}
|
||||||
@@ -294,7 +318,7 @@ function ChannelsList() {
|
|||||||
refetch();
|
refetch();
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
Refresh Channels
|
{i18n.t("settings.admin.kitchen_sink.refresh_channels")}
|
||||||
</Button>
|
</Button>
|
||||||
</InnerCard>
|
</InnerCard>
|
||||||
<OpenChannel refetchChannels={refetch} />
|
<OpenChannel refetchChannels={refetch} />
|
||||||
@@ -303,6 +327,7 @@ function ChannelsList() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function OpenChannel(props: { refetchChannels: RefetchChannelsListType }) {
|
function OpenChannel(props: { refetchChannels: RefetchChannelsListType }) {
|
||||||
|
const i18n = useI18n();
|
||||||
const [state, _] = useMegaStore();
|
const [state, _] = useMegaStore();
|
||||||
|
|
||||||
const [creationError, setCreationError] = createSignal<Error>();
|
const [creationError, setCreationError] = createSignal<Error>();
|
||||||
@@ -354,7 +379,7 @@ function OpenChannel(props: { refetchChannels: RefetchChannelsListType }) {
|
|||||||
class="flex flex-col gap-2"
|
class="flex flex-col gap-2"
|
||||||
>
|
>
|
||||||
<TextField.Label class="text-sm font-semibold uppercase">
|
<TextField.Label class="text-sm font-semibold uppercase">
|
||||||
Pubkey
|
{i18n.t("settings.admin.kitchen_sink.pubkey")}
|
||||||
</TextField.Label>
|
</TextField.Label>
|
||||||
<TextField.Input class="w-full p-2 rounded-lg text-black" />
|
<TextField.Input class="w-full p-2 rounded-lg text-black" />
|
||||||
</TextField.Root>
|
</TextField.Root>
|
||||||
@@ -364,7 +389,7 @@ function OpenChannel(props: { refetchChannels: RefetchChannelsListType }) {
|
|||||||
class="flex flex-col gap-2"
|
class="flex flex-col gap-2"
|
||||||
>
|
>
|
||||||
<TextField.Label class="text-sm font-semibold uppercase">
|
<TextField.Label class="text-sm font-semibold uppercase">
|
||||||
Amount
|
{i18n.t("settings.admin.kitchen_sink.amount")}
|
||||||
</TextField.Label>
|
</TextField.Label>
|
||||||
<TextField.Input
|
<TextField.Input
|
||||||
type="number"
|
type="number"
|
||||||
@@ -372,7 +397,7 @@ function OpenChannel(props: { refetchChannels: RefetchChannelsListType }) {
|
|||||||
/>
|
/>
|
||||||
</TextField.Root>
|
</TextField.Root>
|
||||||
<Button layout="small" type="submit">
|
<Button layout="small" type="submit">
|
||||||
Open Channel
|
{i18n.t("settings.admin.kitchen_sink.open_channel")}
|
||||||
</Button>
|
</Button>
|
||||||
</form>
|
</form>
|
||||||
</InnerCard>
|
</InnerCard>
|
||||||
@@ -387,7 +412,7 @@ function OpenChannel(props: { refetchChannels: RefetchChannelsListType }) {
|
|||||||
network
|
network
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
View Transaction
|
{i18n.t("common.view_transaction")}
|
||||||
</ExternalLink>
|
</ExternalLink>
|
||||||
</Show>
|
</Show>
|
||||||
<Show when={creationError()}>
|
<Show when={creationError()}>
|
||||||
@@ -398,6 +423,7 @@ function OpenChannel(props: { refetchChannels: RefetchChannelsListType }) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function ListNodes() {
|
function ListNodes() {
|
||||||
|
const i18n = useI18n();
|
||||||
const [state, _] = useMegaStore();
|
const [state, _] = useMegaStore();
|
||||||
|
|
||||||
const getNodeIds = async () => {
|
const getNodeIds = async () => {
|
||||||
@@ -408,9 +434,16 @@ function ListNodes() {
|
|||||||
const [nodeIds] = createResource(getNodeIds);
|
const [nodeIds] = createResource(getNodeIds);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<InnerCard title="Nodes">
|
<InnerCard title={i18n.t("settings.admin.kitchen_sink.nodes")}>
|
||||||
<Suspense>
|
<Suspense>
|
||||||
<For each={nodeIds()} fallback={<code>No nodes</code>}>
|
<For
|
||||||
|
each={nodeIds()}
|
||||||
|
fallback={
|
||||||
|
<code>
|
||||||
|
{i18n.t("settings.admin.kitchen_sink.no_nodes")}
|
||||||
|
</code>
|
||||||
|
}
|
||||||
|
>
|
||||||
{(nodeId) => <MiniStringShower text={nodeId} />}
|
{(nodeId) => <MiniStringShower text={nodeId} />}
|
||||||
</For>
|
</For>
|
||||||
</Suspense>
|
</Suspense>
|
||||||
|
|||||||
@@ -1,22 +1,24 @@
|
|||||||
import { Progress } from "@kobalte/core";
|
import { Progress } from "@kobalte/core";
|
||||||
import { Show } from "solid-js";
|
import { Show } from "solid-js";
|
||||||
|
import { useI18n } from "~/i18n/context";
|
||||||
import { useMegaStore } from "~/state/megaStore";
|
import { useMegaStore } from "~/state/megaStore";
|
||||||
|
|
||||||
export function LoadingBar(props: { value: number; max: number }) {
|
export function LoadingBar(props: { value: number; max: number }) {
|
||||||
|
const i18n = useI18n();
|
||||||
function valueToStage(value: number) {
|
function valueToStage(value: number) {
|
||||||
switch (value) {
|
switch (value) {
|
||||||
case 0:
|
case 0:
|
||||||
return "Just getting started";
|
return i18n.t("modals.loading.default");
|
||||||
case 1:
|
case 1:
|
||||||
return "Double checking something";
|
return i18n.t("modals.loading.double_checking");
|
||||||
case 2:
|
case 2:
|
||||||
return "Downloading";
|
return i18n.t("modals.loading.downloading");
|
||||||
case 3:
|
case 3:
|
||||||
return "Setup";
|
return i18n.t("modals.loading.setup");
|
||||||
case 4:
|
case 4:
|
||||||
return "Done";
|
return i18n.t("modals.loading.done");
|
||||||
default:
|
default:
|
||||||
return "Just getting started";
|
return i18n.t("modals.loading.default");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return (
|
return (
|
||||||
@@ -24,7 +26,9 @@ export function LoadingBar(props: { value: number; max: number }) {
|
|||||||
value={props.value}
|
value={props.value}
|
||||||
minValue={0}
|
minValue={0}
|
||||||
maxValue={props.max}
|
maxValue={props.max}
|
||||||
getValueLabel={({ value }) => `Loading: ${valueToStage(value)}`}
|
getValueLabel={({ value }) =>
|
||||||
|
i18n.t("modals.loading.loading", { stage: valueToStage(value) })
|
||||||
|
}
|
||||||
class="w-full flex flex-col gap-2"
|
class="w-full flex flex-col gap-2"
|
||||||
>
|
>
|
||||||
<Progress.ValueLabel class="text-sm text-m-grey-400" />
|
<Progress.ValueLabel class="text-sm text-m-grey-400" />
|
||||||
|
|||||||
@@ -10,20 +10,20 @@ export function FeesModal(props: { icon?: boolean }) {
|
|||||||
const i18n = useI18n();
|
const i18n = useI18n();
|
||||||
return (
|
return (
|
||||||
<MoreInfoModal
|
<MoreInfoModal
|
||||||
title={i18n.t("whats_with_the_fees")}
|
title={i18n.t("modals.more_info.whats_with_the_fees")}
|
||||||
linkText={
|
linkText={
|
||||||
props.icon ? (
|
props.icon ? (
|
||||||
<img src={help} alt="help" class="w-4 h-4 cursor-pointer" />
|
<img src={help} alt="help" class="w-4 h-4 cursor-pointer" />
|
||||||
) : (
|
) : (
|
||||||
i18n.t("why")
|
i18n.t("common.why")
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
<p>{i18n.t("more_info_modal_p1")}</p>
|
<p>{i18n.t("modals.more_info.self_custodial")}</p>
|
||||||
<p>{i18n.t("more_info_modal_p2")}</p>
|
<p>{i18n.t("modals.more_info.future_payments")}</p>
|
||||||
<p>
|
<p>
|
||||||
<ExternalLink href="https://github.com/MutinyWallet/mutiny-web/wiki/Understanding-liquidity">
|
<ExternalLink href="https://github.com/MutinyWallet/mutiny-web/wiki/Understanding-liquidity">
|
||||||
{i18n.t("learn_more_about_liquidity")}
|
{i18n.t("modals.more_info.liquidity")}
|
||||||
</ExternalLink>
|
</ExternalLink>
|
||||||
</p>
|
</p>
|
||||||
</MoreInfoModal>
|
</MoreInfoModal>
|
||||||
|
|||||||
@@ -5,8 +5,10 @@ import { showToast } from "./Toaster";
|
|||||||
import save from "~/assets/icons/save.svg";
|
import save from "~/assets/icons/save.svg";
|
||||||
import close from "~/assets/icons/close.svg";
|
import close from "~/assets/icons/close.svg";
|
||||||
import restore from "~/assets/icons/upload.svg";
|
import restore from "~/assets/icons/upload.svg";
|
||||||
|
import { useI18n } from "~/i18n/context";
|
||||||
|
|
||||||
export function OnboardWarning() {
|
export function OnboardWarning() {
|
||||||
|
const i18n = useI18n();
|
||||||
const [state, actions] = useMegaStore();
|
const [state, actions] = useMegaStore();
|
||||||
const [dismissedBackup, setDismissedBackup] = createSignal(
|
const [dismissedBackup, setDismissedBackup] = createSignal(
|
||||||
sessionStorage.getItem("dismissed_backup") ?? false
|
sessionStorage.getItem("dismissed_backup") ?? false
|
||||||
@@ -31,11 +33,13 @@ export function OnboardWarning() {
|
|||||||
</div>
|
</div>
|
||||||
<div class="flex md:flex-row flex-col items-center gap-4">
|
<div class="flex md:flex-row flex-col items-center gap-4">
|
||||||
<div class="flex flex-col">
|
<div class="flex flex-col">
|
||||||
<SmallHeader>Welcome!</SmallHeader>
|
<SmallHeader>
|
||||||
|
{i18n.t("modals.onboarding.welcome")}
|
||||||
|
</SmallHeader>
|
||||||
<p class="text-base font-light">
|
<p class="text-base font-light">
|
||||||
If you've used Mutiny before you can restore
|
{i18n.t(
|
||||||
from a backup. Otherwise you can skip this and
|
"modals.onboarding.restore_from_backup"
|
||||||
enjoy your new wallet!
|
)}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<Button
|
<Button
|
||||||
@@ -44,12 +48,14 @@ export function OnboardWarning() {
|
|||||||
class="self-start md:self-auto"
|
class="self-start md:self-auto"
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
showToast({
|
showToast({
|
||||||
title: "Unimplemented",
|
title: i18n.t("common.error_unimplemented"),
|
||||||
description: "We don't do that yet"
|
description: i18n.t(
|
||||||
|
"modals.onboarding.not_available"
|
||||||
|
)
|
||||||
});
|
});
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
Restore
|
{i18n.t("settings.restore.title")}
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
<button
|
<button
|
||||||
@@ -72,10 +78,11 @@ export function OnboardWarning() {
|
|||||||
</div>
|
</div>
|
||||||
<div class="flex flex-row max-md:items-center justify-between gap-4">
|
<div class="flex flex-row max-md:items-center justify-between gap-4">
|
||||||
<div class="flex flex-col">
|
<div class="flex flex-col">
|
||||||
<SmallHeader>Secure your funds</SmallHeader>
|
<SmallHeader>
|
||||||
|
{i18n.t("modals.onboarding.secure_your_funds")}
|
||||||
|
</SmallHeader>
|
||||||
<p class="text-base font-light max-md:hidden">
|
<p class="text-base font-light max-md:hidden">
|
||||||
You have money stored in this browser. Let's
|
{i18n.t("modals.onboarding.make_backup")}
|
||||||
make sure you have a backup.
|
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex items-center">
|
<div class="flex items-center">
|
||||||
@@ -85,7 +92,7 @@ export function OnboardWarning() {
|
|||||||
class="self-auto"
|
class="self-auto"
|
||||||
href="/settings/backup"
|
href="/settings/backup"
|
||||||
>
|
>
|
||||||
Backup
|
{i18n.t("settings.backup.title")}
|
||||||
</ButtonLink>
|
</ButtonLink>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -20,6 +20,7 @@ import { InfoBox } from "./InfoBox";
|
|||||||
import eify from "~/utils/eify";
|
import eify from "~/utils/eify";
|
||||||
import { A } from "solid-start";
|
import { A } from "solid-start";
|
||||||
import { createDeepSignal } from "~/utils/deepSignal";
|
import { createDeepSignal } from "~/utils/deepSignal";
|
||||||
|
import { useI18n } from "~/i18n/context";
|
||||||
|
|
||||||
type PendingItem = {
|
type PendingItem = {
|
||||||
id: string;
|
id: string;
|
||||||
@@ -29,6 +30,7 @@ type PendingItem = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export function PendingNwc() {
|
export function PendingNwc() {
|
||||||
|
const i18n = useI18n();
|
||||||
const [state, _actions] = useMegaStore();
|
const [state, _actions] = useMegaStore();
|
||||||
|
|
||||||
const [error, setError] = createSignal<Error>();
|
const [error, setError] = createSignal<Error>();
|
||||||
@@ -109,7 +111,7 @@ export function PendingNwc() {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<Show when={pendingRequests() && pendingRequests()!.length > 0}>
|
<Show when={pendingRequests() && pendingRequests()!.length > 0}>
|
||||||
<Card title="Pending Requests">
|
<Card title={i18n.t("settings.connections.pending_nwc.title")}>
|
||||||
<div class="p-1" />
|
<div class="p-1" />
|
||||||
<VStack>
|
<VStack>
|
||||||
<Show when={error()}>
|
<Show when={error()}>
|
||||||
@@ -183,7 +185,7 @@ export function PendingNwc() {
|
|||||||
href="/settings/connections"
|
href="/settings/connections"
|
||||||
class="text-m-red active:text-m-red/80 font-semibold no-underline self-center"
|
class="text-m-red active:text-m-red/80 font-semibold no-underline self-center"
|
||||||
>
|
>
|
||||||
Configure
|
{i18n.t("settings.connections.pending_nwc.configure_link")}
|
||||||
</A>
|
</A>
|
||||||
</Card>
|
</Card>
|
||||||
</Show>
|
</Show>
|
||||||
|
|||||||
@@ -1,7 +1,9 @@
|
|||||||
import { Button, InnerCard, NiceP, VStack } from "~/components/layout";
|
import { Button, InnerCard, NiceP, VStack } from "~/components/layout";
|
||||||
import { useMegaStore } from "~/state/megaStore";
|
import { useMegaStore } from "~/state/megaStore";
|
||||||
|
import { useI18n } from "~/i18n/context";
|
||||||
|
|
||||||
export function ResetRouter() {
|
export function ResetRouter() {
|
||||||
|
const i18n = useI18n();
|
||||||
const [state, _] = useMegaStore();
|
const [state, _] = useMegaStore();
|
||||||
|
|
||||||
async function reset() {
|
async function reset() {
|
||||||
@@ -15,12 +17,9 @@ export function ResetRouter() {
|
|||||||
return (
|
return (
|
||||||
<InnerCard>
|
<InnerCard>
|
||||||
<VStack>
|
<VStack>
|
||||||
<NiceP>
|
<NiceP>{i18n.t("error.reset_router.payments_failing")}</NiceP>
|
||||||
Failing to make payments? Try resetting the lightning
|
|
||||||
router.
|
|
||||||
</NiceP>
|
|
||||||
<Button intent="red" onClick={reset}>
|
<Button intent="red" onClick={reset}>
|
||||||
Reset Router
|
{i18n.t("error.reset_router.reset_router")}
|
||||||
</Button>
|
</Button>
|
||||||
</VStack>
|
</VStack>
|
||||||
</InnerCard>
|
</InnerCard>
|
||||||
|
|||||||
@@ -1,8 +1,10 @@
|
|||||||
import { createSignal } from "solid-js";
|
import { createSignal } from "solid-js";
|
||||||
import { Button, InnerCard, NiceP, VStack } from "~/components/layout";
|
import { Button, InnerCard, NiceP, VStack } from "~/components/layout";
|
||||||
|
import { useI18n } from "~/i18n/context";
|
||||||
import { useMegaStore } from "~/state/megaStore";
|
import { useMegaStore } from "~/state/megaStore";
|
||||||
|
|
||||||
export function Restart() {
|
export function Restart() {
|
||||||
|
const i18n = useI18n();
|
||||||
const [state, _] = useMegaStore();
|
const [state, _] = useMegaStore();
|
||||||
const [hasStopped, setHasStopped] = createSignal(false);
|
const [hasStopped, setHasStopped] = createSignal(false);
|
||||||
|
|
||||||
@@ -23,14 +25,14 @@ export function Restart() {
|
|||||||
return (
|
return (
|
||||||
<InnerCard>
|
<InnerCard>
|
||||||
<VStack>
|
<VStack>
|
||||||
<NiceP>
|
<NiceP>{i18n.t("error.restart.title")}</NiceP>
|
||||||
Something *extra* screwy going on? Stop the nodes!
|
|
||||||
</NiceP>
|
|
||||||
<Button
|
<Button
|
||||||
intent={hasStopped() ? "green" : "red"}
|
intent={hasStopped() ? "green" : "red"}
|
||||||
onClick={toggle}
|
onClick={toggle}
|
||||||
>
|
>
|
||||||
{hasStopped() ? "Start" : "Stop"}
|
{hasStopped()
|
||||||
|
? i18n.t("error.restart.start")
|
||||||
|
: i18n.t("error.restart.stop")}
|
||||||
</Button>
|
</Button>
|
||||||
</VStack>
|
</VStack>
|
||||||
</InnerCard>
|
</InnerCard>
|
||||||
|
|||||||
@@ -1,7 +1,9 @@
|
|||||||
import { Button, InnerCard, NiceP, VStack } from "~/components/layout";
|
import { Button, InnerCard, NiceP, VStack } from "~/components/layout";
|
||||||
|
import { useI18n } from "~/i18n/context";
|
||||||
import { useMegaStore } from "~/state/megaStore";
|
import { useMegaStore } from "~/state/megaStore";
|
||||||
|
|
||||||
export function ResyncOnchain() {
|
export function ResyncOnchain() {
|
||||||
|
const i18n = useI18n();
|
||||||
const [state, _] = useMegaStore();
|
const [state, _] = useMegaStore();
|
||||||
|
|
||||||
async function reset() {
|
async function reset() {
|
||||||
@@ -15,12 +17,9 @@ export function ResyncOnchain() {
|
|||||||
return (
|
return (
|
||||||
<InnerCard>
|
<InnerCard>
|
||||||
<VStack>
|
<VStack>
|
||||||
<NiceP>
|
<NiceP>{i18n.t("error.resync.incorrect_balance")}</NiceP>
|
||||||
On-chain balance seems incorrect? Try re-syncing the
|
|
||||||
on-chain wallet.
|
|
||||||
</NiceP>
|
|
||||||
<Button intent="red" onClick={reset}>
|
<Button intent="red" onClick={reset}>
|
||||||
Resync wallet
|
{i18n.t("error.resync.resync_wallet")}
|
||||||
</Button>
|
</Button>
|
||||||
</VStack>
|
</VStack>
|
||||||
</InnerCard>
|
</InnerCard>
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ import { ImportExport } from "./ImportExport";
|
|||||||
import { Logs } from "./Logs";
|
import { Logs } from "./Logs";
|
||||||
import { DeleteEverything } from "./DeleteEverything";
|
import { DeleteEverything } from "./DeleteEverything";
|
||||||
import { FeedbackLink } from "~/routes/Feedback";
|
import { FeedbackLink } from "~/routes/Feedback";
|
||||||
|
import { useI18n } from "~/i18n/context";
|
||||||
|
|
||||||
function ErrorFooter() {
|
function ErrorFooter() {
|
||||||
return (
|
return (
|
||||||
@@ -26,86 +27,100 @@ function ErrorFooter() {
|
|||||||
|
|
||||||
export default function SetupErrorDisplay(props: { initialError: Error }) {
|
export default function SetupErrorDisplay(props: { initialError: Error }) {
|
||||||
// Error shouldn't be reactive, so we assign to it so it just gets rendered with the first value
|
// Error shouldn't be reactive, so we assign to it so it just gets rendered with the first value
|
||||||
|
const i18n = useI18n();
|
||||||
const error = props.initialError;
|
const error = props.initialError;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<SafeArea>
|
<SafeArea>
|
||||||
<Switch>
|
<Switch>
|
||||||
<Match when={error.message.startsWith("Existing tab")}>
|
<Match when={error.message.startsWith("Existing tab")}>
|
||||||
<Title>Multiple tabs detected</Title>
|
<Title>{i18n.t("error.on_boot.existing_tab.title")}</Title>
|
||||||
<DefaultMain>
|
<DefaultMain>
|
||||||
<LargeHeader>Multiple tabs detected</LargeHeader>
|
<LargeHeader>
|
||||||
|
{i18n.t("error.on_boot.existing_tab.title")}
|
||||||
|
</LargeHeader>
|
||||||
<p class="bg-white/10 rounded-xl p-4 font-mono">
|
<p class="bg-white/10 rounded-xl p-4 font-mono">
|
||||||
<span class="font-bold">{error.name}</span>:{" "}
|
<span class="font-bold">{error.name}</span>:{" "}
|
||||||
{error.message}
|
{error.message}
|
||||||
</p>
|
</p>
|
||||||
<NiceP>
|
<NiceP>
|
||||||
Mutiny currently only supports use in one tab at a
|
{i18n.t("error.on_boot.existing_tab.description")}
|
||||||
time. It looks like you have another tab open with
|
|
||||||
Mutiny running. Please close that tab and refresh
|
|
||||||
this page, or close this tab and refresh the other
|
|
||||||
one.
|
|
||||||
</NiceP>
|
</NiceP>
|
||||||
<ErrorFooter />
|
<ErrorFooter />
|
||||||
</DefaultMain>
|
</DefaultMain>
|
||||||
</Match>
|
</Match>
|
||||||
<Match when={error.message.startsWith("Browser error")}>
|
<Match when={error.message.startsWith("Browser error")}>
|
||||||
<Title>Incompatible browser</Title>
|
<Title>
|
||||||
|
{i18n.t("error.on_boot.incompatible_browser.title")}
|
||||||
|
</Title>
|
||||||
<DefaultMain>
|
<DefaultMain>
|
||||||
<LargeHeader>Incompatible browser detected</LargeHeader>
|
<LargeHeader>
|
||||||
|
{i18n.t(
|
||||||
|
"error.on_boot.incompatible_browser.header"
|
||||||
|
)}
|
||||||
|
</LargeHeader>
|
||||||
<p class="bg-white/10 rounded-xl p-4 font-mono">
|
<p class="bg-white/10 rounded-xl p-4 font-mono">
|
||||||
<span class="font-bold">{error.name}</span>:{" "}
|
<span class="font-bold">{error.name}</span>:{" "}
|
||||||
{error.message}
|
{error.message}
|
||||||
</p>
|
</p>
|
||||||
<NiceP>
|
<NiceP>
|
||||||
Mutiny requires a modern browser that supports
|
{i18n.t(
|
||||||
WebAssembly, LocalStorage, and IndexedDB. Some
|
"error.on_boot.incompatible_browser.description"
|
||||||
browsers disable these features in private mode.
|
)}
|
||||||
</NiceP>
|
</NiceP>
|
||||||
<NiceP>
|
<NiceP>
|
||||||
Please make sure your browser supports all these
|
{i18n.t(
|
||||||
features, or consider trying another browser. You
|
"error.on_boot.incompatible_browser.try_different_browser"
|
||||||
might also try disabling certain extensions or
|
)}
|
||||||
"shields" that block these features.
|
|
||||||
</NiceP>
|
</NiceP>
|
||||||
<NiceP>
|
<NiceP>
|
||||||
(We'd love to support more private browsers, but we
|
{i18n.t(
|
||||||
have to save your wallet data to browser storage or
|
"error.on_boot.incompatible_browser.browser_storage"
|
||||||
else you will lose funds.)
|
)}
|
||||||
</NiceP>
|
</NiceP>
|
||||||
<ExternalLink href="https://github.com/MutinyWallet/mutiny-web/wiki/Browser-Compatibility">
|
<ExternalLink href="https://github.com/MutinyWallet/mutiny-web/wiki/Browser-Compatibility">
|
||||||
Supported Browsers
|
{i18n.t(
|
||||||
|
"error.on_boot.incompatible_browser.browsers_link"
|
||||||
|
)}
|
||||||
</ExternalLink>
|
</ExternalLink>
|
||||||
|
|
||||||
<ErrorFooter />
|
<ErrorFooter />
|
||||||
</DefaultMain>
|
</DefaultMain>
|
||||||
</Match>
|
</Match>
|
||||||
<Match when={true}>
|
<Match when={true}>
|
||||||
<Title>Failed to load</Title>
|
<Title>
|
||||||
|
{i18n.t("error.on_boot.loading_failed.title")}
|
||||||
|
</Title>
|
||||||
<DefaultMain>
|
<DefaultMain>
|
||||||
<LargeHeader>Failed to load Mutiny</LargeHeader>
|
<LargeHeader>
|
||||||
|
{i18n.t("error.on_boot.loading_failed.header")}
|
||||||
|
</LargeHeader>
|
||||||
<p class="bg-white/10 rounded-xl p-4 font-mono">
|
<p class="bg-white/10 rounded-xl p-4 font-mono">
|
||||||
<span class="font-bold">{error.name}</span>:{" "}
|
<span class="font-bold">{error.name}</span>:{" "}
|
||||||
{error.message}
|
{error.message}
|
||||||
</p>
|
</p>
|
||||||
<NiceP>
|
<NiceP>
|
||||||
Something went wrong while booting up Mutiny Wallet.
|
{i18n.t("error.on_boot.loading_failed.description")}
|
||||||
</NiceP>
|
</NiceP>
|
||||||
<NiceP>
|
<NiceP>
|
||||||
If your wallet seems broken, here are some tools to
|
{i18n.t(
|
||||||
try to debug and repair it.
|
"error.on_boot.loading_failed.repair_options"
|
||||||
|
)}
|
||||||
</NiceP>
|
</NiceP>
|
||||||
<NiceP>
|
<NiceP>
|
||||||
If you have any questions on what these buttons do,
|
{i18n.t("error.on_boot.loading_failed.questions")}{" "}
|
||||||
please{" "}
|
|
||||||
<ExternalLink href="https://matrix.to/#/#mutiny-community:lightninghackers.com">
|
<ExternalLink href="https://matrix.to/#/#mutiny-community:lightninghackers.com">
|
||||||
reach out to us for support.
|
{i18n.t(
|
||||||
|
"error.on_boot.loading_failed.support_link"
|
||||||
|
)}
|
||||||
</ExternalLink>
|
</ExternalLink>
|
||||||
</NiceP>
|
</NiceP>
|
||||||
<ImportExport emergency />
|
<ImportExport emergency />
|
||||||
<Logs />
|
<Logs />
|
||||||
<div class="rounded-xl p-4 flex flex-col gap-2 bg-m-red">
|
<div class="rounded-xl p-4 flex flex-col gap-2 bg-m-red">
|
||||||
<SmallHeader>Danger zone</SmallHeader>
|
<SmallHeader>
|
||||||
|
{i18n.t("settings.danger_zone")}
|
||||||
|
</SmallHeader>
|
||||||
<DeleteEverything emergency />
|
<DeleteEverything emergency />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import shareBlack from "~/assets/icons/share-black.svg";
|
|||||||
import eyeIcon from "~/assets/icons/eye.svg";
|
import eyeIcon from "~/assets/icons/eye.svg";
|
||||||
import { Show, createSignal } from "solid-js";
|
import { Show, createSignal } from "solid-js";
|
||||||
import { JsonModal } from "./JsonModal";
|
import { JsonModal } from "./JsonModal";
|
||||||
|
import { useI18n } from "~/i18n/context";
|
||||||
|
|
||||||
const STYLE =
|
const STYLE =
|
||||||
"px-4 py-2 rounded-xl border-2 border-white flex gap-2 items-center font-semibold hover:text-m-blue transition-colors";
|
"px-4 py-2 rounded-xl border-2 border-white flex gap-2 items-center font-semibold hover:text-m-blue transition-colors";
|
||||||
@@ -15,13 +16,14 @@ export function ShareButton(props: {
|
|||||||
receiveString: string;
|
receiveString: string;
|
||||||
whiteBg?: boolean;
|
whiteBg?: boolean;
|
||||||
}) {
|
}) {
|
||||||
|
const i18n = useI18n();
|
||||||
async function share(receiveString: string) {
|
async function share(receiveString: string) {
|
||||||
// If the browser doesn't support share we can just copy the address
|
// If the browser doesn't support share we can just copy the address
|
||||||
if (!navigator.share) {
|
if (!navigator.share) {
|
||||||
console.error("Share not supported");
|
console.error("Share not supported");
|
||||||
}
|
}
|
||||||
const shareData: ShareData = {
|
const shareData: ShareData = {
|
||||||
title: "Mutiny Wallet",
|
title: i18n.t("common.title"),
|
||||||
text: receiveString
|
text: receiveString
|
||||||
};
|
};
|
||||||
try {
|
try {
|
||||||
@@ -33,7 +35,7 @@ export function ShareButton(props: {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<button class={STYLE} onClick={(_) => share(props.receiveString)}>
|
<button class={STYLE} onClick={(_) => share(props.receiveString)}>
|
||||||
<span>Share</span>
|
<span>{i18n.t("modals.share")}</span>
|
||||||
<img src={props.whiteBg ? shareBlack : shareIcon} alt="share" />
|
<img src={props.whiteBg ? shareBlack : shareIcon} alt="share" />
|
||||||
</button>
|
</button>
|
||||||
);
|
);
|
||||||
@@ -57,13 +59,14 @@ export function TruncateMiddle(props: { text: string; whiteBg?: boolean }) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function StringShower(props: { text: string }) {
|
export function StringShower(props: { text: string }) {
|
||||||
|
const i18n = useI18n();
|
||||||
const [open, setOpen] = createSignal(false);
|
const [open, setOpen] = createSignal(false);
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<JsonModal
|
<JsonModal
|
||||||
open={open()}
|
open={open()}
|
||||||
plaintext={props.text}
|
plaintext={props.text}
|
||||||
title="Details"
|
title={i18n.t("modals.details")}
|
||||||
setOpen={setOpen}
|
setOpen={setOpen}
|
||||||
/>
|
/>
|
||||||
<div class="w-full grid grid-cols-[minmax(0,_1fr)_auto]">
|
<div class="w-full grid grid-cols-[minmax(0,_1fr)_auto]">
|
||||||
@@ -81,6 +84,7 @@ export function CopyButton(props: {
|
|||||||
title?: string;
|
title?: string;
|
||||||
whiteBg?: boolean;
|
whiteBg?: boolean;
|
||||||
}) {
|
}) {
|
||||||
|
const i18n = useI18n();
|
||||||
const [copy, copied] = useCopy({ copiedTimeout: 1000 });
|
const [copy, copied] = useCopy({ copiedTimeout: 1000 });
|
||||||
|
|
||||||
function handleCopy() {
|
function handleCopy() {
|
||||||
@@ -89,7 +93,9 @@ export function CopyButton(props: {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<button class={STYLE} onClick={handleCopy}>
|
<button class={STYLE} onClick={handleCopy}>
|
||||||
{copied() ? "Copied" : props.title ?? "Copy"}
|
{copied()
|
||||||
|
? i18n.t("common.copied")
|
||||||
|
: props.title ?? i18n.t("common.copy")}
|
||||||
<img src={props.whiteBg ? copyBlack : copyIcon} alt="copy" />
|
<img src={props.whiteBg ? copyBlack : copyIcon} alt="copy" />
|
||||||
</button>
|
</button>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -153,7 +153,7 @@ export const FullscreenLoader = () => {
|
|||||||
<p class="max-w-[20rem] text-neutral-400">
|
<p class="max-w-[20rem] text-neutral-400">
|
||||||
{i18n.t("error.load_time.stuck")}{" "}
|
{i18n.t("error.load_time.stuck")}{" "}
|
||||||
<A class="text-white" href="/emergencykit">
|
<A class="text-white" href="/emergencykit">
|
||||||
{i18n.t("error.load_time.emergency_link")}
|
{i18n.t("error.emergency_link")}
|
||||||
</A>
|
</A>
|
||||||
</p>
|
</p>
|
||||||
</Show>
|
</Show>
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import { use } from "i18next";
|
|||||||
import LanguageDetector from "i18next-browser-languagedetector";
|
import LanguageDetector from "i18next-browser-languagedetector";
|
||||||
import en from "~/i18n/en/translations";
|
import en from "~/i18n/en/translations";
|
||||||
import pt from "~/i18n/pt/translations";
|
import pt from "~/i18n/pt/translations";
|
||||||
|
import ko from "~/i18n/ko/translations";
|
||||||
|
|
||||||
export const resources = {
|
export const resources = {
|
||||||
en: {
|
en: {
|
||||||
@@ -11,6 +12,9 @@ export const resources = {
|
|||||||
},
|
},
|
||||||
pt: {
|
pt: {
|
||||||
translations: pt
|
translations: pt
|
||||||
|
},
|
||||||
|
ko: {
|
||||||
|
translations: ko
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
export default {
|
export default {
|
||||||
common: {
|
common: {
|
||||||
|
title: "Mutiny Wallet",
|
||||||
nice: "Nice",
|
nice: "Nice",
|
||||||
home: "Home",
|
home: "Home",
|
||||||
sats: "SATS",
|
sats: "SATS",
|
||||||
@@ -9,10 +10,33 @@ export default {
|
|||||||
send: "Send",
|
send: "Send",
|
||||||
receive: "Receive",
|
receive: "Receive",
|
||||||
dangit: "Dangit",
|
dangit: "Dangit",
|
||||||
back: "Back"
|
back: "Back",
|
||||||
|
coming_soon: "(coming soon)",
|
||||||
|
copy: "Copy",
|
||||||
|
copied: "Copied",
|
||||||
|
continue: "Continue",
|
||||||
|
error_unimplemented: "Unimplemented",
|
||||||
|
why: "Why?",
|
||||||
|
private_tags: "Private tags",
|
||||||
|
view_transaction: "View Transaction",
|
||||||
|
pending: "Pending"
|
||||||
},
|
},
|
||||||
char: {
|
contacts: {
|
||||||
del: "DEL"
|
new: "new",
|
||||||
|
add_contact: "Add Contact",
|
||||||
|
new_contact: "New Contact",
|
||||||
|
create_contact: "Create contact",
|
||||||
|
edit_contact: "Edit contact",
|
||||||
|
save_contact: "Save contact",
|
||||||
|
payment_history: "Payment history",
|
||||||
|
no_payments: "No payments yet with",
|
||||||
|
edit: "Edit",
|
||||||
|
pay: "Pay",
|
||||||
|
name: "Name",
|
||||||
|
placeholder: "Satoshi",
|
||||||
|
unimplemented: "Unimplemented",
|
||||||
|
not_available: "We don't do that yet",
|
||||||
|
error_name: "We at least need a name"
|
||||||
},
|
},
|
||||||
receive: {
|
receive: {
|
||||||
receive_bitcoin: "Receive Bitcoin",
|
receive_bitcoin: "Receive Bitcoin",
|
||||||
@@ -22,16 +46,48 @@ export default {
|
|||||||
payment_received: "Payment Received",
|
payment_received: "Payment Received",
|
||||||
payment_initiated: "Payment Initiated",
|
payment_initiated: "Payment Initiated",
|
||||||
receive_add_the_sender: "Add the sender for your records",
|
receive_add_the_sender: "Add the sender for your records",
|
||||||
|
keep_mutiny_open: "Keep Mutiny open to complete the payment.",
|
||||||
|
choose_payment_format: "Choose payment format",
|
||||||
|
unified_label: "Unified",
|
||||||
|
unified_caption:
|
||||||
|
"Combines a bitcoin address and a lightning invoice. Sender chooses payment method.",
|
||||||
|
lightning_label: "Lightning invoice",
|
||||||
|
lightning_caption:
|
||||||
|
"Ideal for small transactions. Usually lower fees than on-chain.",
|
||||||
|
onchain_label: "Bitcoin address",
|
||||||
|
onchain_caption:
|
||||||
|
"On-chain, just like Satoshi did it. Ideal for very large transactions.",
|
||||||
|
unified_setup_fee:
|
||||||
|
"A lightning setup fee of {{amount}} SATS will be charged if paid over lightning.",
|
||||||
|
lightning_setup_fee:
|
||||||
|
"A lightning setup fee of {{amount}} SATS will be charged for this receive.",
|
||||||
|
amount: "Amount",
|
||||||
|
fee: "+ Fee",
|
||||||
|
total: "Total",
|
||||||
|
spendable: "Spendable",
|
||||||
|
channel_size: "Channel size",
|
||||||
|
channel_reserve: "- Channel reserve",
|
||||||
amount_editable: {
|
amount_editable: {
|
||||||
receive_too_small:
|
receive_too_small:
|
||||||
"Your first lightning receive needs to be {{amount}} sats or greater. A setup fee will be deducted from the requested amount.",
|
"Your first lightning receive needs to be {{amount}} SATS or greater. A setup fee will be deducted from the requested amount.",
|
||||||
setup_fee_lightning:
|
setup_fee_lightning:
|
||||||
"A lightning setup fee will be charged if paid over lightning.",
|
"A lightning setup fee will be charged if paid over lightning.",
|
||||||
too_big_for_beta:
|
too_big_for_beta:
|
||||||
"That's a lot of sats. You do know Mutiny Wallet is still in beta, yeah?",
|
"That's a lot of sats. You do know Mutiny Wallet is still in beta, yeah?",
|
||||||
more_than_21m: "There are only 21 million bitcoin.",
|
more_than_21m: "There are only 21 million bitcoin.",
|
||||||
set_amount: "Set amount",
|
set_amount: "Set amount",
|
||||||
max: "MAX"
|
max: "MAX",
|
||||||
|
fix_amounts: {
|
||||||
|
ten_k: "10k",
|
||||||
|
one_hundred_k: "100k",
|
||||||
|
one_million: "1m"
|
||||||
|
},
|
||||||
|
del: "DEL"
|
||||||
|
},
|
||||||
|
integrated_qr: {
|
||||||
|
onchain: "On-chain",
|
||||||
|
lightning: "Lightning",
|
||||||
|
unified: "Unified"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
send: {
|
send: {
|
||||||
@@ -39,10 +95,21 @@ export default {
|
|||||||
confirm_send: "Confirm Send",
|
confirm_send: "Confirm Send",
|
||||||
contact_placeholder: "Add the receiver for your records",
|
contact_placeholder: "Add the receiver for your records",
|
||||||
start_over: "Start Over",
|
start_over: "Start Over",
|
||||||
|
send_bitcoin: "Send Bitcoin",
|
||||||
|
paste: "Paste",
|
||||||
|
scan_qr: "Scan QR",
|
||||||
|
payment_initiated: "Payment Initiated",
|
||||||
|
payment_sent: "Payment Sent",
|
||||||
|
destination: "Destination",
|
||||||
progress_bar: {
|
progress_bar: {
|
||||||
of: "of",
|
of: "of",
|
||||||
sats_sent: "sats sent"
|
sats_sent: "sats sent"
|
||||||
}
|
},
|
||||||
|
error_low_balance:
|
||||||
|
"We do not have enough balance to pay the given amount.",
|
||||||
|
error_clipboard: "Clipboard not supported",
|
||||||
|
error_keysend: "Keysend failed",
|
||||||
|
error_LNURL: "LNURL Pay failed"
|
||||||
},
|
},
|
||||||
feedback: {
|
feedback: {
|
||||||
header: "Give us feedback!",
|
header: "Give us feedback!",
|
||||||
@@ -51,9 +118,8 @@ export default {
|
|||||||
more: "Got more to say?",
|
more: "Got more to say?",
|
||||||
tracking:
|
tracking:
|
||||||
"Mutiny doesn't track or spy on your behavior, so your feedback is incredibly helpful.",
|
"Mutiny doesn't track or spy on your behavior, so your feedback is incredibly helpful.",
|
||||||
github_one: "If you're comfortable with GitHub you can also",
|
github: "If you're comfortable with GitHub you can also",
|
||||||
github_two: ".",
|
create_issue: "create an issue.",
|
||||||
create_issue: "create an issue",
|
|
||||||
link: "Feedback?",
|
link: "Feedback?",
|
||||||
feedback_placeholder: "Bugs, feature requests, feedback, etc.",
|
feedback_placeholder: "Bugs, feature requests, feedback, etc.",
|
||||||
info_label: "Include contact info",
|
info_label: "Include contact info",
|
||||||
@@ -67,24 +133,52 @@ export default {
|
|||||||
invalid_feedback: "Please say something!",
|
invalid_feedback: "Please say something!",
|
||||||
need_contact: "We need some way to contact you",
|
need_contact: "We need some way to contact you",
|
||||||
invalid_email: "That doesn't look like an email address to me",
|
invalid_email: "That doesn't look like an email address to me",
|
||||||
error: "Error submitting feedback",
|
error: "Error submitting feedback {{error}}",
|
||||||
try_again: "Please try again later."
|
try_again: "Please try again later."
|
||||||
},
|
},
|
||||||
activity: {
|
activity: {
|
||||||
|
title: "Activity",
|
||||||
|
mutiny: "Mutiny",
|
||||||
|
nostr: "Nostr",
|
||||||
view_all: "View all",
|
view_all: "View all",
|
||||||
receive_some_sats_to_get_started: "Receive some sats to get started",
|
receive_some_sats_to_get_started: "Receive some sats to get started",
|
||||||
channel_open: "Channel Open",
|
channel_open: "Channel Open",
|
||||||
channel_close: "Channel Close",
|
channel_close: "Channel Close",
|
||||||
unknown: "Unknown"
|
unknown: "Unknown",
|
||||||
|
import_contacts:
|
||||||
|
"Import your contacts from nostr to see who they're zapping.",
|
||||||
|
coming_soon: "Coming soon"
|
||||||
|
},
|
||||||
|
redshift: {
|
||||||
|
title: "Redshift",
|
||||||
|
unknown: "Unknown",
|
||||||
|
what_happened: "What happened?",
|
||||||
|
starting_amount: "Starting amount",
|
||||||
|
fees_paid: "Fees paid",
|
||||||
|
change: "Change",
|
||||||
|
outbound_channel: "Outbound channel",
|
||||||
|
return_channel: "Return channel",
|
||||||
|
where_this_goes: "Where is this going?",
|
||||||
|
watch_it_go: "Watch it go!",
|
||||||
|
choose_your: "Choose your",
|
||||||
|
utxo_to_begin: "UTXO to begin",
|
||||||
|
unshifted_utxo: "Unshifted UTXOs",
|
||||||
|
redshifted: "Redshifted",
|
||||||
|
utxos: "UTXOs",
|
||||||
|
no_utxos_empty_state: "No utxos (empty state)",
|
||||||
|
utxo_label: "UTXO",
|
||||||
|
utxo_caption: "Trade in your UTXO for a fresh UTXO",
|
||||||
|
lightning_label: "Lightning",
|
||||||
|
lightning_caption: "Convert your UTXO into Lightning",
|
||||||
|
oh_dear: "Oh dear",
|
||||||
|
here_is_error: "Here's what happened:"
|
||||||
},
|
},
|
||||||
redshift: {},
|
|
||||||
scanner: {
|
scanner: {
|
||||||
paste: "Paste Something",
|
paste: "Paste Something",
|
||||||
cancel: "Cancel"
|
cancel: "Cancel"
|
||||||
},
|
},
|
||||||
settings: {
|
settings: {
|
||||||
header: "Settings",
|
header: "Settings",
|
||||||
mutiny_plus: "MUTINY+",
|
|
||||||
support: "Learn how to support Mutiny",
|
support: "Learn how to support Mutiny",
|
||||||
general: "GENERAL",
|
general: "GENERAL",
|
||||||
beta_features: "BETA FEATURES",
|
beta_features: "BETA FEATURES",
|
||||||
@@ -97,7 +191,33 @@ export default {
|
|||||||
warning_one:
|
warning_one:
|
||||||
"If you know what you're doing you're in the right place.",
|
"If you know what you're doing you're in the right place.",
|
||||||
warning_two:
|
warning_two:
|
||||||
"These are internal tools we use to debug and test the app. Please be careful!"
|
"These are internal tools we use to debug and test the app. Please be careful!",
|
||||||
|
kitchen_sink: {
|
||||||
|
disconnect: "Disconnect",
|
||||||
|
peers: "Peers",
|
||||||
|
no_peers: "No peers",
|
||||||
|
refresh_peers: "Refresh Peers",
|
||||||
|
connect_peer: "Connect Peer",
|
||||||
|
expect_a_value: "Expecting a value...",
|
||||||
|
connect: "Connect",
|
||||||
|
close_channel: "Close Channel",
|
||||||
|
force_close: "Force close Channel",
|
||||||
|
abandon_channel: "Abandon Channel",
|
||||||
|
confirm_close_channel:
|
||||||
|
"Are you sure you want to close this channel?",
|
||||||
|
confirm_force_close:
|
||||||
|
"Are you sure you want to force close this channel? Your funds will take a few days to redeem on chain.",
|
||||||
|
confirm_abandon_channel:
|
||||||
|
"Are you sure you want to abandon this channel? Typically only do this if the opening transaction will never confirm. Otherwise, you will lose funds.",
|
||||||
|
channels: "Channels",
|
||||||
|
no_channels: "No Channels",
|
||||||
|
refresh_channels: "Refresh Channels",
|
||||||
|
pubkey: "Pubkey",
|
||||||
|
amount: "Amount",
|
||||||
|
open_channel: "Open Channel",
|
||||||
|
nodes: "Nodes",
|
||||||
|
no_nodes: "No nodes"
|
||||||
|
}
|
||||||
},
|
},
|
||||||
backup: {
|
backup: {
|
||||||
title: "Backup",
|
title: "Backup",
|
||||||
@@ -141,8 +261,13 @@ export default {
|
|||||||
new_connection_label: "Name",
|
new_connection_label: "Name",
|
||||||
new_connection_placeholder: "My favorite nostr client...",
|
new_connection_placeholder: "My favorite nostr client...",
|
||||||
create_connection: "Create Connection",
|
create_connection: "Create Connection",
|
||||||
|
relay: "Relay",
|
||||||
authorize:
|
authorize:
|
||||||
"Authorize external services to request payments from your wallet. Pairs great with Nostr clients."
|
"Authorize external services to request payments from your wallet. Pairs great with Nostr clients.",
|
||||||
|
pending_nwc: {
|
||||||
|
title: "Pending Requests",
|
||||||
|
configure_link: "Configure"
|
||||||
|
}
|
||||||
},
|
},
|
||||||
emergency_kit: {
|
emergency_kit: {
|
||||||
title: "Emergency Kit",
|
title: "Emergency Kit",
|
||||||
@@ -180,17 +305,99 @@ export default {
|
|||||||
deleted_description: "Deleted all data"
|
deleted_description: "Deleted all data"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
encrypt: {},
|
encrypt: {
|
||||||
lnurl_auth: {
|
header: "Encrypt your seed words",
|
||||||
title: "LNURL Auth"
|
hot_wallet_warning:
|
||||||
|
"Mutiny is a ”hot wallet” so it needs your seed word to operate, but you can optionally encrypt those words with a password.",
|
||||||
|
password_tip:
|
||||||
|
"That way, if someone gets access to your browser, they still won't have access to your funds.",
|
||||||
|
optional: "(optional)",
|
||||||
|
existing_password: "Existing password",
|
||||||
|
existing_password_caption:
|
||||||
|
"Leave blank if you haven't set a password yet.",
|
||||||
|
new_password_label: "Password",
|
||||||
|
new_password_placeholder: "Enter a password",
|
||||||
|
new_password_caption:
|
||||||
|
"This password will be used to encrypt your seed words. If you forget it, you will need to re-enter your seed words to access your funds. You did write down your seed words, right?",
|
||||||
|
confirm_password_label: "Confirm Password",
|
||||||
|
confirm_password_placeholder: "Enter the same password",
|
||||||
|
encrypt: "Encrypt",
|
||||||
|
skip: "Skip",
|
||||||
|
error_match: "Passwords do not match"
|
||||||
|
},
|
||||||
|
decrypt: {
|
||||||
|
title: "Enter your password",
|
||||||
|
decrypt_wallet: "Decrypt Wallet",
|
||||||
|
forgot_password_link: "Forgot Password?",
|
||||||
|
error_wrong_password: "Invalid Password"
|
||||||
|
},
|
||||||
|
lnurl_auth: {
|
||||||
|
title: "LNURL Auth",
|
||||||
|
auth: "Auth",
|
||||||
|
expected: "Expecting something like LNURL..."
|
||||||
|
},
|
||||||
|
plus: {
|
||||||
|
title: "Mutiny+",
|
||||||
|
join: "Join",
|
||||||
|
sats_per_month: "for {{amount}} sats a month.",
|
||||||
|
lightning_balance:
|
||||||
|
"You'll need at least {{amount}} sats in your lightning balance to get started. Try before you buy!",
|
||||||
|
restore: "Restore Subscription",
|
||||||
|
ready_to_join: "Ready to join",
|
||||||
|
click_confirm: "Click confirm to pay for your first month.",
|
||||||
|
open_source: "Mutiny is open source and self-hostable.",
|
||||||
|
optional_pay: "But also you can pay for it.",
|
||||||
|
paying_for: "Paying for",
|
||||||
|
supports_dev:
|
||||||
|
"helps support ongoing development and unlocks early access to new features and premium functionality:",
|
||||||
|
thanks: "You're part of the mutiny! Enjoy the following perks:",
|
||||||
|
renewal_time: "You'll get a renewal payment request around",
|
||||||
|
cancel: "To cancel your subscription just don't pay. You can also disable the Mutiny+",
|
||||||
|
wallet_connection: "Wallet Connection.",
|
||||||
|
subscribe: "Subscribe",
|
||||||
|
error_no_plan: "No plans found",
|
||||||
|
error_failure: "Couldn't subscribe",
|
||||||
|
error_no_subscription: "No existing subscription found",
|
||||||
|
satisfaction: "Smug satisfaction",
|
||||||
|
gifting: "Gifting",
|
||||||
|
multi_device: "Multi-device access",
|
||||||
|
more: "... and more to come"
|
||||||
},
|
},
|
||||||
plus: {},
|
|
||||||
restore: {
|
restore: {
|
||||||
title: "Restore"
|
title: "Restore",
|
||||||
|
all_twelve: "You need to enter all 12 words",
|
||||||
|
wrong_word: "Wrong word",
|
||||||
|
paste: "Dangerously Paste from Clipboard",
|
||||||
|
confirm_text:
|
||||||
|
"Are you sure you want to restore to this wallet? Your existing wallet will be deleted!",
|
||||||
|
restore_tip:
|
||||||
|
"You can restore an existing Mutiny Wallet from your 12 word seed phrase. This will replace your existing wallet, so make sure you know what you're doing!",
|
||||||
|
multi_browser_warning:
|
||||||
|
"Do not use on multiple browsers at the same time.",
|
||||||
|
error_clipboard: "Clipboard not supported",
|
||||||
|
error_word_number: "Wrong number of words",
|
||||||
|
error_invalid_seed: "Invalid seed phrase"
|
||||||
},
|
},
|
||||||
servers: {
|
servers: {
|
||||||
title: "Servers",
|
title: "Servers",
|
||||||
caption: "Don't trust us! Use your own servers to back Mutiny."
|
caption: "Don't trust us! Use your own servers to back Mutiny.",
|
||||||
|
link: "Learn more about self-hosting",
|
||||||
|
proxy_label: "Websockets Proxy",
|
||||||
|
proxy_caption:
|
||||||
|
"How your lightning node communicates with the rest of the network.",
|
||||||
|
error_proxy: "Should be a url starting with wss://",
|
||||||
|
esplora_label: "Esplora",
|
||||||
|
esplora_caption: "Block data for on-chain information.",
|
||||||
|
error_esplora: "That doesn't look like a URL",
|
||||||
|
rgs_label: "RGS",
|
||||||
|
rgs_caption:
|
||||||
|
"Rapid Gossip Sync. Network data about the lightning network used for routing.",
|
||||||
|
error_rgs: "That doesn't look like a URL",
|
||||||
|
lsp_label: "LSP",
|
||||||
|
lsp_caption:
|
||||||
|
"Lightning Service Provider. Automatically opens channels to you for inbound liquidity. Also wraps invoices for privacy.",
|
||||||
|
error_lsp: "That doesn't look like a URL",
|
||||||
|
save: "Save"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
swap: {
|
swap: {
|
||||||
@@ -210,26 +417,139 @@ export default {
|
|||||||
confirm_swap: "Confirm Swap"
|
confirm_swap: "Confirm Swap"
|
||||||
},
|
},
|
||||||
error: {
|
error: {
|
||||||
|
title: "Error",
|
||||||
|
emergency_link: "emergency kit.",
|
||||||
|
restart: {
|
||||||
|
title: "Something *extra* screwy going on? Stop the nodes!",
|
||||||
|
start: "Start",
|
||||||
|
stop: "Stop"
|
||||||
|
},
|
||||||
|
general: {
|
||||||
|
oh_no: "Oh no!",
|
||||||
|
never_should_happen: "This never should've happened",
|
||||||
|
try_reloading:
|
||||||
|
"Try reloading this page or clicking the ”Dangit” button. If you keep having problems,",
|
||||||
|
support_link: "reach out to us for support.",
|
||||||
|
getting_desperate: "Getting desperate? Try the"
|
||||||
|
},
|
||||||
load_time: {
|
load_time: {
|
||||||
stuck: "Stuck on this screen? Try reloading. If that doesn't work, check out the",
|
stuck: "Stuck on this screen? Try reloading. If that doesn't work, check out the"
|
||||||
emergency_link: "emergency kit."
|
|
||||||
},
|
},
|
||||||
not_found: {
|
not_found: {
|
||||||
title: "Not Found",
|
title: "Not Found",
|
||||||
wtf_paul: "This is probably Paul's fault."
|
wtf_paul: "This is probably Paul's fault."
|
||||||
|
},
|
||||||
|
reset_router: {
|
||||||
|
payments_failing:
|
||||||
|
"Failing to make payments? Try resetting the lightning router.",
|
||||||
|
reset_router: "Reset Router"
|
||||||
|
},
|
||||||
|
resync: {
|
||||||
|
incorrect_balance:
|
||||||
|
"On-chain balance seems incorrect? Try re-syncing the on-chain wallet.",
|
||||||
|
resync_wallet: "Resync wallet"
|
||||||
|
},
|
||||||
|
on_boot: {
|
||||||
|
existing_tab: {
|
||||||
|
title: "Multiple tabs detected",
|
||||||
|
description:
|
||||||
|
"Mutiny currently only supports use in one tab at a time. It looks like you have another tab open with Mutiny running. Please close that tab and refresh this page, or close this tab and refresh the other one."
|
||||||
|
},
|
||||||
|
incompatible_browser: {
|
||||||
|
title: "Incompatible browser",
|
||||||
|
header: "Incompatible browser detected",
|
||||||
|
description:
|
||||||
|
"Mutiny requires a modern browser that supports WebAssembly, LocalStorage, and IndexedDB. Some browsers disable these features in private mode.",
|
||||||
|
try_different_browser:
|
||||||
|
"Please make sure your browser supports all these features, or consider trying another browser. You might also try disabling certain extensions or ”shields” that block these features.",
|
||||||
|
browser_storage:
|
||||||
|
"(We'd love to support more private browsers, but we have to save your wallet data to browser storage or else you will lose funds.)",
|
||||||
|
browsers_link: "Supported Browsers"
|
||||||
|
},
|
||||||
|
loading_failed: {
|
||||||
|
title: "Failed to load",
|
||||||
|
header: "Failed to load Mutiny",
|
||||||
|
description:
|
||||||
|
"Something went wrong while booting up Mutiny Wallet.",
|
||||||
|
repair_options:
|
||||||
|
"If your wallet seems broken, here are some tools to try to debug and repair it.",
|
||||||
|
questions:
|
||||||
|
"If you have any questions on what these buttons do, please",
|
||||||
|
support_link: "reach out to us for support."
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
create_an_issue: "Create an issue",
|
modals: {
|
||||||
send_bitcoin: "Send Bitcoin",
|
share: "Share",
|
||||||
view_transaction: "View Transaction",
|
details: "Details",
|
||||||
why: "Why?",
|
loading: {
|
||||||
more_info_modal_p1:
|
loading: "Loading: {{stage}}",
|
||||||
"Mutiny is a self-custodial wallet. To initiate a lightning payment we must open a lightning channel, which requires a minimum amount and a setup fee.",
|
default: "Just getting started",
|
||||||
more_info_modal_p2:
|
double_checking: "Double checking something",
|
||||||
"Future payments, both send and recieve, will only incur normal network fees and a nominal service fee unless your channel runs out of inbound capacity.",
|
downloading: "Downloading",
|
||||||
learn_more_about_liquidity: "Learn more about liquidity",
|
setup: "Setup",
|
||||||
whats_with_the_fees: "What's with the fees?",
|
done: "Done"
|
||||||
private_tags: "Private tags",
|
},
|
||||||
continue: "Continue",
|
onboarding: {
|
||||||
keep_mutiny_open: "Keep Mutiny open to complete the payment."
|
welcome: "Welcome!",
|
||||||
|
restore_from_backup:
|
||||||
|
"If you've used Mutiny before you can restore from a backup. Otherwise you can skip this and enjoy your new wallet!",
|
||||||
|
not_available: "We don't do that yet",
|
||||||
|
secure_your_funds: "Secure your funds",
|
||||||
|
make_backup:
|
||||||
|
"You have money stored in this browser. Let's make sure you have a backup."
|
||||||
|
},
|
||||||
|
beta_warning: {
|
||||||
|
title: "Warning: beta software",
|
||||||
|
beta_warning:
|
||||||
|
"We're so glad you're here. But we do want to warn you: Mutiny Wallet is in beta, and there are still bugs and rough edges.",
|
||||||
|
be_careful:
|
||||||
|
"Please be careful and don't put more money into Mutiny than you're willing to lose.",
|
||||||
|
beta_link: "Learn more about the beta",
|
||||||
|
pretend_money:
|
||||||
|
"If you want to use pretend money to test out Mutiny without risk,",
|
||||||
|
signet_link: "check out our Signet version."
|
||||||
|
},
|
||||||
|
transaction_details: {
|
||||||
|
lightning_receive: "Lightning receive",
|
||||||
|
lightning_send: "Lightning send",
|
||||||
|
channel_open: "Channel open",
|
||||||
|
channel_close: "Channel close",
|
||||||
|
onchain_receive: "On-chain receive",
|
||||||
|
onchain_send: "On-chain send",
|
||||||
|
paid: "Paid",
|
||||||
|
unpaid: "Unpaid",
|
||||||
|
status: "Status",
|
||||||
|
when: "When",
|
||||||
|
description: "Description",
|
||||||
|
fee: "Fee",
|
||||||
|
fees: "Fees",
|
||||||
|
bolt11: "Bolt11",
|
||||||
|
payment_hash: "Payment Hash",
|
||||||
|
preimage: "Preimage",
|
||||||
|
txid: "Txid",
|
||||||
|
balance: "Balance",
|
||||||
|
reserve: "Reserve",
|
||||||
|
peer: "Peer",
|
||||||
|
channel_id: "Channel ID",
|
||||||
|
reason: "Reasson",
|
||||||
|
confirmed: "Confirmed",
|
||||||
|
unconfirmed: "Unconfirmed",
|
||||||
|
no_details:
|
||||||
|
"No channel details found, which means this channel has likely been closed."
|
||||||
|
},
|
||||||
|
more_info: {
|
||||||
|
whats_with_the_fees: "What's with the fees?",
|
||||||
|
self_custodial:
|
||||||
|
"Mutiny is a self-custodial wallet. To initiate a lightning payment we must open a lightning channel, which requires a minimum amount and a setup fee.",
|
||||||
|
future_payments:
|
||||||
|
"Future payments, both send and recieve, will only incur normal network fees and a nominal service fee unless your channel runs out of inbound capacity.",
|
||||||
|
liquidity: "Learn more about liquidity"
|
||||||
|
},
|
||||||
|
confirm_dialog: {
|
||||||
|
are_you_sure: "Are you sure?",
|
||||||
|
cancel: "Cancel",
|
||||||
|
confirm: "Confirm"
|
||||||
|
}
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|||||||
545
src/i18n/ko/translations.ts
Normal file
545
src/i18n/ko/translations.ts
Normal file
@@ -0,0 +1,545 @@
|
|||||||
|
export default {
|
||||||
|
common: {
|
||||||
|
title: "Mutiny 지갑",
|
||||||
|
nice: "멋지다",
|
||||||
|
home: "홈",
|
||||||
|
sats: "SATS",
|
||||||
|
sat: "SAT",
|
||||||
|
usd: "USD",
|
||||||
|
fee: "수수료",
|
||||||
|
send: "보내기",
|
||||||
|
receive: "받기",
|
||||||
|
dangit: "땡글",
|
||||||
|
back: "뒤로",
|
||||||
|
coming_soon: "(곧 출시 예정)",
|
||||||
|
copy: "복사",
|
||||||
|
copied: "복사됨",
|
||||||
|
continue: "계속",
|
||||||
|
error_unimplemented: "미구현",
|
||||||
|
why: "왜?",
|
||||||
|
view_transaction: "거래 보기",
|
||||||
|
private_tags: "비공개 태그",
|
||||||
|
pending: "대기 중"
|
||||||
|
},
|
||||||
|
contacts: {
|
||||||
|
new: "새로 만들기",
|
||||||
|
add_contact: "연락처 추가",
|
||||||
|
new_contact: "새 연락처",
|
||||||
|
create_contact: "연락처 생성",
|
||||||
|
edit_contact: "연락처 수정",
|
||||||
|
save_contact: "연락처 저장",
|
||||||
|
payment_history: "결제 기록",
|
||||||
|
no_payments: "아직 결제 기록이 없습니다.",
|
||||||
|
edit: "수정",
|
||||||
|
pay: "지불",
|
||||||
|
name: "이름",
|
||||||
|
placeholder: "사토시",
|
||||||
|
unimplemented: "미구현",
|
||||||
|
not_available: "아직 제공되지 않습니다.",
|
||||||
|
error_name: "이름은 필수입니다."
|
||||||
|
},
|
||||||
|
receive: {
|
||||||
|
receive_bitcoin: "비트코인 받기",
|
||||||
|
edit: "수정",
|
||||||
|
checking: "확인 중",
|
||||||
|
choose_format: "포맷 선택",
|
||||||
|
payment_received: "결제 완료",
|
||||||
|
payment_initiated: "결제 시작됨",
|
||||||
|
receive_add_the_sender: "송신자를 기록에 추가하세요.",
|
||||||
|
choose_payment_format: "결제 포맷 선택",
|
||||||
|
unified_label: "통합",
|
||||||
|
unified_caption:
|
||||||
|
"비트코인 주소와 라이트닝 인보이스를 결합합니다. 송신자가 결제 방법을 선택합니다.",
|
||||||
|
lightning_label: "라이트닝 인보이스",
|
||||||
|
lightning_caption:
|
||||||
|
"작은 거래에 적합합니다. 보통 온체인 수수료보다 낮습니다.",
|
||||||
|
onchain_label: "비트코인 주소",
|
||||||
|
onchain_caption:
|
||||||
|
"온체인, 사토시가 한 것처럼. 아주 큰 거래에 적합합니다.",
|
||||||
|
unified_setup_fee:
|
||||||
|
"라이트닝으로 지불하는 경우 {{amount}} SATS의 라이트닝 설치 비용이 부과됩니다.",
|
||||||
|
lightning_setup_fee:
|
||||||
|
"이 받기에는 {{amount}} SATS의 라이트닝 설치 비용이 부과됩니다.",
|
||||||
|
amount: "금액",
|
||||||
|
fee: "+ 수수료",
|
||||||
|
total: "합계",
|
||||||
|
spendable: "사용 가능",
|
||||||
|
channel_size: "채널 크기",
|
||||||
|
channel_reserve: "- 채널 예비금",
|
||||||
|
amount_editable: {
|
||||||
|
receive_too_small:
|
||||||
|
"첫 라이트닝 받기는 {{amount}} SATS 이상이어야 합니다. 요청한 금액에서 설정 비용이 차감됩니다.",
|
||||||
|
setup_fee_lightning:
|
||||||
|
"라이트닝으로 지불하는 경우 라이트닝 설치 비용이 부과됩니다.",
|
||||||
|
too_big_for_beta:
|
||||||
|
"많은 SATS입니다. Mutiny Wallet이 여전히 베타 버전임을 알고 계시겠죠?",
|
||||||
|
more_than_21m: "비트코인은 총 2,100만 개밖에 없습니다.",
|
||||||
|
set_amount: "금액 설정",
|
||||||
|
max: "최대",
|
||||||
|
fix_amounts: {
|
||||||
|
ten_k: "1만",
|
||||||
|
one_hundred_k: "10만",
|
||||||
|
one_million: "100만"
|
||||||
|
},
|
||||||
|
del: "삭제"
|
||||||
|
},
|
||||||
|
integrated_qr: {
|
||||||
|
onchain: "온체인",
|
||||||
|
lightning: "라이트닝",
|
||||||
|
unified: "통합"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
send: {
|
||||||
|
sending: "보내는 중...",
|
||||||
|
confirm_send: "보내기 확인",
|
||||||
|
contact_placeholder: "기록을 위해 수신자 추가",
|
||||||
|
start_over: "다시 시작",
|
||||||
|
paste: "붙여넣기",
|
||||||
|
scan_qr: "QR 스캔",
|
||||||
|
payment_initiated: "결제 시작됨",
|
||||||
|
payment_sent: "결제 완료",
|
||||||
|
destination: "수신처",
|
||||||
|
progress_bar: {
|
||||||
|
of: "/",
|
||||||
|
sats_sent: "SATS 보냄"
|
||||||
|
},
|
||||||
|
error_low_balance: "지불할 금액보다 충분한 잔액이 없습니다.",
|
||||||
|
error_clipboard: "클립보드를 지원하지 않습니다.",
|
||||||
|
error_keysend: "KeySend 실패",
|
||||||
|
error_LNURL: "LNURL Pay 실패"
|
||||||
|
},
|
||||||
|
feedback: {
|
||||||
|
header: "피드백 주세요!",
|
||||||
|
received: "피드백이 수신되었습니다!",
|
||||||
|
thanks: "문제가 발생했음을 알려주셔서 감사합니다.",
|
||||||
|
more: "더 하실 말씀이 있으신가요?",
|
||||||
|
tracking:
|
||||||
|
"Mutiny는 사용자 행동을 추적하거나 감시하지 않기 때문에 피드백이 매우 유용합니다.",
|
||||||
|
github_one: "GitHub에 익숙하시다면",
|
||||||
|
github_two: "를 사용하여",
|
||||||
|
create_issue: "이슈를 생성하세요",
|
||||||
|
link: "피드백?",
|
||||||
|
feedback_placeholder: "버그, 기능 요청, 피드백 등",
|
||||||
|
info_label: "연락처 정보 포함",
|
||||||
|
info_caption: "문제에 대한 후속 조치를 필요로 하는 경우",
|
||||||
|
email: "이메일",
|
||||||
|
email_caption: "일회용 이메일 사용 가능",
|
||||||
|
nostr: "Nostr",
|
||||||
|
nostr_caption: "신선한 npub",
|
||||||
|
nostr_label: "Nostr npub 또는 NIP-05",
|
||||||
|
send_feedback: "피드백 보내기",
|
||||||
|
invalid_feedback: "피드백을 입력하세요.",
|
||||||
|
need_contact: "연락처 정보가 필요합니다.",
|
||||||
|
invalid_email: "올바른 이메일 주소가 아닙니다.",
|
||||||
|
error: "피드백 전송 오류",
|
||||||
|
try_again: "나중에 다시 시도하세요."
|
||||||
|
},
|
||||||
|
activity: {
|
||||||
|
title: "활동",
|
||||||
|
mutiny: "Mutiny",
|
||||||
|
nostr: "Nostr",
|
||||||
|
view_all: "전체 보기",
|
||||||
|
receive_some_sats_to_get_started: "시작하려면 일부 SATS를 받으세요",
|
||||||
|
channel_open: "채널 오픈",
|
||||||
|
channel_close: "채널 닫기",
|
||||||
|
unknown: "알 수 없음",
|
||||||
|
import_contacts:
|
||||||
|
"Nostr에서 연락처를 가져와 누가 체널을 열고 있는지 확인하세요.",
|
||||||
|
coming_soon: "곧 출시 예정"
|
||||||
|
},
|
||||||
|
redshift: {
|
||||||
|
title: "레드시프트",
|
||||||
|
unknown: "알 수 없음",
|
||||||
|
what_happened: "무슨 일이 발생했나요?",
|
||||||
|
where_this_goes: "이것은 어디로 가나요?",
|
||||||
|
watch_it_go: "보기",
|
||||||
|
choose_your: "선택하세요",
|
||||||
|
utxo_to_begin: "시작할 UTXO",
|
||||||
|
unshifted_utxo: "전환되지 않은 UTXO",
|
||||||
|
redshifted: "레드시프트된",
|
||||||
|
utxos: "UTXO",
|
||||||
|
no_utxos_empty_state: "UTXO가 없습니다.",
|
||||||
|
utxo_label: "UTXO",
|
||||||
|
utxo_caption: "새 UTXO와 교환하세요.",
|
||||||
|
lightning_label: "라이트닝",
|
||||||
|
lightning_caption: "UTXO를 라이트닝으로 전환하세요.",
|
||||||
|
oh_dear: "오 디얼!",
|
||||||
|
here_is_error: "다음과 같은 오류가 발생했습니다:"
|
||||||
|
},
|
||||||
|
scanner: {
|
||||||
|
paste: "붙여넣기",
|
||||||
|
cancel: "취소"
|
||||||
|
},
|
||||||
|
settings: {
|
||||||
|
header: "설정",
|
||||||
|
support: "Mutiny 지원 방법 알아보기",
|
||||||
|
general: "일반",
|
||||||
|
beta_features: "베타 기능",
|
||||||
|
debug_tools: "디버그 도구",
|
||||||
|
danger_zone: "위험 지역",
|
||||||
|
admin: {
|
||||||
|
title: "관리자 페이지",
|
||||||
|
caption: "내부 디버그 도구입니다. 신중하게 사용하세요!",
|
||||||
|
header: "비밀 디버그 도구",
|
||||||
|
warning_one: "잘 알고 있는 경우 올바른 위치입니다.",
|
||||||
|
warning_two:
|
||||||
|
"디버그 및 테스트에 사용하는 내부 도구입니다. 주의하세요!",
|
||||||
|
kitchen_sink: {
|
||||||
|
disconnect: "연결 끊기",
|
||||||
|
peers: "피어",
|
||||||
|
no_peers: "피어 없음",
|
||||||
|
refresh_peers: "피어 새로고침",
|
||||||
|
connect_peer: "피어 연결",
|
||||||
|
expect_a_value: "값을 입력하세요...",
|
||||||
|
connect: "연결",
|
||||||
|
close_channel: "채널 종료",
|
||||||
|
force_close: "강제 종료",
|
||||||
|
abandon_channel: "채널 포기",
|
||||||
|
confirm_close_channel: "이 채널을 종료하시겠습니까?",
|
||||||
|
confirm_force_close:
|
||||||
|
"이 채널을 강제로 종료하시겠습니까? 자금은 몇 일 이후에 체인상에서 사용 가능해집니다.",
|
||||||
|
confirm_abandon_channel:
|
||||||
|
"이 채널을 포기하시겠습니까? 대개 개방 트랜잭션이 확인되지 않는다면 이렇게 하십시오. 그렇지 않으면 자금을 잃게 될 수 있습니다.",
|
||||||
|
channels: "채널",
|
||||||
|
no_channels: "채널 없음",
|
||||||
|
refresh_channels: "채널 새로고침",
|
||||||
|
pubkey: "퍼블릭 키",
|
||||||
|
amount: "금액",
|
||||||
|
open_channel: "채널 개설",
|
||||||
|
nodes: "노드",
|
||||||
|
no_nodes: "노드 없음"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
backup: {
|
||||||
|
title: "백업",
|
||||||
|
secure_funds: "자금을 안전하게 보호하세요.",
|
||||||
|
twelve_words_tip:
|
||||||
|
"12개의 단어를 보여드립니다. 12개의 단어를 기록하세요.",
|
||||||
|
warning_one:
|
||||||
|
"브라우저 기록을 지우거나 기기를 분실하면 이 12개의 단어만으로 지갑을 복원할 수 있습니다.",
|
||||||
|
warning_two:
|
||||||
|
"Mutiny는 사용자의 자산을 사용자 스스로 관리해야 합니다...",
|
||||||
|
confirm: "12개의 단어를 기록했습니다.",
|
||||||
|
responsibility: "자금이 사용자 스스로의 책임임을 이해합니다.",
|
||||||
|
liar: "속이려는 것이 아닙니다.",
|
||||||
|
seed_words: {
|
||||||
|
reveal: "씨드 단어 공개",
|
||||||
|
hide: "숨기기",
|
||||||
|
copy: "클립보드에 복사",
|
||||||
|
copied: "복사됨!"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
channels: {
|
||||||
|
title: "라이트닝 채널",
|
||||||
|
outbound: "송신",
|
||||||
|
inbound: "수신",
|
||||||
|
have_channels: "라이트닝 채널이",
|
||||||
|
have_channels_one: "개 있습니다.",
|
||||||
|
have_channels_many: "개 있습니다.",
|
||||||
|
inbound_outbound_tip:
|
||||||
|
"송신은 라이트닝으로 지출할 수 있는 금액을 나타냅니다. 수신은 수수료 없이 받을 수 있는 금액을 나타냅니다.",
|
||||||
|
no_channels:
|
||||||
|
"아직 채널이 없는 것 같습니다. 먼저 라이트닝으로 몇 sats를 받거나 체인상 자금을 채널로 바꾸세요. 시작해보세요!"
|
||||||
|
},
|
||||||
|
connections: {
|
||||||
|
title: "지갑 연결",
|
||||||
|
error_name: "이름을 입력하세요.",
|
||||||
|
error_connection: "지갑 연결 생성에 실패했습니다.",
|
||||||
|
add_connection: "연결 추가",
|
||||||
|
manage_connections: "연결 관리",
|
||||||
|
disable_connection: "비활성화",
|
||||||
|
enable_connection: "활성화",
|
||||||
|
new_connection: "새로운 연결",
|
||||||
|
new_connection_label: "이름",
|
||||||
|
new_connection_placeholder: "내가 좋아하는 nostr 클라이언트...",
|
||||||
|
create_connection: "연결 생성",
|
||||||
|
authorize:
|
||||||
|
"외부 서비스가 지갑에서 결제를 요청할 수 있도록 인증합니다. nostr 클라이언트와 잘 맞습니다.",
|
||||||
|
pending_nwc: {
|
||||||
|
title: "대기 중인 요청",
|
||||||
|
configure_link: "설정"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
emergency_kit: {
|
||||||
|
title: "비상 키트",
|
||||||
|
caption: "지갑 문제를 진단하고 해결하는 도구입니다.",
|
||||||
|
emergency_tip:
|
||||||
|
"지갑이 망가지는 것 같다면 이 도구를 사용하여 문제를 진단하고 해결하세요.",
|
||||||
|
questions:
|
||||||
|
"이 버튼들이 무엇을 하는지 궁금하다면, 지원을 받으시려면",
|
||||||
|
link: "연락처를 통해 문의해주세요.",
|
||||||
|
import_export: {
|
||||||
|
title: "지갑 상태 내보내기",
|
||||||
|
error_password: "비밀번호가 필요합니다.",
|
||||||
|
error_read_file: "파일 읽기 오류",
|
||||||
|
error_no_text: "파일에서 텍스트를 찾을 수 없습니다.",
|
||||||
|
tip: "Mutiny 지갑 상태 전체를 파일로 내보내서 새 브라우저에 가져와서 복원할 수 있습니다. 보통 동작합니다!",
|
||||||
|
caveat_header: "주의 사항:",
|
||||||
|
caveat: "내보낸 후에는 원래 브라우저에서 아무 동작도 수행하지 마세요. 그렇게 하면 다시 내보내야 합니다. 성공적인 가져오기 후에는 원래 브라우저의 상태를 초기화하는 것이 좋습니다.",
|
||||||
|
save_state: "상태를 파일로 저장",
|
||||||
|
import_state: "파일에서 상태 가져오기",
|
||||||
|
confirm_replace: "상태를 다음으로 대체하시겠습니까?",
|
||||||
|
password: "복호화를 위해 비밀번호 입력",
|
||||||
|
decrypt_wallet: "지갑 복호화"
|
||||||
|
},
|
||||||
|
logs: {
|
||||||
|
title: "디버그 로그 다운로드",
|
||||||
|
something_screwy: "문제가 발생했나요? 로그를 확인하세요!",
|
||||||
|
download_logs: "로그 다운로드"
|
||||||
|
},
|
||||||
|
delete_everything: {
|
||||||
|
delete: "모두 삭제",
|
||||||
|
confirm: "노드 상태가 모두 삭제됩니다. 복구할 수 없습니다!",
|
||||||
|
deleted: "삭제됨",
|
||||||
|
deleted_description: "모든 데이터 삭제됨"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
encrypt: {
|
||||||
|
header: "시드 단어 암호화",
|
||||||
|
hot_wallet_warning:
|
||||||
|
"Mutiny는 ”핫 월렛”이므로 시드 단어를 사용하여 작동하지만 선택적으로 비밀번호로 암호화할 수 있습니다.",
|
||||||
|
password_tip:
|
||||||
|
"이렇게 하면 다른 사람이 브라우저에 접근하더라도 자금에 접근할 수 없습니다.",
|
||||||
|
optional: "(선택 사항)",
|
||||||
|
existing_password: "기존 비밀번호",
|
||||||
|
existing_password_caption:
|
||||||
|
"비밀번호를 설정하지 않았다면 비워 두세요.",
|
||||||
|
new_password_label: "비밀번호",
|
||||||
|
new_password_placeholder: "비밀번호를 입력하세요",
|
||||||
|
new_password_caption:
|
||||||
|
"이 비밀번호는 시드 단어를 암호화하는 데 사용됩니다. 이를 잊어버리면 자금에 접근하려면 시드 단어를 다시 입력해야 합니다. 시드 단어를 기록해 두었나요?",
|
||||||
|
confirm_password_label: "비밀번호 확인",
|
||||||
|
confirm_password_placeholder: "동일한 비밀번호를 입력하세요",
|
||||||
|
encrypt: "암호화",
|
||||||
|
skip: "건너뛰기",
|
||||||
|
error_match: "비밀번호가 일치하지 않습니다."
|
||||||
|
},
|
||||||
|
|
||||||
|
decrypt: {
|
||||||
|
title: "비밀번호를 입력하세요",
|
||||||
|
decrypt_wallet: "지갑 복호화",
|
||||||
|
forgot_password_link: "비밀번호를 잊으셨나요?",
|
||||||
|
error_wrong_password: "유효하지 않은 비밀번호"
|
||||||
|
},
|
||||||
|
lnurl_auth: {
|
||||||
|
title: "LNURL 인증",
|
||||||
|
auth: "인증",
|
||||||
|
expected: "LNURL과 같은 형식으로 입력해주세요."
|
||||||
|
},
|
||||||
|
plus: {
|
||||||
|
title: "Mutiny+",
|
||||||
|
join: "가입",
|
||||||
|
for: "에 대해",
|
||||||
|
sats_per_month: "sats 월별 비용입니다.",
|
||||||
|
you_need: "적어도 다음 금액이 필요합니다.",
|
||||||
|
lightning_balance:
|
||||||
|
"라이트닝 잔액에서 sats를 지불하세요. 먼저 시험해보세요!",
|
||||||
|
restore: "구독 복원",
|
||||||
|
ready_to_join: "가입할 준비가 되었습니다.",
|
||||||
|
click_confirm: "첫 달 비용을 지불하려면 확인을 클릭하세요.",
|
||||||
|
open_source: "Mutiny는 오픈 소스이며 스스로 호스팅할 수 있습니다.",
|
||||||
|
optional_pay: "또한 지불할 수도 있습니다.",
|
||||||
|
paying_for: "지불 대상:",
|
||||||
|
supports_dev:
|
||||||
|
"는 지속적인 개발을 지원하고 새 기능과 프리미엄 기능의 조기 액세스를 제공합니다.",
|
||||||
|
thanks: "Mutiny의 일원이 되셨습니다! 다음 혜택을 즐기세요:",
|
||||||
|
renewal_time: "다음 시기에 갱신 요청이 도착합니다.",
|
||||||
|
cancel: "구독을 취소하려면 결제하지 않으세요. 또는 Mutiny+ 기능을 비활성화할 수도 있습니다.",
|
||||||
|
wallet_connection: "지갑 연결 기능.",
|
||||||
|
subscribe: "구독하기",
|
||||||
|
error_no_plan: "",
|
||||||
|
error_failure: "",
|
||||||
|
error_no_subscription: "기존 구독이 없습니다.",
|
||||||
|
satisfaction: "만족함",
|
||||||
|
gifting: "선물하기",
|
||||||
|
multi_device: "다중 장치 접속",
|
||||||
|
more: "... 그리고 더 많은 기능이 추가될 예정입니다."
|
||||||
|
},
|
||||||
|
restore: {
|
||||||
|
title: "복원",
|
||||||
|
all_twelve: "12개 단어를 모두 입력해야 합니다.",
|
||||||
|
wrong_word: "잘못된 단어",
|
||||||
|
paste: "클립보드에서 붙여넣기 (위험)",
|
||||||
|
confirm_text:
|
||||||
|
"이 지갑으로 복원하시겠습니까? 기존 지갑이 삭제됩니다!",
|
||||||
|
restore_tip:
|
||||||
|
"기존 Mutiny 지갑을 12개의 씨드 단어로 복원할 수 있습니다. 기존 지갑이 대체됩니다. 신중하게 사용하세요!",
|
||||||
|
multi_browser_warning: "여러 브라우저에서 동시에 사용하지 마세요.",
|
||||||
|
error_clipboard: "클립보드를 지원하지 않습니다.",
|
||||||
|
error_word_number: "잘못된 단어 개수",
|
||||||
|
error_invalid_seed: "잘못된 씨드 단어입니다."
|
||||||
|
},
|
||||||
|
servers: {
|
||||||
|
title: "서버",
|
||||||
|
caption:
|
||||||
|
"우리를 믿지 마세요! Mutiny를 백업하기 위해 자체 서버를 사용하세요.",
|
||||||
|
link: "자체 호스팅에 대해 자세히 알아보기",
|
||||||
|
proxy_label: "웹소켓 프록시",
|
||||||
|
proxy_caption: "라이트닝 노드가 네트워크와 통신하는 방법입니다.",
|
||||||
|
error_proxy: "wss://로 시작하는 URL이어야 합니다.",
|
||||||
|
esplora_label: "Esplora",
|
||||||
|
esplora_caption: "온체인 정보를 위한 블록 데이터입니다.",
|
||||||
|
error_esplora: "URL처럼 보이지 않습니다.",
|
||||||
|
rgs_label: "RGS",
|
||||||
|
rgs_caption:
|
||||||
|
"Rapid Gossip Sync. 라우팅을 위해 사용되는 라이트닝 네트워크에 대한 네트워크 데이터입니다.",
|
||||||
|
error_rgs: "URL처럼 보이지 않습니다.",
|
||||||
|
lsp_label: "LSP",
|
||||||
|
lsp_caption:
|
||||||
|
"라이트닝 서비스 공급자. 인바운드 유동성을 위해 자동으로 채널을 열고, 개인 정보 보호를 위해 인보이스를 래핑합니다.",
|
||||||
|
error_lsp: "URL처럼 보이지 않습니다.",
|
||||||
|
save: "저장"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
swap: {
|
||||||
|
peer_not_found: "피어를 찾을 수 없음",
|
||||||
|
channel_too_small:
|
||||||
|
"{{amount}} sats보다 작은 채널을 만드는 것은 그저 어리석은 짓입니다.",
|
||||||
|
insufficient_funds: "이 채널을 만들기에 충분한 자금이 없습니다.",
|
||||||
|
header: "라이트닝으로 스왑",
|
||||||
|
initiated: "스왑 시작됨",
|
||||||
|
sats_added: "sats가 라이트닝 잔액에 추가됩니다.",
|
||||||
|
use_existing: "기존 피어 사용",
|
||||||
|
choose_peer: "피어 선택",
|
||||||
|
peer_connect_label: "새 피어 연결",
|
||||||
|
peer_connect_placeholder: "피어 연결 문자열",
|
||||||
|
connect: "연결",
|
||||||
|
connecting: "연결 중...",
|
||||||
|
confirm_swap: "스왑 확인"
|
||||||
|
},
|
||||||
|
error: {
|
||||||
|
title: "오류",
|
||||||
|
emergency_link: "긴급 킷.",
|
||||||
|
restart: "문제가 *더* 발생했나요? 노드를 중지하세요!",
|
||||||
|
general: {
|
||||||
|
oh_no: "앗!",
|
||||||
|
never_should_happen: "이런 일은 일어나면 안 됩니다.",
|
||||||
|
try_reloading:
|
||||||
|
"이 페이지를 새로 고치거나 ”얘들아” 버튼을 눌러보세요. 계속해서 문제가 발생하면",
|
||||||
|
support_link: "지원을 요청하세요.",
|
||||||
|
getting_desperate: "좀 답답하신가요? 다음을 시도해보세요."
|
||||||
|
},
|
||||||
|
load_time: {
|
||||||
|
stuck: "이 화면에 멈춰있나요? 다시 로드해보세요. 그래도 동작하지 않으면 다음을 확인하세요."
|
||||||
|
},
|
||||||
|
not_found: {
|
||||||
|
title: "찾을 수 없음",
|
||||||
|
wtf_paul: "이건 아마 폴의 잘못입니다."
|
||||||
|
},
|
||||||
|
reset_router: {
|
||||||
|
payments_failing:
|
||||||
|
"결제 실패하고 있나요? 라이트닝 라우터를 초기화해보세요.",
|
||||||
|
reset_router: "라우터 초기화"
|
||||||
|
},
|
||||||
|
resync: {
|
||||||
|
incorrect_balance:
|
||||||
|
"온체인 잔액이 잘못된 것 같나요? 온체인 월렛을 다시 동기화해보세요.",
|
||||||
|
resync_wallet: "월렛 다시 동기화"
|
||||||
|
},
|
||||||
|
on_boot: {
|
||||||
|
existing_tab: {
|
||||||
|
title: "여러 탭 감지됨",
|
||||||
|
description:
|
||||||
|
"현재 Mutiny Wallet을 한 번에 한 탭에서만 사용할 수 있습니다. Mutiny가 실행 중인 다른 탭이 열려 있습니다. 해당 탭을 닫고 이 페이지를 새로 고치거나, 이 탭을 닫고 다른 탭을 새로 고치세요."
|
||||||
|
},
|
||||||
|
incompatible_browser: {
|
||||||
|
title: "호환되지 않는 브라우저",
|
||||||
|
header: "호환되지 않는 브라우저가 감지되었습니다.",
|
||||||
|
description:
|
||||||
|
"Mutiny Wallet은 WebAssembly, LocalStorage 및 IndexedDB를 지원하는 현대적인 브라우저를 필요로 합니다. 일부 브라우저는 이러한 기능을 비활성화하는 경우도 있습니다.",
|
||||||
|
try_different_browser:
|
||||||
|
"이러한 모든 기능을 지원하는 브라우저를 사용하는지 확인하거나 다른 브라우저를 시도하세요. 또는 이러한 기능을 차단하는 특정 확장 기능이나 ”보호 기능”을 비활성화해보세요.",
|
||||||
|
browser_storage:
|
||||||
|
"(더 많은 프라이버시 브라우저를 지원하고 싶지만, 월렛 데이터를 브라우저 저장소에 저장해야 하므로 그렇게 할 수 없습니다. )",
|
||||||
|
browsers_link: "지원되는 브라우저"
|
||||||
|
},
|
||||||
|
loading_failed: {
|
||||||
|
title: "로드 실패",
|
||||||
|
header: "Mutiny 로드 실패",
|
||||||
|
description:
|
||||||
|
"Mutiny Wallet을 부팅하는 동안 문제가 발생했습니다.",
|
||||||
|
repair_options:
|
||||||
|
"월렛이 손상된 것 같다면, 디버그 및 복구를 시도하기 위한 몇 가지 도구입니다.",
|
||||||
|
questions: "이러한 버튼이 무엇을 하는지 궁금하다면,",
|
||||||
|
support_link: "지원을 요청하세요."
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
modals: {
|
||||||
|
share: "공유",
|
||||||
|
details: "상세정보",
|
||||||
|
loading: {
|
||||||
|
loading: "로딩 중:",
|
||||||
|
default: "시작 중",
|
||||||
|
double_checking: "검증 중",
|
||||||
|
downloading: "다운로드 중",
|
||||||
|
setup: "설정 중",
|
||||||
|
done: "완료"
|
||||||
|
},
|
||||||
|
onboarding: {
|
||||||
|
welcome: "환영합니다!",
|
||||||
|
restore_from_backup:
|
||||||
|
"이미 Mutiny를 사용한 적이 있으시다면 백업에서 복원할 수 있습니다. 그렇지 않다면 이 단계를 건너뛰고 새로운 지갑을 즐기실 수 있습니다!",
|
||||||
|
not_available: "아직 이 기능은 지원하지 않습니다",
|
||||||
|
secure_your_funds: "자금을 안전하게 보호하세요",
|
||||||
|
make_backup:
|
||||||
|
"이 브라우저에 자금이 저장되어 있습니다. 백업이 되어 있는지 확인해 봅시다."
|
||||||
|
},
|
||||||
|
beta_warning: {
|
||||||
|
title: "경고: 베타 버전 소프트웨어",
|
||||||
|
beta_warning:
|
||||||
|
"저희가 여러분을 여기서 맞이할 수 있게 되어 기쁩니다. 그러나 경고하고 싶습니다: Mutiny Wallet은 베타 버전이며 여전히 버그와 미흡한 점이 있을 수 있습니다.",
|
||||||
|
be_careful:
|
||||||
|
"Mutiny에 지금보다 더 많은 자금을 투자하지 않도록 주의하세요.",
|
||||||
|
beta_Link: "베타 버전에 대해 자세히 알아보기",
|
||||||
|
pretend_money:
|
||||||
|
"위험 없이 Mutiny를 테스트하려면 가상 자금을 사용하려면",
|
||||||
|
signet_link: "Signet 버전을 확인하세요."
|
||||||
|
},
|
||||||
|
transaction_details: {
|
||||||
|
lightning_receive: "라이트닝 입금",
|
||||||
|
lightning_send: "라이트닝 송금",
|
||||||
|
channel_open: "채널 개설",
|
||||||
|
channel_close: "채널 종료",
|
||||||
|
onchain_receive: "체인상 입금",
|
||||||
|
onchain_send: "체인상 송금",
|
||||||
|
paid: "지불 완료",
|
||||||
|
unpaid: "미지불",
|
||||||
|
status: "상태",
|
||||||
|
when: "시간",
|
||||||
|
description: "설명",
|
||||||
|
fee: "수수료",
|
||||||
|
fees: "수수료",
|
||||||
|
bolt11: "Bolt11",
|
||||||
|
payment_hash: "지불 해시",
|
||||||
|
preimage: "사전 이미지",
|
||||||
|
txid: "거래 ID",
|
||||||
|
balance: "잔고",
|
||||||
|
reserve: "리저브",
|
||||||
|
peer: "피어",
|
||||||
|
channel_id: "채널 ID",
|
||||||
|
reason: "이유",
|
||||||
|
confirmed: "확인됨",
|
||||||
|
unconfirmed: "확인 대기",
|
||||||
|
no_details:
|
||||||
|
"채널 상세정보를 찾을 수 없습니다. 이는 해당 채널이 종료된 것으로 보입니다."
|
||||||
|
},
|
||||||
|
more_info: {
|
||||||
|
whats_with_the_fees: "수수료는 어떻게 되나요?",
|
||||||
|
self_custodial:
|
||||||
|
"Mutiny는 자체 보관 월렛입니다. 라이트닝 지불을 시작하려면 라이트닝 채널을 개설해야 하며, 이는 최소 금액과 설정 비용이 필요합니다.",
|
||||||
|
future_payments:
|
||||||
|
"앞으로의 송금 및 입금은 일반 네트워크 수수료와 노말 서비스 수수료만 부과되며, 채널에 인바운드 용량이 부족한 경우에만 추가 수수료가 발생합니다.",
|
||||||
|
liquidity: "유동성에 대해 자세히 알아보기"
|
||||||
|
},
|
||||||
|
confirm_dialog: {
|
||||||
|
are_you_sure: "확실합니까?",
|
||||||
|
cancel: "취소",
|
||||||
|
confirm: "확인"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
create_an_issue: "이슈 생성",
|
||||||
|
send_bitcoin: "비트코인 전송",
|
||||||
|
continue: "계속하기",
|
||||||
|
keep_mutiny_open: "결제를 완료하기 위해 Mutiny를 열어두세요."
|
||||||
|
};
|
||||||
@@ -20,8 +20,10 @@ import { useMegaStore } from "~/state/megaStore";
|
|||||||
import { Contact } from "@mutinywallet/mutiny-wasm";
|
import { Contact } from "@mutinywallet/mutiny-wasm";
|
||||||
import { showToast } from "~/components/Toaster";
|
import { showToast } from "~/components/Toaster";
|
||||||
import { LoadingShimmer } from "~/components/BalanceBox";
|
import { LoadingShimmer } from "~/components/BalanceBox";
|
||||||
|
import { useI18n } from "~/i18n/context";
|
||||||
|
|
||||||
function ContactRow() {
|
function ContactRow() {
|
||||||
|
const i18n = useI18n();
|
||||||
const [state, _actions] = useMegaStore();
|
const [state, _actions] = useMegaStore();
|
||||||
const [contacts, { refetch }] = createResource(async () => {
|
const [contacts, { refetch }] = createResource(async () => {
|
||||||
try {
|
try {
|
||||||
@@ -55,7 +57,7 @@ function ContactRow() {
|
|||||||
|
|
||||||
//
|
//
|
||||||
async function saveContact(_contact: ContactFormValues) {
|
async function saveContact(_contact: ContactFormValues) {
|
||||||
showToast(new Error("Unimplemented"));
|
showToast(new Error(i18n.t("common.error_unimplemented")));
|
||||||
// await editContact(contact)
|
// await editContact(contact)
|
||||||
refetch();
|
refetch();
|
||||||
}
|
}
|
||||||
@@ -84,27 +86,28 @@ const TAB =
|
|||||||
"flex-1 inline-block px-8 py-4 text-lg font-semibold rounded-lg ui-selected:bg-white/10 bg-neutral-950 hover:bg-white/10";
|
"flex-1 inline-block px-8 py-4 text-lg font-semibold rounded-lg ui-selected:bg-white/10 bg-neutral-950 hover:bg-white/10";
|
||||||
|
|
||||||
export default function Activity() {
|
export default function Activity() {
|
||||||
|
const i18n = useI18n();
|
||||||
const [state, _actions] = useMegaStore();
|
const [state, _actions] = useMegaStore();
|
||||||
return (
|
return (
|
||||||
<MutinyWalletGuard>
|
<MutinyWalletGuard>
|
||||||
<SafeArea>
|
<SafeArea>
|
||||||
<DefaultMain>
|
<DefaultMain>
|
||||||
<BackLink />
|
<BackLink />
|
||||||
<LargeHeader>Activity</LargeHeader>
|
<LargeHeader>{i18n.t("activity.title")}</LargeHeader>
|
||||||
<ContactRow />
|
<ContactRow />
|
||||||
<Tabs.Root defaultValue="mutiny">
|
<Tabs.Root defaultValue="mutiny">
|
||||||
<Tabs.List class="relative flex justify-around mt-4 mb-8 gap-1 bg-neutral-950 p-1 rounded-xl">
|
<Tabs.List class="relative flex justify-around mt-4 mb-8 gap-1 bg-neutral-950 p-1 rounded-xl">
|
||||||
<Tabs.Trigger value="mutiny" class={TAB}>
|
<Tabs.Trigger value="mutiny" class={TAB}>
|
||||||
Mutiny
|
{i18n.t("activity.mutiny")}
|
||||||
</Tabs.Trigger>
|
</Tabs.Trigger>
|
||||||
<Tabs.Trigger value="nostr" class={TAB}>
|
<Tabs.Trigger value="nostr" class={TAB}>
|
||||||
Nostr
|
{i18n.t("activity.nostr")}
|
||||||
</Tabs.Trigger>
|
</Tabs.Trigger>
|
||||||
{/* <Tabs.Indicator class="absolute bg-m-blue transition-all bottom-[-1px] h-[2px]" /> */}
|
{/* <Tabs.Indicator class="absolute bg-m-blue transition-all bottom-[-1px] h-[2px]" /> */}
|
||||||
</Tabs.List>
|
</Tabs.List>
|
||||||
<Tabs.Content value="mutiny">
|
<Tabs.Content value="mutiny">
|
||||||
{/* <MutinyActivity /> */}
|
{/* <MutinyActivity /> */}
|
||||||
<Card title="Activity">
|
<Card title={i18n.t("activity.title")}>
|
||||||
<div class="p-1" />
|
<div class="p-1" />
|
||||||
<VStack>
|
<VStack>
|
||||||
<Suspense>
|
<Suspense>
|
||||||
@@ -122,11 +125,10 @@ export default function Activity() {
|
|||||||
<VStack>
|
<VStack>
|
||||||
<div class="my-8 flex flex-col items-center gap-4 text-center max-w-[20rem] mx-auto">
|
<div class="my-8 flex flex-col items-center gap-4 text-center max-w-[20rem] mx-auto">
|
||||||
<NiceP>
|
<NiceP>
|
||||||
Import your contacts from nostr to see
|
{i18n.t("activity.import_contacts")}
|
||||||
who they're zapping.
|
|
||||||
</NiceP>
|
</NiceP>
|
||||||
<Button disabled intent="blue">
|
<Button disabled intent="blue">
|
||||||
Coming soon
|
{i18n.t("activity.coming_soon")}
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</VStack>
|
</VStack>
|
||||||
|
|||||||
@@ -145,7 +145,7 @@ function FeedbackForm(props: { onSubmitted: () => void }) {
|
|||||||
|
|
||||||
if (!res.ok) {
|
if (!res.ok) {
|
||||||
throw new Error(
|
throw new Error(
|
||||||
`${i18n.t("feedback.error")}: ${res.statusText}`
|
i18n.t("feedback.error", { error: `: ${res.statusText}` })
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -155,9 +155,9 @@ function FeedbackForm(props: { onSubmitted: () => void }) {
|
|||||||
props.onSubmitted();
|
props.onSubmitted();
|
||||||
} else {
|
} else {
|
||||||
throw new Error(
|
throw new Error(
|
||||||
`${i18n.t("feedback.error")}. ${i18n.t(
|
i18n.t("feedback.error", {
|
||||||
"feedback.try_again"
|
error: `. ${i18n.t("feedback.try_again")}`
|
||||||
)}`
|
})
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
@@ -327,11 +327,10 @@ export default function Feedback() {
|
|||||||
<LargeHeader>{i18n.t("feedback.header")}</LargeHeader>
|
<LargeHeader>{i18n.t("feedback.header")}</LargeHeader>
|
||||||
<NiceP>{i18n.t("feedback.tracking")}</NiceP>
|
<NiceP>{i18n.t("feedback.tracking")}</NiceP>
|
||||||
<NiceP>
|
<NiceP>
|
||||||
{i18n.t("feedback.github_one")}{" "}
|
{i18n.t("feedback.github")}{" "}
|
||||||
<ExternalLink href="https://github.com/MutinyWallet/mutiny-web/issues">
|
<ExternalLink href="https://github.com/MutinyWallet/mutiny-web/issues">
|
||||||
{i18n.t("feedback.create_issue")}
|
{i18n.t("feedback.create_issue")}
|
||||||
</ExternalLink>
|
</ExternalLink>
|
||||||
{i18n.t("feedback.github_two")}
|
|
||||||
</NiceP>
|
</NiceP>
|
||||||
<FeedbackForm onSubmitted={() => setSubmitted(true)} />
|
<FeedbackForm onSubmitted={() => setSubmitted(true)} />
|
||||||
</Match>
|
</Match>
|
||||||
|
|||||||
@@ -27,7 +27,7 @@ import NavBar from "~/components/NavBar";
|
|||||||
import { useMegaStore } from "~/state/megaStore";
|
import { useMegaStore } from "~/state/megaStore";
|
||||||
import { objectToSearchParams } from "~/utils/objectToSearchParams";
|
import { objectToSearchParams } from "~/utils/objectToSearchParams";
|
||||||
import mempoolTxUrl from "~/utils/mempoolTxUrl";
|
import mempoolTxUrl from "~/utils/mempoolTxUrl";
|
||||||
import { AmountSats, AmountFiat, AmountSmall } from "~/components/Amount";
|
import { AmountSats, AmountFiat } from "~/components/Amount";
|
||||||
import { BackLink } from "~/components/layout/BackLink";
|
import { BackLink } from "~/components/layout/BackLink";
|
||||||
import { TagEditor } from "~/components/TagEditor";
|
import { TagEditor } from "~/components/TagEditor";
|
||||||
import { StyledRadioGroup } from "~/components/layout/Radio";
|
import { StyledRadioGroup } from "~/components/layout/Radio";
|
||||||
@@ -44,9 +44,9 @@ import { InfoBox } from "~/components/InfoBox";
|
|||||||
import { FeesModal } from "~/components/MoreInfoModal";
|
import { FeesModal } from "~/components/MoreInfoModal";
|
||||||
import { IntegratedQr } from "~/components/IntegratedQR";
|
import { IntegratedQr } from "~/components/IntegratedQR";
|
||||||
import side2side from "~/assets/icons/side-to-side.svg";
|
import side2side from "~/assets/icons/side-to-side.svg";
|
||||||
import { useI18n } from "~/i18n/context";
|
|
||||||
import eify from "~/utils/eify";
|
import eify from "~/utils/eify";
|
||||||
import { matchError } from "~/logic/errorDispatch";
|
import { matchError } from "~/logic/errorDispatch";
|
||||||
|
import { useI18n } from "~/i18n/context";
|
||||||
import { Fee } from "~/components/Fee";
|
import { Fee } from "~/components/Fee";
|
||||||
|
|
||||||
type OnChainTx = {
|
type OnChainTx = {
|
||||||
@@ -73,48 +73,30 @@ type OnChainTx = {
|
|||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
const RECEIVE_FLAVORS = [
|
|
||||||
{
|
|
||||||
value: "unified",
|
|
||||||
label: "Unified",
|
|
||||||
caption:
|
|
||||||
"Combines a bitcoin address and a lightning invoice. Sender chooses payment method."
|
|
||||||
},
|
|
||||||
{
|
|
||||||
value: "lightning",
|
|
||||||
label: "Lightning invoice",
|
|
||||||
caption:
|
|
||||||
"Ideal for small transactions. Usually lower fees than on-chain."
|
|
||||||
},
|
|
||||||
{
|
|
||||||
value: "onchain",
|
|
||||||
label: "Bitcoin address",
|
|
||||||
caption:
|
|
||||||
"On-chain, just like Satoshi did it. Ideal for very large transactions."
|
|
||||||
}
|
|
||||||
];
|
|
||||||
|
|
||||||
export type ReceiveFlavor = "unified" | "lightning" | "onchain";
|
export type ReceiveFlavor = "unified" | "lightning" | "onchain";
|
||||||
type ReceiveState = "edit" | "show" | "paid";
|
type ReceiveState = "edit" | "show" | "paid";
|
||||||
type PaidState = "lightning_paid" | "onchain_paid";
|
type PaidState = "lightning_paid" | "onchain_paid";
|
||||||
|
|
||||||
function FeeWarning(props: { fee: bigint; flavor: ReceiveFlavor }) {
|
function FeeWarning(props: { fee: bigint; flavor: ReceiveFlavor }) {
|
||||||
|
const i18n = useI18n();
|
||||||
return (
|
return (
|
||||||
// TODO: probably won't always be fixed 2500?
|
// TODO: probably won't always be fixed 2500?
|
||||||
<Show when={props.fee > 1000n}>
|
<Show when={props.fee > 1000n}>
|
||||||
<Switch>
|
<Switch>
|
||||||
<Match when={props.flavor === "unified"}>
|
<Match when={props.flavor === "unified"}>
|
||||||
<InfoBox accent="blue">
|
<InfoBox accent="blue">
|
||||||
A lightning setup fee of{" "}
|
{i18n.t("receive.unified_setup_fee", {
|
||||||
<AmountSmall amountSats={props.fee} /> will be charged
|
amount: props.fee.toLocaleString()
|
||||||
if paid over lightning. <FeesModal />
|
})}
|
||||||
|
<FeesModal />
|
||||||
</InfoBox>
|
</InfoBox>
|
||||||
</Match>
|
</Match>
|
||||||
<Match when={props.flavor === "lightning"}>
|
<Match when={props.flavor === "lightning"}>
|
||||||
<InfoBox accent="blue">
|
<InfoBox accent="blue">
|
||||||
A lightning setup fee of{" "}
|
{i18n.t("receive.lightning_setup_fee", {
|
||||||
<AmountSmall amountSats={props.fee} /> will be charged
|
amount: props.fee.toLocaleString()
|
||||||
for this receive. <FeesModal />
|
})}
|
||||||
|
<FeesModal />
|
||||||
</InfoBox>
|
</InfoBox>
|
||||||
</Match>
|
</Match>
|
||||||
</Switch>
|
</Switch>
|
||||||
@@ -151,6 +133,24 @@ export default function Receive() {
|
|||||||
// loading state for the continue button
|
// loading state for the continue button
|
||||||
const [loading, setLoading] = createSignal(false);
|
const [loading, setLoading] = createSignal(false);
|
||||||
|
|
||||||
|
const RECEIVE_FLAVORS = [
|
||||||
|
{
|
||||||
|
value: "unified",
|
||||||
|
label: i18n.t("receive.unified_label"),
|
||||||
|
caption: i18n.t("receive.unified_caption")
|
||||||
|
},
|
||||||
|
{
|
||||||
|
value: "lightning",
|
||||||
|
label: i18n.t("receive.lightning_label"),
|
||||||
|
caption: i18n.t("receive.lightning_caption")
|
||||||
|
},
|
||||||
|
{
|
||||||
|
value: "onchain",
|
||||||
|
label: i18n.t("receive.onchain_label"),
|
||||||
|
caption: i18n.t("receive.onchain_caption")
|
||||||
|
}
|
||||||
|
];
|
||||||
|
|
||||||
const receiveString = createMemo(() => {
|
const receiveString = createMemo(() => {
|
||||||
if (unified() && receiveState() === "show") {
|
if (unified() && receiveState() === "show") {
|
||||||
if (flavor() === "unified") {
|
if (flavor() === "unified") {
|
||||||
@@ -350,7 +350,7 @@ export default function Receive() {
|
|||||||
>
|
>
|
||||||
<BackButton
|
<BackButton
|
||||||
onClick={() => setReceiveState("edit")}
|
onClick={() => setReceiveState("edit")}
|
||||||
title={`${i18n.t("receive.edit")}`}
|
title={i18n.t("receive.edit")}
|
||||||
showOnDesktop
|
showOnDesktop
|
||||||
/>
|
/>
|
||||||
</Show>
|
</Show>
|
||||||
@@ -376,7 +376,7 @@ export default function Receive() {
|
|||||||
exitRoute={amount() ? "/receive" : "/"}
|
exitRoute={amount() ? "/receive" : "/"}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<Card title={i18n.t("private_tags")}>
|
<Card title={i18n.t("common.private_tags")}>
|
||||||
<TagEditor
|
<TagEditor
|
||||||
selectedValues={selectedValues()}
|
selectedValues={selectedValues()}
|
||||||
setSelectedValues={setSelectedValues}
|
setSelectedValues={setSelectedValues}
|
||||||
@@ -394,7 +394,7 @@ export default function Receive() {
|
|||||||
onClick={onSubmit}
|
onClick={onSubmit}
|
||||||
loading={loading()}
|
loading={loading()}
|
||||||
>
|
>
|
||||||
{i18n.t("continue")}
|
{i18n.t("common.continue")}
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</Match>
|
</Match>
|
||||||
@@ -406,7 +406,7 @@ export default function Receive() {
|
|||||||
kind={flavor()}
|
kind={flavor()}
|
||||||
/>
|
/>
|
||||||
<p class="text-neutral-400 text-center">
|
<p class="text-neutral-400 text-center">
|
||||||
{i18n.t("keep_mutiny_open")}
|
{i18n.t("receive.keep_mutiny_open")}
|
||||||
</p>
|
</p>
|
||||||
{/* Only show method chooser when we have an invoice */}
|
{/* Only show method chooser when we have an invoice */}
|
||||||
<Show when={bip21Raw()?.invoice}>
|
<Show when={bip21Raw()?.invoice}>
|
||||||
@@ -420,7 +420,9 @@ export default function Receive() {
|
|||||||
<img class="w-4 h-4" src={side2side} />
|
<img class="w-4 h-4" src={side2side} />
|
||||||
</button>
|
</button>
|
||||||
<SimpleDialog
|
<SimpleDialog
|
||||||
title="Choose payment format"
|
title={i18n.t(
|
||||||
|
"receive.choose_payment_format"
|
||||||
|
)}
|
||||||
open={methodChooserOpen()}
|
open={methodChooserOpen()}
|
||||||
setOpen={(open) =>
|
setOpen={(open) =>
|
||||||
setMethodChooserOpen(open)
|
setMethodChooserOpen(open)
|
||||||
@@ -503,7 +505,7 @@ export default function Receive() {
|
|||||||
network
|
network
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
{i18n.t("view_transaction")}
|
{i18n.t("common.view_transaction")}
|
||||||
</ExternalLink>
|
</ExternalLink>
|
||||||
</Show>
|
</Show>
|
||||||
</SuccessModal>
|
</SuccessModal>
|
||||||
|
|||||||
@@ -44,6 +44,7 @@ import mempoolTxUrl from "~/utils/mempoolTxUrl";
|
|||||||
import { AmountSats } from "~/components/Amount";
|
import { AmountSats } from "~/components/Amount";
|
||||||
import { getRedshifted, setRedshifted } from "~/utils/fakeLabels";
|
import { getRedshifted, setRedshifted } from "~/utils/fakeLabels";
|
||||||
import { Network } from "~/logic/mutinyWalletSetup";
|
import { Network } from "~/logic/mutinyWalletSetup";
|
||||||
|
import { useI18n } from "~/i18n/context";
|
||||||
|
|
||||||
type ShiftOption = "utxo" | "lightning";
|
type ShiftOption = "utxo" | "lightning";
|
||||||
|
|
||||||
@@ -87,6 +88,7 @@ const dummyRedshift: RedshiftResult = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
function RedshiftReport(props: { redshift: RedshiftResult; utxo: UtxoItem }) {
|
function RedshiftReport(props: { redshift: RedshiftResult; utxo: UtxoItem }) {
|
||||||
|
const i18n = useI18n();
|
||||||
const [state, _actions] = useMegaStore();
|
const [state, _actions] = useMegaStore();
|
||||||
|
|
||||||
const getUtXos = async () => {
|
const getUtXos = async () => {
|
||||||
@@ -155,7 +157,7 @@ function RedshiftReport(props: { redshift: RedshiftResult; utxo: UtxoItem }) {
|
|||||||
</Show>
|
</Show>
|
||||||
</VStack> */}
|
</VStack> */}
|
||||||
<VStack>
|
<VStack>
|
||||||
<NiceP>What happened?</NiceP>
|
<NiceP>{i18n.t("redshift.what_happened")}</NiceP>
|
||||||
<Show when={redshiftResource()}>
|
<Show when={redshiftResource()}>
|
||||||
<Card>
|
<Card>
|
||||||
<VStack biggap>
|
<VStack biggap>
|
||||||
@@ -164,22 +166,22 @@ function RedshiftReport(props: { redshift: RedshiftResult; utxo: UtxoItem }) {
|
|||||||
<Utxo item={inputUtxo()!} />
|
<Utxo item={inputUtxo()!} />
|
||||||
</Show>
|
</Show>
|
||||||
</KV> */}
|
</KV> */}
|
||||||
<KV key="Starting amount">
|
<KV key={i18n.t("redshift.starting_amount")}>
|
||||||
<AmountSats
|
<AmountSats
|
||||||
amountSats={redshiftResource()!.amount_sats}
|
amountSats={redshiftResource()!.amount_sats}
|
||||||
/>
|
/>
|
||||||
</KV>
|
</KV>
|
||||||
<KV key="Fees paid">
|
<KV key={i18n.t("redshift.fees_paid")}>
|
||||||
<AmountSats
|
<AmountSats
|
||||||
amountSats={redshiftResource()!.fees_paid}
|
amountSats={redshiftResource()!.fees_paid}
|
||||||
/>
|
/>
|
||||||
</KV>
|
</KV>
|
||||||
<KV key="Change">
|
<KV key={i18n.t("redshift.change")}>
|
||||||
<AmountSats
|
<AmountSats
|
||||||
amountSats={redshiftResource()!.change_amt}
|
amountSats={redshiftResource()!.change_amt}
|
||||||
/>
|
/>
|
||||||
</KV>
|
</KV>
|
||||||
<KV key="Outbound channel">
|
<KV key={i18n.t("redshift.outbound_channel")}>
|
||||||
<VStack>
|
<VStack>
|
||||||
<pre class="whitespace-pre-wrap break-all">
|
<pre class="whitespace-pre-wrap break-all">
|
||||||
{
|
{
|
||||||
@@ -198,12 +200,12 @@ function RedshiftReport(props: { redshift: RedshiftResult; utxo: UtxoItem }) {
|
|||||||
target="_blank"
|
target="_blank"
|
||||||
rel="noreferrer"
|
rel="noreferrer"
|
||||||
>
|
>
|
||||||
View on mempool
|
{i18n.t("common.view_transaction")}
|
||||||
</a>
|
</a>
|
||||||
</VStack>
|
</VStack>
|
||||||
</KV>
|
</KV>
|
||||||
<Show when={redshiftResource()!.output_channel}>
|
<Show when={redshiftResource()!.output_channel}>
|
||||||
<KV key="Return channel">
|
<KV key={i18n.t("redshift.return_channel")}>
|
||||||
<VStack>
|
<VStack>
|
||||||
<pre class="whitespace-pre-wrap break-all">
|
<pre class="whitespace-pre-wrap break-all">
|
||||||
{redshiftResource()!.output_channel}
|
{redshiftResource()!.output_channel}
|
||||||
@@ -219,7 +221,7 @@ function RedshiftReport(props: { redshift: RedshiftResult; utxo: UtxoItem }) {
|
|||||||
target="_blank"
|
target="_blank"
|
||||||
rel="noreferrer"
|
rel="noreferrer"
|
||||||
>
|
>
|
||||||
View on mempool
|
{i18n.t("common.view_transaction")}
|
||||||
</a>
|
</a>
|
||||||
</VStack>
|
</VStack>
|
||||||
</KV>
|
</KV>
|
||||||
@@ -232,20 +234,8 @@ function RedshiftReport(props: { redshift: RedshiftResult; utxo: UtxoItem }) {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const SHIFT_OPTIONS = [
|
|
||||||
{
|
|
||||||
value: "utxo",
|
|
||||||
label: "UTXO",
|
|
||||||
caption: "Trade your UTXO for a fresh UTXO"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
value: "lightning",
|
|
||||||
label: "Lightning",
|
|
||||||
caption: "Convert your UTXO into Lightning"
|
|
||||||
}
|
|
||||||
];
|
|
||||||
|
|
||||||
export function Utxo(props: { item: UtxoItem; onClick?: () => void }) {
|
export function Utxo(props: { item: UtxoItem; onClick?: () => void }) {
|
||||||
|
const i18n = useI18n();
|
||||||
const redshifted = createMemo(() => getRedshifted(props.item.outpoint));
|
const redshifted = createMemo(() => getRedshifted(props.item.outpoint));
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
@@ -260,9 +250,15 @@ export function Utxo(props: { item: UtxoItem; onClick?: () => void }) {
|
|||||||
<div class="flex gap-2">
|
<div class="flex gap-2">
|
||||||
<Show
|
<Show
|
||||||
when={redshifted()}
|
when={redshifted()}
|
||||||
fallback={<h2 class={MISSING_LABEL}>Unknown</h2>}
|
fallback={
|
||||||
|
<h2 class={MISSING_LABEL}>
|
||||||
|
{i18n.t("redshift.unknown")}
|
||||||
|
</h2>
|
||||||
|
}
|
||||||
>
|
>
|
||||||
<h2 class={REDSHIFT_LABEL}>Redshift</h2>
|
<h2 class={REDSHIFT_LABEL}>
|
||||||
|
{i18n.t("redshift.title")}
|
||||||
|
</h2>
|
||||||
</Show>
|
</Show>
|
||||||
</div>
|
</div>
|
||||||
<SmallAmount amount={props.item.txout.value} />
|
<SmallAmount amount={props.item.txout.value} />
|
||||||
@@ -293,6 +289,7 @@ function ShiftObserver(props: {
|
|||||||
setShiftStage: (stage: ShiftStage) => void;
|
setShiftStage: (stage: ShiftStage) => void;
|
||||||
redshiftId: string;
|
redshiftId: string;
|
||||||
}) {
|
}) {
|
||||||
|
const i18n = useI18n();
|
||||||
const [_state, _actions] = useMegaStore();
|
const [_state, _actions] = useMegaStore();
|
||||||
|
|
||||||
const [fakeStage, _setFakeStage] = createSignal(2);
|
const [fakeStage, _setFakeStage] = createSignal(2);
|
||||||
@@ -348,7 +345,7 @@ function ShiftObserver(props: {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<NiceP>Watch it go!</NiceP>
|
<NiceP>{i18n.t("redshift.watch_it_go")}</NiceP>
|
||||||
<Card>
|
<Card>
|
||||||
<VStack>
|
<VStack>
|
||||||
<pre class="self-center">{FAKE_STATES[fakeStage()]}</pre>
|
<pre class="self-center">{FAKE_STATES[fakeStage()]}</pre>
|
||||||
@@ -370,6 +367,7 @@ const KV: ParentComponent<{ key: string }> = (props) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export default function Redshift() {
|
export default function Redshift() {
|
||||||
|
const i18n = useI18n();
|
||||||
const [state, _actions] = useMegaStore();
|
const [state, _actions] = useMegaStore();
|
||||||
|
|
||||||
const [shiftStage, setShiftStage] = createSignal<ShiftStage>("choose");
|
const [shiftStage, setShiftStage] = createSignal<ShiftStage>("choose");
|
||||||
@@ -377,6 +375,19 @@ export default function Redshift() {
|
|||||||
|
|
||||||
const [chosenUtxo, setChosenUtxo] = createSignal<UtxoItem>();
|
const [chosenUtxo, setChosenUtxo] = createSignal<UtxoItem>();
|
||||||
|
|
||||||
|
const SHIFT_OPTIONS = [
|
||||||
|
{
|
||||||
|
value: "utxo",
|
||||||
|
label: i18n.t("redshift.utxo_label"),
|
||||||
|
caption: i18n.t("redshift.utxo_caption")
|
||||||
|
},
|
||||||
|
{
|
||||||
|
value: "lightning",
|
||||||
|
label: i18n.t("redshift.lightning_label"),
|
||||||
|
caption: i18n.t("redshift.lightning_caption")
|
||||||
|
}
|
||||||
|
];
|
||||||
|
|
||||||
const getUtXos = async () => {
|
const getUtXos = async () => {
|
||||||
console.log("Getting utxos");
|
console.log("Getting utxos");
|
||||||
return (await state.mutiny_wallet?.list_utxos()) as UtxoItem[];
|
return (await state.mutiny_wallet?.list_utxos()) as UtxoItem[];
|
||||||
@@ -439,14 +450,19 @@ export default function Redshift() {
|
|||||||
<SafeArea>
|
<SafeArea>
|
||||||
<DefaultMain>
|
<DefaultMain>
|
||||||
<BackLink />
|
<BackLink />
|
||||||
<LargeHeader>Redshift (coming soon)</LargeHeader>
|
<LargeHeader>
|
||||||
|
{i18n.t("redshift.title")}{" "}
|
||||||
|
{i18n.t("common.coming_soon")}
|
||||||
|
</LargeHeader>
|
||||||
<div class="relative filter grayscale pointer-events-none opacity-75">
|
<div class="relative filter grayscale pointer-events-none opacity-75">
|
||||||
<VStack biggap>
|
<VStack biggap>
|
||||||
{/* <pre>{JSON.stringify(redshiftResource(), null, 2)}</pre> */}
|
{/* <pre>{JSON.stringify(redshiftResource(), null, 2)}</pre> */}
|
||||||
<Switch>
|
<Switch>
|
||||||
<Match when={shiftStage() === "choose"}>
|
<Match when={shiftStage() === "choose"}>
|
||||||
<VStack>
|
<VStack>
|
||||||
<NiceP>Where is this going?</NiceP>
|
<NiceP>
|
||||||
|
{i18n.t("redshift.where_this_goes")}
|
||||||
|
</NiceP>
|
||||||
<StyledRadioGroup
|
<StyledRadioGroup
|
||||||
accent="red"
|
accent="red"
|
||||||
value={shiftType()}
|
value={shiftType()}
|
||||||
@@ -460,7 +476,7 @@ export default function Redshift() {
|
|||||||
</VStack>
|
</VStack>
|
||||||
<VStack>
|
<VStack>
|
||||||
<NiceP>
|
<NiceP>
|
||||||
Choose your{" "}
|
{i18n.t("redshift.choose_your")}{" "}
|
||||||
<span class="inline-block">
|
<span class="inline-block">
|
||||||
<img
|
<img
|
||||||
class="h-4"
|
class="h-4"
|
||||||
@@ -468,10 +484,14 @@ export default function Redshift() {
|
|||||||
alt="sine wave"
|
alt="sine wave"
|
||||||
/>
|
/>
|
||||||
</span>{" "}
|
</span>{" "}
|
||||||
UTXO to begin
|
{i18n.t("redshift.utxo_to_begin")}
|
||||||
</NiceP>
|
</NiceP>
|
||||||
<Suspense>
|
<Suspense>
|
||||||
<Card title="Unshifted UTXOs">
|
<Card
|
||||||
|
title={i18n.t(
|
||||||
|
"redshift.unshifted_utxo"
|
||||||
|
)}
|
||||||
|
>
|
||||||
<Switch>
|
<Switch>
|
||||||
<Match when={utxos.loading}>
|
<Match when={utxos.loading}>
|
||||||
<LoadingSpinner wide />
|
<LoadingSpinner wide />
|
||||||
@@ -485,8 +505,9 @@ export default function Redshift() {
|
|||||||
}
|
}
|
||||||
>
|
>
|
||||||
<code>
|
<code>
|
||||||
No utxos (empty
|
{i18n.t(
|
||||||
state)
|
"redshift.no_utxos_empty_state"
|
||||||
|
)}
|
||||||
</code>
|
</code>
|
||||||
</Match>
|
</Match>
|
||||||
<Match
|
<Match
|
||||||
@@ -521,9 +542,13 @@ export default function Redshift() {
|
|||||||
titleElement={
|
titleElement={
|
||||||
<SmallHeader>
|
<SmallHeader>
|
||||||
<span class="text-m-red">
|
<span class="text-m-red">
|
||||||
Redshifted{" "}
|
{i18n.t(
|
||||||
|
"redshift.redshifted"
|
||||||
|
)}{" "}
|
||||||
</span>
|
</span>
|
||||||
UTXOs
|
{i18n.t(
|
||||||
|
"redshift.utxos"
|
||||||
|
)}
|
||||||
</SmallHeader>
|
</SmallHeader>
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
@@ -540,8 +565,9 @@ export default function Redshift() {
|
|||||||
}
|
}
|
||||||
>
|
>
|
||||||
<code>
|
<code>
|
||||||
No utxos (empty
|
{i18n.t(
|
||||||
state)
|
"redshift.no_utxos_empty_state"
|
||||||
|
)}
|
||||||
</code>
|
</code>
|
||||||
</Match>
|
</Match>
|
||||||
<Match
|
<Match
|
||||||
@@ -594,15 +620,17 @@ export default function Redshift() {
|
|||||||
intent="red"
|
intent="red"
|
||||||
onClick={resetState}
|
onClick={resetState}
|
||||||
>
|
>
|
||||||
Nice
|
{i18n.t("common.nice")}
|
||||||
</Button>
|
</Button>
|
||||||
</VStack>
|
</VStack>
|
||||||
</Match>
|
</Match>
|
||||||
<Match when={shiftStage() === "failure"}>
|
<Match when={shiftStage() === "failure"}>
|
||||||
<NiceP>Oh dear</NiceP>
|
<NiceP>{i18n.t("redshift.oh_dear")}</NiceP>
|
||||||
<NiceP>Here's what happened:</NiceP>
|
<NiceP>
|
||||||
|
{i18n.t("redshift.here_is_error")}
|
||||||
|
</NiceP>
|
||||||
<Button intent="red" onClick={resetState}>
|
<Button intent="red" onClick={resetState}>
|
||||||
Dangit
|
{i18n.t("common.dangit")}
|
||||||
</Button>
|
</Button>
|
||||||
</Match>
|
</Match>
|
||||||
</Switch>
|
</Switch>
|
||||||
|
|||||||
@@ -132,9 +132,10 @@ function DestinationInput(props: {
|
|||||||
handleDecode: () => void;
|
handleDecode: () => void;
|
||||||
handlePaste: () => void;
|
handlePaste: () => void;
|
||||||
}) {
|
}) {
|
||||||
|
const i18n = useI18n();
|
||||||
return (
|
return (
|
||||||
<VStack>
|
<VStack>
|
||||||
<SmallHeader>Destination</SmallHeader>
|
<SmallHeader>{i18n.t("send.destination")}</SmallHeader>
|
||||||
<textarea
|
<textarea
|
||||||
value={props.fieldDestination}
|
value={props.fieldDestination}
|
||||||
onInput={(e) => {
|
onInput={(e) => {
|
||||||
@@ -149,19 +150,19 @@ function DestinationInput(props: {
|
|||||||
intent="blue"
|
intent="blue"
|
||||||
onClick={props.handleDecode}
|
onClick={props.handleDecode}
|
||||||
>
|
>
|
||||||
Continue
|
{i18n.t("common.continue")}
|
||||||
</Button>
|
</Button>
|
||||||
<HStack>
|
<HStack>
|
||||||
<Button onClick={props.handlePaste}>
|
<Button onClick={props.handlePaste}>
|
||||||
<div class="flex flex-col gap-2 items-center">
|
<div class="flex flex-col gap-2 items-center">
|
||||||
<Paste />
|
<Paste />
|
||||||
<span>Paste</span>
|
<span>{i18n.t("send.paste")}</span>
|
||||||
</div>
|
</div>
|
||||||
</Button>
|
</Button>
|
||||||
<ButtonLink href="/scanner">
|
<ButtonLink href="/scanner">
|
||||||
<div class="flex flex-col gap-2 items-center">
|
<div class="flex flex-col gap-2 items-center">
|
||||||
<Scan />
|
<Scan />
|
||||||
<span>Scan QR</span>
|
<span>{i18n.t("send.scan_qr")}</span>
|
||||||
</div>
|
</div>
|
||||||
</ButtonLink>
|
</ButtonLink>
|
||||||
</HStack>
|
</HStack>
|
||||||
@@ -260,9 +261,7 @@ export default function Send() {
|
|||||||
if (source() === "lightning") {
|
if (source() === "lightning") {
|
||||||
return (
|
return (
|
||||||
(state.balance?.lightning ?? 0n) <= amountSats() &&
|
(state.balance?.lightning ?? 0n) <= amountSats() &&
|
||||||
setError(
|
setError(i18n.t("send.error_low_balance"))
|
||||||
"We do not have enough balance to pay the given amount."
|
|
||||||
)
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@@ -395,7 +394,7 @@ export default function Send() {
|
|||||||
text = value;
|
text = value;
|
||||||
} else {
|
} else {
|
||||||
if (!navigator.clipboard.readText) {
|
if (!navigator.clipboard.readText) {
|
||||||
return showToast(new Error("Clipboard not supported"));
|
return showToast(new Error(i18n.t("send.error_clipboard")));
|
||||||
}
|
}
|
||||||
text = await navigator.clipboard.readText();
|
text = await navigator.clipboard.readText();
|
||||||
}
|
}
|
||||||
@@ -486,7 +485,7 @@ export default function Send() {
|
|||||||
|
|
||||||
// TODO: handle timeouts
|
// TODO: handle timeouts
|
||||||
if (!payment?.paid) {
|
if (!payment?.paid) {
|
||||||
throw new Error("Keysend failed");
|
throw new Error(i18n.t("send.error_keysend"));
|
||||||
} else {
|
} else {
|
||||||
sentDetails.amount = amountSats();
|
sentDetails.amount = amountSats();
|
||||||
}
|
}
|
||||||
@@ -501,7 +500,7 @@ export default function Send() {
|
|||||||
);
|
);
|
||||||
|
|
||||||
if (!payment?.paid) {
|
if (!payment?.paid) {
|
||||||
throw new Error("Lnurl Pay failed");
|
throw new Error(i18n.t("send.error_LNURL"));
|
||||||
} else {
|
} else {
|
||||||
sentDetails.amount = amountSats();
|
sentDetails.amount = amountSats();
|
||||||
}
|
}
|
||||||
@@ -575,7 +574,7 @@ export default function Send() {
|
|||||||
title={i18n.t("send.start_over")}
|
title={i18n.t("send.start_over")}
|
||||||
/>
|
/>
|
||||||
</Show>
|
</Show>
|
||||||
<LargeHeader>{i18n.t("send_bitcoin")}</LargeHeader>
|
<LargeHeader>{i18n.t("send.send_bitcoin")}</LargeHeader>
|
||||||
<SuccessModal
|
<SuccessModal
|
||||||
confirmText={
|
confirmText={
|
||||||
sentDetails()?.amount
|
sentDetails()?.amount
|
||||||
@@ -596,7 +595,9 @@ export default function Send() {
|
|||||||
<MegaEx />
|
<MegaEx />
|
||||||
<h1 class="w-full mt-4 mb-2 text-2xl font-semibold text-center md:text-3xl">
|
<h1 class="w-full mt-4 mb-2 text-2xl font-semibold text-center md:text-3xl">
|
||||||
{sentDetails()?.amount
|
{sentDetails()?.amount
|
||||||
? "Payment Initiated"
|
? source() === "onchain"
|
||||||
|
? i18n.t("send.payment_initiated")
|
||||||
|
: i18n.t("send.payment_sent")
|
||||||
: sentDetails()?.failure_reason}
|
: sentDetails()?.failure_reason}
|
||||||
</h1>
|
</h1>
|
||||||
{/*TODO: add failure hint logic for different failure conditions*/}
|
{/*TODO: add failure hint logic for different failure conditions*/}
|
||||||
@@ -605,7 +606,9 @@ export default function Send() {
|
|||||||
<MegaCheck />
|
<MegaCheck />
|
||||||
<h1 class="w-full mt-4 mb-2 text-2xl font-semibold text-center md:text-3xl">
|
<h1 class="w-full mt-4 mb-2 text-2xl font-semibold text-center md:text-3xl">
|
||||||
{sentDetails()?.amount
|
{sentDetails()?.amount
|
||||||
? "Payment Initiated"
|
? source() === "onchain"
|
||||||
|
? i18n.t("send.payment_initiated")
|
||||||
|
: i18n.t("send.payment_sent")
|
||||||
: sentDetails()?.failure_reason}
|
: sentDetails()?.failure_reason}
|
||||||
</h1>
|
</h1>
|
||||||
<div class="flex flex-col gap-1 items-center">
|
<div class="flex flex-col gap-1 items-center">
|
||||||
@@ -631,7 +634,7 @@ export default function Send() {
|
|||||||
network
|
network
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
{i18n.t("view_transaction")}
|
{i18n.t("common.view_transaction")}
|
||||||
</ExternalLink>
|
</ExternalLink>
|
||||||
</Show>
|
</Show>
|
||||||
</Match>
|
</Match>
|
||||||
@@ -652,7 +655,7 @@ export default function Send() {
|
|||||||
setSource={setSource}
|
setSource={setSource}
|
||||||
both={!!address() && !!invoice()}
|
both={!!address() && !!invoice()}
|
||||||
/>
|
/>
|
||||||
<Card title="Destination">
|
<Card title={i18n.t("send.destination")}>
|
||||||
<VStack>
|
<VStack>
|
||||||
<DestinationShower
|
<DestinationShower
|
||||||
source={source()}
|
source={source()}
|
||||||
@@ -664,7 +667,7 @@ export default function Send() {
|
|||||||
clearAll={clearAll}
|
clearAll={clearAll}
|
||||||
/>
|
/>
|
||||||
<SmallHeader>
|
<SmallHeader>
|
||||||
{i18n.t("private_tags")}
|
{i18n.t("common.private_tags")}
|
||||||
</SmallHeader>
|
</SmallHeader>
|
||||||
<TagEditor
|
<TagEditor
|
||||||
selectedValues={selectedContacts()}
|
selectedValues={selectedContacts()}
|
||||||
|
|||||||
@@ -328,7 +328,7 @@ export default function Swap() {
|
|||||||
network
|
network
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
{i18n.t("view_transaction")}
|
{i18n.t("common.view_transaction")}
|
||||||
</ExternalLink>
|
</ExternalLink>
|
||||||
</Show>
|
</Show>
|
||||||
{/* <pre>{JSON.stringify(channelOpenResult()?.channel?.value, null, 2)}</pre> */}
|
{/* <pre>{JSON.stringify(channelOpenResult()?.channel?.value, null, 2)}</pre> */}
|
||||||
|
|||||||
@@ -108,7 +108,11 @@ function Nwc() {
|
|||||||
activityLight={profile.enabled ? "on" : "off"}
|
activityLight={profile.enabled ? "on" : "off"}
|
||||||
>
|
>
|
||||||
<VStack>
|
<VStack>
|
||||||
<KeyValue key="Relay">
|
<KeyValue
|
||||||
|
key={i18n.t(
|
||||||
|
"settings.connections.relay"
|
||||||
|
)}
|
||||||
|
>
|
||||||
<MiniStringShower
|
<MiniStringShower
|
||||||
text={profile.relay}
|
text={profile.relay}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -41,7 +41,9 @@ export default function Encrypt() {
|
|||||||
validate: (values) => {
|
validate: (values) => {
|
||||||
const errors: Record<string, string> = {};
|
const errors: Record<string, string> = {};
|
||||||
if (values.password !== values.confirmPassword) {
|
if (values.password !== values.confirmPassword) {
|
||||||
errors.confirmPassword = "Passwords do not match";
|
errors.confirmPassword = i18n.t(
|
||||||
|
"settings.encrypt.error_match"
|
||||||
|
);
|
||||||
}
|
}
|
||||||
return errors;
|
return errors;
|
||||||
}
|
}
|
||||||
@@ -73,18 +75,15 @@ export default function Encrypt() {
|
|||||||
title={i18n.t("settings.header")}
|
title={i18n.t("settings.header")}
|
||||||
/>
|
/>
|
||||||
<LargeHeader>
|
<LargeHeader>
|
||||||
Encrypt your seed words (optional)
|
{`${i18n.t("settings.encrypt.header")} ${i18n.t(
|
||||||
|
"settings.encrypt.optional"
|
||||||
|
)}`}
|
||||||
</LargeHeader>
|
</LargeHeader>
|
||||||
<VStack>
|
<VStack>
|
||||||
<NiceP>
|
<NiceP>
|
||||||
Mutiny is a "hot wallet" so it needs your seed word
|
{i18n.t("settings.encrypt.hot_wallet_warning")}
|
||||||
to operate, but you can optionally encrypt those
|
|
||||||
words with a password.
|
|
||||||
</NiceP>
|
|
||||||
<NiceP>
|
|
||||||
That way, if someone gets access to your browser,
|
|
||||||
they still won't have access to your funds.
|
|
||||||
</NiceP>
|
</NiceP>
|
||||||
|
<NiceP>{i18n.t("settings.encrypt.password_tip")}</NiceP>
|
||||||
<Form onSubmit={handleFormSubmit}>
|
<Form onSubmit={handleFormSubmit}>
|
||||||
<VStack>
|
<VStack>
|
||||||
<Field name="existingPassword">
|
<Field name="existingPassword">
|
||||||
@@ -93,9 +92,17 @@ export default function Encrypt() {
|
|||||||
{...props}
|
{...props}
|
||||||
{...field}
|
{...field}
|
||||||
type="password"
|
type="password"
|
||||||
label="Existing Password (optional)"
|
label={`${i18n.t(
|
||||||
placeholder="Existing password"
|
"settings.encrypt.existing_password"
|
||||||
caption="Leave blank if you haven't set a password yet."
|
)} ${i18n.t(
|
||||||
|
"settings.encrypt.optional"
|
||||||
|
)}`}
|
||||||
|
placeholder={i18n.t(
|
||||||
|
"settings.encrypt.existing_password"
|
||||||
|
)}
|
||||||
|
caption={i18n.t(
|
||||||
|
"settings.encrypt.existing_password_caption"
|
||||||
|
)}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</Field>
|
</Field>
|
||||||
@@ -105,9 +112,15 @@ export default function Encrypt() {
|
|||||||
{...props}
|
{...props}
|
||||||
{...field}
|
{...field}
|
||||||
type="password"
|
type="password"
|
||||||
label="Password"
|
label={i18n.t(
|
||||||
placeholder="Enter a password"
|
"settings.encrypt.new_password_label"
|
||||||
caption="This password will be used to encrypt your seed words. If you forget it, you will need to re-enter your seed words to access your funds. You did write down your seed words, right?"
|
)}
|
||||||
|
placeholder={i18n.t(
|
||||||
|
"settings.encrypt.new_password_placeholder"
|
||||||
|
)}
|
||||||
|
caption={i18n.t(
|
||||||
|
"settings.encrypt.new_password_caption"
|
||||||
|
)}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</Field>
|
</Field>
|
||||||
@@ -117,8 +130,12 @@ export default function Encrypt() {
|
|||||||
{...props}
|
{...props}
|
||||||
{...field}
|
{...field}
|
||||||
type="password"
|
type="password"
|
||||||
label="Confirm Password"
|
label={i18n.t(
|
||||||
placeholder="Enter the same password"
|
"settings.encrypt.confirm_password_label"
|
||||||
|
)}
|
||||||
|
placeholder={i18n.t(
|
||||||
|
"settings.encrypt.confirm_password_placeholder"
|
||||||
|
)}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</Field>
|
</Field>
|
||||||
@@ -129,12 +146,12 @@ export default function Encrypt() {
|
|||||||
</Show>
|
</Show>
|
||||||
<div />
|
<div />
|
||||||
<Button intent="blue" loading={loading()}>
|
<Button intent="blue" loading={loading()}>
|
||||||
Encrypt
|
{i18n.t("settings.encrypt.encrypt")}
|
||||||
</Button>
|
</Button>
|
||||||
</VStack>
|
</VStack>
|
||||||
</Form>
|
</Form>
|
||||||
<ButtonLink href="/settings" intent="green">
|
<ButtonLink href="/settings" intent="green">
|
||||||
Skip
|
{i18n.t("settings.encrypt.skip")}
|
||||||
</ButtonLink>
|
</ButtonLink>
|
||||||
</VStack>
|
</VStack>
|
||||||
</DefaultMain>
|
</DefaultMain>
|
||||||
|
|||||||
@@ -11,8 +11,10 @@ import {
|
|||||||
} from "~/components/layout";
|
} from "~/components/layout";
|
||||||
import { BackLink } from "~/components/layout/BackLink";
|
import { BackLink } from "~/components/layout/BackLink";
|
||||||
import { useMegaStore } from "~/state/megaStore";
|
import { useMegaStore } from "~/state/megaStore";
|
||||||
|
import { useI18n } from "~/i18n/context";
|
||||||
|
|
||||||
export default function LnUrlAuth() {
|
export default function LnUrlAuth() {
|
||||||
|
const i18n = useI18n();
|
||||||
const [state, _] = useMegaStore();
|
const [state, _] = useMegaStore();
|
||||||
|
|
||||||
const [value, setValue] = createSignal("");
|
const [value, setValue] = createSignal("");
|
||||||
@@ -30,8 +32,13 @@ export default function LnUrlAuth() {
|
|||||||
<MutinyWalletGuard>
|
<MutinyWalletGuard>
|
||||||
<SafeArea>
|
<SafeArea>
|
||||||
<DefaultMain>
|
<DefaultMain>
|
||||||
<BackLink href="/settings" title="Settings" />
|
<BackLink
|
||||||
<LargeHeader>LNURL Auth</LargeHeader>
|
href="/settings"
|
||||||
|
title={i18n.t("settings.header")}
|
||||||
|
/>
|
||||||
|
<LargeHeader>
|
||||||
|
{i18n.t("settings.lnurl_auth.title")}
|
||||||
|
</LargeHeader>
|
||||||
<InnerCard>
|
<InnerCard>
|
||||||
<form class="flex flex-col gap-4" onSubmit={onSubmit}>
|
<form class="flex flex-col gap-4" onSubmit={onSubmit}>
|
||||||
<TextField.Root
|
<TextField.Root
|
||||||
@@ -46,18 +53,18 @@ export default function LnUrlAuth() {
|
|||||||
class="flex flex-col gap-4"
|
class="flex flex-col gap-4"
|
||||||
>
|
>
|
||||||
<TextField.Label class="text-sm font-semibold uppercase">
|
<TextField.Label class="text-sm font-semibold uppercase">
|
||||||
LNURL Auth
|
{i18n.t("settings.lnurl_auth.title")}
|
||||||
</TextField.Label>
|
</TextField.Label>
|
||||||
<TextField.Input
|
<TextField.Input
|
||||||
class="w-full p-2 rounded-lg text-black"
|
class="w-full p-2 rounded-lg text-black"
|
||||||
placeholder="LNURL..."
|
placeholder="LNURL..."
|
||||||
/>
|
/>
|
||||||
<TextField.ErrorMessage class="text-red-500">
|
<TextField.ErrorMessage class="text-red-500">
|
||||||
Expecting something like LNURL...
|
{i18n.t("settings.lnurl_auth.expected")}
|
||||||
</TextField.ErrorMessage>
|
</TextField.ErrorMessage>
|
||||||
</TextField.Root>
|
</TextField.Root>
|
||||||
<Button layout="small" type="submit">
|
<Button layout="small" type="submit">
|
||||||
Auth
|
{i18n.t("settings.lnurl_auth.auth")}
|
||||||
</Button>
|
</Button>
|
||||||
</form>
|
</form>
|
||||||
</InnerCard>
|
</InnerCard>
|
||||||
|
|||||||
@@ -26,28 +26,34 @@ import { useMegaStore } from "~/state/megaStore";
|
|||||||
import eify from "~/utils/eify";
|
import eify from "~/utils/eify";
|
||||||
import party from "~/assets/party.gif";
|
import party from "~/assets/party.gif";
|
||||||
import { LoadingShimmer } from "~/components/BalanceBox";
|
import { LoadingShimmer } from "~/components/BalanceBox";
|
||||||
|
import { useI18n } from "~/i18n/context";
|
||||||
|
|
||||||
function Perks(props: { alreadySubbed?: boolean }) {
|
function Perks(props: { alreadySubbed?: boolean }) {
|
||||||
|
const i18n = useI18n();
|
||||||
return (
|
return (
|
||||||
<ul class="list-disc ml-8 font-light text-lg">
|
<ul class="list-disc ml-8 font-light text-lg">
|
||||||
<Show when={props.alreadySubbed}>
|
<Show when={props.alreadySubbed}>
|
||||||
<li>Smug satisfaction</li>
|
<li>{i18n.t("settings.plus.satisfaction")}</li>
|
||||||
</Show>
|
</Show>
|
||||||
<li>
|
<li>
|
||||||
Redshift <em>(coming soon)</em>
|
{i18n.t("redshift.title")}{" "}
|
||||||
|
<em>{i18n.t("common.coming_soon")}</em>
|
||||||
</li>
|
</li>
|
||||||
<li>
|
<li>
|
||||||
Gifting <em>(coming soon)</em>
|
{i18n.t("settings.plus.gifting")}{" "}
|
||||||
|
<em>{i18n.t("common.coming_soon")}</em>
|
||||||
</li>
|
</li>
|
||||||
<li>
|
<li>
|
||||||
Multi-device access <em>(coming soon)</em>
|
{i18n.t("settings.plus.multi_device")}{" "}
|
||||||
|
<em>{i18n.t("common.coming_soon")}</em>
|
||||||
</li>
|
</li>
|
||||||
<li>... and more to come</li>
|
<li>{i18n.t("settings.plus.more")}</li>
|
||||||
</ul>
|
</ul>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function PlusCTA() {
|
function PlusCTA() {
|
||||||
|
const i18n = useI18n();
|
||||||
const [state, actions] = useMegaStore();
|
const [state, actions] = useMegaStore();
|
||||||
|
|
||||||
const [subbing, setSubbing] = createSignal(false);
|
const [subbing, setSubbing] = createSignal(false);
|
||||||
@@ -73,13 +79,14 @@ function PlusCTA() {
|
|||||||
setError(undefined);
|
setError(undefined);
|
||||||
|
|
||||||
if (planDetails()?.id === undefined || planDetails()?.id === null)
|
if (planDetails()?.id === undefined || planDetails()?.id === null)
|
||||||
throw new Error("No plans found");
|
throw new Error(i18n.t("settings.plus.error_no_plan"));
|
||||||
|
|
||||||
const invoice = await state.mutiny_wallet?.subscribe_to_plan(
|
const invoice = await state.mutiny_wallet?.subscribe_to_plan(
|
||||||
planDetails().id
|
planDetails().id
|
||||||
);
|
);
|
||||||
|
|
||||||
if (!invoice?.bolt11) throw new Error("Couldn't subscribe");
|
if (!invoice?.bolt11)
|
||||||
|
throw new Error(i18n.t("settings.plus.error_failure"));
|
||||||
|
|
||||||
await state.mutiny_wallet?.pay_subscription_invoice(
|
await state.mutiny_wallet?.pay_subscription_invoice(
|
||||||
invoice?.bolt11
|
invoice?.bolt11
|
||||||
@@ -102,7 +109,9 @@ function PlusCTA() {
|
|||||||
setRestoring(true);
|
setRestoring(true);
|
||||||
await actions.checkForSubscription();
|
await actions.checkForSubscription();
|
||||||
if (!state.subscription_timestamp) {
|
if (!state.subscription_timestamp) {
|
||||||
setError(new Error("No existing subscription found"));
|
setError(
|
||||||
|
new Error(i18n.t("settings.plus.error_no_subscription"))
|
||||||
|
);
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error(e);
|
console.error(e);
|
||||||
@@ -121,19 +130,26 @@ function PlusCTA() {
|
|||||||
<Show when={planDetails()}>
|
<Show when={planDetails()}>
|
||||||
<VStack>
|
<VStack>
|
||||||
<NiceP>
|
<NiceP>
|
||||||
Join <strong class="text-white">Mutiny+</strong> for{" "}
|
{i18n.t("settings.plus.join")}{" "}
|
||||||
{Number(planDetails().amount_sat).toLocaleString()} sats a
|
<strong class="text-white">
|
||||||
month.
|
{i18n.t("settings.plus.title")}
|
||||||
|
</strong>{" "}
|
||||||
|
{i18n.t("settings.plus.sats_per_month", {
|
||||||
|
amount: Number(
|
||||||
|
planDetails().amount_sat
|
||||||
|
).toLocaleString()
|
||||||
|
})}
|
||||||
</NiceP>
|
</NiceP>
|
||||||
<Show when={error()}>
|
<Show when={error()}>
|
||||||
<InfoBox accent="red">{error()!.message}</InfoBox>
|
<InfoBox accent="red">{error()!.message}</InfoBox>
|
||||||
</Show>
|
</Show>
|
||||||
<Show when={!hasEnough()}>
|
<Show when={!hasEnough()}>
|
||||||
<TinyText>
|
<TinyText>
|
||||||
You'll need at least{" "}
|
{i18n.t("settings.plus.lightning_balance", {
|
||||||
{Number(planDetails().amount_sat).toLocaleString()} sats
|
amount: Number(
|
||||||
in your lightning balance to get started. Try before you
|
planDetails().amount_sat
|
||||||
buy!
|
).toLocaleString()
|
||||||
|
})}
|
||||||
</TinyText>
|
</TinyText>
|
||||||
</Show>
|
</Show>
|
||||||
<div class="flex gap-2">
|
<div class="flex gap-2">
|
||||||
@@ -143,7 +159,7 @@ function PlusCTA() {
|
|||||||
onClick={() => setConfirmOpen(true)}
|
onClick={() => setConfirmOpen(true)}
|
||||||
disabled={!hasEnough()}
|
disabled={!hasEnough()}
|
||||||
>
|
>
|
||||||
Join
|
{i18n.t("settings.plus.join")}
|
||||||
</Button>
|
</Button>
|
||||||
<Button
|
<Button
|
||||||
intent="green"
|
intent="green"
|
||||||
@@ -151,7 +167,7 @@ function PlusCTA() {
|
|||||||
onClick={restore}
|
onClick={restore}
|
||||||
loading={restoring()}
|
loading={restoring()}
|
||||||
>
|
>
|
||||||
Restore Subscription
|
{i18n.t("settings.plus.restore")}
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</VStack>
|
</VStack>
|
||||||
@@ -162,8 +178,11 @@ function PlusCTA() {
|
|||||||
onCancel={() => setConfirmOpen(false)}
|
onCancel={() => setConfirmOpen(false)}
|
||||||
>
|
>
|
||||||
<p>
|
<p>
|
||||||
Ready to join <strong class="text-white">Mutiny+</strong>?
|
{i18n.t("settings.plus.ready_to_join")}{" "}
|
||||||
Click confirm to pay for your first month.
|
<strong class="text-white">
|
||||||
|
{i18n.t("settings.plus.title")}
|
||||||
|
</strong>
|
||||||
|
?{i18n.t("settings.plus.click_confirm")}
|
||||||
</p>
|
</p>
|
||||||
</ConfirmDialog>
|
</ConfirmDialog>
|
||||||
</Show>
|
</Show>
|
||||||
@@ -171,25 +190,26 @@ function PlusCTA() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export default function Plus() {
|
export default function Plus() {
|
||||||
|
const i18n = useI18n();
|
||||||
const [state, _actions] = useMegaStore();
|
const [state, _actions] = useMegaStore();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<MutinyWalletGuard>
|
<MutinyWalletGuard>
|
||||||
<SafeArea>
|
<SafeArea>
|
||||||
<DefaultMain>
|
<DefaultMain>
|
||||||
<BackLink href="/settings" title="Settings" />
|
<BackLink
|
||||||
<LargeHeader>Mutiny+</LargeHeader>
|
href="/settings"
|
||||||
|
title={i18n.t("settings.header")}
|
||||||
|
/>
|
||||||
|
<LargeHeader>{i18n.t("settings.plus.title")}</LargeHeader>
|
||||||
<VStack>
|
<VStack>
|
||||||
<Switch>
|
<Switch>
|
||||||
<Match when={state.mutiny_plus}>
|
<Match when={state.mutiny_plus}>
|
||||||
<img src={party} class="w-1/2 mx-auto" />
|
<img src={party} class="w-1/2 mx-auto" />
|
||||||
<NiceP>
|
<NiceP>{i18n.t("settings.plus.thanks")}</NiceP>
|
||||||
You're part of the mutiny! Enjoy the
|
|
||||||
following perks:
|
|
||||||
</NiceP>
|
|
||||||
<Perks alreadySubbed />
|
<Perks alreadySubbed />
|
||||||
<NiceP>
|
<NiceP>
|
||||||
You'll get a renewal payment request around{" "}
|
{i18n.t("settings.plus.renewal_time")}{" "}
|
||||||
<strong class="text-white">
|
<strong class="text-white">
|
||||||
{new Date(
|
{new Date(
|
||||||
state.subscription_timestamp! * 1000
|
state.subscription_timestamp! * 1000
|
||||||
@@ -198,29 +218,32 @@ export default function Plus() {
|
|||||||
.
|
.
|
||||||
</NiceP>
|
</NiceP>
|
||||||
<NiceP>
|
<NiceP>
|
||||||
To cancel your subscription just don't pay.
|
{i18n.t("settings.plus.cancel")}{" "}
|
||||||
You can also disable the Mutiny+{" "}
|
|
||||||
<A href="/settings/connections">
|
<A href="/settings/connections">
|
||||||
Wallet Connection.
|
{i18n.t(
|
||||||
|
"settings.plus.wallet_connection"
|
||||||
|
)}
|
||||||
</A>
|
</A>
|
||||||
</NiceP>
|
</NiceP>
|
||||||
</Match>
|
</Match>
|
||||||
<Match when={!state.mutiny_plus}>
|
<Match when={!state.mutiny_plus}>
|
||||||
<NiceP>
|
<NiceP>
|
||||||
Mutiny is open source and self-hostable.{" "}
|
{i18n.t("settings.plus.open_source")}{" "}
|
||||||
<strong>
|
<strong>
|
||||||
But also you can pay for it.
|
{i18n.t("settings.plus.optional_pay")}
|
||||||
</strong>
|
</strong>
|
||||||
</NiceP>
|
</NiceP>
|
||||||
<NiceP>
|
<NiceP>
|
||||||
Paying for{" "}
|
{i18n.t("settings.plus.paying_for")}{" "}
|
||||||
<strong class="text-white">Mutiny+</strong>{" "}
|
<strong class="text-white">
|
||||||
helps support ongoing development and
|
{i18n.t("settings.plus.title")}
|
||||||
unlocks early access to new features and
|
</strong>{" "}
|
||||||
premium functionality:
|
{i18n.t("settings.plus.supports_dev")}
|
||||||
</NiceP>
|
</NiceP>
|
||||||
<Perks />
|
<Perks />
|
||||||
<FancyCard title="Subscribe">
|
<FancyCard
|
||||||
|
title={i18n.t("settings.plus.subscribe")}
|
||||||
|
>
|
||||||
<Suspense fallback={<LoadingShimmer />}>
|
<Suspense fallback={<LoadingShimmer />}>
|
||||||
<PlusCTA />
|
<PlusCTA />
|
||||||
</Suspense>
|
</Suspense>
|
||||||
|
|||||||
@@ -29,6 +29,7 @@ import { WORDS_EN } from "~/utils/words";
|
|||||||
import { InfoBox } from "~/components/InfoBox";
|
import { InfoBox } from "~/components/InfoBox";
|
||||||
import { Clipboard } from "@capacitor/clipboard";
|
import { Clipboard } from "@capacitor/clipboard";
|
||||||
import { Capacitor } from "@capacitor/core";
|
import { Capacitor } from "@capacitor/core";
|
||||||
|
import { useI18n } from "~/i18n/context";
|
||||||
|
|
||||||
type SeedWordsForm = {
|
type SeedWordsForm = {
|
||||||
words: string[];
|
words: string[];
|
||||||
@@ -78,6 +79,7 @@ export function SeedTextField(props: TextFieldProps) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function TwelveWordsEntry() {
|
function TwelveWordsEntry() {
|
||||||
|
const i18n = useI18n();
|
||||||
const [state, _actions] = useMegaStore();
|
const [state, _actions] = useMegaStore();
|
||||||
|
|
||||||
const [error, setError] = createSignal<Error>();
|
const [error, setError] = createSignal<Error>();
|
||||||
@@ -101,7 +103,9 @@ function TwelveWordsEntry() {
|
|||||||
text = value;
|
text = value;
|
||||||
} else {
|
} else {
|
||||||
if (!navigator.clipboard.readText) {
|
if (!navigator.clipboard.readText) {
|
||||||
return showToast(new Error("Clipboard not supported"));
|
return showToast(
|
||||||
|
new Error(i18n.t("settings.restore.error_clipboard"))
|
||||||
|
);
|
||||||
}
|
}
|
||||||
text = await navigator.clipboard.readText();
|
text = await navigator.clipboard.readText();
|
||||||
}
|
}
|
||||||
@@ -110,7 +114,9 @@ function TwelveWordsEntry() {
|
|||||||
const words = text.split(/[\s\n]+/);
|
const words = text.split(/[\s\n]+/);
|
||||||
|
|
||||||
if (words.length !== 12) {
|
if (words.length !== 12) {
|
||||||
return showToast(new Error("Wrong number of words"));
|
return showToast(
|
||||||
|
new Error(i18n.t("settings.restore.error_word_number"))
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
setValues(seedWordsForm, "words", words);
|
setValues(seedWordsForm, "words", words);
|
||||||
@@ -150,7 +156,7 @@ function TwelveWordsEntry() {
|
|||||||
const valid = values.words?.every(validateWord);
|
const valid = values.words?.every(validateWord);
|
||||||
|
|
||||||
if (!valid) {
|
if (!valid) {
|
||||||
setError(new Error("Invalid seed phrase"));
|
setError(new Error(i18n.t("settings.restore.error_invalid_seed")));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -180,11 +186,15 @@ function TwelveWordsEntry() {
|
|||||||
name={`words.${index()}`}
|
name={`words.${index()}`}
|
||||||
validate={[
|
validate={[
|
||||||
required(
|
required(
|
||||||
"You need to enter all 12 words"
|
i18n.t(
|
||||||
|
"settings.restore.all_twelve"
|
||||||
|
)
|
||||||
),
|
),
|
||||||
custom(
|
custom(
|
||||||
validateWord,
|
validateWord,
|
||||||
"Wrong word"
|
i18n.t(
|
||||||
|
"settings.restore.wrong_word"
|
||||||
|
)
|
||||||
)
|
)
|
||||||
]}
|
]}
|
||||||
>
|
>
|
||||||
@@ -209,7 +219,7 @@ function TwelveWordsEntry() {
|
|||||||
type="button"
|
type="button"
|
||||||
>
|
>
|
||||||
<div class="flex items-center gap-2">
|
<div class="flex items-center gap-2">
|
||||||
<span>Dangerously Paste from Clipboard</span>
|
<span>{i18n.t("settings.restore.paste")}</span>
|
||||||
<img
|
<img
|
||||||
src={pasteIcon}
|
src={pasteIcon}
|
||||||
alt="paste"
|
alt="paste"
|
||||||
@@ -224,7 +234,7 @@ function TwelveWordsEntry() {
|
|||||||
intent="red"
|
intent="red"
|
||||||
disabled={seedWordsForm.invalid || !seedWordsForm.dirty}
|
disabled={seedWordsForm.invalid || !seedWordsForm.dirty}
|
||||||
>
|
>
|
||||||
Restore
|
{i18n.t("settings.restore.title")}
|
||||||
</Button>
|
</Button>
|
||||||
</Form>
|
</Form>
|
||||||
<ConfirmDialog
|
<ConfirmDialog
|
||||||
@@ -233,29 +243,25 @@ function TwelveWordsEntry() {
|
|||||||
onCancel={() => setConfirmOpen(false)}
|
onCancel={() => setConfirmOpen(false)}
|
||||||
loading={confirmLoading()}
|
loading={confirmLoading()}
|
||||||
>
|
>
|
||||||
<p>
|
<p>{i18n.t("settings.restore.confirm_text")}</p>
|
||||||
Are you sure you want to restore to this wallet? Your
|
|
||||||
existing wallet will be deleted!
|
|
||||||
</p>
|
|
||||||
</ConfirmDialog>
|
</ConfirmDialog>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function RestorePage() {
|
export default function RestorePage() {
|
||||||
|
const i18n = useI18n();
|
||||||
return (
|
return (
|
||||||
<SafeArea>
|
<SafeArea>
|
||||||
<DefaultMain>
|
<DefaultMain>
|
||||||
<BackLink title="Settings" href="/settings" />
|
<BackLink title={i18n.t("settings.header")} href="/settings" />
|
||||||
<LargeHeader>Restore</LargeHeader>
|
<LargeHeader>{i18n.t("settings.restore.title")}</LargeHeader>
|
||||||
<VStack>
|
<VStack>
|
||||||
<NiceP>
|
<NiceP>
|
||||||
|
<p>{i18n.t("settings.restore.restore_tip")}</p>
|
||||||
<p>
|
<p>
|
||||||
You can restore an existing Mutiny Wallet from your
|
{i18n.t("settings.restore.multi_browser_warning")}
|
||||||
12 word seed phrase. This will replace your existing
|
|
||||||
wallet, so make sure you know what you're doing!
|
|
||||||
</p>
|
</p>
|
||||||
<p>Do not use on multiple browsers at the same time.</p>
|
|
||||||
</NiceP>
|
</NiceP>
|
||||||
<TwelveWordsEntry />
|
<TwelveWordsEntry />
|
||||||
</VStack>
|
</VStack>
|
||||||
|
|||||||
@@ -19,8 +19,10 @@ import eify from "~/utils/eify";
|
|||||||
import { ExternalLink } from "~/components/layout/ExternalLink";
|
import { ExternalLink } from "~/components/layout/ExternalLink";
|
||||||
import { BackLink } from "~/components/layout/BackLink";
|
import { BackLink } from "~/components/layout/BackLink";
|
||||||
import NavBar from "~/components/NavBar";
|
import NavBar from "~/components/NavBar";
|
||||||
|
import { useI18n } from "~/i18n/context";
|
||||||
|
|
||||||
export function SettingsStringsEditor() {
|
export function SettingsStringsEditor() {
|
||||||
|
const i18n = useI18n();
|
||||||
const existingSettings = getExistingSettings();
|
const existingSettings = getExistingSettings();
|
||||||
const [settingsForm, { Form, Field }] =
|
const [settingsForm, { Form, Field }] =
|
||||||
createForm<MutinyWalletSettingStrings>({
|
createForm<MutinyWalletSettingStrings>({
|
||||||
@@ -40,68 +42,66 @@ export function SettingsStringsEditor() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Card title="Servers">
|
<Card title={i18n.t("settings.servers.title")}>
|
||||||
<Form onSubmit={handleSubmit} class="flex flex-col gap-4">
|
<Form onSubmit={handleSubmit} class="flex flex-col gap-4">
|
||||||
<NiceP>
|
<NiceP>{i18n.t("settings.servers.caption")}</NiceP>
|
||||||
Don't trust us! Use your own servers to back Mutiny.
|
|
||||||
</NiceP>
|
|
||||||
<ExternalLink href="https://github.com/MutinyWallet/mutiny-web/wiki/Self-hosting">
|
<ExternalLink href="https://github.com/MutinyWallet/mutiny-web/wiki/Self-hosting">
|
||||||
Learn more about self-hosting
|
{i18n.t("settings.servers.link")}
|
||||||
</ExternalLink>
|
</ExternalLink>
|
||||||
<div />
|
<div />
|
||||||
<Field
|
<Field
|
||||||
name="proxy"
|
name="proxy"
|
||||||
validate={[url("Should be a url starting with wss://")]}
|
validate={[url(i18n.t("settings.servers.error_proxy"))]}
|
||||||
>
|
>
|
||||||
{(field, props) => (
|
{(field, props) => (
|
||||||
<TextField
|
<TextField
|
||||||
{...props}
|
{...props}
|
||||||
value={field.value}
|
value={field.value}
|
||||||
error={field.error}
|
error={field.error}
|
||||||
label="Websockets Proxy"
|
label={i18n.t("settings.servers.proxy_label")}
|
||||||
caption="How your lightning node communicates with the rest of the network."
|
caption={i18n.t("settings.servers.proxy_caption")}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</Field>
|
</Field>
|
||||||
<Field
|
<Field
|
||||||
name="esplora"
|
name="esplora"
|
||||||
validate={[url("That doesn't look like a URL")]}
|
validate={[url(i18n.t("settings.servers.error_esplora"))]}
|
||||||
>
|
>
|
||||||
{(field, props) => (
|
{(field, props) => (
|
||||||
<TextField
|
<TextField
|
||||||
{...props}
|
{...props}
|
||||||
value={field.value}
|
value={field.value}
|
||||||
error={field.error}
|
error={field.error}
|
||||||
label="Esplora"
|
label={i18n.t("settings.servers.esplora_label")}
|
||||||
caption="Block data for on-chain information."
|
caption={i18n.t("settings.servers.esplora_caption")}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</Field>
|
</Field>
|
||||||
<Field
|
<Field
|
||||||
name="rgs"
|
name="rgs"
|
||||||
validate={[url("That doesn't look like a URL")]}
|
validate={[url(i18n.t("settings.servers.error_rgs"))]}
|
||||||
>
|
>
|
||||||
{(field, props) => (
|
{(field, props) => (
|
||||||
<TextField
|
<TextField
|
||||||
{...props}
|
{...props}
|
||||||
value={field.value}
|
value={field.value}
|
||||||
error={field.error}
|
error={field.error}
|
||||||
label="RGS"
|
label={i18n.t("settings.servers.rgs_label")}
|
||||||
caption="Rapid Gossip Sync. Network data about the lightning network used for routing."
|
caption={i18n.t("settings.servers.rgs_caption")}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</Field>
|
</Field>
|
||||||
<Field
|
<Field
|
||||||
name="lsp"
|
name="lsp"
|
||||||
validate={[url("That doesn't look like a URL")]}
|
validate={[url(i18n.t("settings.servers.error_lsp"))]}
|
||||||
>
|
>
|
||||||
{(field, props) => (
|
{(field, props) => (
|
||||||
<TextField
|
<TextField
|
||||||
{...props}
|
{...props}
|
||||||
value={field.value}
|
value={field.value}
|
||||||
error={field.error}
|
error={field.error}
|
||||||
label="LSP"
|
label={i18n.t("settings.servers.lsp_label")}
|
||||||
caption="Lightning Service Provider. Automatically opens channels to you for inbound liquidity. Also wraps invoices for privacy."
|
caption={i18n.t("settings.servers.lsp_caption")}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</Field>
|
</Field>
|
||||||
@@ -111,7 +111,7 @@ export function SettingsStringsEditor() {
|
|||||||
disabled={!settingsForm.dirty}
|
disabled={!settingsForm.dirty}
|
||||||
intent="blue"
|
intent="blue"
|
||||||
>
|
>
|
||||||
Save
|
{i18n.t("settings.servers.save")}
|
||||||
</Button>
|
</Button>
|
||||||
</Form>
|
</Form>
|
||||||
</Card>
|
</Card>
|
||||||
@@ -119,12 +119,18 @@ export function SettingsStringsEditor() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export default function Servers() {
|
export default function Servers() {
|
||||||
|
const i18n = useI18n();
|
||||||
return (
|
return (
|
||||||
<MutinyWalletGuard>
|
<MutinyWalletGuard>
|
||||||
<SafeArea>
|
<SafeArea>
|
||||||
<DefaultMain>
|
<DefaultMain>
|
||||||
<BackLink href="/settings" title="Settings" />
|
<BackLink
|
||||||
<LargeHeader>Backup</LargeHeader>
|
href="/settings"
|
||||||
|
title={i18n.t("settings.header")}
|
||||||
|
/>
|
||||||
|
<LargeHeader>
|
||||||
|
{i18n.t("settings.servers.title")}
|
||||||
|
</LargeHeader>
|
||||||
<SettingsStringsEditor />
|
<SettingsStringsEditor />
|
||||||
</DefaultMain>
|
</DefaultMain>
|
||||||
<NavBar activeTab="settings" />
|
<NavBar activeTab="settings" />
|
||||||
|
|||||||
@@ -66,7 +66,7 @@ export default function Settings() {
|
|||||||
<LargeHeader>{i18n.t("settings.header")}</LargeHeader>
|
<LargeHeader>{i18n.t("settings.header")}</LargeHeader>
|
||||||
<VStack biggap>
|
<VStack biggap>
|
||||||
<SettingsLinkList
|
<SettingsLinkList
|
||||||
header={i18n.t("settings.mutiny_plus")}
|
header={i18n.t("settings.plus.title")}
|
||||||
links={[
|
links={[
|
||||||
{
|
{
|
||||||
href: "/settings/plus",
|
href: "/settings/plus",
|
||||||
|
|||||||
Reference in New Issue
Block a user