remove waitlist

This commit is contained in:
Paul Miller
2023-07-11 16:37:56 -05:00
committed by Tony Giorgio
parent 9a5b2d3bcd
commit 20ac1da50c
9 changed files with 41 additions and 500 deletions

View File

@@ -47,7 +47,6 @@
"class-variance-authority": "^0.4.0", "class-variance-authority": "^0.4.0",
"i18next": "^22.5.1", "i18next": "^22.5.1",
"i18next-browser-languagedetector": "^7.1.0", "i18next-browser-languagedetector": "^7.1.0",
"nostr-tools": "^1.11.1",
"qr-scanner": "^1.4.2", "qr-scanner": "^1.4.2",
"solid-js": "^1.7.7", "solid-js": "^1.7.7",
"solid-qr-code": "^0.0.8", "solid-qr-code": "^0.0.8",

42
pnpm-lock.yaml generated
View File

@@ -37,9 +37,6 @@ dependencies:
i18next-browser-languagedetector: i18next-browser-languagedetector:
specifier: ^7.1.0 specifier: ^7.1.0
version: 7.1.0 version: 7.1.0
nostr-tools:
specifier: ^1.11.1
version: 1.12.1
qr-scanner: qr-scanner:
specifier: ^1.4.2 specifier: ^1.4.2
version: 1.4.2 version: 1.4.2
@@ -1880,16 +1877,6 @@ packages:
resolution: {integrity: sha512-3Yc1fUTs69MG/uZbJlLSI3JISMn2UV2rg+1D/vROUqZyh3l6iYHCs7GMp+M40ZD7yOdDbYjJcU1oTJhrc+dGKg==} resolution: {integrity: sha512-3Yc1fUTs69MG/uZbJlLSI3JISMn2UV2rg+1D/vROUqZyh3l6iYHCs7GMp+M40ZD7yOdDbYjJcU1oTJhrc+dGKg==}
hasBin: true hasBin: true
/@noble/curves@1.0.0:
resolution: {integrity: sha512-2upgEu0iLiDVDZkNLeFV2+ht0BAVgQnEmCk6JsOch9Rp8xfkMCbvbAZlA2pBHQc73dbl+vFOXfqkf4uemdn0bw==}
dependencies:
'@noble/hashes': 1.3.0
dev: false
/@noble/hashes@1.3.0:
resolution: {integrity: sha512-ilHEACi9DwqJB0pw7kv+Apvh50jiiSyR/cQ3y4W7lOR5mhvn/50FLUfsnfJz0BDZtl/RR16kXvptiv6q1msYZg==}
dev: false
/@nodelib/fs.scandir@2.1.5: /@nodelib/fs.scandir@2.1.5:
resolution: {integrity: sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==} resolution: {integrity: sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==}
engines: {node: '>= 8'} engines: {node: '>= 8'}
@@ -2050,25 +2037,6 @@ packages:
picomatch: 2.3.1 picomatch: 2.3.1
rollup: 3.26.2 rollup: 3.26.2
/@scure/base@1.1.1:
resolution: {integrity: sha512-ZxOhsSyxYwLJj3pLZCefNitxsj093tb2vq90mp2txoYeBqbcjDjqFhyM8eUjq/uFm6zJ+mUuqxlS2FkuSY1MTA==}
dev: false
/@scure/bip32@1.3.0:
resolution: {integrity: sha512-bcKpo1oj54hGholplGLpqPHRbIsnbixFtc06nwuNM5/dwSXOq/AAYoIBRsBmnZJSdfeNW5rnff7NTAz3ZCqR9Q==}
dependencies:
'@noble/curves': 1.0.0
'@noble/hashes': 1.3.0
'@scure/base': 1.1.1
dev: false
/@scure/bip39@1.2.0:
resolution: {integrity: sha512-SX/uKq52cuxm4YFXWFaVByaSHJh2w3BnokVSeUJVCv6K7WulT9u2BuNRBhuFl8vAuYnzx9bEu9WgpcNYTrYieg==}
dependencies:
'@noble/hashes': 1.3.0
'@scure/base': 1.1.1
dev: false
/@sideway/address@4.1.4: /@sideway/address@4.1.4:
resolution: {integrity: sha512-7vwq+rOHVWjyXxVlR76Agnvhy8I9rpzjosTESvmhNeXOXdZZB15Fl+TI9x1SiHZH5Jv2wTGduSxFDIaq0m3DUw==} resolution: {integrity: sha512-7vwq+rOHVWjyXxVlR76Agnvhy8I9rpzjosTESvmhNeXOXdZZB15Fl+TI9x1SiHZH5Jv2wTGduSxFDIaq0m3DUw==}
dependencies: dependencies:
@@ -4459,16 +4427,6 @@ packages:
engines: {node: '>=0.10.0'} engines: {node: '>=0.10.0'}
dev: true dev: true
/nostr-tools@1.12.1:
resolution: {integrity: sha512-ZeoV7g3jBUAlb4mKa3C+6hrc84htPkbebMShfGNgV4vAiz18e/sQukUBFL6vb/+sxZy+dBQFkRwsJIaVFs8Gfw==}
dependencies:
'@noble/curves': 1.0.0
'@noble/hashes': 1.3.0
'@scure/base': 1.1.1
'@scure/bip32': 1.3.0
'@scure/bip39': 1.2.0
dev: false
/npm-run-path@4.0.1: /npm-run-path@4.0.1:
resolution: {integrity: sha512-S48WzZW777zhNIrn7gxOlISNAqi9ZC/uQFnRdbeIHhZhCA6UqpkOT8T1G7BvfdgP4Er8gF4sUbaS0i7QvIfCWw==} resolution: {integrity: sha512-S48WzZW777zhNIrn7gxOlISNAqi9ZC/uQFnRdbeIHhZhCA6UqpkOT8T1G7BvfdgP4Er8gF4sUbaS0i7QvIfCWw==}
engines: {node: '>=8'} engines: {node: '>=8'}

