diff --git a/.github/workflows/deploy-next.yml b/.github/workflows/deploy-next.yml
index 66e27bc46..b38f14059 100644
--- a/.github/workflows/deploy-next.yml
+++ b/.github/workflows/deploy-next.yml
@@ -34,6 +34,8 @@ jobs:
- name: Build
env:
VITE_TENOR_API_KEY: ${{ secrets.VITE_TENOR_API_KEY }}
+ VITE_PAYWALL_NIP05: "/.well-known/nostr.json"
+ VITE_PAYWALL_MESSAGE: "This is the latest alpha build of noStrudel.\nIf your enjoying the new features consider supporting the project by donating some sats and adding your message on the support page."
run: pnpm build
- name: Redirect 404 to Index for SPA
diff --git a/.gitignore b/.gitignore
index 587bbcb36..1be530786 100644
--- a/.gitignore
+++ b/.gitignore
@@ -2,3 +2,4 @@ dist
node_modules
stats.html
exe/bin
+.env
diff --git a/src/components/layout/components/support-paywall.tsx b/src/components/layout/components/support-paywall.tsx
new file mode 100644
index 000000000..e247c663f
--- /dev/null
+++ b/src/components/layout/components/support-paywall.tsx
@@ -0,0 +1,46 @@
+import { useObservable } from "applesauce-react/hooks";
+
+import { paywall, hidePaywall } from "../../../services/paywall";
+import { Button, Modal, ModalBody, ModalContent, ModalFooter, ModalHeader, ModalOverlay } from "@chakra-ui/react";
+import { unixNow } from "applesauce-core/helpers";
+import RouterLink from "../../router-link";
+import { useMatch } from "react-router-dom";
+import { LightningIcon } from "../../icons";
+import { PAYWALL_MESSAGE } from "../../../env";
+
+export default function SupportPaywall() {
+ const isSupportPage = useMatch("/support");
+ const paid = useObservable(paywall);
+
+ const dismiss = () => {
+ hidePaywall.next(unixNow() + 60 * 60 * 24);
+ };
+
+ if (!paid && !isSupportPage)
+ return (
+
+
+
+ Support the app
+
+ {PAYWALL_MESSAGE || "If your enjoying this app consider supporting the developer by donating some sats"}
+
+
+
+
+ }
+ >
+ Support
+
+
+
+
+ );
+ else return null;
+}
diff --git a/src/components/layout/desktop/index.tsx b/src/components/layout/desktop/index.tsx
index c51064a6c..650b85afc 100644
--- a/src/components/layout/desktop/index.tsx
+++ b/src/components/layout/desktop/index.tsx
@@ -4,10 +4,12 @@ import { Outlet } from "react-router-dom";
import DesktopSideNav from "./side-nav";
import { ErrorBoundary } from "../../error-boundary";
+import SupportPaywall from "../components/support-paywall";
export default function DesktopLayout() {
return (
<>
+
}>
diff --git a/src/components/layout/mobile/index.tsx b/src/components/layout/mobile/index.tsx
index e8d21470a..8a86fe773 100644
--- a/src/components/layout/mobile/index.tsx
+++ b/src/components/layout/mobile/index.tsx
@@ -1,13 +1,15 @@
+import { Suspense } from "react";
import { Outlet } from "react-router-dom";
+import { Spinner } from "@chakra-ui/react";
import MobileBottomNav from "./bottom-nav";
import { ErrorBoundary } from "../../error-boundary";
-import { Suspense } from "react";
-import { Spinner } from "@chakra-ui/react";
+import SupportPaywall from "../components/support-paywall";
export default function MobileLayout() {
return (
<>
+
}>
diff --git a/src/env.ts b/src/env.ts
index e1285ddc3..8214bd930 100644
--- a/src/env.ts
+++ b/src/env.ts
@@ -5,3 +5,6 @@ export const CAP_IS_WEB = platform === "web";
export const CAP_IS_NATIVE = platform === "ios" || platform === "android";
export const CAP_IS_ANDROID = platform === "android";
export const CAP_IS_IOS = platform === "ios";
+
+export const PAYWALL_NIP05 = import.meta.env.VITE_PAYWALL_NIP05 as string | undefined;
+export const PAYWALL_MESSAGE = import.meta.env.VITE_PAYWALL_MESSAGE as string | undefined;
diff --git a/src/services/paywall.ts b/src/services/paywall.ts
new file mode 100644
index 000000000..e7e7f303f
--- /dev/null
+++ b/src/services/paywall.ts
@@ -0,0 +1,68 @@
+import {
+ BehaviorSubject,
+ combineLatest,
+ filter,
+ from,
+ map,
+ merge,
+ Observable,
+ of,
+ shareReplay,
+ startWith,
+ Subject,
+ switchMap,
+ tap,
+} from "rxjs";
+import { DomainIdentityJson } from "applesauce-loaders/helpers/dns-identity";
+import { unixNow } from "applesauce-core/helpers";
+
+import accounts from "./accounts";
+import { PAYWALL_NIP05 } from "../env";
+import { logger } from "../helpers/debug";
+
+const log = logger.extend("paywall");
+
+export const hidePaywall = new BehaviorSubject(
+ localStorage.getItem("paywall-dismiss") ? parseInt(localStorage.getItem("paywall-dismiss")!) : null,
+);
+
+hidePaywall.subscribe((ts) => {
+ if (ts) localStorage.setItem("paywall-dismiss", String(ts));
+});
+
+let paywall: Observable;
+if (PAYWALL_NIP05) {
+ const accountPaid = accounts.active$.pipe(
+ // ignore empty accounts
+ filter((a) => !!a),
+ // fetch the identity document
+ switchMap((account) => {
+ log(`Fetching NIP-05 document`);
+ const document = from(fetch(PAYWALL_NIP05!).then((res) => res.json() as Promise));
+ return combineLatest([of(account), document]);
+ }),
+ // check if account is in document
+ map(([account, document]) => {
+ log(`Found document`, document);
+ return document.names ? Object.values(document.names).includes(account.pubkey) : false;
+ }),
+ // start with true until document is checked
+ startWith(true),
+ tap((paid) => log(`Account paid ${paid}`)),
+ );
+
+ const dismiss = hidePaywall.pipe(
+ map((hideUntil) => (hideUntil ? hideUntil > unixNow() : false)),
+ tap((dismissed) => log(`Paywall dismissed ${dismissed}`)),
+ );
+
+ paywall = combineLatest([dismiss, accountPaid]).pipe(
+ map(([dismiss, account]) => dismiss || account),
+ // share results for UI
+ shareReplay(1),
+ );
+} else {
+ paywall = of(true);
+}
+
+export { paywall };