forked from Kieran/snort
Merge pull request #522 from v0l/subscription-handle
Subscription handle
This commit is contained in:
commit
4f1e5df1a8
@ -21,6 +21,7 @@ import { useUserProfile } from "Hooks/useUserProfile";
|
|||||||
import useEventPublisher from "Feed/EventPublisher";
|
import useEventPublisher from "Feed/EventPublisher";
|
||||||
import { debounce } from "Util";
|
import { debounce } from "Util";
|
||||||
import useLogin from "Hooks/useLogin";
|
import useLogin from "Hooks/useLogin";
|
||||||
|
import SnortServiceProvider from "Nip05/SnortServiceProvider";
|
||||||
|
|
||||||
import messages from "./messages";
|
import messages from "./messages";
|
||||||
|
|
||||||
@ -31,6 +32,7 @@ type Nip05ServiceProps = {
|
|||||||
link: string;
|
link: string;
|
||||||
supportLink: string;
|
supportLink: string;
|
||||||
helpText?: boolean;
|
helpText?: boolean;
|
||||||
|
forSubscription?: string;
|
||||||
onChange?(h: string): void;
|
onChange?(h: string): void;
|
||||||
onSuccess?(h: string): void;
|
onSuccess?(h: string): void;
|
||||||
};
|
};
|
||||||
@ -188,6 +190,22 @@ export default function Nip5Service(props: Nip05ServiceProps) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function claimForSubscription(handle: string, domain: string, sub: string) {
|
||||||
|
if (!pubkey || !publisher) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const svcEx = new SnortServiceProvider(publisher, props.service);
|
||||||
|
const rsp = await svcEx.registerForSubscription(handle, domain, sub);
|
||||||
|
if ("error" in rsp) {
|
||||||
|
setError(rsp);
|
||||||
|
} else {
|
||||||
|
if (props.onSuccess) {
|
||||||
|
const nip05 = `${handle}@${domain}`;
|
||||||
|
props.onSuccess(nip05);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
async function updateProfile(handle: string, domain: string) {
|
async function updateProfile(handle: string, domain: string) {
|
||||||
if (user && publisher) {
|
if (user && publisher) {
|
||||||
const nip05 = `${handle}@${domain}`;
|
const nip05 = `${handle}@${domain}`;
|
||||||
@ -245,16 +263,27 @@ export default function Nip5Service(props: Nip05ServiceProps) {
|
|||||||
)}
|
)}
|
||||||
{availabilityResponse?.available && !registerStatus && (
|
{availabilityResponse?.available && !registerStatus && (
|
||||||
<div className="flex">
|
<div className="flex">
|
||||||
<div className="mr10">
|
{!props.forSubscription && (
|
||||||
<FormattedMessage
|
<div className="mr10">
|
||||||
{...messages.Sats}
|
<FormattedMessage
|
||||||
values={{ n: formatShort(unwrap(availabilityResponse.quote?.price)) }}
|
{...messages.Sats}
|
||||||
/>
|
values={{ n: formatShort(unwrap(availabilityResponse.quote?.price)) }}
|
||||||
<br />
|
/>
|
||||||
<small>{availabilityResponse.quote?.data.type}</small>
|
<br />
|
||||||
</div>
|
<small>{availabilityResponse.quote?.data.type}</small>
|
||||||
<AsyncButton onClick={() => startBuy(handle, domain)}>
|
</div>
|
||||||
<FormattedMessage {...messages.BuyNow} />
|
)}
|
||||||
|
<AsyncButton
|
||||||
|
onClick={() =>
|
||||||
|
props.forSubscription
|
||||||
|
? claimForSubscription(handle, domain, props.forSubscription)
|
||||||
|
: startBuy(handle, domain)
|
||||||
|
}>
|
||||||
|
{props.forSubscription ? (
|
||||||
|
<FormattedMessage defaultMessage="Claim Now" />
|
||||||
|
) : (
|
||||||
|
<FormattedMessage {...messages.BuyNow} />
|
||||||
|
)}
|
||||||
</AsyncButton>
|
</AsyncButton>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
@ -42,6 +42,15 @@ export default class SnortServiceProvider extends ServiceProvider {
|
|||||||
return this.getJsonAuthd<object>(`/${id}`, "PATCH", obj);
|
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>(
|
async getJsonAuthd<T>(
|
||||||
path: string,
|
path: string,
|
||||||
method?: "GET" | string,
|
method?: "GET" | string,
|
||||||
|
@ -7,14 +7,16 @@ import messages from "./messages";
|
|||||||
|
|
||||||
import "./Verification.css";
|
import "./Verification.css";
|
||||||
|
|
||||||
export const services = [
|
export const SnortNostrAddressService = {
|
||||||
{
|
name: "Snort",
|
||||||
name: "Snort",
|
service: `${ApiHost}/api/v1/n5sp`,
|
||||||
service: `${ApiHost}/api/v1/n5sp`,
|
link: "https://snort.social/",
|
||||||
link: "https://snort.social/",
|
supportLink: "https://snort.social/help",
|
||||||
supportLink: "https://snort.social/help",
|
about: <FormattedMessage {...messages.SnortSocialNip} />,
|
||||||
about: <FormattedMessage {...messages.SnortSocialNip} />,
|
};
|
||||||
},
|
|
||||||
|
export const Nip5Services = [
|
||||||
|
SnortNostrAddressService,
|
||||||
{
|
{
|
||||||
name: "Nostr Plebs",
|
name: "Nostr Plebs",
|
||||||
service: "https://nostrplebs.com/api/v1",
|
service: "https://nostrplebs.com/api/v1",
|
||||||
@ -48,7 +50,7 @@ export default function VerificationPage() {
|
|||||||
</li>
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
|
|
||||||
{services.map(a => (
|
{Nip5Services.map(a => (
|
||||||
<Nip5Service key={a.name} {...a} />
|
<Nip5Service key={a.name} {...a} />
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
|
@ -3,7 +3,7 @@ import { FormattedMessage } from "react-intl";
|
|||||||
import { useNavigate } from "react-router-dom";
|
import { useNavigate } from "react-router-dom";
|
||||||
|
|
||||||
import Logo from "Element/Logo";
|
import Logo from "Element/Logo";
|
||||||
import { services } from "Pages/Verification";
|
import { Nip5Services } from "Pages/Verification";
|
||||||
import Nip5Service from "Element/Nip5Service";
|
import Nip5Service from "Element/Nip5Service";
|
||||||
import ProfileImage from "Element/ProfileImage";
|
import ProfileImage from "Element/ProfileImage";
|
||||||
import { useUserProfile } from "Hooks/useUserProfile";
|
import { useUserProfile } from "Hooks/useUserProfile";
|
||||||
@ -66,7 +66,7 @@ export default function GetVerified() {
|
|||||||
<div className="nip-container">
|
<div className="nip-container">
|
||||||
<Nip5Service
|
<Nip5Service
|
||||||
key="snort"
|
key="snort"
|
||||||
{...services[0]}
|
{...Nip5Services[0]}
|
||||||
helpText={false}
|
helpText={false}
|
||||||
onChange={setNip05}
|
onChange={setNip05}
|
||||||
onSuccess={() => setIsVerified(true)}
|
onSuccess={() => setIsVerified(true)}
|
||||||
@ -85,7 +85,7 @@ export default function GetVerified() {
|
|||||||
<div className="nip-container">
|
<div className="nip-container">
|
||||||
<Nip5Service
|
<Nip5Service
|
||||||
key="nostrplebs"
|
key="nostrplebs"
|
||||||
{...services[1]}
|
{...Nip5Services[1]}
|
||||||
helpText={false}
|
helpText={false}
|
||||||
onChange={setNip05}
|
onChange={setNip05}
|
||||||
onSuccess={() => setIsVerified(true)}
|
onSuccess={() => setIsVerified(true)}
|
||||||
|
@ -1,19 +1,19 @@
|
|||||||
import { useEffect, useState } from "react";
|
import { useEffect, useState } from "react";
|
||||||
import { FormattedDate, FormattedMessage, FormattedNumber } from "react-intl";
|
import { FormattedMessage } from "react-intl";
|
||||||
import { Link } from "react-router-dom";
|
import { Link } from "react-router-dom";
|
||||||
|
|
||||||
import PageSpinner from "Element/PageSpinner";
|
import PageSpinner from "Element/PageSpinner";
|
||||||
import useEventPublisher from "Feed/EventPublisher";
|
import useEventPublisher from "Feed/EventPublisher";
|
||||||
import SnortApi, { Subscription } from "SnortApi";
|
import SnortApi, { Subscription, SubscriptionError } from "SnortApi";
|
||||||
import { mapPlanName } from ".";
|
import { mapSubscriptionErrorCode } from ".";
|
||||||
import Icon from "Icons/Icon";
|
import SubscriptionCard from "./SubscriptionCard";
|
||||||
|
|
||||||
export default function ManageSubscriptionPage() {
|
export default function ManageSubscriptionPage() {
|
||||||
const publisher = useEventPublisher();
|
const publisher = useEventPublisher();
|
||||||
const api = new SnortApi(undefined, publisher);
|
const api = new SnortApi(undefined, publisher);
|
||||||
|
|
||||||
const [subs, setSubs] = useState<Array<Subscription>>();
|
const [subs, setSubs] = useState<Array<Subscription>>();
|
||||||
const [error, setError] = useState("");
|
const [error, setError] = useState<SubscriptionError>();
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
(async () => {
|
(async () => {
|
||||||
@ -21,10 +21,8 @@ export default function ManageSubscriptionPage() {
|
|||||||
const s = await api.listSubscriptions();
|
const s = await api.listSubscriptions();
|
||||||
setSubs(s);
|
setSubs(s);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
if (e instanceof Error) {
|
if (e instanceof SubscriptionError) {
|
||||||
setError(e.message);
|
setError(e);
|
||||||
} else {
|
|
||||||
setError("Unknown error");
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
})();
|
})();
|
||||||
@ -38,71 +36,9 @@ export default function ManageSubscriptionPage() {
|
|||||||
<h2>
|
<h2>
|
||||||
<FormattedMessage defaultMessage="Subscriptions" />
|
<FormattedMessage defaultMessage="Subscriptions" />
|
||||||
</h2>
|
</h2>
|
||||||
{subs.map(a => {
|
{subs.map(a => (
|
||||||
const created = new Date(a.created);
|
<SubscriptionCard sub={a} key={a.id} />
|
||||||
const expires = new Date(a.expires);
|
))}
|
||||||
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;
|
|
||||||
return (
|
|
||||||
<div key={a.id} className="card">
|
|
||||||
<div className="flex card-title">
|
|
||||||
<Icon name="badge" className="mr5" size={25} />
|
|
||||||
{mapPlanName(a.type)}
|
|
||||||
</div>
|
|
||||||
<div className="flex">
|
|
||||||
<p className="f-1">
|
|
||||||
<FormattedMessage defaultMessage="Created" />
|
|
||||||
:
|
|
||||||
<time dateTime={created.toISOString()}>
|
|
||||||
<FormattedDate value={created} dateStyle="full" />
|
|
||||||
</time>
|
|
||||||
</p>
|
|
||||||
{daysToExpire >= 1 && (
|
|
||||||
<p className="f-1">
|
|
||||||
<FormattedMessage defaultMessage="Expires" />
|
|
||||||
:
|
|
||||||
<time dateTime={expires.toISOString()}>
|
|
||||||
<FormattedMessage
|
|
||||||
defaultMessage="{n} days"
|
|
||||||
values={{
|
|
||||||
n: <FormattedNumber value={daysToExpire} maximumFractionDigits={0} />,
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</time>
|
|
||||||
</p>
|
|
||||||
)}
|
|
||||||
{daysToExpire >= 0 && daysToExpire < 1 && (
|
|
||||||
<p className="f-1">
|
|
||||||
<FormattedMessage defaultMessage="Expires" />
|
|
||||||
:
|
|
||||||
<time dateTime={expires.toISOString()}>
|
|
||||||
<FormattedMessage
|
|
||||||
defaultMessage="{n} hours"
|
|
||||||
values={{
|
|
||||||
n: <FormattedNumber value={hoursToExpire} maximumFractionDigits={0} />,
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</time>
|
|
||||||
</p>
|
|
||||||
)}
|
|
||||||
{daysToExpire < 0 && (
|
|
||||||
<p className="f-1 error">
|
|
||||||
<FormattedMessage defaultMessage="Expired" />
|
|
||||||
</p>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
{isExpired && (
|
|
||||||
<div className="flex">
|
|
||||||
<button>
|
|
||||||
<FormattedMessage defaultMessage="Renew" />
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
})}
|
|
||||||
{subs.length === 0 && (
|
{subs.length === 0 && (
|
||||||
<p>
|
<p>
|
||||||
<FormattedMessage
|
<FormattedMessage
|
||||||
@ -117,7 +53,7 @@ export default function ManageSubscriptionPage() {
|
|||||||
/>
|
/>
|
||||||
</p>
|
</p>
|
||||||
)}
|
)}
|
||||||
{error && <b className="error">{error}</b>}
|
{error && <b className="error">{mapSubscriptionErrorCode(error)}</b>}
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
137
packages/app/src/Pages/subscribe/SubscriptionCard.tsx
Normal file
137
packages/app/src/Pages/subscribe/SubscriptionCard.tsx
Normal file
@ -0,0 +1,137 @@
|
|||||||
|
import { FormattedMessage, FormattedDate, FormattedNumber, useIntl } from "react-intl";
|
||||||
|
import { useState } from "react";
|
||||||
|
|
||||||
|
import SnortApi, { Subscription, SubscriptionError } from "SnortApi";
|
||||||
|
import { mapPlanName, mapSubscriptionErrorCode } from ".";
|
||||||
|
import AsyncButton from "Element/AsyncButton";
|
||||||
|
import Icon from "Icons/Icon";
|
||||||
|
import useEventPublisher from "Feed/EventPublisher";
|
||||||
|
import SendSats from "Element/SendSats";
|
||||||
|
import Nip5Service from "Element/Nip5Service";
|
||||||
|
import { SnortNostrAddressService } from "Pages/Verification";
|
||||||
|
import Nip05 from "Element/Nip05";
|
||||||
|
|
||||||
|
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();
|
||||||
|
const daysToExpire = Math.floor((expires.getTime() - now.getTime()) / 8.64e7);
|
||||||
|
const hoursToExpire = Math.floor((expires.getTime() - now.getTime()) / 3.6e6);
|
||||||
|
const isExpired = sub.state === "expired";
|
||||||
|
const isNew = sub.state === "new";
|
||||||
|
const isPaid = sub.state === "paid";
|
||||||
|
|
||||||
|
const [invoice, setInvoice] = useState("");
|
||||||
|
const [error, setError] = useState<SubscriptionError>();
|
||||||
|
|
||||||
|
async function renew(id: string) {
|
||||||
|
const api = new SnortApi(undefined, publisher);
|
||||||
|
try {
|
||||||
|
const rsp = await api.renewSubscription(id);
|
||||||
|
setInvoice(rsp.pr);
|
||||||
|
} catch (e) {
|
||||||
|
if (e instanceof SubscriptionError) {
|
||||||
|
setError(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function subFeatures() {
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
{!sub.handle && (
|
||||||
|
<>
|
||||||
|
<h3>
|
||||||
|
<FormattedMessage defaultMessage="Claim your included Snort nostr address" />
|
||||||
|
</h3>
|
||||||
|
<Nip5Service
|
||||||
|
{...SnortNostrAddressService}
|
||||||
|
helpText={false}
|
||||||
|
forSubscription={sub.id}
|
||||||
|
onSuccess={h => (sub.handle = h)}
|
||||||
|
/>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
{sub.handle && <Nip05 nip05={sub.handle} pubkey={""} verifyNip={false} />}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<div className="card">
|
||||||
|
<div className="flex card-title">
|
||||||
|
<Icon name="badge" className="mr5" size={25} />
|
||||||
|
{mapPlanName(sub.type)}
|
||||||
|
</div>
|
||||||
|
<div className="flex">
|
||||||
|
<p className="f-1">
|
||||||
|
<FormattedMessage defaultMessage="Created" />
|
||||||
|
:
|
||||||
|
<time dateTime={created.toISOString()}>
|
||||||
|
<FormattedDate value={created} dateStyle="full" />
|
||||||
|
</time>
|
||||||
|
</p>
|
||||||
|
{daysToExpire >= 1 && (
|
||||||
|
<p className="f-1">
|
||||||
|
<FormattedMessage defaultMessage="Expires" />
|
||||||
|
:
|
||||||
|
<time dateTime={expires.toISOString()}>
|
||||||
|
<FormattedMessage
|
||||||
|
defaultMessage="{n} days"
|
||||||
|
values={{
|
||||||
|
n: <FormattedNumber value={daysToExpire} maximumFractionDigits={0} />,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</time>
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
{daysToExpire >= 0 && daysToExpire < 1 && (
|
||||||
|
<p className="f-1">
|
||||||
|
<FormattedMessage defaultMessage="Expires" />
|
||||||
|
:
|
||||||
|
<time dateTime={expires.toISOString()}>
|
||||||
|
<FormattedMessage
|
||||||
|
defaultMessage="{n} hours"
|
||||||
|
values={{
|
||||||
|
n: <FormattedNumber value={hoursToExpire} maximumFractionDigits={0} />,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</time>
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
{isExpired && (
|
||||||
|
<p className="f-1 error">
|
||||||
|
<FormattedMessage defaultMessage="Expired" />
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
{isNew && (
|
||||||
|
<p className="f-1">
|
||||||
|
<FormattedMessage defaultMessage="Unpaid" />
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
{(isExpired || isNew) && (
|
||||||
|
<div className="flex">
|
||||||
|
<AsyncButton onClick={() => renew(sub.id)}>
|
||||||
|
{isExpired ? <FormattedMessage defaultMessage="Renew" /> : <FormattedMessage defaultMessage="Pay Now" />}
|
||||||
|
</AsyncButton>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{isPaid && subFeatures()}
|
||||||
|
</div>
|
||||||
|
<SendSats
|
||||||
|
invoice={invoice}
|
||||||
|
show={invoice !== ""}
|
||||||
|
onClose={() => setInvoice("")}
|
||||||
|
title={formatMessage({
|
||||||
|
defaultMessage: "Pay for subscription",
|
||||||
|
})}
|
||||||
|
/>
|
||||||
|
{error && <b className="error">{mapSubscriptionErrorCode(error)}</b>}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
@ -9,7 +9,7 @@ import { LockedFeatures, Plans, SubscriptionType } from "Subscription";
|
|||||||
import ManageSubscriptionPage from "Pages/subscribe/ManageSubscription";
|
import ManageSubscriptionPage from "Pages/subscribe/ManageSubscription";
|
||||||
import AsyncButton from "Element/AsyncButton";
|
import AsyncButton from "Element/AsyncButton";
|
||||||
import useEventPublisher from "Feed/EventPublisher";
|
import useEventPublisher from "Feed/EventPublisher";
|
||||||
import SnortApi from "SnortApi";
|
import SnortApi, { SubscriptionError, SubscriptionErrorCode } from "SnortApi";
|
||||||
import SendSats from "Element/SendSats";
|
import SendSats from "Element/SendSats";
|
||||||
|
|
||||||
export function mapPlanName(id: number) {
|
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() {
|
export function SubscribePage() {
|
||||||
const publisher = useEventPublisher();
|
const publisher = useEventPublisher();
|
||||||
const api = new SnortApi(undefined, publisher);
|
const api = new SnortApi(undefined, publisher);
|
||||||
const [invoice, setInvoice] = useState("");
|
const [invoice, setInvoice] = useState("");
|
||||||
|
const [error, setError] = useState<SubscriptionError>();
|
||||||
|
|
||||||
async function subscribe(type: number) {
|
async function subscribe(type: number) {
|
||||||
const rsp = await api.createSubscription(type);
|
setError(undefined);
|
||||||
setInvoice(rsp.pr);
|
try {
|
||||||
|
const rsp = await api.createSubscription(type);
|
||||||
|
setInvoice(rsp.pr);
|
||||||
|
} catch (e) {
|
||||||
|
if (e instanceof SubscriptionError) {
|
||||||
|
setError(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex subscribe-page">
|
<>
|
||||||
{Plans.map(a => {
|
<div className="flex subscribe-page">
|
||||||
const lower = Plans.filter(b => b.id < a.id);
|
{Plans.map(a => {
|
||||||
return (
|
const lower = Plans.filter(b => b.id < a.id);
|
||||||
<div className={`card flex f-col${a.disabled ? " disabled" : ""}`}>
|
return (
|
||||||
<div className="f-grow">
|
<div className={`card flex f-col${a.disabled ? " disabled" : ""}`}>
|
||||||
<h2>{mapPlanName(a.id)}</h2>
|
<div className="f-grow">
|
||||||
<p>
|
<h2>{mapPlanName(a.id)}</h2>
|
||||||
<FormattedMessage
|
<p>
|
||||||
defaultMessage="Subscribe to Snort {plan} for {price} and receive the following rewards"
|
<FormattedMessage
|
||||||
values={{
|
defaultMessage="Subscribe to Snort {plan} for {price} and receive the following rewards"
|
||||||
plan: mapPlanName(a.id),
|
values={{
|
||||||
price: <b>{formatShort(a.price)} sats/mo</b>,
|
plan: mapPlanName(a.id),
|
||||||
}}
|
price: <b>{formatShort(a.price)} sats/mo</b>,
|
||||||
/>
|
}}
|
||||||
:
|
/>
|
||||||
</p>
|
:
|
||||||
<b>
|
</p>
|
||||||
<FormattedMessage defaultMessage="Not all features are built yet, more features to be added soon!" />
|
<b>
|
||||||
</b>
|
<FormattedMessage defaultMessage="Not all features are built yet, more features to be added soon!" />
|
||||||
<ul>
|
</b>
|
||||||
{a.unlocks.map(b => (
|
<ul>
|
||||||
<li>{mapFeatureName(b)} </li>
|
{a.unlocks.map(b => (
|
||||||
))}
|
<li>{mapFeatureName(b)} </li>
|
||||||
{lower.map(b => (
|
))}
|
||||||
<li>
|
{lower.map(b => (
|
||||||
<FormattedMessage
|
<li>
|
||||||
defaultMessage="Everything in {plan}"
|
<FormattedMessage
|
||||||
values={{
|
defaultMessage="Everything in {plan}"
|
||||||
plan: mapPlanName(b.id),
|
values={{
|
||||||
}}
|
plan: mapPlanName(b.id),
|
||||||
/>
|
}}
|
||||||
</li>
|
/>
|
||||||
))}
|
</li>
|
||||||
</ul>
|
))}
|
||||||
|
</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>
|
||||||
<div className="flex f-center w-max mb10">
|
);
|
||||||
<AsyncButton className="button" disabled={a.disabled} onClick={() => subscribe(a.id)}>
|
})}
|
||||||
{a.disabled ? (
|
</div>
|
||||||
<FormattedMessage defaultMessage="Coming soon" />
|
{error && <b className="error">{mapSubscriptionErrorCode(error)}</b>}
|
||||||
) : (
|
|
||||||
<FormattedMessage defaultMessage="Subscribe" />
|
|
||||||
)}
|
|
||||||
</AsyncButton>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
})}
|
|
||||||
<SendSats invoice={invoice} show={invoice !== ""} onClose={() => setInvoice("")} />
|
<SendSats invoice={invoice} show={invoice !== ""} onClose={() => setInvoice("")} />
|
||||||
</div>
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -20,8 +20,25 @@ export interface InvoiceResponse {
|
|||||||
export interface Subscription {
|
export interface Subscription {
|
||||||
id: string;
|
id: string;
|
||||||
type: SubscriptionType;
|
type: SubscriptionType;
|
||||||
created: string;
|
created: number;
|
||||||
expires: string;
|
expires: number;
|
||||||
|
state: "new" | "expired" | "paid";
|
||||||
|
handle?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
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 {
|
export default class SnortApi {
|
||||||
@ -49,6 +66,10 @@ export default class SnortApi {
|
|||||||
return this.#getJsonAuthd<InvoiceResponse>(`api/v1/subscription?type=${type}`, "PUT");
|
return this.#getJsonAuthd<InvoiceResponse>(`api/v1/subscription?type=${type}`, "PUT");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
renewSubscription(id: string) {
|
||||||
|
return this.#getJsonAuthd<InvoiceResponse>(`api/v1/subscription/${id}/renew`, "GET");
|
||||||
|
}
|
||||||
|
|
||||||
listSubscriptions() {
|
listSubscriptions() {
|
||||||
return this.#getJsonAuthd<Array<Subscription>>("api/v1/subscription");
|
return this.#getJsonAuthd<Array<Subscription>>("api/v1/subscription");
|
||||||
}
|
}
|
||||||
@ -93,7 +114,7 @@ export default class SnortApi {
|
|||||||
|
|
||||||
const obj = await rsp.json();
|
const obj = await rsp.json();
|
||||||
if ("error" in obj) {
|
if ("error" in obj) {
|
||||||
throw new Error(obj.error);
|
throw new SubscriptionError(obj.error, obj.code);
|
||||||
}
|
}
|
||||||
return obj as T;
|
return obj as T;
|
||||||
}
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user