diff --git a/src/assets/mutiny-plus-logo.png b/src/assets/mutiny-plus-logo.png new file mode 100644 index 0000000..b3b9628 Binary files /dev/null and b/src/assets/mutiny-plus-logo.png differ diff --git a/src/assets/party.gif b/src/assets/party.gif new file mode 100644 index 0000000..056c956 Binary files /dev/null and b/src/assets/party.gif differ diff --git a/src/components/Activity.tsx b/src/components/Activity.tsx index 85d59f5..685cdfd 100644 --- a/src/components/Activity.tsx +++ b/src/components/Activity.tsx @@ -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 ( - <> + }>
- {i18n.t("receive_some_sats_to_get_started")} + + {i18n.t("receive_some_sats_to_get_started")} +
- +
); } diff --git a/src/components/App.tsx b/src/components/App.tsx index 97ae5f8..62c0cb2 100644 --- a/src/components/App.tsx +++ b/src/components/App.tsx @@ -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() {
- + + + + + + + + - Important caveats:{" "} - 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. + Important caveats: 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.
diff --git a/src/components/PendingNwc.tsx b/src/components/PendingNwc.tsx index 4b97211..9f6a5d5 100644 --- a/src/components/PendingNwc.tsx +++ b/src/components/PendingNwc.tsx @@ -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 ( - - 0}> - -
- - - {error()?.message} - - - {(pendingItem) => ( -
- onchain -
- - {pendingItem.name_of_connection} - - -
-
- -
-
- - - - - - - - - -
+ 0}> + +
+ + + {error()?.message} + + + {(pendingItem) => ( +
+ onchain +
+ + {pendingItem.name_of_connection} + +
- )} - - - - Configure - - - - +
+ +
+
+ + + + + + + + + +
+
+ )} +
+
+ + 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(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);