-
-
-
- {pendingItem.name_of_connection}
-
-
- Expires {timeAgo(pendingItem.date)}
-
-
-
-
-
-
-
- payItem(pendingItem)
- }
- >
-
-
-
- rejectItem(pendingItem)
- }
- >
-
-
-
-
-
-
-
-
+
0}>
+
+
+
+
+ {error()?.message}
+
+
+ {(pendingItem) => (
+
+
+
+
+ {pendingItem.name_of_connection}
+
+
+ Expires {timeAgo(pendingItem.date)}
+
- )}
-
-
-
- Configure
-
-
-
-
+
+
+
+
+
+ payItem(pendingItem)
+ }
+ >
+
+
+
+ rejectItem(pendingItem)
+ }
+ >
+
+
+
+
+
+
+
+
+
+ )}
+
+
+
+ Configure
+
+
+
);
}
diff --git a/src/root.css b/src/root.css
index 108aafe..6b709e9 100644
--- a/src/root.css
+++ b/src/root.css
@@ -63,3 +63,7 @@ select {
background-size: 20px 20px;
background-repeat: no-repeat;
}
+
+strong {
+@apply font-semibold text-m-red;
+}
\ No newline at end of file
diff --git a/src/routes/settings/Connections.tsx b/src/routes/settings/Connections.tsx
index b127946..952151a 100644
--- a/src/routes/settings/Connections.tsx
+++ b/src/routes/settings/Connections.tsx
@@ -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);
diff --git a/src/routes/settings/Plus.tsx b/src/routes/settings/Plus.tsx
new file mode 100644
index 0000000..fae1a51
--- /dev/null
+++ b/src/routes/settings/Plus.tsx
@@ -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 (
+
+
+ Smug satisfaction
+
+
+ Redshift (coming soon)
+
+
+ Gifting (coming soon)
+
+
+ Multi-device access (coming soon)
+
+ ... and more to come
+
+ );
+}
+
+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
();
+
+ 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 (
+
+
+
+ Join Mutiny+ for{" "}
+ {Number(planDetails().amount_sat).toLocaleString()} sats a
+ month.
+
+
+ {error()!.message}
+
+
+
+ You'll need at least{" "}
+ {Number(planDetails().amount_sat).toLocaleString()} sats
+ in your lightning balance to get started. Try before you
+ buy!
+
+
+
+ setConfirmOpen(true)}
+ disabled={!hasEnough()}
+ >
+ Join
+
+
+ Restore Subscription
+
+
+
+ setConfirmOpen(false)}
+ >
+
+ Ready to join Mutiny+ ?
+ Click confirm to pay for your first month.
+
+
+
+ );
+}
+
+export default function Plus() {
+ const [state, _actions] = useMegaStore();
+
+ return (
+
+
+
+
+ Mutiny+
+
+
+
+
+
+ You're part of the mutiny! Enjoy the
+ following perks:
+
+
+
+ You'll get a renewal payment request around{" "}
+
+ {new Date(
+ state.subscription_timestamp! * 1000
+ ).toLocaleDateString()}
+
+ .
+
+
+ To cancel your subscription just don't pay.
+ You can also disable the Mutiny+{" "}
+
+ Wallet Connection.
+
+
+
+
+
+ Mutiny is open source and self-hostable.{" "}
+
+ But also you can pay for it.
+
+
+
+ Paying for{" "}
+ Mutiny+ {" "}
+ helps support ongoing development and
+ unlocks early access to new features and
+ premium functionality:
+
+
+
+ }>
+
+
+
+
+
+
+
+
+
+
+ );
+}
diff --git a/src/routes/settings/index.tsx b/src/routes/settings/index.tsx
index 70f6da8..d7fcae5 100644
--- a/src/routes/settings/index.tsx
+++ b/src/routes/settings/index.tsx
@@ -58,6 +58,15 @@ export default function Settings() {
Settings
+
();
-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;
@@ -58,6 +60,7 @@ export type MegaStore = [
listTags(): Promise;
syncActivity(): Promise;
checkBrowserCompat(): Promise;
+ checkForSubscription(justPaid?: boolean): Promise;
}
];
@@ -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 {
+ 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 {
@@ -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) {
diff --git a/src/utils/gradientHash.ts b/src/utils/gradientHash.ts
index 906e363..0a47e37 100644
--- a/src/utils/gradientHash.ts
+++ b/src/utils/gradientHash.ts
@@ -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);