feat: renew sub task

This commit is contained in:
Kieran 2023-10-18 14:47:45 +01:00
parent 0ba1ba05ac
commit 98c3d901ae
Signed by: Kieran
GPG Key ID: DE71CEB3925BE941
8 changed files with 143 additions and 64 deletions

View File

@ -0,0 +1,84 @@
import { useState } from "react";
import { FormattedMessage, useIntl } from "react-intl";
import { unixNow, unwrap } from "@snort/shared";
import AsyncButton from "Element/AsyncButton";
import SendSats from "Element/SendSats";
import useEventPublisher from "Hooks/useEventPublisher";
import SnortApi, { Subscription, SubscriptionError } from "SnortApi";
import { mapPlanName, mapSubscriptionErrorCode } from ".";
import useLogin from "Hooks/useLogin";
import { mostRecentSubscription } from "Subscription";
export function RenewSub({ sub: s }: { sub?: Subscription }) {
const { subscriptions } = useLogin(s => ({ subscriptions: s.subscriptions }));
const { publisher } = useEventPublisher();
const { formatMessage } = useIntl();
const [invoice, setInvoice] = useState("");
const [error, setError] = useState<SubscriptionError>();
const [months, setMonths] = useState(1);
const recentSub = mostRecentSubscription(subscriptions);
const sub =
s ?? recentSub
? ({
id: unwrap(recentSub).id,
type: unwrap(recentSub).type,
created: unwrap(recentSub).start,
expires: unwrap(recentSub).end,
state: unwrap(recentSub).end > unixNow() ? "expired" : "paid",
} as Subscription)
: undefined;
async function renew(id: string, months: number) {
const api = new SnortApi(undefined, publisher);
try {
const rsp = await api.renewSubscription(id, months);
setInvoice(rsp.pr);
} catch (e) {
if (e instanceof SubscriptionError) {
setError(e);
}
}
}
if (!sub) return;
return (
<>
<div className="flex g8">
<div className="flex flex-col g4">
<small>
<FormattedMessage defaultMessage="Months" />
</small>
<input type="number" value={months} onChange={e => setMonths(Number(e.target.value))} min={1} />
</div>
<div className="flex flex-col g4">
<span>&nbsp;</span>
<AsyncButton onClick={() => renew(sub.id, months)}>
{sub.state === "expired" ? (
<FormattedMessage
defaultMessage="Renew {tier}"
values={{
tier: mapPlanName(sub.type),
}}
/>
) : (
<FormattedMessage defaultMessage="Pay Now" />
)}
</AsyncButton>
</div>
</div>
<SendSats
invoice={invoice}
show={invoice !== ""}
onClose={() => setInvoice("")}
title={formatMessage({
defaultMessage: "Pay for subscription",
})}
/>
{error && <b className="error">{mapSubscriptionErrorCode(error)}</b>}
</>
);
}

View File

