add support modal

This commit is contained in:
hzrd149 2025-03-08 14:22:21 +00:00
parent 2ed579783d
commit d0b11e9081
7 changed files with 126 additions and 2 deletions

View File

@ -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

1
.gitignore vendored
View File

@ -2,3 +2,4 @@ dist
node_modules
stats.html
exe/bin
.env

View File

@ -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 (
<Modal isOpen={!paid} onClose={dismiss} size="lg" closeOnOverlayClick={false}>
<ModalOverlay />
<ModalContent>
<ModalHeader>Support the app</ModalHeader>
<ModalBody>
{PAYWALL_MESSAGE || "If your enjoying this app consider supporting the developer by donating some sats"}
</ModalBody>
<ModalFooter gap="2">
<Button variant="link" px="4" py="2" onClick={dismiss}>
Dismiss for a day
</Button>
<Button
colorScheme="primary"
as={RouterLink}
to="/support"
leftIcon={<LightningIcon color="yellow.400" boxSize={5} />}
>
Support
</Button>
</ModalFooter>
</ModalContent>
</Modal>
);
else return null;
}

View File

@ -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 (
<>
<SupportPaywall />
<DesktopSideNav />
<ErrorBoundary>
<Suspense fallback={<Spinner />}>

View File

@ -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 (
<>
<SupportPaywall />
<Suspense fallback={<Spinner />}>
<ErrorBoundary>
<Outlet />

View File

@ -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;

68
src/services/paywall.ts Normal file
View File

@ -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<boolean>;
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<DomainIdentityJson>));
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 };