mirror of
https://github.com/aljazceru/mutiny-web.git
synced 2025-12-19 07:14:22 +01:00
mutiny+
This commit is contained in:
BIN
src/assets/mutiny-plus-logo.png
Normal file
BIN
src/assets/mutiny-plus-logo.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 893 B |
BIN
src/assets/party.gif
Normal file
BIN
src/assets/party.gif
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 269 KiB |
@@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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">
|
||||
<img
|
||||
id="mutiny-logo"
|
||||
src={pixelLogo}
|
||||
class="h-[25px] w-[75px]"
|
||||
alt="Mutiny logo"
|
||||
/>
|
||||
<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 &&
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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,90 +94,84 @@ export function PendingNwc() {
|
||||
});
|
||||
|
||||
return (
|
||||
<Suspense>
|
||||
<Show when={pendingRequests() && pendingRequests()!.length > 0}>
|
||||
<Card title="Pending Requests">
|
||||
<div class="p-1" />
|
||||
<VStack>
|
||||
<Show when={error()}>
|
||||
<InfoBox accent="red">{error()?.message}</InfoBox>
|
||||
</Show>
|
||||
<For each={pendingRequests()}>
|
||||
{(pendingItem) => (
|
||||
<div class="grid grid-cols-[auto_minmax(0,_1fr)_minmax(0,_max-content)_auto] items-center pb-4 gap-4 border-b border-neutral-800 last:border-b-0">
|
||||
<img
|
||||
class="w-[1rem]"
|
||||
src={bolt}
|
||||
alt="onchain"
|
||||
/>
|
||||
<div class="flex flex-col">
|
||||
<span class="text-base font-semibold truncate">
|
||||
{pendingItem.name_of_connection}
|
||||
</span>
|
||||
<time class="text-sm text-neutral-500">
|
||||
Expires {timeAgo(pendingItem.date)}
|
||||
</time>
|
||||
</div>
|
||||
<div>
|
||||
<ActivityAmount
|
||||
amount={
|
||||
pendingItem.amount_sats?.toString() ||
|
||||
"0"
|
||||
}
|
||||
price={state.price}
|
||||
/>
|
||||
</div>
|
||||
<div class="flex gap-2 w-[5rem]">
|
||||
<Switch>
|
||||
<Match
|
||||
when={
|
||||
paying() !== pendingItem.id
|
||||
}
|
||||
>
|
||||
<button
|
||||
onClick={() =>
|
||||
payItem(pendingItem)
|
||||
}
|
||||
>
|
||||
<img
|
||||
class="h-[2.5rem] w-[2.5rem]"
|
||||
src={greenCheck}
|
||||
alt="Approve"
|
||||
/>
|
||||
</button>
|
||||
<button
|
||||
onClick={() =>
|
||||
rejectItem(pendingItem)
|
||||
}
|
||||
>
|
||||
<img
|
||||
class="h-[2rem] w-[2rem]"
|
||||
src={redClose}
|
||||
alt="Reject"
|
||||
/>
|
||||
</button>
|
||||
</Match>
|
||||
<Match
|
||||
when={
|
||||
paying() === pendingItem.id
|
||||
}
|
||||
>
|
||||
<LoadingSpinner wide />
|
||||
</Match>
|
||||
</Switch>
|
||||
</div>
|
||||
<Show when={pendingRequests() && pendingRequests()!.length > 0}>
|
||||
<Card title="Pending Requests">
|
||||
<div class="p-1" />
|
||||
<VStack>
|
||||
<Show when={error()}>
|
||||
<InfoBox accent="red">{error()?.message}</InfoBox>
|
||||
</Show>
|
||||
<For each={pendingRequests()}>
|
||||
{(pendingItem) => (
|
||||
<div class="grid grid-cols-[auto_minmax(0,_1fr)_minmax(0,_max-content)_auto] items-center pb-4 gap-4 border-b border-neutral-800 last:border-b-0">
|
||||
<img
|
||||
class="w-[1rem]"
|
||||
src={bolt}
|
||||
alt="onchain"
|
||||
/>
|
||||
<div class="flex flex-col">
|
||||
<span class="text-base font-semibold truncate">
|
||||
{pendingItem.name_of_connection}
|
||||
</span>
|
||||
<time class="text-sm text-neutral-500">
|
||||
Expires {timeAgo(pendingItem.date)}
|
||||
</time>
|
||||
</div>
|
||||
)}
|
||||
</For>
|
||||
</VStack>
|
||||
<A
|
||||
href="/settings/connections"
|
||||
class="text-m-red active:text-m-red/80 font-semibold no-underline self-center"
|
||||
>
|
||||
Configure
|
||||
</A>
|
||||
</Card>
|
||||
</Show>
|
||||
</Suspense>
|
||||
<div>
|
||||
<ActivityAmount
|
||||
amount={
|
||||
pendingItem.amount_sats?.toString() ||
|
||||
"0"
|
||||
}
|
||||
price={state.price}
|
||||
/>
|
||||
</div>
|
||||
<div class="flex gap-2 w-[5rem]">
|
||||
<Switch>
|
||||
<Match
|
||||
when={paying() !== pendingItem.id}
|
||||
>
|
||||
<button
|
||||
onClick={() =>
|
||||
payItem(pendingItem)
|
||||
}
|
||||
>
|
||||
<img
|
||||
class="h-[2.5rem] w-[2.5rem]"
|
||||
src={greenCheck}
|
||||
alt="Approve"
|
||||
/>
|
||||
</button>
|
||||
<button
|
||||
onClick={() =>
|
||||
rejectItem(pendingItem)
|
||||
}
|
||||
>
|
||||
<img
|
||||
class="h-[2rem] w-[2rem]"
|
||||
src={redClose}
|
||||
alt="Reject"
|
||||
/>
|
||||
</button>
|
||||
</Match>
|
||||
<Match
|
||||
when={paying() === pendingItem.id}
|
||||
>
|
||||
<LoadingSpinner wide />
|
||||
</Match>
|
||||
</Switch>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</For>
|
||||
</VStack>
|
||||
<A
|
||||
href="/settings/connections"
|
||||
class="text-m-red active:text-m-red/80 font-semibold no-underline self-center"
|
||||
>
|
||||
Configure
|
||||
</A>
|
||||
</Card>
|
||||
</Show>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -63,3 +63,7 @@ select {
|
||||
background-size: 20px 20px;
|
||||
background-repeat: no-repeat;
|
||||
}
|
||||
|
||||
strong {
|
||||
@apply font-semibold text-m-red;
|
||||
}
|
||||
@@ -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);
|
||||
|
||||
236
src/routes/settings/Plus.tsx
Normal file
236
src/routes/settings/Plus.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
@@ -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={[
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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);
|
||||
|
||||
Reference in New Issue
Block a user