mirror of
https://github.com/aljazceru/mutiny-web.git
synced 2025-12-18 23:04:25 +01:00
add nostr wallet auth support
This commit is contained in:
@@ -169,10 +169,10 @@ test("visit each route", async ({ page }) => {
|
|||||||
);
|
);
|
||||||
checklist.set("/gift", true);
|
checklist.set("/gift", true);
|
||||||
|
|
||||||
// Visit connections with AutoZap params
|
// Visit connections nwa params
|
||||||
const autoZapParams =
|
const nwaParams =
|
||||||
"/settings/connections?return_to=https%3A%2F%2Fwww.zapplepay.com%2Fautozap%2Fnpub1xtscya34g58tk0z605fvr788k263gsu6cy9x0mhnm87echrgufzsevkk5s&name=AutoZap-jb55&budget_renewal=day&max_amount=420";
|
"/settings/connections?nwa=nostr%2Bwalletauth%3A%2F%2Fe552dec5821ef94dc1b9138a347b4b1d8dcb595e31f5c89352e50dc11255e0f4%3Frelay%3Dwss%253A%252F%252Frelay.damus.io%252F%26secret%3D0bfe616c5e126a7c%26required_commands%3Dpay_invoice%26budget%3D21%252Fday%26identity%3D32e1827635450ebb3c5a7d12c1f8e7b2b514439ac10a67eef3d9fd9c5c68e245";
|
||||||
await page.goto("http://localhost:3420" + autoZapParams);
|
await page.goto("http://localhost:3420" + nwaParams);
|
||||||
await expect(page.locator('[role="dialog"] h2 header').first()).toHaveText(
|
await expect(page.locator('[role="dialog"] h2 header').first()).toHaveText(
|
||||||
"Add Connection"
|
"Add Connection"
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,244 +0,0 @@
|
|||||||
import {
|
|
||||||
createForm,
|
|
||||||
getValue,
|
|
||||||
required,
|
|
||||||
setValue,
|
|
||||||
SubmitHandler
|
|
||||||
} from "@modular-forms/solid";
|
|
||||||
import { NwcProfile } from "@mutinywallet/mutiny-wasm";
|
|
||||||
import { For, Show } from "solid-js";
|
|
||||||
|
|
||||||
import {
|
|
||||||
AmountEditable,
|
|
||||||
AmountSats,
|
|
||||||
Button,
|
|
||||||
Checkbox,
|
|
||||||
InfoBox,
|
|
||||||
KeyValue,
|
|
||||||
TextField,
|
|
||||||
TinyText,
|
|
||||||
VStack
|
|
||||||
} from "~/components";
|
|
||||||
import { useI18n } from "~/i18n/context";
|
|
||||||
|
|
||||||
export type BudgetForm = {
|
|
||||||
connection_name: string;
|
|
||||||
auto_approve: boolean;
|
|
||||||
budget_amount: string; // modular forms doesn't like bigint
|
|
||||||
interval: "Day" | "Week" | "Month" | "Year";
|
|
||||||
};
|
|
||||||
|
|
||||||
export function NWCBudgetEditor(props: {
|
|
||||||
initialProfile?: NwcProfile;
|
|
||||||
initialName?: string;
|
|
||||||
initialAmount?: string;
|
|
||||||
initialInterval?: "Day" | "Week" | "Month" | "Year";
|
|
||||||
onSave: (value: BudgetForm) => Promise<void>;
|
|
||||||
}) {
|
|
||||||
const i18n = useI18n();
|
|
||||||
|
|
||||||
const connection_name =
|
|
||||||
props.initialProfile?.name ?? props.initialName ?? "";
|
|
||||||
|
|
||||||
// If there's an initial profile, look at that, otherwise default to false
|
|
||||||
const auto_approve =
|
|
||||||
props.initialAmount !== undefined ||
|
|
||||||
props.initialInterval !== undefined ||
|
|
||||||
(props.initialProfile?.require_approval !== undefined
|
|
||||||
? !props.initialProfile?.require_approval
|
|
||||||
: false);
|
|
||||||
|
|
||||||
// prop amount -> profile editing -> subscriptions -> 0
|
|
||||||
// (ternaries take precendence so I put it in parens)
|
|
||||||
const budget_amount =
|
|
||||||
props.initialAmount ??
|
|
||||||
props.initialProfile?.budget_amount?.toString() ??
|
|
||||||
(props.initialProfile?.index === 0 ? "21000" : "0");
|
|
||||||
|
|
||||||
// prop intervail -> profile editing -> subscriptions -> day
|
|
||||||
const interval =
|
|
||||||
props.initialInterval ??
|
|
||||||
(props.initialProfile?.budget_period
|
|
||||||
? (props.initialProfile?.budget_period as BudgetForm["interval"])
|
|
||||||
: props.initialProfile?.index === 0
|
|
||||||
? "Month"
|
|
||||||
: "Day");
|
|
||||||
|
|
||||||
const [budgetForm, { Form, Field }] = createForm<BudgetForm>({
|
|
||||||
initialValues: {
|
|
||||||
connection_name,
|
|
||||||
auto_approve,
|
|
||||||
budget_amount,
|
|
||||||
interval
|
|
||||||
},
|
|
||||||
validate: (values) => {
|
|
||||||
const errors: Record<string, string> = {};
|
|
||||||
if (values.auto_approve && values.budget_amount === "0") {
|
|
||||||
errors.budget_amount = i18n.t(
|
|
||||||
"settings.connections.error_budget_zero"
|
|
||||||
);
|
|
||||||
}
|
|
||||||
return errors;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
const handleFormSubmit: SubmitHandler<BudgetForm> = async (
|
|
||||||
f: BudgetForm
|
|
||||||
) => {
|
|
||||||
// If this throws the message will be caught by the form
|
|
||||||
await props.onSave(f);
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Form onSubmit={handleFormSubmit}>
|
|
||||||
<VStack>
|
|
||||||
<Field
|
|
||||||
name="connection_name"
|
|
||||||
validate={[
|
|
||||||
required(i18n.t("settings.connections.error_name"))
|
|
||||||
]}
|
|
||||||
>
|
|
||||||
{(field, fieldProps) => (
|
|
||||||
<TextField
|
|
||||||
disabled={props.initialProfile?.name !== undefined}
|
|
||||||
value={field.value}
|
|
||||||
{...fieldProps}
|
|
||||||
name="name"
|
|
||||||
error={field.error}
|
|
||||||
placeholder={i18n.t(
|
|
||||||
"settings.connections.new_connection_placeholder"
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
</Field>
|
|
||||||
<Field name="auto_approve" type="boolean">
|
|
||||||
{(field, _fieldProps) => (
|
|
||||||
<Checkbox
|
|
||||||
checked={field.value || false}
|
|
||||||
label="Auto Approve"
|
|
||||||
onChange={(c) =>
|
|
||||||
setValue(budgetForm, "auto_approve", c)
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
</Field>
|
|
||||||
<Show when={getValue(budgetForm, "auto_approve")}>
|
|
||||||
<VStack>
|
|
||||||
<TinyText>
|
|
||||||
{i18n.t("settings.connections.careful")}
|
|
||||||
</TinyText>
|
|
||||||
<KeyValue key={i18n.t("settings.connections.budget")}>
|
|
||||||
<Field name="budget_amount">
|
|
||||||
{(field, _fieldProps) => (
|
|
||||||
<div class="flex flex-col items-end gap-2">
|
|
||||||
<Show
|
|
||||||
when={
|
|
||||||
props.initialProfile?.tag !==
|
|
||||||
"Subscription"
|
|
||||||
}
|
|
||||||
fallback={
|
|
||||||
<AmountSats
|
|
||||||
amountSats={
|
|
||||||
Number(field.value) || 0
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
}
|
|
||||||
>
|
|
||||||
<AmountEditable
|
|
||||||
initialOpen={false}
|
|
||||||
initialAmountSats={
|
|
||||||
field.value || "0"
|
|
||||||
}
|
|
||||||
showWarnings={false}
|
|
||||||
setAmountSats={(a) => {
|
|
||||||
setValue(
|
|
||||||
budgetForm,
|
|
||||||
"budget_amount",
|
|
||||||
a.toString()
|
|
||||||
);
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</Show>
|
|
||||||
<p class="text-sm text-m-red">
|
|
||||||
{field.error}
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</Field>
|
|
||||||
</KeyValue>
|
|
||||||
<KeyValue
|
|
||||||
key={i18n.t("settings.connections.resets_every")}
|
|
||||||
>
|
|
||||||
<Field name="interval">
|
|
||||||
{(field, fieldProps) => (
|
|
||||||
<Show
|
|
||||||
when={
|
|
||||||
props.initialProfile?.tag !==
|
|
||||||
"Subscription"
|
|
||||||
}
|
|
||||||
fallback={
|
|
||||||
budgetForm.internal.initialValues
|
|
||||||
.interval
|
|
||||||
}
|
|
||||||
>
|
|
||||||
<select
|
|
||||||
{...fieldProps}
|
|
||||||
class="w-full rounded-lg bg-m-grey-750 py-2 pl-4 pr-12 text-base font-normal text-white"
|
|
||||||
>
|
|
||||||
<For
|
|
||||||
each={[
|
|
||||||
{
|
|
||||||
label: "Day",
|
|
||||||
value: "Day"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
label: "Week",
|
|
||||||
value: "Week"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
label: "Month",
|
|
||||||
value: "Month"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
label: "Year",
|
|
||||||
value: "Year"
|
|
||||||
}
|
|
||||||
]}
|
|
||||||
>
|
|
||||||
{({ label, value }) => (
|
|
||||||
<option
|
|
||||||
value={value}
|
|
||||||
selected={
|
|
||||||
field.value ===
|
|
||||||
value
|
|
||||||
}
|
|
||||||
>
|
|
||||||
{label}
|
|
||||||
</option>
|
|
||||||
)}
|
|
||||||
</For>
|
|
||||||
</select>
|
|
||||||
</Show>
|
|
||||||
)}
|
|
||||||
</Field>
|
|
||||||
</KeyValue>
|
|
||||||
</VStack>
|
|
||||||
</Show>
|
|
||||||
<Show when={budgetForm.response.message}>
|
|
||||||
<InfoBox accent="red">
|
|
||||||
{budgetForm.response.message}
|
|
||||||
</InfoBox>
|
|
||||||
</Show>
|
|
||||||
<Button
|
|
||||||
type="submit"
|
|
||||||
intent="blue"
|
|
||||||
loading={budgetForm.submitting}
|
|
||||||
>
|
|
||||||
{props.initialProfile
|
|
||||||
? i18n.t("settings.connections.save_connection")
|
|
||||||
: i18n.t("settings.connections.create_connection")}
|
|
||||||
</Button>
|
|
||||||
</VStack>
|
|
||||||
</Form>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
519
src/components/NWCEditor.tsx
Normal file
519
src/components/NWCEditor.tsx
Normal file
@@ -0,0 +1,519 @@
|
|||||||
|
import {
|
||||||
|
createForm,
|
||||||
|
getValue,
|
||||||
|
required,
|
||||||
|
setValue,
|
||||||
|
SubmitHandler
|
||||||
|
} from "@modular-forms/solid";
|
||||||
|
import { BudgetPeriod, Contact, NwcProfile } from "@mutinywallet/mutiny-wasm";
|
||||||
|
import {
|
||||||
|
createMemo,
|
||||||
|
createResource,
|
||||||
|
For,
|
||||||
|
Match,
|
||||||
|
ResourceFetcher,
|
||||||
|
Show,
|
||||||
|
Switch
|
||||||
|
} from "solid-js";
|
||||||
|
|
||||||
|
import {
|
||||||
|
AmountEditable,
|
||||||
|
AmountSats,
|
||||||
|
Avatar,
|
||||||
|
Button,
|
||||||
|
Checkbox,
|
||||||
|
InfoBox,
|
||||||
|
KeyValue,
|
||||||
|
LoadingShimmer,
|
||||||
|
TextField,
|
||||||
|
TinyText,
|
||||||
|
VStack
|
||||||
|
} from "~/components";
|
||||||
|
import { useI18n } from "~/i18n/context";
|
||||||
|
import { useMegaStore } from "~/state/megaStore";
|
||||||
|
import { fetchNostrProfile } from "~/utils";
|
||||||
|
|
||||||
|
type BudgetInterval = "Day" | "Week" | "Month" | "Year";
|
||||||
|
export type BudgetForm = {
|
||||||
|
connection_name: string;
|
||||||
|
auto_approve: boolean;
|
||||||
|
budget_amount: string; // modular forms doesn't like bigint
|
||||||
|
interval: BudgetInterval;
|
||||||
|
profileIndex?: number;
|
||||||
|
nwaString?: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
type FormMode = "createnwa" | "createnwc" | "editnwc";
|
||||||
|
type BudgetMode = "fixed" | "editable";
|
||||||
|
|
||||||
|
function parseNWA(nwaString?: string) {
|
||||||
|
if (!nwaString) return undefined;
|
||||||
|
const nwa = decodeURI(nwaString);
|
||||||
|
if (nwa) {
|
||||||
|
// Examples:
|
||||||
|
// Mainnet
|
||||||
|
// nostr+walletauth://a957bc527d4b7cea5134308412719fa675671ed38eb313adcf89b96c6982480e?relay=wss%3A%2F%2Frelay.damus.io%2F&secret=a2522b4c6d6ae729&required_commands=pay_invoice&budget=21%2Fday&identity=04c915daefee38317fa734444acee390a8269fe5810b2241e5e6dd343dfbecc9
|
||||||
|
// Signet
|
||||||
|
// nostr+walletauth://e9a09c45e3d412d694796041e45cb0ab8b92edbceec459ae76376b98111c9a3c?relay=wss%3A%2F%2Frelay.damus.io%2F&secret=5f894e2db96e0c63&required_commands=pay_invoice&budget=21%2Fday&identity=024f93e1890e9e470fb729ea24426766508c0e0c5618b5b475f2d027d0814d09
|
||||||
|
const url = new URL(nwa);
|
||||||
|
return {
|
||||||
|
budget: url.searchParams.get("budget"),
|
||||||
|
identity: url.searchParams.get("identity"),
|
||||||
|
required_commands: url.searchParams.get("required_commands")
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// nwa is "day" but waila parses it as "daily"
|
||||||
|
function mapNwaInterval(interval: string): BudgetInterval | undefined {
|
||||||
|
switch (interval) {
|
||||||
|
case "day":
|
||||||
|
case "daily":
|
||||||
|
return "Day";
|
||||||
|
case "week":
|
||||||
|
case "weekly":
|
||||||
|
return "Week";
|
||||||
|
case "month":
|
||||||
|
case "monthly":
|
||||||
|
return "Month";
|
||||||
|
case "year":
|
||||||
|
case "yearly":
|
||||||
|
return "Year";
|
||||||
|
default:
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function mapIntervalToBudgetPeriod(
|
||||||
|
interval: "Day" | "Week" | "Month" | "Year"
|
||||||
|
): BudgetPeriod {
|
||||||
|
switch (interval) {
|
||||||
|
case "Day":
|
||||||
|
return 0;
|
||||||
|
case "Week":
|
||||||
|
return 1;
|
||||||
|
case "Month":
|
||||||
|
return 2;
|
||||||
|
case "Year":
|
||||||
|
return 3;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function NWCEditor(props: {
|
||||||
|
initialProfileIndex?: number;
|
||||||
|
initialNWA?: string;
|
||||||
|
onSave: (indexToOpen?: number, nwcUriForCallback?: string) => Promise<void>;
|
||||||
|
}) {
|
||||||
|
const [state] = useMegaStore();
|
||||||
|
const i18n = useI18n();
|
||||||
|
|
||||||
|
const nwa = createMemo(() => parseNWA(props.initialNWA));
|
||||||
|
|
||||||
|
const formMode = createMemo(() => {
|
||||||
|
const mode: "createnwa" | "createnwc" | "editnwc" = nwa()
|
||||||
|
? "createnwa"
|
||||||
|
: props.initialProfileIndex
|
||||||
|
? "editnwc"
|
||||||
|
: "createnwc";
|
||||||
|
return mode;
|
||||||
|
});
|
||||||
|
|
||||||
|
// NWA HANDLING SECTION
|
||||||
|
|
||||||
|
// for "createNwa" we need to parse the nwa and fetch the nostr profile if applicable
|
||||||
|
const [nostrProfile] = createResource(
|
||||||
|
() => nwa()?.identity,
|
||||||
|
fetchNostrProfile
|
||||||
|
);
|
||||||
|
|
||||||
|
const name = createMemo(() => {
|
||||||
|
if (!nostrProfile.latest) return;
|
||||||
|
const parsed = JSON.parse(nostrProfile.latest.content);
|
||||||
|
const name = parsed.display_name || parsed.name;
|
||||||
|
return name;
|
||||||
|
});
|
||||||
|
|
||||||
|
const image = createMemo(() => {
|
||||||
|
if (!nostrProfile.latest) return;
|
||||||
|
const parsed = JSON.parse(nostrProfile.latest.content);
|
||||||
|
const image_url = parsed.picture;
|
||||||
|
return image_url;
|
||||||
|
});
|
||||||
|
|
||||||
|
const parsedBudget = createMemo(() => {
|
||||||
|
if (!nwa()?.budget) return;
|
||||||
|
const [amount, interval] = nwa()!.budget!.split("/");
|
||||||
|
return {
|
||||||
|
amount,
|
||||||
|
interval: mapNwaInterval(interval)
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
async function createNwa(f: BudgetForm) {
|
||||||
|
if (!f.nwaString) throw new Error("We lost the NWA string!");
|
||||||
|
try {
|
||||||
|
await state.mutiny_wallet?.approve_nostr_wallet_auth(
|
||||||
|
f.connection_name || "Nostr Wallet Auth",
|
||||||
|
// can we do better than ! here?
|
||||||
|
f.nwaString
|
||||||
|
);
|
||||||
|
} catch (e) {
|
||||||
|
console.error(e);
|
||||||
|
} finally {
|
||||||
|
props.onSave();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// END NWA HANDLING SECTION
|
||||||
|
|
||||||
|
// REGULAR NWC STUFF
|
||||||
|
// If the profile has a label we can fetch the contact for showing the profile image
|
||||||
|
|
||||||
|
const nwcProfileFetcher: ResourceFetcher<
|
||||||
|
number,
|
||||||
|
NwcProfile | undefined
|
||||||
|
> = async (index, _last) => {
|
||||||
|
console.log("fetching nwc profile", index);
|
||||||
|
if (!index) return undefined;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const profile: NwcProfile | undefined =
|
||||||
|
await state.mutiny_wallet?.get_nwc_profile(index);
|
||||||
|
console.log(profile);
|
||||||
|
return profile;
|
||||||
|
} catch (e) {
|
||||||
|
console.error(e);
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const [profile] = createResource(
|
||||||
|
props.initialProfileIndex,
|
||||||
|
nwcProfileFetcher
|
||||||
|
);
|
||||||
|
|
||||||
|
// TODO: this should get the contact so we can get the image, but not getting a contact tagged on the nwc right now
|
||||||
|
const contactFetcher: ResourceFetcher<string, Contact | undefined> = async (
|
||||||
|
label,
|
||||||
|
_last
|
||||||
|
) => {
|
||||||
|
console.log("fetching contact", label);
|
||||||
|
if (!label) return undefined;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const contact: Contact | undefined =
|
||||||
|
await state.mutiny_wallet?.get_contact(label);
|
||||||
|
return contact;
|
||||||
|
} catch (e) {
|
||||||
|
console.error(e);
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const [contact] = createResource(profile()?.label, contactFetcher);
|
||||||
|
|
||||||
|
async function saveConnection(f: BudgetForm) {
|
||||||
|
let newProfile: NwcProfile | undefined = undefined;
|
||||||
|
if (!f.profileIndex) throw new Error("No profile index!");
|
||||||
|
if (!f.auto_approve || f.budget_amount === "0") {
|
||||||
|
newProfile =
|
||||||
|
await state.mutiny_wallet?.set_nwc_profile_require_approval(
|
||||||
|
f.profileIndex
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
newProfile = await state.mutiny_wallet?.set_nwc_profile_budget(
|
||||||
|
f.profileIndex,
|
||||||
|
BigInt(f.budget_amount),
|
||||||
|
mapIntervalToBudgetPeriod(f.interval)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!newProfile) {
|
||||||
|
// This will be caught by the form
|
||||||
|
throw new Error(i18n.t("settings.connections.error_connection"));
|
||||||
|
} else {
|
||||||
|
// Remember the index so the collapser is open after creation
|
||||||
|
props.onSave(newProfile.index);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function createConnection(f: BudgetForm) {
|
||||||
|
let newProfile: NwcProfile | undefined = undefined;
|
||||||
|
|
||||||
|
if (!f.auto_approve || f.budget_amount === "0") {
|
||||||
|
newProfile = await state.mutiny_wallet?.create_nwc_profile(
|
||||||
|
f.connection_name
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
newProfile = await state.mutiny_wallet?.create_budget_nwc_profile(
|
||||||
|
f.connection_name,
|
||||||
|
BigInt(f.budget_amount),
|
||||||
|
mapIntervalToBudgetPeriod(f.interval),
|
||||||
|
undefined
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!newProfile) {
|
||||||
|
throw new Error(i18n.t("settings.connections.error_connection"));
|
||||||
|
} else {
|
||||||
|
if (newProfile.nwc_uri) {
|
||||||
|
props.onSave(newProfile.index, newProfile.nwc_uri);
|
||||||
|
} else {
|
||||||
|
props.onSave(newProfile.index);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Switch>
|
||||||
|
<Match when={formMode() === "createnwc"}>
|
||||||
|
<NWCEditorForm
|
||||||
|
initialValues={{
|
||||||
|
connection_name: "",
|
||||||
|
auto_approve: false,
|
||||||
|
budget_amount: "0",
|
||||||
|
interval: "Day"
|
||||||
|
}}
|
||||||
|
formMode={formMode()}
|
||||||
|
budgetMode="editable"
|
||||||
|
onSave={createConnection}
|
||||||
|
/>
|
||||||
|
</Match>
|
||||||
|
<Match when={formMode() === "createnwa"}>
|
||||||
|
<Show
|
||||||
|
when={nwa()?.identity ? nostrProfile()?.content : true}
|
||||||
|
fallback={<LoadingShimmer />}
|
||||||
|
>
|
||||||
|
<Avatar large image_url={image()} />
|
||||||
|
<NWCEditorForm
|
||||||
|
initialValues={{
|
||||||
|
connection_name: name(),
|
||||||
|
auto_approve: nwa()?.budget ? true : false,
|
||||||
|
budget_amount:
|
||||||
|
nwa()?.budget && parsedBudget()?.amount
|
||||||
|
? parsedBudget()!.amount
|
||||||
|
: "0",
|
||||||
|
interval: parsedBudget()?.interval ?? "Day",
|
||||||
|
nwaString: props.initialNWA
|
||||||
|
}}
|
||||||
|
formMode={formMode()}
|
||||||
|
budgetMode={nwa()?.budget ? "fixed" : "editable"}
|
||||||
|
onSave={createNwa}
|
||||||
|
/>
|
||||||
|
</Show>
|
||||||
|
</Match>
|
||||||
|
<Match when={formMode() === "editnwc"}>
|
||||||
|
{/* FIXME: not getting the contact rn */}
|
||||||
|
<Show when={profile()}>
|
||||||
|
<Show when={profile()?.label && contact()?.image_url}>
|
||||||
|
<Avatar large image_url={contact()?.image_url} />
|
||||||
|
<pre>{JSON.stringify(contact(), null, 2)}</pre>
|
||||||
|
</Show>
|
||||||
|
<NWCEditorForm
|
||||||
|
initialValues={{
|
||||||
|
connection_name: profile()?.name ?? "",
|
||||||
|
auto_approve: !profile()?.require_approval,
|
||||||
|
budget_amount:
|
||||||
|
profile()?.budget_amount?.toString() ?? "0",
|
||||||
|
interval:
|
||||||
|
(profile()
|
||||||
|
?.budget_period as BudgetForm["interval"]) ??
|
||||||
|
"Day",
|
||||||
|
profileIndex: profile()?.index
|
||||||
|
}}
|
||||||
|
formMode={formMode()}
|
||||||
|
budgetMode={
|
||||||
|
profile()?.tag === "Subscription"
|
||||||
|
? "fixed"
|
||||||
|
: "editable"
|
||||||
|
}
|
||||||
|
onSave={saveConnection}
|
||||||
|
/>
|
||||||
|
</Show>
|
||||||
|
</Match>
|
||||||
|
</Switch>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function NWCEditorForm(props: {
|
||||||
|
initialValues: BudgetForm;
|
||||||
|
formMode: FormMode;
|
||||||
|
budgetMode: BudgetMode;
|
||||||
|
onSave: (f: BudgetForm) => Promise<void>;
|
||||||
|
}) {
|
||||||
|
const i18n = useI18n();
|
||||||
|
|
||||||
|
const [budgetForm, { Form, Field }] = createForm<BudgetForm>({
|
||||||
|
initialValues: props.initialValues,
|
||||||
|
validate: (values) => {
|
||||||
|
const errors: Record<string, string> = {};
|
||||||
|
if (values.auto_approve && values.budget_amount === "0") {
|
||||||
|
errors.budget_amount = i18n.t(
|
||||||
|
"settings.connections.error_budget_zero"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return errors;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const handleFormSubmit: SubmitHandler<BudgetForm> = async (
|
||||||
|
f: BudgetForm
|
||||||
|
) => {
|
||||||
|
// If this throws the message will be caught by the form
|
||||||
|
await props.onSave({
|
||||||
|
...f,
|
||||||
|
profileIndex: props.initialValues.profileIndex,
|
||||||
|
nwaString: props.initialValues.nwaString
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Form onSubmit={handleFormSubmit}>
|
||||||
|
<VStack>
|
||||||
|
<Field
|
||||||
|
name="connection_name"
|
||||||
|
validate={[
|
||||||
|
required(i18n.t("settings.connections.error_name"))
|
||||||
|
]}
|
||||||
|
>
|
||||||
|
{(field, fieldProps) => (
|
||||||
|
<TextField
|
||||||
|
disabled={
|
||||||
|
props.initialValues?.connection_name !== ""
|
||||||
|
}
|
||||||
|
value={field.value}
|
||||||
|
{...fieldProps}
|
||||||
|
name="name"
|
||||||
|
error={field.error}
|
||||||
|
placeholder={i18n.t(
|
||||||
|
"settings.connections.new_connection_placeholder"
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</Field>
|
||||||
|
<Field name="auto_approve" type="boolean">
|
||||||
|
{(field, _fieldProps) => (
|
||||||
|
<Checkbox
|
||||||
|
checked={field.value || false}
|
||||||
|
label="Auto Approve"
|
||||||
|
onChange={(c) =>
|
||||||
|
setValue(budgetForm, "auto_approve", c)
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</Field>
|
||||||
|
<Show when={getValue(budgetForm, "auto_approve")}>
|
||||||
|
<VStack>
|
||||||
|
<TinyText>
|
||||||
|
{i18n.t("settings.connections.careful")}
|
||||||
|
</TinyText>
|
||||||
|
<KeyValue key={i18n.t("settings.connections.budget")}>
|
||||||
|
<Field name="budget_amount">
|
||||||
|
{(field, _fieldProps) => (
|
||||||
|
<div class="flex flex-col items-end gap-2">
|
||||||
|
<Show
|
||||||
|
when={
|
||||||
|
props.budgetMode === "editable"
|
||||||
|
}
|
||||||
|
fallback={
|
||||||
|
<AmountSats
|
||||||
|
amountSats={
|
||||||
|
Number(field.value) || 0
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<AmountEditable
|
||||||
|
initialOpen={false}
|
||||||
|
initialAmountSats={
|
||||||
|
field.value || "0"
|
||||||
|
}
|
||||||
|
showWarnings={false}
|
||||||
|
setAmountSats={(a) => {
|
||||||
|
setValue(
|
||||||
|
budgetForm,
|
||||||
|
"budget_amount",
|
||||||
|
a.toString()
|
||||||
|
);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</Show>
|
||||||
|
<p class="text-sm text-m-red">
|
||||||
|
{field.error}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</Field>
|
||||||
|
</KeyValue>
|
||||||
|
<KeyValue
|
||||||
|
key={i18n.t("settings.connections.resets_every")}
|
||||||
|
>
|
||||||
|
<Field name="interval">
|
||||||
|
{(field, fieldProps) => (
|
||||||
|
<Show
|
||||||
|
when={props.budgetMode === "editable"}
|
||||||
|
fallback={
|
||||||
|
budgetForm.internal.initialValues
|
||||||
|
.interval
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<select
|
||||||
|
{...fieldProps}
|
||||||
|
class="w-full rounded-lg bg-m-grey-750 py-2 pl-4 pr-12 text-base font-normal text-white"
|
||||||
|
>
|
||||||
|
<For
|
||||||
|
each={[
|
||||||
|
{
|
||||||
|
label: "Day",
|
||||||
|
value: "Day"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: "Week",
|
||||||
|
value: "Week"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: "Month",
|
||||||
|
value: "Month"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: "Year",
|
||||||
|
value: "Year"
|
||||||
|
}
|
||||||
|
]}
|
||||||
|
>
|
||||||
|
{({ label, value }) => (
|
||||||
|
<option
|
||||||
|
value={value}
|
||||||
|
selected={
|
||||||
|
field.value ===
|
||||||
|
value
|
||||||
|
}
|
||||||
|
>
|
||||||
|
{label}
|
||||||
|
</option>
|
||||||
|
)}
|
||||||
|
</For>
|
||||||
|
</select>
|
||||||
|
</Show>
|
||||||
|
)}
|
||||||
|
</Field>
|
||||||
|
</KeyValue>
|
||||||
|
</VStack>
|
||||||
|
</Show>
|
||||||
|
<Show when={budgetForm.response.message}>
|
||||||
|
<InfoBox accent="red">
|
||||||
|
{budgetForm.response.message}
|
||||||
|
</InfoBox>
|
||||||
|
</Show>
|
||||||
|
<Button
|
||||||
|
type="submit"
|
||||||
|
intent="blue"
|
||||||
|
loading={budgetForm.submitting}
|
||||||
|
>
|
||||||
|
{props.formMode === "editnwc"
|
||||||
|
? i18n.t("settings.connections.save_connection")
|
||||||
|
: i18n.t("settings.connections.create_connection")}
|
||||||
|
</Button>
|
||||||
|
</VStack>
|
||||||
|
</Form>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -14,9 +14,14 @@ import { useMegaStore } from "~/state/megaStore";
|
|||||||
import { fetchZaps, hexpubFromNpub } from "~/utils";
|
import { fetchZaps, hexpubFromNpub } from "~/utils";
|
||||||
import { timeAgo } from "~/utils/prettyPrintTime";
|
import { timeAgo } from "~/utils/prettyPrintTime";
|
||||||
|
|
||||||
function Avatar(props: { image_url?: string }) {
|
export function Avatar(props: { image_url?: string; large?: boolean }) {
|
||||||
return (
|
return (
|
||||||
<div class="flex h-[3rem] w-[3rem] flex-none items-center justify-center self-center overflow-clip rounded-full border-b border-t border-b-white/10 border-t-white/50 bg-neutral-700 text-3xl uppercase">
|
<div
|
||||||
|
class="flex h-[3rem] w-[3rem] flex-none items-center justify-center self-center overflow-clip rounded-full border-b border-t border-b-white/10 border-t-white/50 bg-neutral-700 text-3xl uppercase"
|
||||||
|
classList={{
|
||||||
|
"h-[6rem] w-[6rem]": props.large
|
||||||
|
}}
|
||||||
|
>
|
||||||
<Switch>
|
<Switch>
|
||||||
<Match when={props.image_url}>
|
<Match when={props.image_url}>
|
||||||
<img src={props.image_url} alt={"image"} />
|
<img src={props.image_url} alt={"image"} />
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ export type ParsedParams = {
|
|||||||
node_pubkey?: string;
|
node_pubkey?: string;
|
||||||
lnurl?: string;
|
lnurl?: string;
|
||||||
lightning_address?: string;
|
lightning_address?: string;
|
||||||
|
nostr_wallet_auth?: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
export function toParsedParams(
|
export function toParsedParams(
|
||||||
@@ -56,7 +57,8 @@ export function toParsedParams(
|
|||||||
memo: params.memo,
|
memo: params.memo,
|
||||||
node_pubkey: params.node_pubkey,
|
node_pubkey: params.node_pubkey,
|
||||||
lnurl: params.lnurl,
|
lnurl: params.lnurl,
|
||||||
lightning_address: params.lightning_address
|
lightning_address: params.lightning_address,
|
||||||
|
nostr_wallet_auth: params.nostr_wallet_auth
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,8 +1,9 @@
|
|||||||
import { NwcProfile, type BudgetPeriod } from "@mutinywallet/mutiny-wasm";
|
import { NwcProfile } from "@mutinywallet/mutiny-wasm";
|
||||||
import { useSearchParams } from "@solidjs/router";
|
import { A, useSearchParams } from "@solidjs/router";
|
||||||
import { createResource, createSignal, For, Show } from "solid-js";
|
import { createResource, createSignal, For, Show } from "solid-js";
|
||||||
import { QRCodeSVG } from "solid-qr-code";
|
import { QRCodeSVG } from "solid-qr-code";
|
||||||
|
|
||||||
|
import scan from "~/assets/icons/scan.svg";
|
||||||
import {
|
import {
|
||||||
AmountSats,
|
AmountSats,
|
||||||
AmountSmall,
|
AmountSmall,
|
||||||
@@ -24,44 +25,11 @@ import {
|
|||||||
TinyText,
|
TinyText,
|
||||||
VStack
|
VStack
|
||||||
} from "~/components";
|
} from "~/components";
|
||||||
import { BudgetForm, NWCBudgetEditor } from "~/components/NWCBudgetEditor";
|
import { NWCEditor } from "~/components/NWCEditor";
|
||||||
import { useI18n } from "~/i18n/context";
|
import { useI18n } from "~/i18n/context";
|
||||||
import { useMegaStore } from "~/state/megaStore";
|
import { useMegaStore } from "~/state/megaStore";
|
||||||
import { createDeepSignal, openLinkProgrammatically } from "~/utils";
|
import { createDeepSignal, openLinkProgrammatically } from "~/utils";
|
||||||
|
|
||||||
function mapIntervalToBudgetPeriod(
|
|
||||||
interval: "Day" | "Week" | "Month" | "Year"
|
|
||||||
): BudgetPeriod {
|
|
||||||
switch (interval) {
|
|
||||||
case "Day":
|
|
||||||
return 0;
|
|
||||||
case "Week":
|
|
||||||
return 1;
|
|
||||||
case "Month":
|
|
||||||
return 2;
|
|
||||||
case "Year":
|
|
||||||
return 3;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function mapBudgetRenewalToInterval(
|
|
||||||
budgetRenewal?: string
|
|
||||||
): undefined | "Day" | "Week" | "Month" | "Year" {
|
|
||||||
if (!budgetRenewal) return undefined;
|
|
||||||
switch (budgetRenewal) {
|
|
||||||
case "day":
|
|
||||||
return "Day";
|
|
||||||
case "week":
|
|
||||||
return "Week";
|
|
||||||
case "month":
|
|
||||||
return "Month";
|
|
||||||
case "year":
|
|
||||||
return "Year";
|
|
||||||
default:
|
|
||||||
return undefined;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function Spending(props: { spent: number; remaining: number }) {
|
function Spending(props: { spent: number; remaining: number }) {
|
||||||
const i18n = useI18n();
|
const i18n = useI18n();
|
||||||
return (
|
return (
|
||||||
@@ -144,7 +112,11 @@ function NwcDetails(props: {
|
|||||||
</Show>
|
</Show>
|
||||||
|
|
||||||
<Show when={!props.profile.require_approval}>
|
<Show when={!props.profile.require_approval}>
|
||||||
<TinyText>{i18n.t("settings.connections.careful")}</TinyText>
|
<Show when={props.profile.nwc_uri}>
|
||||||
|
<TinyText>
|
||||||
|
{i18n.t("settings.connections.careful")}
|
||||||
|
</TinyText>
|
||||||
|
</Show>
|
||||||
<Spending
|
<Spending
|
||||||
spent={Number(
|
spent={Number(
|
||||||
Number(props.profile.budget_amount || 0) -
|
Number(props.profile.budget_amount || 0) -
|
||||||
@@ -227,16 +199,24 @@ function Nwc() {
|
|||||||
});
|
});
|
||||||
|
|
||||||
const [searchParams, setSearchParams] = useSearchParams();
|
const [searchParams, setSearchParams] = useSearchParams();
|
||||||
const queryName = searchParams.name;
|
|
||||||
const [callbackDialogOpen, setCallbackDialogOpen] = createSignal(false);
|
const [callbackDialogOpen, setCallbackDialogOpen] = createSignal(false);
|
||||||
const [callbackUri, setCallbackUri] = createSignal<string>();
|
const [callbackUri, setCallbackUri] = createSignal<string>();
|
||||||
|
|
||||||
// Profile creation / editing
|
// Profile creation / editing
|
||||||
const [dialogOpen, setDialogOpen] = createSignal(!!queryName);
|
const [dialogOpen, setDialogOpen] = createSignal(
|
||||||
const [profileToOpen, setProfileToOpen] = createSignal<NwcProfile>();
|
!!searchParams.queryName || !!searchParams.nwa
|
||||||
|
);
|
||||||
|
|
||||||
|
function handleToggleOpen(open: boolean) {
|
||||||
|
setDialogOpen(open);
|
||||||
|
// If they close the dialog clear the search params
|
||||||
|
setSearchParams({ nwa: undefined, name: undefined });
|
||||||
|
}
|
||||||
|
|
||||||
|
const [profileToOpen, setProfileToOpen] = createSignal<number>();
|
||||||
|
|
||||||
function editProfile(profile: NwcProfile) {
|
function editProfile(profile: NwcProfile) {
|
||||||
setProfileToOpen(profile);
|
setProfileToOpen(profile.index);
|
||||||
setDialogOpen(true);
|
setDialogOpen(true);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -247,74 +227,28 @@ function Nwc() {
|
|||||||
|
|
||||||
const [newConnection, setNewConnection] = createSignal<number>();
|
const [newConnection, setNewConnection] = createSignal<number>();
|
||||||
|
|
||||||
async function createConnection(f: BudgetForm) {
|
async function handleSave(
|
||||||
let newProfile: NwcProfile | undefined = undefined;
|
indexToOpen?: number,
|
||||||
|
nwcUriForCallback?: string
|
||||||
// If the form was editing, we want to call the edit methods
|
) {
|
||||||
if (profileToOpen()) {
|
|
||||||
if (!f.auto_approve || f.budget_amount === "0") {
|
|
||||||
newProfile =
|
|
||||||
await state.mutiny_wallet?.set_nwc_profile_require_approval(
|
|
||||||
profileToOpen()!.index
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
newProfile = await state.mutiny_wallet?.set_nwc_profile_budget(
|
|
||||||
profileToOpen()!.index,
|
|
||||||
BigInt(f.budget_amount),
|
|
||||||
mapIntervalToBudgetPeriod(f.interval)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
if (!f.auto_approve || f.budget_amount === "0") {
|
|
||||||
newProfile = await state.mutiny_wallet?.create_nwc_profile(
|
|
||||||
f.connection_name
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
newProfile =
|
|
||||||
await state.mutiny_wallet?.create_budget_nwc_profile(
|
|
||||||
f.connection_name,
|
|
||||||
BigInt(f.budget_amount),
|
|
||||||
mapIntervalToBudgetPeriod(f.interval),
|
|
||||||
undefined
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!newProfile) {
|
|
||||||
// This will be caught by the form
|
|
||||||
throw new Error(i18n.t("settings.connections.error_connection"));
|
|
||||||
} else {
|
|
||||||
// Remember the index so the collapser is open after creation
|
|
||||||
setNewConnection(newProfile.index);
|
|
||||||
}
|
|
||||||
|
|
||||||
setSearchParams({ name: "" });
|
|
||||||
setDialogOpen(false);
|
setDialogOpen(false);
|
||||||
refetch();
|
refetch();
|
||||||
|
|
||||||
// If there's a "return_to" param we use that instead of the callbackUri scheme
|
if (indexToOpen) {
|
||||||
const returnUrl = searchParams.return_to;
|
setNewConnection(indexToOpen);
|
||||||
if (returnUrl && newProfile.nwc_uri) {
|
|
||||||
// add the nwc query param to the return url
|
|
||||||
const fullURI =
|
|
||||||
returnUrl +
|
|
||||||
(returnUrl.includes("?") ? "&" : "?") +
|
|
||||||
"nwc=" +
|
|
||||||
encodeURIComponent(newProfile.nwc_uri);
|
|
||||||
|
|
||||||
setCallbackUri(fullURI);
|
|
||||||
setCallbackDialogOpen(true);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const callbackUriScheme = searchParams.callbackUri;
|
const callbackUriScheme = searchParams.callbackUri;
|
||||||
if (callbackUriScheme && newProfile.nwc_uri) {
|
if (callbackUriScheme && nwcUriForCallback) {
|
||||||
const fullURI = newProfile.nwc_uri.replace(
|
const fullURI = nwcUriForCallback.replace(
|
||||||
"nostr+walletconnect://",
|
"nostr+walletconnect://",
|
||||||
`${callbackUriScheme}://`
|
`${callbackUriScheme}://`
|
||||||
);
|
);
|
||||||
setCallbackUri(fullURI);
|
setCallbackUri(fullURI);
|
||||||
setCallbackDialogOpen(true);
|
setCallbackDialogOpen(true);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
setSearchParams({ nwa: undefined, name: undefined });
|
||||||
}
|
}
|
||||||
|
|
||||||
async function openCallbackUri() {
|
async function openCallbackUri() {
|
||||||
@@ -351,25 +285,17 @@ function Nwc() {
|
|||||||
</Show>
|
</Show>
|
||||||
<SimpleDialog
|
<SimpleDialog
|
||||||
open={dialogOpen()}
|
open={dialogOpen()}
|
||||||
setOpen={setDialogOpen}
|
setOpen={handleToggleOpen}
|
||||||
title={
|
title={
|
||||||
profileToOpen()
|
profileToOpen()
|
||||||
? i18n.t("settings.connections.edit_connection")
|
? i18n.t("settings.connections.edit_connection")
|
||||||
: i18n.t("settings.connections.add_connection")
|
: i18n.t("settings.connections.add_connection")
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
<NWCBudgetEditor
|
<NWCEditor
|
||||||
initialName={queryName}
|
initialNWA={searchParams.nwa}
|
||||||
initialProfile={profileToOpen()}
|
initialProfileIndex={profileToOpen()}
|
||||||
onSave={createConnection}
|
onSave={handleSave}
|
||||||
initialAmount={
|
|
||||||
searchParams.max_amount
|
|
||||||
? searchParams.max_amount
|
|
||||||
: undefined
|
|
||||||
}
|
|
||||||
initialInterval={mapBudgetRenewalToInterval(
|
|
||||||
searchParams.budget_renewal
|
|
||||||
)}
|
|
||||||
/>
|
/>
|
||||||
</SimpleDialog>
|
</SimpleDialog>
|
||||||
<SimpleDialog
|
<SimpleDialog
|
||||||
@@ -391,10 +317,18 @@ export function Connections() {
|
|||||||
<MutinyWalletGuard>
|
<MutinyWalletGuard>
|
||||||
<SafeArea>
|
<SafeArea>
|
||||||
<DefaultMain>
|
<DefaultMain>
|
||||||
<BackLink
|
<div class="flex items-center justify-between">
|
||||||
href="/settings"
|
<BackLink
|
||||||
title={i18n.t("settings.header")}
|
href="/settings"
|
||||||
/>
|
title={i18n.t("settings.header")}
|
||||||
|
/>
|
||||||
|
<A
|
||||||
|
class="rounded-lg p-2 hover:bg-white/5 active:bg-m-blue md:hidden"
|
||||||
|
href="/scanner"
|
||||||
|
>
|
||||||
|
<img src={scan} alt="Scan" class="h-6 w-6" />
|
||||||
|
</A>{" "}
|
||||||
|
</div>
|
||||||
<LargeHeader>
|
<LargeHeader>
|
||||||
{i18n.t("settings.connections.title")}
|
{i18n.t("settings.connections.title")}
|
||||||
</LargeHeader>
|
</LargeHeader>
|
||||||
|
|||||||
@@ -367,6 +367,16 @@ export const Provider: ParentComponent = (props) => {
|
|||||||
onSuccess(result.value);
|
onSuccess(result.value);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if (result.value?.nostr_wallet_auth) {
|
||||||
|
console.log(
|
||||||
|
"nostr_wallet_auth",
|
||||||
|
result.value?.nostr_wallet_auth
|
||||||
|
);
|
||||||
|
navigate(
|
||||||
|
"/settings/connections/?nwa=" +
|
||||||
|
encodeURIComponent(result.value?.nostr_wallet_auth)
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
setBetaWarned() {
|
setBetaWarned() {
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
|
/* @refresh reload */
|
||||||
|
|
||||||
import { MutinyWallet } from "@mutinywallet/mutiny-wasm";
|
import { MutinyWallet } from "@mutinywallet/mutiny-wasm";
|
||||||
import { ResourceFetcher } from "solid-js";
|
import { ResourceFetcher } from "solid-js";
|
||||||
|
|
||||||
@@ -114,7 +116,7 @@ async function simpleZapFromEvent(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const PRIMAL_API = import.meta.env.VITE_PRIMAL;
|
export const PRIMAL_API = import.meta.env.VITE_PRIMAL;
|
||||||
|
|
||||||
async function fetchFollows(npub: string): Promise<string[]> {
|
async function fetchFollows(npub: string): Promise<string[]> {
|
||||||
let pubkey = undefined;
|
let pubkey = undefined;
|
||||||
@@ -281,3 +283,36 @@ export const fetchZaps: ResourceFetcher<
|
|||||||
throw new Error("Failed to load zaps");
|
throw new Error("Failed to load zaps");
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const fetchNostrProfile: ResourceFetcher<
|
||||||
|
string,
|
||||||
|
NostrProfile | undefined
|
||||||
|
> = async (hexpub, _info) => {
|
||||||
|
try {
|
||||||
|
if (!PRIMAL_API)
|
||||||
|
throw new Error("Missing PRIMAL_API environment variable");
|
||||||
|
|
||||||
|
const response = await fetch(PRIMAL_API, {
|
||||||
|
method: "POST",
|
||||||
|
headers: {
|
||||||
|
"Content-Type": "application/json"
|
||||||
|
},
|
||||||
|
body: JSON.stringify(["user_profile", { pubkey: hexpub }])
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error(`Failed to load profile`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const data = await response.json();
|
||||||
|
|
||||||
|
for (const object of data) {
|
||||||
|
if (object.kind === 0) {
|
||||||
|
return object as NostrProfile;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
console.error("Failed to load profile: ", e);
|
||||||
|
throw new Error("Failed to load profile");
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|||||||
Reference in New Issue
Block a user