1
0
forked from Kieran/snort

feat: implement renewal/complete new

This commit is contained in:
Kieran 2023-04-17 21:33:55 +01:00
parent c59dda1e49
commit b6cc1765db
Signed by: Kieran
GPG Key ID: DE71CEB3925BE941
4 changed files with 151 additions and 67 deletions

View File

@ -42,6 +42,15 @@ export default class SnortServiceProvider extends ServiceProvider {
return this.getJsonAuthd<object>(`/${id}`, "PATCH", obj);
}
async registerForSubscription(handle: string, domain: string, id: string) {
return this.getJsonAuthd<object>(`/registration/register/${id}`, "PUT", {
name: handle,
domain,
pk: "",
ref: "snort",
});
}
async getJsonAuthd<T>(
path: string,
method?: "GET" | string,

View File

@ -1,19 +1,23 @@
import { useEffect, useState } from "react";
import { FormattedDate, FormattedMessage, FormattedNumber } from "react-intl";
import { FormattedDate, FormattedMessage, FormattedNumber, useIntl } from "react-intl";
import { Link } from "react-router-dom";
import PageSpinner from "Element/PageSpinner";
import useEventPublisher from "Feed/EventPublisher";
import SnortApi, { Subscription } from "SnortApi";
import { mapPlanName } from ".";
import SnortApi, { Subscription, SubscriptionError } from "SnortApi";
import { mapPlanName, mapSubscriptionErrorCode } from ".";
import Icon from "Icons/Icon";
import AsyncButton from "Element/AsyncButton";
import SendSats from "Element/SendSats";
export default function ManageSubscriptionPage() {
const publisher = useEventPublisher();
const { formatMessage } = useIntl();
const api = new SnortApi(undefined, publisher);
const [subs, setSubs] = useState<Array<Subscription>>();
const [error, setError] = useState("");
const [error, setError] = useState<SubscriptionError>();
const [invoice, setInvoice] = useState("");
useEffect(() => {
(async () => {
@ -21,15 +25,24 @@ export default function ManageSubscriptionPage() {
const s = await api.listSubscriptions();
setSubs(s);
} catch (e) {
if (e instanceof Error) {
setError(e.message);
} else {
setError("Unknown error");
if (e instanceof SubscriptionError) {
setError(e);
}
}
})();
}, []);
async function renew(id: string) {
try {
const rsp = await api.renewSubscription(id);
setInvoice(rsp.pr);
} catch (e) {
if (e instanceof SubscriptionError) {
setError(e);
}
}
}
if (subs === undefined) {
return <PageSpinner />;
}
@ -44,7 +57,8 @@ export default function ManageSubscriptionPage() {
const now = new Date();
const daysToExpire = Math.floor((expires.getTime() - now.getTime()) / 8.64e7);
const hoursToExpire = Math.floor((expires.getTime() - now.getTime()) / 3.6e6);
const isExpired = expires < now;
const isExpired = a.state === "expired";
const isNew = a.state === "new";
return (
<div key={a.id} className="card">
<div className="flex card-title">
@ -87,17 +101,26 @@ export default function ManageSubscriptionPage() {
</time>
</p>
)}
{daysToExpire < 0 && (
{isExpired && (
<p className="f-1 error">
<FormattedMessage defaultMessage="Expired" />
</p>
)}
{isNew && (
<p className="f-1">
<FormattedMessage defaultMessage="Unpaid" />
</p>
)}
</div>
{isExpired && (
{(isExpired || isNew) && (
<div className="flex">
<button>
<FormattedMessage defaultMessage="Renew" />
</button>
<AsyncButton onClick={() => renew(a.id)}>
{isExpired ? (
<FormattedMessage defaultMessage="Renew" />
) : (
<FormattedMessage defaultMessage="Pay Now" />
)}
</AsyncButton>
</div>
)}
</div>
@ -117,7 +140,15 @@ export default function ManageSubscriptionPage() {
/>
</p>
)}
{error && <b className="error">{error}</b>}
{error && <b className="error">{mapSubscriptionErrorCode(error)}</b>}
<SendSats
invoice={invoice}
show={invoice !== ""}
onClose={() => setInvoice("")}
title={formatMessage({
defaultMessage: "Renew subscription",
})}
/>
</>
);
}

View File

@ -9,7 +9,7 @@ import { LockedFeatures, Plans, SubscriptionType } from "Subscription";
import ManageSubscriptionPage from "Pages/subscribe/ManageSubscription";
import AsyncButton from "Element/AsyncButton";
import useEventPublisher from "Feed/EventPublisher";
import SnortApi from "SnortApi";
import SnortApi, { SubscriptionError, SubscriptionErrorCode } from "SnortApi";
import SendSats from "Element/SendSats";
export function mapPlanName(id: number) {
@ -38,67 +38,91 @@ export function mapFeatureName(k: LockedFeatures) {
}
}
export function mapSubscriptionErrorCode(c: SubscriptionError) {
switch (c.code) {
case SubscriptionErrorCode.InternalError:
return <FormattedMessage defaultMessage="Internal error: {msg}" values={{ msg: c.message }} />;
case SubscriptionErrorCode.SubscriptionActive:
return <FormattedMessage defaultMessage="You subscription is still active, you can't renew yet" />;
case SubscriptionErrorCode.Duplicate:
return <FormattedMessage defaultMessage="You already have a subscription of this type, please renew or pay" />;
default:
return c.message;
}
}
export function SubscribePage() {
const publisher = useEventPublisher();
const api = new SnortApi(undefined, publisher);
const [invoice, setInvoice] = useState("");
const [error, setError] = useState<SubscriptionError>();
async function subscribe(type: number) {
const rsp = await api.createSubscription(type);
setInvoice(rsp.pr);
setError(undefined);
try {
const rsp = await api.createSubscription(type);
setInvoice(rsp.pr);
} catch (e) {
if (e instanceof SubscriptionError) {
setError(e);
}
}
}
return (
<div className="flex subscribe-page">
{Plans.map(a => {
const lower = Plans.filter(b => b.id < a.id);
return (
<div className={`card flex f-col${a.disabled ? " disabled" : ""}`}>
<div className="f-grow">
<h2>{mapPlanName(a.id)}</h2>
<p>
<FormattedMessage
defaultMessage="Subscribe to Snort {plan} for {price} and receive the following rewards"
values={{
plan: mapPlanName(a.id),
price: <b>{formatShort(a.price)} sats/mo</b>,
}}
/>
:
</p>
<b>
<FormattedMessage defaultMessage="Not all features are built yet, more features to be added soon!" />
</b>
<ul>
{a.unlocks.map(b => (
<li>{mapFeatureName(b)} </li>
))}
{lower.map(b => (
<li>
<FormattedMessage
defaultMessage="Everything in {plan}"
values={{
plan: mapPlanName(b.id),
}}
/>
</li>
))}
</ul>
<>
<div className="flex subscribe-page">
{Plans.map(a => {
const lower = Plans.filter(b => b.id < a.id);
return (
<div className={`card flex f-col${a.disabled ? " disabled" : ""}`}>
<div className="f-grow">
<h2>{mapPlanName(a.id)}</h2>
<p>
<FormattedMessage
defaultMessage="Subscribe to Snort {plan} for {price} and receive the following rewards"
values={{
plan: mapPlanName(a.id),
price: <b>{formatShort(a.price)} sats/mo</b>,
}}
/>
:
</p>
<b>
<FormattedMessage defaultMessage="Not all features are built yet, more features to be added soon!" />
</b>
<ul>
{a.unlocks.map(b => (
<li>{mapFeatureName(b)} </li>
))}
{lower.map(b => (
<li>
<FormattedMessage
defaultMessage="Everything in {plan}"
values={{
plan: mapPlanName(b.id),
}}
/>
</li>
))}
</ul>
</div>
<div className="flex f-center w-max mb10">
<AsyncButton className="button" disabled={a.disabled} onClick={() => subscribe(a.id)}>
{a.disabled ? (
<FormattedMessage defaultMessage="Coming soon" />
) : (
<FormattedMessage defaultMessage="Subscribe" />
)}
</AsyncButton>
</div>
</div>
<div className="flex f-center w-max mb10">
<AsyncButton className="button" disabled={a.disabled} onClick={() => subscribe(a.id)}>
{a.disabled ? (
<FormattedMessage defaultMessage="Coming soon" />
) : (
<FormattedMessage defaultMessage="Subscribe" />
)}
</AsyncButton>
</div>
</div>
);
})}
);
})}
</div>
{error && <b className="error">{mapSubscriptionErrorCode(error)}</b>}
<SendSats invoice={invoice} show={invoice !== ""} onClose={() => setInvoice("")} />
</div>
</>
);
}

View File

@ -22,6 +22,22 @@ export interface Subscription {
type: SubscriptionType;
created: string;
expires: string;
state: "new" | "expired" | "paid";
}
export enum SubscriptionErrorCode {
InternalError = 1,
SubscriptionActive = 2,
Duplicate = 3,
}
export class SubscriptionError extends Error {
code: SubscriptionErrorCode;
constructor(msg: string, code: SubscriptionErrorCode) {
super(msg);
this.code = code;
}
}
export default class SnortApi {
@ -49,6 +65,10 @@ export default class SnortApi {
return this.#getJsonAuthd<InvoiceResponse>(`api/v1/subscription?type=${type}`, "PUT");
}
renewSubscription(id: string) {
return this.#getJsonAuthd<InvoiceResponse>(`api/v1/subscription/${id}/renew`, "GET");
}
listSubscriptions() {
return this.#getJsonAuthd<Array<Subscription>>("api/v1/subscription");
}
@ -93,7 +113,7 @@ export default class SnortApi {
const obj = await rsp.json();
if ("error" in obj) {
throw new Error(obj.error);
throw new SubscriptionError(obj.error, obj.code);
}
return obj as T;
}