View File

@@ -8,14 +8,12 @@ export function LoadingBar(props: { value: number; max: number }) {
case 0: case 0:
return "Just getting started"; return "Just getting started";
case 1: case 1:
return "Checking user status";
case 2:
return "Double checking something"; return "Double checking something";
case 3: case 2:
return "Downloading"; return "Downloading";
case 4: case 3:
return "Setup"; return "Setup";
case 5: case 4:
return "Done"; return "Done";
default: default:
return "Just getting started"; return "Just getting started";
@@ -44,16 +42,14 @@ export function LoadingIndicator() {
switch (state.load_stage) { switch (state.load_stage) {
case "fresh": case "fresh":
return 0; return 0;
case "checking_user":
return 1;
case "checking_double_init": case "checking_double_init":
return 2; return 1;
case "downloading": case "downloading":
return 3; return 2;
case "setup": case "setup":
return 4; return 3;
case "done": case "done":
return 5; return 4;
default: default:
return 0; return 0;
} }
@@ -61,7 +57,7 @@ export function LoadingIndicator() {
return ( return (
<Show when={state.load_stage !== "done"}> <Show when={state.load_stage !== "done"}>
<LoadingBar value={loadStageValue()} max={5} /> <LoadingBar value={loadStageValue()} max={4} />
</Show> </Show>
); );
} }

View File

@@ -1,78 +0,0 @@
import { Component, For, createEffect, createSignal } from "solid-js";
import { nip19 } from "nostr-tools";
import { Linkify } from "~/components/layout";
type NostrEvent = {
content: string;
created_at: number;
id?: string;
tags: string;
};
const Note: Component<{ e: NostrEvent }> = (props) => {
const linkRoot = "https://snort.social/e/";
const [noteId, setNoteId] = createSignal("");
createEffect(() => {
if (props.e.id) {
setNoteId(nip19.noteEncode(props.e.id));
}
});
return (
<div class="flex gap-4 border-b border-faint-white py-6 items-start w-full">
<img
class="bg-black rounded-xl flex-0"
src="../180.png"
width={45}
height={45}
/>
<div class="flex flex-col gap-2 flex-1">
<p class="break-words">
{/* {props.e.content} */}
<Linkify initialText={props.e.content} />
</p>
<a
class="no-underline hover:underline hover:decoration-light-text"
href={`${linkRoot}${noteId()}`}
>
<small class="text-light-text">
{new Date(props.e.created_at * 1000).toLocaleString()}
</small>
</a>
</div>
</div>
);
};
function filterReplies(event: NostrEvent) {
// If there's a "p" tag or an "e" tag we want to return false, otherwise true
for (const tag of event.tags) {
if (tag[0] === "p" || tag[0] === "e") {
return false;
}
}
return true;
}
const Notes: Component<{ notes: NostrEvent[] }> = (props) => {
return (
<ul class="flex flex-col">
<For
each={props.notes
.filter(filterReplies)
.sort((a, b) => b.created_at - a.created_at)}
>
{(item) => (
<li class="w-full">
<Note e={item as NostrEvent} />
</li>
)}
</For>
</ul>
);
};
export default Notes;

View File

@@ -1,60 +0,0 @@
import { createResource, Show } from "solid-js";
const relayUrls = [
"wss://nostr.zebedee.cloud",
"wss://relay.snort.social",
"wss://nos.lol",
"wss://nostr.fmt.wiz.biz",
"wss://relay.damus.io",
"wss://eden.nostr.land"
];
import { SimplePool } from "nostr-tools";
import { LoadingSpinner } from "~/components/layout";
import Notes from "~/components/waitlist/Notes";
import logo from "~/assets/icons/mutiny-logo.svg";
const pool = new SimplePool();
const postsFetcher = async () => {
const filter = {
authors: [
"df173277182f3155d37b330211ba1de4a81500c02d195e964f91be774ec96708"
],
since: 0,
kinds: [1]
};
const events = await pool.list(relayUrls, [filter]);
return events;
};
export function WaitlistAlreadyIn() {
const [posts] = createResource("", postsFetcher);
return (
<main class="flex flex-col gap-4 sm:gap-4 py-8 px-4 max-w-xl mx-auto items-start drop-shadow-blue-glow">
<a href="https://mutinywallet.com">
<img src={logo} class="h-10" alt="logo" />
</a>
<h1 class="text-4xl font-bold">You're on a list!</h1>
<h2 class="text-xl pr-4">
We'll message you when Mutiny Wallet is ready.
</h2>
<div class="px-4 sm:px-8 py-8 rounded-xl bg-half-black w-full">
<h2 class="text-sm font-semibold uppercase">Recent Updates</h2>
<Show
when={!posts.loading}
fallback={
<div class="h-[10rem]">
<LoadingSpinner big wide />
</div>
}
>
<Notes notes={(posts() && posts()) || []} />
</Show>
</div>
</main>
);
}

View File

@@ -1,191 +0,0 @@
import { Match, Switch, createSignal } from "solid-js";
import { Button } from "~/components/layout";
import { StyledRadioGroup } from "../layout/Radio";
import { TextField } from "../layout/TextField";
import {
SubmitHandler,
createForm,
email,
getValue,
required,
setValue
} from "@modular-forms/solid";
import { showToast } from "../Toaster";
import eify from "~/utils/eify";
import logo from "~/assets/icons/mutiny-logo.svg";
const WAITLIST_ENDPOINT =
"https://waitlist.mutiny-waitlist.workers.dev/waitlist";
const COMMUNICATION_METHODS = [
{ value: "nostr", label: "Nostr", caption: "Your freshest npub" },
{ value: "email", label: "Email", caption: "Burners welcome" }
];
type WaitlistForm = {
user_type: "nostr" | "email";
id: string;
comment?: string;
};
const initialValues: WaitlistForm = { user_type: "nostr", id: "", comment: "" };
export default function WaitlistForm() {
const [waitlistForm, { Form, Field }] = createForm<WaitlistForm>({
initialValues
});
const [loading, setLoading] = createSignal(false);
const newHandleSubmit: SubmitHandler<WaitlistForm> = async (
f: WaitlistForm
) => {
console.log(f);
// TODO: not sure why waitlistForm.submitting doesn't work for me
// https://modularforms.dev/solid/guides/handle-submission
setLoading(true);
try {
const res = await fetch(WAITLIST_ENDPOINT, {
method: "POST",
headers: {
"Content-Type": "application/json"
},
body: JSON.stringify(f)
});
if (res.status !== 200) {
throw new Error("nope");
} else {
// On success set the id in local storage and reload the page
localStorage.setItem("waitlist_id", f.id);
window.location.reload();
}
} catch (e) {
if (f.user_type === "nostr") {
const error = new Error(
"Something went wrong. Are you sure that's a valid npub?"
);
showToast(eify(error));
} else {
const error = new Error("Something went wrong. Not sure what.");
showToast(eify(error));
}
return;
} finally {
setLoading(false);
}
};
return (
<main class="flex flex-col gap-8 py-8 px-4 max-w-xl mx-auto">
<a href="https://mutinywallet.com">
<img src={logo} class="h-10" alt="logo" />
</a>
<h1 class="text-4xl font-bold">Join Waitlist</h1>
<h2 class="text-xl">
Sign up for our waitlist and we'll send a message when Mutiny
Wallet is ready for you.
</h2>
<Form onSubmit={newHandleSubmit} class="flex flex-col gap-8">
<Field name="user_type">
{(field, _props) => (
// TODO: there's probably a "real" way to do this with modular-forms
<StyledRadioGroup
value={field.value || "nostr"}
onValueChange={(newValue) =>
setValue(
waitlistForm,
"user_type",
newValue as "nostr" | "email"
)
}
choices={COMMUNICATION_METHODS}
/>
)}
</Field>
<Switch>
<Match
when={
getValue(waitlistForm, "user_type", {
shouldActive: false
}) === "nostr"
}
>
<Field
name="id"
validate={[
required("We need some way to contact you")
]}
>
{(field, props) => (
<TextField
{...props}
value={field.value}
error={field.error}
label="Nostr npub or NIP-05"
placeholder="npub..."
/>
)}
</Field>
</Match>
<Match
when={
getValue(waitlistForm, "user_type", {
shouldActive: false
}) === "email"
}
>
<Field
name="id"
validate={[
required("We need some way to contact you"),
email(
"That doesn't look like an email address to me"
)
]}
>
{(field, props) => (
<TextField
{...props}
value={field.value}
error={field.error}
type="email"
label="Email"
placeholder="email@nokycemail.com"
/>
)}
</Field>
</Match>
</Switch>
<Field name="comment">
{(field, props) => (
<TextField
multiline
{...props}
value={field.value}
error={field.error}
label="Comments"
placeholder="I want a lightning wallet that does..."
/>
)}
</Field>
<Button
loading={loading()}
disabled={
loading() ||
!waitlistForm.dirty ||
waitlistForm.submitting ||
waitlistForm.invalid
}
class="self-start"
intent="red"
type="submit"
layout="pad"
>
Submit
</Button>
</Form>
</main>
);
}

View File

@@ -32,14 +32,14 @@ export default function Root() {
<Meta name="theme-color" content="rgb(23,23,23)" /> <Meta name="theme-color" content="rgb(23,23,23)" />
<Meta <Meta
name="description" name="description"
content="Lightning wallet for the web" content="Mutiny is a self-custodial lightning wallet that runs in the browser."
/> />
<Link rel="icon" href="/favicon.ico" /> <Link rel="icon" href="/favicon.ico" />
<Meta name="twitter:card" content="summary_large_image" /> <Meta name="twitter:card" content="summary_large_image" />
<Meta name="twitter:title" content="Mutiny Wallet" /> <Meta name="twitter:title" content="Mutiny Wallet" />
<Meta <Meta
name="twitter:description" name="twitter:description"
content="Sign up for our waitlist and we'll send a message when Mutiny Wallet is ready for you." content="Mutiny is a self-custodial lightning wallet that runs in the browser."
/> />
<Meta <Meta
name="twitter:site" name="twitter:site"
@@ -53,7 +53,7 @@ export default function Root() {
<Meta property="og:title" content="Mutiny Wallet" /> <Meta property="og:title" content="Mutiny Wallet" />
<Meta <Meta
property="og:description" property="og:description"
content="Sign up for our waitlist and we'll send a message when Mutiny Wallet is ready for you." content="Mutiny is a self-custodial lightning wallet that runs in the browser."
/> />
<Meta <Meta
property="og:url" property="og:url"

View File

@@ -1,7 +1,5 @@
import App from "~/components/App"; import App from "~/components/App";
import { Switch, Match } from "solid-js"; import { Switch, Match } from "solid-js";
import { WaitlistAlreadyIn } from "~/components/waitlist/WaitlistAlreadyIn";
import WaitlistForm from "~/components/waitlist/WaitlistForm";
import { useMegaStore } from "~/state/megaStore"; import { useMegaStore } from "~/state/megaStore";
import { FullscreenLoader } from "~/components/layout"; import { FullscreenLoader } from "~/components/layout";
import SetupErrorDisplay from "~/components/SetupErrorDisplay"; import SetupErrorDisplay from "~/components/SetupErrorDisplay";
@@ -14,15 +12,9 @@ export default function Home() {
<Match when={state.setup_error}> <Match when={state.setup_error}>
<SetupErrorDisplay initialError={state.setup_error!} /> <SetupErrorDisplay initialError={state.setup_error!} />
</Match> </Match>
<Match when={state.user_status === "approved"}> <Match when={true}>
<App /> <App />
</Match> </Match>
<Match when={state.user_status === "waitlisted"}>
<WaitlistAlreadyIn />
</Match>
<Match when={state.user_status === "new_here"}>
<WaitlistForm />
</Match>
</Switch> </Switch>
); );
} }

