feat: renew sub task
This commit is contained in:
parent
0ba1ba05ac
commit
98c3d901ae
84
packages/app/src/Pages/subscribe/RenewSub.tsx
Normal file
84
packages/app/src/Pages/subscribe/RenewSub.tsx
Normal 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> </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>}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
@ -1,20 +1,14 @@
|
|||||||
import { FormattedMessage, FormattedDate, FormattedNumber, useIntl } from "react-intl";
|
import { FormattedMessage, FormattedDate, FormattedNumber } from "react-intl";
|
||||||
import { useState } from "react";
|
|
||||||
|
|
||||||
import SnortApi, { Subscription, SubscriptionError } from "SnortApi";
|
import { Subscription } from "SnortApi";
|
||||||
import { mapPlanName, mapSubscriptionErrorCode } from ".";
|
import { mapPlanName } from ".";
|
||||||
import AsyncButton from "Element/AsyncButton";
|
|
||||||
import Icon from "Icons/Icon";
|
import Icon from "Icons/Icon";
|
||||||
import useEventPublisher from "Hooks/useEventPublisher";
|
|
||||||
import SendSats from "Element/SendSats";
|
|
||||||
import Nip5Service from "Element/Nip5Service";
|
import Nip5Service from "Element/Nip5Service";
|
||||||
import { SnortNostrAddressService } from "Pages/NostrAddressPage";
|
import { SnortNostrAddressService } from "Pages/NostrAddressPage";
|
||||||
import Nip05 from "Element/User/Nip05";
|
import Nip05 from "Element/User/Nip05";
|
||||||
|
import { RenewSub } from "./RenewSub";
|
||||||
|
|
||||||
export default function SubscriptionCard({ sub }: { sub: Subscription }) {
|
export default function SubscriptionCard({ sub }: { sub: Subscription }) {
|
||||||
const { publisher } = useEventPublisher();
|
|
||||||
const { formatMessage } = useIntl();
|
|
||||||
|
|
||||||
const created = new Date(sub.created * 1000);
|
const created = new Date(sub.created * 1000);
|
||||||
const expires = new Date(sub.expires * 1000);
|
const expires = new Date(sub.expires * 1000);
|
||||||
const now = new Date();
|
const now = new Date();
|
||||||
@ -24,22 +18,6 @@ export default function SubscriptionCard({ sub }: { sub: Subscription }) {
|
|||||||
const isNew = sub.state === "new";
|
const isNew = sub.state === "new";
|
||||||
const isPaid = sub.state === "paid";
|
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() {
|
function subFeatures() {
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
@ -115,37 +93,9 @@ export default function SubscriptionCard({ sub }: { sub: Subscription }) {
|
|||||||
</p>
|
</p>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
{(isExpired || isNew) && (
|
{(isExpired || isNew) && <RenewSub sub={sub} />}
|
||||||
<div className="flex g8">
|
|
||||||
<div className="flex flex-col g4">
|
|
||||||
<span> </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>
|
|
||||||
)}
|
|
||||||
{isPaid && subFeatures()}
|
{isPaid && subFeatures()}
|
||||||
</div>
|
</div>
|
||||||
<SendSats
|
|
||||||
invoice={invoice}
|
|
||||||
show={invoice !== ""}
|
|
||||||
onClose={() => setInvoice("")}
|
|
||||||
title={formatMessage({
|
|
||||||
defaultMessage: "Pay for subscription",
|
|
||||||
})}
|
|
||||||
/>
|
|
||||||
{error && <b className="error">{mapSubscriptionErrorCode(error)}</b>}
|
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -58,3 +58,7 @@ export function getActiveSubscriptions(s: Array<SubscriptionEvent>) {
|
|||||||
export function getCurrentSubscription(s: Array<SubscriptionEvent>) {
|
export function getCurrentSubscription(s: Array<SubscriptionEvent>) {
|
||||||
return getActiveSubscriptions(s)[0];
|
return getActiveSubscriptions(s)[0];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function mostRecentSubscription(s: Array<SubscriptionEvent>) {
|
||||||
|
return [...s].sort((a, b) => (b.start > a.start ? -1 : 1)).at(0);
|
||||||
|
}
|
||||||
|
32
packages/app/src/Tasks/RenewSubscription.tsx
Normal file
32
packages/app/src/Tasks/RenewSubscription.tsx
Normal 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 />
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
@ -6,13 +6,17 @@ import Icon from "Icons/Icon";
|
|||||||
import { UITask } from "Tasks";
|
import { UITask } from "Tasks";
|
||||||
import { DonateTask } from "./DonateTask";
|
import { DonateTask } from "./DonateTask";
|
||||||
import { Nip5Task } from "./Nip5Task";
|
import { Nip5Task } from "./Nip5Task";
|
||||||
|
import { RenewSubTask } from "./RenewSubscription";
|
||||||
|
|
||||||
const AllTasks: Array<UITask> = [new Nip5Task(), new DonateTask()];
|
const AllTasks: Array<UITask> = [new Nip5Task(), new DonateTask()];
|
||||||
|
if (CONFIG.features.subscriptions) {
|
||||||
|
AllTasks.push(new RenewSubTask());
|
||||||
|
}
|
||||||
AllTasks.forEach(a => a.load());
|
AllTasks.forEach(a => a.load());
|
||||||
|
|
||||||
export const TaskList = () => {
|
export const TaskList = () => {
|
||||||
const publicKey = useLogin().publicKey;
|
const session = useLogin();
|
||||||
const user = useUserProfile(publicKey);
|
const user = useUserProfile(session.publicKey);
|
||||||
const [, setTick] = useState<number>(0);
|
const [, setTick] = useState<number>(0);
|
||||||
|
|
||||||
function muteTask(t: UITask) {
|
function muteTask(t: UITask) {
|
||||||
@ -22,7 +26,7 @@ export const TaskList = () => {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
{AllTasks.filter(a => (user ? a.check(user) : false)).map(a => {
|
{AllTasks.filter(a => (user ? a.check(user, session) : false)).map(a => {
|
||||||
return (
|
return (
|
||||||
<div key={a.id} className="card">
|
<div key={a.id} className="card">
|
||||||
<div className="header">
|
<div className="header">
|
||||||
|
@ -1,11 +1,12 @@
|
|||||||
import { MetadataCache } from "@snort/system";
|
import { MetadataCache } from "@snort/system";
|
||||||
|
import { LoginSession } from "Login";
|
||||||
|
|
||||||
export interface UITask {
|
export interface UITask {
|
||||||
id: string;
|
id: string;
|
||||||
/**
|
/**
|
||||||
* Run checks to determine if this Task should be triggered for this user
|
* 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;
|
mute(): void;
|
||||||
load(): void;
|
load(): void;
|
||||||
render(): JSX.Element;
|
render(): JSX.Element;
|
||||||
@ -21,7 +22,7 @@ export abstract class BaseUITask implements UITask {
|
|||||||
protected state: UITaskState;
|
protected state: UITaskState;
|
||||||
|
|
||||||
abstract id: string;
|
abstract id: string;
|
||||||
abstract check(user: MetadataCache): boolean;
|
abstract check(user: MetadataCache, session: LoginSession): boolean;
|
||||||
abstract render(): JSX.Element;
|
abstract render(): JSX.Element;
|
||||||
|
|
||||||
constructor() {
|
constructor() {
|
||||||
|
@ -1157,6 +1157,9 @@
|
|||||||
"jA3OE/": {
|
"jA3OE/": {
|
||||||
"defaultMessage": "{n,plural,=1{{n} sat} other{{n} sats}}"
|
"defaultMessage": "{n,plural,=1{{n} sat} other{{n} sats}}"
|
||||||
},
|
},
|
||||||
|
"jAmfGl": {
|
||||||
|
"defaultMessage": "Your {site_name} subscription is expired"
|
||||||
|
},
|
||||||
"jMzO1S": {
|
"jMzO1S": {
|
||||||
"defaultMessage": "Internal error: {msg}"
|
"defaultMessage": "Internal error: {msg}"
|
||||||
},
|
},
|
||||||
@ -1267,9 +1270,6 @@
|
|||||||
"nOaArs": {
|
"nOaArs": {
|
||||||
"defaultMessage": "Setup Profile"
|
"defaultMessage": "Setup Profile"
|
||||||
},
|
},
|
||||||
"nWQFic": {
|
|
||||||
"defaultMessage": "Renew"
|
|
||||||
},
|
|
||||||
"ncbgUU": {
|
"ncbgUU": {
|
||||||
"defaultMessage": "{site} is a Nostr UI, nostr is a decentralised protocol for saving and distributing \"notes\"."
|
"defaultMessage": "{site} is a Nostr UI, nostr is a decentralised protocol for saving and distributing \"notes\"."
|
||||||
},
|
},
|
||||||
@ -1335,6 +1335,9 @@
|
|||||||
"qUJTsT": {
|
"qUJTsT": {
|
||||||
"defaultMessage": "Blocked"
|
"defaultMessage": "Blocked"
|
||||||
},
|
},
|
||||||
|
"qZsKBR": {
|
||||||
|
"defaultMessage": "Renew {tier}"
|
||||||
|
},
|
||||||
"qdGuQo": {
|
"qdGuQo": {
|
||||||
"defaultMessage": "Your Private Key Is (do not share this with anyone)"
|
"defaultMessage": "Your Private Key Is (do not share this with anyone)"
|
||||||
},
|
},
|
||||||
|
@ -379,6 +379,7 @@
|
|||||||
"itPgxd": "Profile",
|
"itPgxd": "Profile",
|
||||||
"izWS4J": "Unfollow",
|
"izWS4J": "Unfollow",
|
||||||
"jA3OE/": "{n,plural,=1{{n} sat} other{{n} sats}}",
|
"jA3OE/": "{n,plural,=1{{n} sat} other{{n} sats}}",
|
||||||
|
"jAmfGl": "Your {site_name} subscription is expired",
|
||||||
"jMzO1S": "Internal error: {msg}",
|
"jMzO1S": "Internal error: {msg}",
|
||||||
"jfV8Wr": "Back",
|
"jfV8Wr": "Back",
|
||||||
"juhqvW": "Improve login security with browser extensions",
|
"juhqvW": "Improve login security with browser extensions",
|
||||||
@ -415,7 +416,6 @@
|
|||||||
"nGBrvw": "Bookmarks",
|
"nGBrvw": "Bookmarks",
|
||||||
"nN9XTz": "Share your thoughts with {link}",
|
"nN9XTz": "Share your thoughts with {link}",
|
||||||
"nOaArs": "Setup Profile",
|
"nOaArs": "Setup Profile",
|
||||||
"nWQFic": "Renew",
|
|
||||||
"ncbgUU": "{site} is a Nostr UI, nostr is a decentralised protocol for saving and distributing \"notes\".",
|
"ncbgUU": "{site} is a Nostr UI, nostr is a decentralised protocol for saving and distributing \"notes\".",
|
||||||
"nihgfo": "Listen to this article",
|
"nihgfo": "Listen to this article",
|
||||||
"nn1qb3": "Your donations are greatly appreciated",
|
"nn1qb3": "Your donations are greatly appreciated",
|
||||||
@ -437,6 +437,7 @@
|
|||||||
"qDwvZ4": "Unknown error",
|
"qDwvZ4": "Unknown error",
|
||||||
"qMx1sA": "Default Zap amount",
|
"qMx1sA": "Default Zap amount",
|
||||||
"qUJTsT": "Blocked",
|
"qUJTsT": "Blocked",
|
||||||
|
"qZsKBR": "Renew {tier}",
|
||||||
"qdGuQo": "Your Private Key Is (do not share this with anyone)",
|
"qdGuQo": "Your Private Key Is (do not share this with anyone)",
|
||||||
"qfmMQh": "This note has been muted",
|
"qfmMQh": "This note has been muted",
|
||||||
"qkvYUb": "Add to Profile",
|
"qkvYUb": "Add to Profile",
|
||||||
|
Loading…
x
Reference in New Issue
Block a user