@ -1,20 +1,14 @@
import { FormattedMessage, FormattedDate, FormattedNumber, useIntl } from "react-intl";
import { useState } from "react";
import { FormattedMessage, FormattedDate, FormattedNumber } from "react-intl";
import SnortApi, { Subscription, SubscriptionError } from "SnortApi";
import { mapPlanName, mapSubscriptionErrorCode } from ".";
import AsyncButton from "Element/AsyncButton";
import { Subscription } from "SnortApi";
import { mapPlanName } from ".";
import Icon from "Icons/Icon";
import useEventPublisher from "Hooks/useEventPublisher";
import SendSats from "Element/SendSats";
import Nip5Service from "Element/Nip5Service";
import { SnortNostrAddressService } from "Pages/NostrAddressPage";
import Nip05 from "Element/User/Nip05";
import { RenewSub } from "./RenewSub";
export default function SubscriptionCard({ sub }: { sub: Subscription }) {
const { publisher } = useEventPublisher();
const { formatMessage } = useIntl();
const created = new Date(sub.created * 1000);
const expires = new Date(sub.expires * 1000);
const now = new Date();
@ -24,22 +18,6 @@ export default function SubscriptionCard({ sub }: { sub: Subscription }) {
const isNew = sub.state === "new";
const isPaid = sub.state === "paid";
const [invoice, setInvoice] = useState("");
const [error, setError] = useState<SubscriptionError>();
const [months, setMonths] = useState(1);
async function renew(id: string, months: number) {
const api = new SnortApi(undefined, publisher);
try {
const rsp = await api.renewSubscription(id, months);
setInvoice(rsp.pr);
} catch (e) {
if (e instanceof SubscriptionError) {
setError(e);
}
}
}
function subFeatures() {
return (
<>
@ -115,37 +93,9 @@ export default function SubscriptionCard({ sub }: { sub: Subscription }) {
</p>
)}
</div>
{(isExpired || isNew) && (
<div className="flex g8">
<div className="flex flex-col g4">
<span>&nbsp;</span>
<AsyncButton onClick={() => renew(sub.id, months)}>
{isExpired ? (
<FormattedMessage defaultMessage="Renew" />
) : (
<FormattedMessage defaultMessage="Pay Now" />
)}
</AsyncButton>
</div>
<div className="flex flex-col g4">
<small>
<FormattedMessage defaultMessage="Months" />
</small>
<input type="number" value={months} onChange={e => setMonths(Number(e.target.value))} min={1} />
</div>
</div>
)}
{(isExpired || isNew) && <RenewSub sub={sub} />}
{isPaid && subFeatures()}
</div>
<SendSats
invoice={invoice}
show={invoice !== ""}
onClose={() => setInvoice("")}
title={formatMessage({
defaultMessage: "Pay for subscription",
})}
/>
{error && <b className="error">{mapSubscriptionErrorCode(error)}</b>}
</>
);
}

View File

@ -58,3 +58,7 @@ export function getActiveSubscriptions(s: Array<SubscriptionEvent>) {
export function getCurrentSubscription(s: Array<SubscriptionEvent>) {
return getActiveSubscriptions(s)[0];
}
export function mostRecentSubscription(s: Array<SubscriptionEvent>) {
return [...s].sort((a, b) => (b.start > a.start ? -1 : 1)).at(0);
}

View File

@ -0,0 +1,32 @@
import { FormattedMessage } from "react-intl";
import { MetadataCache } from "@snort/system";
import { BaseUITask } from "Tasks";
import { LoginSession } from "Login";
import { getCurrentSubscription } from "Subscription";
import { RenewSub } from "Pages/subscribe/RenewSub";
export class RenewSubTask extends BaseUITask {
id = "renew-sub";
check(user: MetadataCache, session: LoginSession): boolean {
const sub = getCurrentSubscription(session.subscriptions);
return !sub && session.subscriptions.length > 0;
}
render(): JSX.Element {
return (
<>
<p>
<FormattedMessage
defaultMessage="Your {site_name} subscription is expired"
values={{
site_name: CONFIG.appName,
}}
/>
</p>
<RenewSub />
</>
);
}
}

View File

@ -6,13 +6,17 @@ import Icon from "Icons/Icon";
import { UITask } from "Tasks";
import { DonateTask } from "./DonateTask";
import { Nip5Task } from "./Nip5Task";
import { RenewSubTask } from "./RenewSubscription";
const AllTasks: Array<UITask> = [new Nip5Task(), new DonateTask()];
if (CONFIG.features.subscriptions) {
AllTasks.push(new RenewSubTask());
}
AllTasks.forEach(a => a.load());
export const TaskList = () => {
const publicKey = useLogin().publicKey;
const user = useUserProfile(publicKey);
const session = useLogin();
const user = useUserProfile(session.publicKey);
const [, setTick] = useState<number>(0);
function muteTask(t: UITask) {
@ -22,7 +26,7 @@ export const TaskList = () => {
return (
<>
{AllTasks.filter(a => (user ? a.check(user) : false)).map(a => {
{AllTasks.filter(a => (user ? a.check(user, session) : false)).map(a => {
return (
<div key={a.id} className="card">
<div className="header">

View File

@ -1,11 +1,12 @@
import { MetadataCache } from "@snort/system";
import { LoginSession } from "Login";
export interface UITask {
id: string;
/**
* Run checks to determine if this Task should be triggered for this user
*/
check(user: MetadataCache): boolean;
check(user: MetadataCache, session: LoginSession): boolean;
mute(): void;
load(): void;
render(): JSX.Element;
@ -21,7 +22,7 @@ export abstract class BaseUITask implements UITask {
protected state: UITaskState;
abstract id: string;
abstract check(user: MetadataCache): boolean;
abstract check(user: MetadataCache, session: LoginSession): boolean;
abstract render(): JSX.Element;
constructor() {

View File

@ -1157,6 +1157,9 @@
"jA3OE/": {
"defaultMessage": "{n,plural,=1{{n} sat} other{{n} sats}}"
},
"jAmfGl": {
"defaultMessage": "Your {site_name} subscription is expired"
},
"jMzO1S": {
"defaultMessage": "Internal error: {msg}"
},
@ -1267,9 +1270,6 @@
"nOaArs": {
"defaultMessage": "Setup Profile"
},
"nWQFic": {
"defaultMessage": "Renew"
},
"ncbgUU": {
"defaultMessage": "{site} is a Nostr UI, nostr is a decentralised protocol for saving and distributing \"notes\"."
},
@ -1335,6 +1335,9 @@
"qUJTsT": {
"defaultMessage": "Blocked"
},
"qZsKBR": {
"defaultMessage": "Renew {tier}"
},
"qdGuQo": {
"defaultMessage": "Your Private Key Is (do not share this with anyone)"
},

View File

@ -379,6 +379,7 @@
"itPgxd": "Profile",
"izWS4J": "Unfollow",
"jA3OE/": "{n,plural,=1{{n} sat} other{{n} sats}}",
"jAmfGl": "Your {site_name} subscription is expired",
"jMzO1S": "Internal error: {msg}",
"jfV8Wr": "Back",
"juhqvW": "Improve login security with browser extensions",
@ -415,7 +416,6 @@
"nGBrvw": "Bookmarks",
"nN9XTz": "Share your thoughts with {link}",
"nOaArs": "Setup Profile",
"nWQFic": "Renew",
"ncbgUU": "{site} is a Nostr UI, nostr is a decentralised protocol for saving and distributing \"notes\".",
"nihgfo": "Listen to this article",
"nn1qb3": "Your donations are greatly appreciated",
@ -437,6 +437,7 @@
"qDwvZ4": "Unknown error",
"qMx1sA": "Default Zap amount",
"qUJTsT": "Blocked",
"qZsKBR": "Renew {tier}",
"qdGuQo": "Your Private Key Is (do not share this with anyone)",
"qfmMQh": "This note has been muted",
"qkvYUb": "Add to Profile",