From b6cc1765db5f820bf1abfc65091aa3577d8df9ee Mon Sep 17 00:00:00 2001
From: Kieran
Date: Mon, 17 Apr 2023 21:33:55 +0100
Subject: [PATCH 1/3] feat: implement renewal/complete new
---
.../app/src/Nip05/SnortServiceProvider.ts | 9 ++
.../Pages/subscribe/ManageSubscription.tsx | 61 ++++++---
packages/app/src/Pages/subscribe/index.tsx | 126 +++++++++++-------
packages/app/src/SnortApi.ts | 22 ++-
4 files changed, 151 insertions(+), 67 deletions(-)
diff --git a/packages/app/src/Nip05/SnortServiceProvider.ts b/packages/app/src/Nip05/SnortServiceProvider.ts
index 4fcc6d36..c4d58a1d 100644
--- a/packages/app/src/Nip05/SnortServiceProvider.ts
+++ b/packages/app/src/Nip05/SnortServiceProvider.ts
@@ -42,6 +42,15 @@ export default class SnortServiceProvider extends ServiceProvider {
return this.getJsonAuthd
)}
- {error && {error}}
+ {error && {mapSubscriptionErrorCode(error)}}
+ setInvoice("")}
+ title={formatMessage({
+ defaultMessage: "Renew subscription",
+ })}
+ />
>
);
}
diff --git a/packages/app/src/Pages/subscribe/index.tsx b/packages/app/src/Pages/subscribe/index.tsx
index ae3d1c74..78cae993 100644
--- a/packages/app/src/Pages/subscribe/index.tsx
+++ b/packages/app/src/Pages/subscribe/index.tsx
@@ -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 ;
+ case SubscriptionErrorCode.SubscriptionActive:
+ return ;
+ case SubscriptionErrorCode.Duplicate:
+ return ;
+ default:
+ return c.message;
+ }
+}
+
export function SubscribePage() {
const publisher = useEventPublisher();
const api = new SnortApi(undefined, publisher);
const [invoice, setInvoice] = useState("");
+ const [error, setError] = useState();
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 (
-
- {Plans.map(a => {
- const lower = Plans.filter(b => b.id < a.id);
- return (
-
-
-
{mapPlanName(a.id)}
-
- {formatShort(a.price)} sats/mo,
- }}
- />
- :
-
-
-
-
-
- {a.unlocks.map(b => (
- - {mapFeatureName(b)}
- ))}
- {lower.map(b => (
- -
-
-
- ))}
-
+ <>
+
+ {Plans.map(a => {
+ const lower = Plans.filter(b => b.id < a.id);
+ return (
+
+
+
{mapPlanName(a.id)}
+
+ {formatShort(a.price)} sats/mo,
+ }}
+ />
+ :
+
+
+
+
+
+ {a.unlocks.map(b => (
+ - {mapFeatureName(b)}
+ ))}
+ {lower.map(b => (
+ -
+
+
+ ))}
+
+
+
+
subscribe(a.id)}>
+ {a.disabled ? (
+
+ ) : (
+
+ )}
+
+
-
-
subscribe(a.id)}>
- {a.disabled ? (
-
- ) : (
-
- )}
-
-
-
- );
- })}
+ );
+ })}
+
+ {error &&
{mapSubscriptionErrorCode(error)}}
setInvoice("")} />
-
+ >
);
}
diff --git a/packages/app/src/SnortApi.ts b/packages/app/src/SnortApi.ts
index 63917cce..f2acee76 100644
--- a/packages/app/src/SnortApi.ts
+++ b/packages/app/src/SnortApi.ts
@@ -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
(`api/v1/subscription?type=${type}`, "PUT");
}
+ renewSubscription(id: string) {
+ return this.#getJsonAuthd(`api/v1/subscription/${id}/renew`, "GET");
+ }
+
listSubscriptions() {
return this.#getJsonAuthd>("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;
}
From 766c1fe472c109d0a43acbcebf3e3b13d7079134 Mon Sep 17 00:00:00 2001
From: Kieran
Date: Mon, 17 Apr 2023 21:35:10 +0100
Subject: [PATCH 2/3] change send sats title
---
packages/app/src/Pages/subscribe/ManageSubscription.tsx | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/packages/app/src/Pages/subscribe/ManageSubscription.tsx b/packages/app/src/Pages/subscribe/ManageSubscription.tsx
index bfd37026..bb5b5aea 100644
--- a/packages/app/src/Pages/subscribe/ManageSubscription.tsx
+++ b/packages/app/src/Pages/subscribe/ManageSubscription.tsx
@@ -146,7 +146,7 @@ export default function ManageSubscriptionPage() {
show={invoice !== ""}
onClose={() => setInvoice("")}
title={formatMessage({
- defaultMessage: "Renew subscription",
+ defaultMessage: "Pay for subscription",
})}
/>
>
From 6354fc3a174a8b19826702c109d10aa65eac2eea Mon Sep 17 00:00:00 2001
From: Kieran
Date: Mon, 17 Apr 2023 22:22:00 +0100
Subject: [PATCH 3/3] claim handle
---
packages/app/src/Element/Nip5Service.tsx | 49 +++++--
packages/app/src/Pages/Verification.tsx | 20 +--
packages/app/src/Pages/new/GetVerified.tsx | 6 +-
.../Pages/subscribe/ManageSubscription.tsx | 107 +-------------
.../src/Pages/subscribe/SubscriptionCard.tsx | 137 ++++++++++++++++++
packages/app/src/SnortApi.ts | 5 +-
6 files changed, 199 insertions(+), 125 deletions(-)
create mode 100644 packages/app/src/Pages/subscribe/SubscriptionCard.tsx
diff --git a/packages/app/src/Element/Nip5Service.tsx b/packages/app/src/Element/Nip5Service.tsx
index 4106d944..20962783 100644
--- a/packages/app/src/Element/Nip5Service.tsx
+++ b/packages/app/src/Element/Nip5Service.tsx
@@ -21,6 +21,7 @@ import { useUserProfile } from "Hooks/useUserProfile";
import useEventPublisher from "Feed/EventPublisher";
import { debounce } from "Util";
import useLogin from "Hooks/useLogin";
+import SnortServiceProvider from "Nip05/SnortServiceProvider";
import messages from "./messages";
@@ -31,6 +32,7 @@ type Nip05ServiceProps = {
link: string;
supportLink: string;
helpText?: boolean;
+ forSubscription?: string;
onChange?(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) {
if (user && publisher) {
const nip05 = `${handle}@${domain}`;
@@ -245,16 +263,27 @@ export default function Nip5Service(props: Nip05ServiceProps) {
)}
{availabilityResponse?.available && !registerStatus && (
-
-
-
- {availabilityResponse.quote?.data.type}
-
-
startBuy(handle, domain)}>
-
+ {!props.forSubscription && (
+
+
+
+ {availabilityResponse.quote?.data.type}
+
+ )}
+
+ props.forSubscription
+ ? claimForSubscription(handle, domain, props.forSubscription)
+ : startBuy(handle, domain)
+ }>
+ {props.forSubscription ? (
+
+ ) : (
+
+ )}
)}
diff --git a/packages/app/src/Pages/Verification.tsx b/packages/app/src/Pages/Verification.tsx
index 25ebfd5b..158a0889 100644
--- a/packages/app/src/Pages/Verification.tsx
+++ b/packages/app/src/Pages/Verification.tsx
@@ -7,14 +7,16 @@ import messages from "./messages";
import "./Verification.css";
-export const services = [
- {
- name: "Snort",
- service: `${ApiHost}/api/v1/n5sp`,
- link: "https://snort.social/",
- supportLink: "https://snort.social/help",
- about: ,
- },
+export const SnortNostrAddressService = {
+ name: "Snort",
+ service: `${ApiHost}/api/v1/n5sp`,
+ link: "https://snort.social/",
+ supportLink: "https://snort.social/help",
+ about: ,
+};
+
+export const Nip5Services = [
+ SnortNostrAddressService,
{
name: "Nostr Plebs",
service: "https://nostrplebs.com/api/v1",
@@ -48,7 +50,7 @@ export default function VerificationPage() {
- {services.map(a => (
+ {Nip5Services.map(a => (
))}
diff --git a/packages/app/src/Pages/new/GetVerified.tsx b/packages/app/src/Pages/new/GetVerified.tsx
index d3d7436f..a420596c 100644
--- a/packages/app/src/Pages/new/GetVerified.tsx
+++ b/packages/app/src/Pages/new/GetVerified.tsx
@@ -3,7 +3,7 @@ import { FormattedMessage } from "react-intl";
import { useNavigate } from "react-router-dom";
import Logo from "Element/Logo";
-import { services } from "Pages/Verification";
+import { Nip5Services } from "Pages/Verification";
import Nip5Service from "Element/Nip5Service";
import ProfileImage from "Element/ProfileImage";
import { useUserProfile } from "Hooks/useUserProfile";
@@ -66,7 +66,7 @@ export default function GetVerified() {
setIsVerified(true)}
@@ -85,7 +85,7 @@ export default function GetVerified() {
setIsVerified(true)}
diff --git a/packages/app/src/Pages/subscribe/ManageSubscription.tsx b/packages/app/src/Pages/subscribe/ManageSubscription.tsx
index bb5b5aea..5da37bd4 100644
--- a/packages/app/src/Pages/subscribe/ManageSubscription.tsx
+++ b/packages/app/src/Pages/subscribe/ManageSubscription.tsx
@@ -1,23 +1,19 @@
import { useEffect, useState } from "react";
-import { FormattedDate, FormattedMessage, FormattedNumber, useIntl } from "react-intl";
+import { FormattedMessage } from "react-intl";
import { Link } from "react-router-dom";
import PageSpinner from "Element/PageSpinner";
import useEventPublisher from "Feed/EventPublisher";
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";
+import { mapSubscriptionErrorCode } from ".";
+import SubscriptionCard from "./SubscriptionCard";
export default function ManageSubscriptionPage() {
const publisher = useEventPublisher();
- const { formatMessage } = useIntl();
const api = new SnortApi(undefined, publisher);
const [subs, setSubs] = useState>();
const [error, setError] = useState();
- const [invoice, setInvoice] = useState("");
useEffect(() => {
(async () => {
@@ -32,17 +28,6 @@ export default function ManageSubscriptionPage() {
})();
}, []);
- 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 ;
}
@@ -51,81 +36,9 @@ export default function ManageSubscriptionPage() {
- {subs.map(a => {
- const created = new Date(a.created);
- 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 = a.state === "expired";
- const isNew = a.state === "new";
- return (
-
-
-
- {mapPlanName(a.type)}
-
-
-
-
- :
-
-
- {daysToExpire >= 1 && (
-
-
- :
-
-
- )}
- {daysToExpire >= 0 && daysToExpire < 1 && (
-
-
- :
-
-
- )}
- {isExpired && (
-
-
-
- )}
- {isNew && (
-
-
-
- )}
-
- {(isExpired || isNew) && (
-
-
renew(a.id)}>
- {isExpired ? (
-
- ) : (
-
- )}
-
-
- )}
-
- );
- })}
+ {subs.map(a => (
+
+ ))}
{subs.length === 0 && (
)}
{error && {mapSubscriptionErrorCode(error)}}
- setInvoice("")}
- title={formatMessage({
- defaultMessage: "Pay for subscription",
- })}
- />
>
);
}
diff --git a/packages/app/src/Pages/subscribe/SubscriptionCard.tsx b/packages/app/src/Pages/subscribe/SubscriptionCard.tsx
new file mode 100644
index 00000000..6bc053be
--- /dev/null
+++ b/packages/app/src/Pages/subscribe/SubscriptionCard.tsx
@@ -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();
+
+ 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 && (
+ <>
+
+
+
+ (sub.handle = h)}
+ />
+ >
+ )}
+ {sub.handle && }
+ >
+ );
+ }
+
+ return (
+ <>
+
+
+
+ {mapPlanName(sub.type)}
+
+
+
+
+ :
+
+
+ {daysToExpire >= 1 && (
+
+
+ :
+
+
+ )}
+ {daysToExpire >= 0 && daysToExpire < 1 && (
+
+
+ :
+
+
+ )}
+ {isExpired && (
+
+
+
+ )}
+ {isNew && (
+
+
+
+ )}
+
+ {(isExpired || isNew) && (
+
+
renew(sub.id)}>
+ {isExpired ? : }
+
+
+ )}
+ {isPaid && subFeatures()}
+
+ setInvoice("")}
+ title={formatMessage({
+ defaultMessage: "Pay for subscription",
+ })}
+ />
+ {error && {mapSubscriptionErrorCode(error)}}
+ >
+ );
+}
diff --git a/packages/app/src/SnortApi.ts b/packages/app/src/SnortApi.ts
index f2acee76..f7625796 100644
--- a/packages/app/src/SnortApi.ts
+++ b/packages/app/src/SnortApi.ts
@@ -20,9 +20,10 @@ export interface InvoiceResponse {
export interface Subscription {
id: string;
type: SubscriptionType;
- created: string;
- expires: string;
+ created: number;
+ expires: number;
state: "new" | "expired" | "paid";
+ handle?: string;
}
export enum SubscriptionErrorCode {