This commit is contained in:
Paul Miller
2023-07-08 17:34:08 -05:00
parent 3b6975a0d9
commit c33e542932
12 changed files with 433 additions and 110 deletions

Binary file not shown.

After

Width:  |  Height:  |  Size: 893 B

BIN
src/assets/party.gif Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 269 KiB

View File

@@ -1,10 +1,19 @@
import { NiceP } from "./layout";
import { For, Match, Show, Switch, createEffect, createSignal } from "solid-js";
import {
For,
Match,
Show,
Suspense,
Switch,
createEffect,
createSignal
} from "solid-js";
import { useMegaStore } from "~/state/megaStore";
import { useI18n } from "~/i18n/context";
import { Contact } from "@mutinywallet/mutiny-wasm";
import { ActivityItem, HackActivityType } from "./ActivityItem";
import { DetailsIdModal } from "./DetailsModal";
import { LoadingShimmer } from "./BalanceBox";
export const THREE_COLUMNS =
"grid grid-cols-[auto,1fr,auto] gap-4 py-2 px-2 border-b border-neutral-800 last:border-b-0";
@@ -105,7 +114,7 @@ export function CombinedActivity(props: { limit?: number }) {
});
return (
<>
<Suspense fallback={<LoadingShimmer />}>
<Show when={detailsId() && detailsKind()}>
<DetailsIdModal
open={detailsOpen()}
@@ -117,7 +126,9 @@ export function CombinedActivity(props: { limit?: number }) {
<Switch>
<Match when={state.activity.length === 0}>
<div class="w-full text-center pb-4">
<NiceP>{i18n.t("receive_some_sats_to_get_started")}</NiceP>
<NiceP>
{i18n.t("receive_some_sats_to_get_started")}
</NiceP>
</div>
</Match>
<Match
@@ -143,6 +154,6 @@ export function CombinedActivity(props: { limit?: number }) {
</For>
</Match>
</Switch>
</>
</Suspense>
);
}

View File

@@ -6,11 +6,12 @@ import { A } from "solid-start";
import { OnboardWarning } from "~/components/OnboardWarning";
import { CombinedActivity } from "./Activity";
import { useMegaStore } from "~/state/megaStore";
import { Show } from "solid-js";
import { Match, Show, Switch, createMemo } from "solid-js";
import { ExternalLink } from "./layout/ExternalLink";
import { BetaWarningModal } from "~/components/BetaWarningModal";
import settings from "~/assets/icons/settings.svg";
import pixelLogo from "~/assets/mutiny-pixel-logo.png";
import plusLogo from "~/assets/mutiny-plus-logo.png";
import { PendingNwc } from "./PendingNwc";
import { useI18n } from "~/i18n/context";
@@ -23,12 +24,24 @@ export default function App() {
<DefaultMain>
<header class="w-full flex justify-between items-center mt-4 mb-2">
<div class="flex items-center gap-2">
<Switch>
<Match when={state.mutiny_plus}>
<img
id="mutiny-logo"
src={plusLogo}
class="h-[25px] w-[86px]"
alt="Mutiny Plus logo"
/>
</Match>
<Match when={true}>
<img
id="mutiny-logo"
src={pixelLogo}
class="h-[25px] w-[75px]"
alt="Mutiny logo"
/>
</Match>
</Switch>
<Show
when={
!state.wallet_loading &&

View File

@@ -89,12 +89,11 @@ export function ImportExport(props: { emergency?: boolean }) {
import it into a new browser. It usually works!
</NiceP>
<NiceP>
<strong class="font-semibold">Important caveats:</strong>{" "}
after exporting don't do any operations in the original
browser. If you do, you'll need to export again. After a
successful import, a best practice is to clear the state of
the original browser just to make sure you don't create
conflicts.
<strong>Important caveats:</strong> after exporting don't do
any operations in the original browser. If you do, you'll
need to export again. After a successful import, a best
practice is to clear the state of the original browser just
to make sure you don't create conflicts.
</NiceP>
<div />
<VStack>

View File

@@ -6,7 +6,6 @@ import {
For,
Match,
Show,
Suspense,
Switch,
createEffect,
createResource,
@@ -53,9 +52,6 @@ export function PendingNwc() {
});
}
}
console.log(pendingItems);
return pendingItems;
});
@@ -98,7 +94,6 @@ export function PendingNwc() {
});
return (
<Suspense>
<Show when={pendingRequests() && pendingRequests()!.length > 0}>
<Card title="Pending Requests">
<div class="p-1" />
@@ -134,9 +129,7 @@ export function PendingNwc() {
<div class="flex gap-2 w-[5rem]">
<Switch>
<Match
when={
paying() !== pendingItem.id
}
when={paying() !== pendingItem.id}
>
<button
onClick={() =>
@@ -162,9 +155,7 @@ export function PendingNwc() {
</button>
</Match>
<Match
when={
paying() === pendingItem.id
}
when={paying() === pendingItem.id}
>
<LoadingSpinner wide />
</Match>
@@ -182,6 +173,5 @@ export function PendingNwc() {
</A>
</Card>
</Show>
</Suspense>
);
}

View File

@@ -63,3 +63,7 @@ select {
background-size: 20px 20px;
background-repeat: no-repeat;
}
strong {
@apply font-semibold text-m-red;
}

View File

@@ -30,8 +30,6 @@ function Nwc() {
const profiles: NwcProfile[] =
await state.mutiny_wallet?.get_nwc_profiles();
console.log("profiles:", profiles);
return profiles;
} catch (e) {
console.error(e);

View File

@@ -0,0 +1,236 @@
import {
Match,
Show,
Suspense,
Switch,
createResource,
createSignal
} from "solid-js";
import { A } from "solid-start";
import { ConfirmDialog } from "~/components/Dialog";
import { InfoBox } from "~/components/InfoBox";
import NavBar from "~/components/NavBar";
import {
Button,
DefaultMain,
FancyCard,
LargeHeader,
MutinyWalletGuard,
NiceP,
SafeArea,
TinyText,
VStack
} from "~/components/layout";
import { BackLink } from "~/components/layout/BackLink";
import { useMegaStore } from "~/state/megaStore";
import eify from "~/utils/eify";
import party from "~/assets/party.gif";
import { LoadingShimmer } from "~/components/BalanceBox";
function Perks(props: { alreadySubbed?: boolean }) {
return (
<ul class="list-disc ml-8 font-light text-lg">
<Show when={props.alreadySubbed}>
<li>Smug satisfaction</li>
</Show>
<li>
Redshift <em>(coming soon)</em>
</li>
<li>
Gifting <em>(coming soon)</em>
</li>
<li>
Multi-device access <em>(coming soon)</em>
</li>
<li>... and more to come</li>
</ul>
);
}
function PlusCTA() {
const [state, actions] = useMegaStore();
const [subbing, setSubbing] = createSignal(false);
const [confirmOpen, setConfirmOpen] = createSignal(false);
const [restoring, setRestoring] = createSignal(false);
const [error, setError] = createSignal<Error>();
const [planDetails] = createResource(async () => {
try {
const plans = await state.mutiny_wallet?.get_subscription_plans();
console.log("plans:", plans);
if (!plans) return undefined;
return plans[0];
} catch (e) {
console.error(e);
}
});
async function handleConfirm() {
try {
setSubbing(true);
setError(undefined);
if (planDetails()?.id === undefined || planDetails()?.id === null)
throw new Error("No plans found");
const invoice = await state.mutiny_wallet?.subscribe_to_plan(
planDetails().id
);
if (!invoice?.bolt11) throw new Error("Couldn't subscribe");
await state.mutiny_wallet?.pay_subscription_invoice(
invoice?.bolt11
);
// "true" flag gives this a fallback to set a timestamp in case the subscription server is down
await actions.checkForSubscription(true);
} catch (e) {
console.error(e);
setError(eify(e));
} finally {
setConfirmOpen(false);
setSubbing(false);
}
}
async function restore() {
try {
setError(undefined);
setRestoring(true);
await actions.checkForSubscription();
if (!state.subscription_timestamp) {
setError(new Error("No existing subscription found"));
}
} catch (e) {
console.error(e);
setError(eify(e));
} finally {
setRestoring(false);
}
}
const hasEnough = () => {
if (!planDetails()) return false;
return (state.balance?.lightning || 0n) > planDetails().amount_sat;
};
return (
<Show when={planDetails()}>
<VStack>
<NiceP>
Join <strong class="text-white">Mutiny+</strong> for{" "}
{Number(planDetails().amount_sat).toLocaleString()} sats a
month.
</NiceP>
<Show when={error()}>
<InfoBox accent="red">{error()!.message}</InfoBox>
</Show>
<Show when={!hasEnough()}>
<TinyText>
You'll need at least{" "}
{Number(planDetails().amount_sat).toLocaleString()} sats
in your lightning balance to get started. Try before you
buy!
</TinyText>
</Show>
<div class="flex gap-2">
<Button
intent="red"
layout="flex"
onClick={() => setConfirmOpen(true)}
disabled={!hasEnough()}
>
Join
</Button>
<Button
intent="green"
layout="flex"
onClick={restore}
loading={restoring()}
>
Restore Subscription
</Button>
</div>
</VStack>
<ConfirmDialog
loading={subbing()}
open={confirmOpen()}
onConfirm={handleConfirm}
onCancel={() => setConfirmOpen(false)}
>
<p>
Ready to join <strong class="text-white">Mutiny+</strong>?
Click confirm to pay for your first month.
</p>
</ConfirmDialog>
</Show>
);
}
export default function Plus() {
const [state, _actions] = useMegaStore();
return (
<MutinyWalletGuard>
<SafeArea>
<DefaultMain>
<BackLink href="/settings" title="Settings" />
<LargeHeader>Mutiny+</LargeHeader>
<VStack>
<Switch>
<Match when={state.mutiny_plus}>
<img src={party} class="w-1/2 mx-auto" />
<NiceP>
You're part of the mutiny! Enjoy the
following perks:
</NiceP>
<Perks alreadySubbed />
<NiceP>
You'll get a renewal payment request around{" "}
<strong class="text-white">
{new Date(
state.subscription_timestamp! * 1000
).toLocaleDateString()}
</strong>
.
</NiceP>
<NiceP>
To cancel your subscription just don't pay.
You can also disable the Mutiny+{" "}
<A href="/settings/connections">
Wallet Connection.
</A>
</NiceP>
</Match>
<Match when={!state.mutiny_plus}>
<NiceP>
Mutiny is open source and self-hostable.{" "}
<strong>
But also you can pay for it.
</strong>
</NiceP>
<NiceP>
Paying for{" "}
<strong class="text-white">Mutiny+</strong>{" "}
helps support ongoing development and
unlocks early access to new features and
premium functionality:
</NiceP>
<Perks />
<FancyCard title="Subscribe">
<Suspense fallback={<LoadingShimmer />}>
<PlusCTA />
</Suspense>
</FancyCard>
</Match>
</Switch>
</VStack>
</DefaultMain>
<NavBar activeTab="settings" />
</SafeArea>
</MutinyWalletGuard>
);
}

View File

@@ -58,6 +58,15 @@ export default function Settings() {
<BackLink />
<LargeHeader>Settings</LargeHeader>
<VStack biggap>
<SettingsLinkList
header="Mutiny+"
links={[
{
href: "/settings/plus",
text: "Learn how to support Mutiny"
}
]}
/>
<SettingsLinkList
header="General"
links={[

View File

@@ -24,7 +24,7 @@ import { ActivityItem } from "~/components/Activity";
const MegaStoreContext = createContext<MegaStore>();
type UserStatus = undefined | "new_here" | "waitlisted" | "approved" | "paid";
type UserStatus = undefined | "new_here" | "waitlisted" | "approved";
export type MegaStore = [
{
@@ -45,6 +45,8 @@ export type MegaStore = [
setup_error?: Error;
is_pwa: boolean;
existing_tab_detected: boolean;
subscription_timestamp?: number;
readonly mutiny_plus: boolean;
},
{
fetchUserStatus(): Promise<UserStatus>;
@@ -58,6 +60,7 @@ export type MegaStore = [
listTags(): Promise<MutinyTagItem[]>;
syncActivity(): Promise<void>;
checkBrowserCompat(): Promise<boolean>;
checkForSubscription(justPaid?: boolean): Promise<void>;
}
];
@@ -82,7 +85,17 @@ export const Provider: ParentComponent = (props) => {
activity: [] as ActivityItem[],
setup_error: undefined as Error | undefined,
is_pwa: window.matchMedia("(display-mode: standalone)").matches,
existing_tab_detected: false
existing_tab_detected: false,
subscription_timestamp: undefined as number | undefined,
get mutiny_plus(): boolean {
// No subscription
if (!state.subscription_timestamp) return false;
// Expired
if (state.subscription_timestamp < Math.ceil(Date.now() / 1000))
return false;
else return true;
}
});
const actions = {
@@ -127,6 +140,26 @@ export const Provider: ParentComponent = (props) => {
return "new_here";
}
},
async checkForSubscription(justPaid?: boolean): Promise<void> {
try {
const timestamp = await state.mutiny_wallet?.check_subscribed();
console.log("timestamp:", timestamp);
if (timestamp) {
localStorage.setItem(
"subscription_timestamp",
timestamp?.toString()
);
setState({ subscription_timestamp: Number(timestamp) });
}
} catch (e) {
if (justPaid) {
// we make a fake timestamp for 24 hours from now, in case the server is down
const timestamp = Math.ceil(Date.now() / 1000) + 86400;
setState({ subscription_timestamp: timestamp });
}
console.error(e);
}
},
async setupMutinyWallet(
settings?: MutinyWalletSettingStrings
): Promise<void> {
@@ -136,12 +169,43 @@ export const Provider: ParentComponent = (props) => {
throw state.setup_error;
}
setState({ wallet_loading: true });
// This is where all the real setup happens
const mutinyWallet = await setupMutinyWallet(settings);
// Get balance optimistically
const balance = await mutinyWallet.get_balance();
// Subscription stuff. Skip if it's not already in localstorage
let subscription_timestamp = undefined;
const stored_subscription_timestamp = localStorage.getItem(
"subscription_timestamp"
);
// If we have a stored timestamp, check if it's still valid
if (stored_subscription_timestamp) {
try {
const timestamp =
await mutinyWallet?.check_subscribed();
// Check that timestamp is a number
if (!timestamp || isNaN(Number(timestamp))) {
throw new Error("Timestamp is not a number");
}
subscription_timestamp = Number(timestamp);
localStorage.setItem(
"subscription_timestamp",
timestamp.toString()
);
} catch (e) {
console.error(e);
}
}
setState({
mutiny_wallet: mutinyWallet,
wallet_loading: false,
subscription_timestamp: subscription_timestamp,
balance
});
} catch (e) {

View File

@@ -14,7 +14,6 @@ export async function generateGradient(str: string) {
}
export async function gradientsPerContact(contacts: Contact[]) {
console.log(contacts);
const gradients = new Map();
for (const contact of contacts) {
const gradient = await generateGradient(contact.name);