add nostr wallet auth support

This commit is contained in:
Paul Miller
2023-11-27 11:27:08 -06:00
parent 9f7c48975f
commit 22103e38a0
8 changed files with 627 additions and 366 deletions

View File

@@ -169,10 +169,10 @@ test("visit each route", async ({ page }) => {
);
checklist.set("/gift", true);
// Visit connections with AutoZap params
const autoZapParams =
"/settings/connections?return_to=https%3A%2F%2Fwww.zapplepay.com%2Fautozap%2Fnpub1xtscya34g58tk0z605fvr788k263gsu6cy9x0mhnm87echrgufzsevkk5s&name=AutoZap-jb55&budget_renewal=day&max_amount=420";
await page.goto("http://localhost:3420" + autoZapParams);
// Visit connections nwa params
const nwaParams =
"/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" + nwaParams);
await expect(page.locator('[role="dialog"] h2 header').first()).toHaveText(
"Add Connection"
);

View File

@@ -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>
);
}

View 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>
);
}

View File

@@ -14,9 +14,14 @@ import { useMegaStore } from "~/state/megaStore";
import { fetchZaps, hexpubFromNpub } from "~/utils";
import { timeAgo } from "~/utils/prettyPrintTime";
function Avatar(props: { image_url?: string }) {
export function Avatar(props: { image_url?: string; large?: boolean }) {
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>
<Match when={props.image_url}>
<img src={props.image_url} alt={"image"} />

View File

@@ -15,6 +15,7 @@ export type ParsedParams = {
node_pubkey?: string;
lnurl?: string;
lightning_address?: string;
nostr_wallet_auth?: string;
};
export function toParsedParams(
@@ -56,7 +57,8 @@ export function toParsedParams(
memo: params.memo,
node_pubkey: params.node_pubkey,
lnurl: params.lnurl,
lightning_address: params.lightning_address
lightning_address: params.lightning_address,
nostr_wallet_auth: params.nostr_wallet_auth
}
};
}

View File

@@ -1,8 +1,9 @@
import { NwcProfile, type BudgetPeriod } from "@mutinywallet/mutiny-wasm";
import { useSearchParams } from "@solidjs/router";
import { NwcProfile } from "@mutinywallet/mutiny-wasm";
import { A, useSearchParams } from "@solidjs/router";
import { createResource, createSignal, For, Show } from "solid-js";
import { QRCodeSVG } from "solid-qr-code";
import scan from "~/assets/icons/scan.svg";
import {
AmountSats,
AmountSmall,
@@ -24,44 +25,11 @@ import {
TinyText,
VStack
} from "~/components";
import { BudgetForm, NWCBudgetEditor } from "~/components/NWCBudgetEditor";
import { NWCEditor } from "~/components/NWCEditor";
import { useI18n } from "~/i18n/context";
import { useMegaStore } from "~/state/megaStore";
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 }) {
const i18n = useI18n();
return (
@@ -144,7 +112,11 @@ function NwcDetails(props: {
</Show>
<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
spent={Number(
Number(props.profile.budget_amount || 0) -
@@ -227,16 +199,24 @@ function Nwc() {
});
const [searchParams, setSearchParams] = useSearchParams();
const queryName = searchParams.name;
const [callbackDialogOpen, setCallbackDialogOpen] = createSignal(false);
const [callbackUri, setCallbackUri] = createSignal<string>();
// Profile creation / editing
const [dialogOpen, setDialogOpen] = createSignal(!!queryName);
const [profileToOpen, setProfileToOpen] = createSignal<NwcProfile>();
const [dialogOpen, setDialogOpen] = createSignal(
!!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) {
setProfileToOpen(profile);
setProfileToOpen(profile.index);
setDialogOpen(true);
}
@@ -247,74 +227,28 @@ function Nwc() {
const [newConnection, setNewConnection] = createSignal<number>();
async function createConnection(f: BudgetForm) {
let newProfile: NwcProfile | undefined = undefined;
// 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: "" });
async function handleSave(
indexToOpen?: number,
nwcUriForCallback?: string
) {
setDialogOpen(false);
refetch();
// If there's a "return_to" param we use that instead of the callbackUri scheme
const returnUrl = searchParams.return_to;
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);
if (indexToOpen) {
setNewConnection(indexToOpen);
}
const callbackUriScheme = searchParams.callbackUri;
if (callbackUriScheme && newProfile.nwc_uri) {
const fullURI = newProfile.nwc_uri.replace(
if (callbackUriScheme && nwcUriForCallback) {
const fullURI = nwcUriForCallback.replace(
"nostr+walletconnect://",
`${callbackUriScheme}://`
);
setCallbackUri(fullURI);
setCallbackDialogOpen(true);
}
setSearchParams({ nwa: undefined, name: undefined });
}
async function openCallbackUri() {
@@ -351,25 +285,17 @@ function Nwc() {
</Show>
<SimpleDialog
open={dialogOpen()}
setOpen={setDialogOpen}
setOpen={handleToggleOpen}
title={
profileToOpen()
? i18n.t("settings.connections.edit_connection")
: i18n.t("settings.connections.add_connection")
}
>
<NWCBudgetEditor
initialName={queryName}
initialProfile={profileToOpen()}
onSave={createConnection}
initialAmount={
searchParams.max_amount
? searchParams.max_amount
: undefined
}
initialInterval={mapBudgetRenewalToInterval(
searchParams.budget_renewal
)}
<NWCEditor
initialNWA={searchParams.nwa}
initialProfileIndex={profileToOpen()}
onSave={handleSave}
/>
</SimpleDialog>
<SimpleDialog
@@ -391,10 +317,18 @@ export function Connections() {
<MutinyWalletGuard>
<SafeArea>
<DefaultMain>
<div class="flex items-center justify-between">
<BackLink
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>
{i18n.t("settings.connections.title")}
</LargeHeader>

View File

@@ -367,6 +367,16 @@ export const Provider: ParentComponent = (props) => {
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() {

View File

@@ -1,3 +1,5 @@
/* @refresh reload */
import { MutinyWallet } from "@mutinywallet/mutiny-wasm";
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[]> {
let pubkey = undefined;
@@ -281,3 +283,36 @@ export const fetchZaps: ResourceFetcher<
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");
}
};