From 98c3d901ae43f8d0e3a695d1abb7ed357e4543d5 Mon Sep 17 00:00:00 2001 From: Kieran Date: Wed, 18 Oct 2023 14:47:45 +0100 Subject: [PATCH] feat: renew sub task --- packages/app/src/Pages/subscribe/RenewSub.tsx | 84 +++++++++++++++++++ .../src/Pages/subscribe/SubscriptionCard.tsx | 60 ++----------- packages/app/src/Subscription/index.ts | 4 + packages/app/src/Tasks/RenewSubscription.tsx | 32 +++++++ packages/app/src/Tasks/TaskList.tsx | 10 ++- packages/app/src/Tasks/index.ts | 5 +- packages/app/src/lang.json | 9 +- packages/app/src/translations/en.json | 3 +- 8 files changed, 143 insertions(+), 64 deletions(-) create mode 100644 packages/app/src/Pages/subscribe/RenewSub.tsx create mode 100644 packages/app/src/Tasks/RenewSubscription.tsx diff --git a/packages/app/src/Pages/subscribe/RenewSub.tsx b/packages/app/src/Pages/subscribe/RenewSub.tsx new file mode 100644 index 00000000..df0f6aa4 --- /dev/null +++ b/packages/app/src/Pages/subscribe/RenewSub.tsx @@ -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(); + 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 ( + <> +
+
+ + + + setMonths(Number(e.target.value))} min={1} /> +
+ +
+   + renew(sub.id, months)}> + {sub.state === "expired" ? ( + + ) : ( + + )} + +
+
+ setInvoice("")} + title={formatMessage({ + defaultMessage: "Pay for subscription", + })} + /> + {error && {mapSubscriptionErrorCode(error)}} + + ); +} diff --git a/packages/app/src/Pages/subscribe/SubscriptionCard.tsx b/packages/app/src/Pages/subscribe/SubscriptionCard.tsx index 150d3c51..9a9aab65 100644 --- a/packages/app/src/Pages/subscribe/SubscriptionCard.tsx +++ b/packages/app/src/Pages/subscribe/SubscriptionCard.tsx @@ -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(); - 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 }) {

)} - {(isExpired || isNew) && ( -
-
-   - renew(sub.id, months)}> - {isExpired ? ( - - ) : ( - - )} - -
-
- - - - setMonths(Number(e.target.value))} min={1} /> -
-
- )} + {(isExpired || isNew) && } {isPaid && subFeatures()} - setInvoice("")} - title={formatMessage({ - defaultMessage: "Pay for subscription", - })} - /> - {error && {mapSubscriptionErrorCode(error)}} ); } diff --git a/packages/app/src/Subscription/index.ts b/packages/app/src/Subscription/index.ts index e3cf86a9..3349e652 100644 --- a/packages/app/src/Subscription/index.ts +++ b/packages/app/src/Subscription/index.ts @@ -58,3 +58,7 @@ export function getActiveSubscriptions(s: Array) { export function getCurrentSubscription(s: Array) { return getActiveSubscriptions(s)[0]; } + +export function mostRecentSubscription(s: Array) { + return [...s].sort((a, b) => (b.start > a.start ? -1 : 1)).at(0); +} diff --git a/packages/app/src/Tasks/RenewSubscription.tsx b/packages/app/src/Tasks/RenewSubscription.tsx new file mode 100644 index 00000000..abaa5661 --- /dev/null +++ b/packages/app/src/Tasks/RenewSubscription.tsx @@ -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 ( + <> +

+ +

+ + + ); + } +} diff --git a/packages/app/src/Tasks/TaskList.tsx b/packages/app/src/Tasks/TaskList.tsx index 11ecf4ef..35b1d6e3 100644 --- a/packages/app/src/Tasks/TaskList.tsx +++ b/packages/app/src/Tasks/TaskList.tsx @@ -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 = [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(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 (
diff --git a/packages/app/src/Tasks/index.ts b/packages/app/src/Tasks/index.ts index c966d1e6..f5323944 100644 --- a/packages/app/src/Tasks/index.ts +++ b/packages/app/src/Tasks/index.ts @@ -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() { diff --git a/packages/app/src/lang.json b/packages/app/src/lang.json index 81d2661b..d9eed889 100644 --- a/packages/app/src/lang.json +++ b/packages/app/src/lang.json @@ -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)" }, diff --git a/packages/app/src/translations/en.json b/packages/app/src/translations/en.json index 8927fb7b..2e7fd80d 100644 --- a/packages/app/src/translations/en.json +++ b/packages/app/src/translations/en.json @@ -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",