View File

@@ -25,11 +25,8 @@ import { ParsedParams } from "~/logic/waila";
const MegaStoreContext = createContext<MegaStore>(); const MegaStoreContext = createContext<MegaStore>();
type UserStatus = undefined | "new_here" | "waitlisted" | "approved";
export type LoadStage = export type LoadStage =
| "fresh" | "fresh"
| "checking_user"
| "checking_double_init" | "checking_double_init"
| "downloading" | "downloading"
| "setup" | "setup"
@@ -37,11 +34,8 @@ export type LoadStage =
export type MegaStore = [ export type MegaStore = [
{ {
already_approved?: boolean;
waitlist_id?: string;
mutiny_wallet?: MutinyWallet; mutiny_wallet?: MutinyWallet;
deleting: boolean; deleting: boolean;
user_status: UserStatus;
scan_result?: ParsedParams; scan_result?: ParsedParams;
balance?: MutinyBalance; balance?: MutinyBalance;
is_syncing?: boolean; is_syncing?: boolean;
@@ -59,13 +53,11 @@ export type MegaStore = [
load_stage: LoadStage; load_stage: LoadStage;
}, },
{ {
fetchUserStatus(): Promise<UserStatus>;
setupMutinyWallet( setupMutinyWallet(
settings?: MutinyWalletSettingStrings, settings?: MutinyWalletSettingStrings,
password?: string password?: string
): Promise<void>; ): Promise<void>;
deleteMutinyWallet(): Promise<void>; deleteMutinyWallet(): Promise<void>;
setWaitlistId(waitlist_id: string): void;
setScanResult(scan_result: ParsedParams | undefined): void; setScanResult(scan_result: ParsedParams | undefined): void;
sync(): Promise<void>; sync(): Promise<void>;
dismissRestorePrompt(): void; dismissRestorePrompt(): void;
@@ -78,13 +70,8 @@ export type MegaStore = [
export const Provider: ParentComponent = (props) => { export const Provider: ParentComponent = (props) => {
const [state, setState] = createStore({ const [state, setState] = createStore({
already_approved:
import.meta.env.VITE_SELFHOSTED === "true" ||
localStorage.getItem("already_approved") === "true",
waitlist_id: localStorage.getItem("waitlist_id"),
mutiny_wallet: undefined as MutinyWallet | undefined, mutiny_wallet: undefined as MutinyWallet | undefined,
deleting: false, deleting: false,
user_status: undefined as UserStatus,
scan_result: undefined as ParsedParams | undefined, scan_result: undefined as ParsedParams | undefined,
price: 0, price: 0,
has_backed_up: localStorage.getItem("has_backed_up") === "true", has_backed_up: localStorage.getItem("has_backed_up") === "true",
@@ -112,47 +99,6 @@ export const Provider: ParentComponent = (props) => {
}); });
const actions = { const actions = {
async fetchUserStatus(): Promise<UserStatus> {
if (state.already_approved) {
console.log("welcome back!");
return "approved";
}
// Using a PWA
if (state.is_pwa) {
localStorage.setItem("already_approved", "true");
return "approved";
}
// Got an invite link
const urlParams = new URLSearchParams(window.location.search);
const invite = urlParams.get("invite");
if (invite === "true") {
localStorage.setItem("already_approved", "true");
return "approved";
}
if (!state.waitlist_id) {
return "new_here";
}
try {
const res = await fetch(
`https://waitlist.mutiny-waitlist.workers.dev/waitlist/${state.waitlist_id}`
);
const data = await res.json();
if (data.approval_date) {
// Remember them so we don't have to check every time
localStorage.setItem("already_approved", "true");
return "approved";
} else {
return "waitlisted";
}
} catch (e) {
return "new_here";
}
},
async checkForSubscription(justPaid?: boolean): Promise<void> { async checkForSubscription(justPaid?: boolean): Promise<void> {
try { try {
const timestamp = await state.mutiny_wallet?.check_subscribed(); const timestamp = await state.mutiny_wallet?.check_subscribed();
@@ -261,9 +207,6 @@ export const Provider: ParentComponent = (props) => {
console.error(e); console.error(e);
} }
}, },
setWaitlistId(waitlist_id: string) {
setState({ waitlist_id });
},
async sync(): Promise<void> { async sync(): Promise<void> {
try { try {
if (state.mutiny_wallet && !state.is_syncing) { if (state.mutiny_wallet && !state.is_syncing) {
@@ -327,58 +270,40 @@ export const Provider: ParentComponent = (props) => {
// Fetch status from remote on load // Fetch status from remote on load
onMount(() => { onMount(() => {
setState({ load_stage: "checking_user" }); function handleExisting() {
// eslint-disable-next-line if (state.existing_tab_detected) {
actions.fetchUserStatus().then((status) => { setState({
setState({ user_status: status }); setup_error: new Error(
"Existing tab detected, aborting setup"
)
});
} else {
console.log("running setup node manager...");
function handleExisting() { actions
if (state.existing_tab_detected) { .setupMutinyWallet()
setState({ .then(() => console.log("node manager setup done"));
setup_error: new Error(
"Existing tab detected, aborting setup"
)
});
} else {
console.log("running setup node manager...");
actions // Setup an event listener to stop the mutiny wallet when the page unloads
.setupMutinyWallet() window.onunload = async (_e) => {
.then(() => console.log("node manager setup done")); console.log("stopping mutiny_wallet");
await state.mutiny_wallet?.stop();
// Setup an event listener to stop the mutiny wallet when the page unloads console.log("mutiny_wallet stopped");
window.onunload = async (_e) => { sessionStorage.removeItem("MUTINY_WALLET_INITIALIZED");
console.log("stopping mutiny_wallet"); };
await state.mutiny_wallet?.stop();
console.log("mutiny_wallet stopped");
sessionStorage.removeItem("MUTINY_WALLET_INITIALIZED");
};
}
} }
}
function handleGoodBrowser() { function handleGoodBrowser() {
console.log("checking if any other tabs are open"); console.log("checking if any other tabs are open");
// 500ms should hopefully be enough time for any tabs to reply // 500ms should hopefully be enough time for any tabs to reply
timeout(500).then(handleExisting); timeout(500).then(handleExisting);
} }
// Only load node manager when status is approved if (!state.mutiny_wallet && !state.deleting) {
if ( console.log("checking for browser compatibility...");
state.user_status === "approved" && actions.checkBrowserCompat().then(handleGoodBrowser);
!state.mutiny_wallet && }
!state.deleting
) {
console.log("checking for browser compatibility...");
actions.checkBrowserCompat().then(handleGoodBrowser);
}
});
});
// Be reactive to changes in waitlist_id
createEffect(() => {
state.waitlist_id
? localStorage.setItem("waitlist_id", state.waitlist_id)
: localStorage.removeItem("waitlist_id");
}); });
createEffect(() => { createEffect(() => {