feat: license upgrade confirm

This commit is contained in:
Tristan Yang
2023-01-19 23:03:40 +08:00
parent bc882e786f
commit 397d1c1c23
9 changed files with 80 additions and 18 deletions
+4 -2
View File
@@ -58,11 +58,13 @@
"price": {
"pro": {
"title": "VoceChat Pro",
"desc": "User Limit: 100"
"desc": "User Limit: 100",
"price": "¥12/year"
},
"supreme": {
"title": "VoceChat Supreme",
"desc": "User Limit: No Limit"
"desc": "User Limit: No Limit",
"price": "$499/year"
}
},
"server_update": {
+4 -1
View File
@@ -90,7 +90,10 @@
"back_home": "Back Home",
"tip_renewing": "Renewing the License, do not close the window!",
"tip_renewed": "Renew the License Successfully!",
"tip_renew_error": "Invalided Stripe Session ID"
"tip_renew_error": "Invalided Stripe Session ID",
"tip_domain": "The license is bound to domain and port, please confirm or update your setting:",
"tip_port": "port 80 or 443 can be ignored",
"tip_confirm": "Start Payment"
},
"bot": {
"add_api_key": "Add API Key",
+4 -2
View File
@@ -60,11 +60,13 @@
"price": {
"pro": {
"title": "VoceChat 专业版",
"desc": "用户数限制: 100人"
"desc": "用户数限制: 100人",
"price": "¥12/年"
},
"supreme": {
"title": "VoceChat 终极版",
"desc": "用户数限制: 无限制"
"desc": "用户数限制: 无限制",
"price": "¥3500/年"
}
},
"server_update": {
+4 -1
View File
@@ -85,7 +85,10 @@
"back_home": "回到首页",
"tip_renewing": "正在更新证书,请勿关闭窗口!",
"tip_renewed": "证书更新成功!",
"tip_renew_error": "无效的Stripe ID"
"tip_renew_error": "无效的Stripe ID",
"tip_domain": "证书与域名以及端口相绑定,请确认或修改:",
"tip_port": "端口号80443,可不填",
"tip_confirm": "开始支付"
},
"bot": {
"add_api_key": "新增API Key",
+6
View File
@@ -9,6 +9,7 @@ const BASE_URL = process.env.REACT_APP_RELEASE
let prices: Price[] = [
{
type: "subscription",
price: "",
sub_dur: "year",
// sub_type: "year", //day month year
title: "Pro",
@@ -18,6 +19,7 @@ let prices: Price[] = [
},
{
type: "payment",
price: "",
title: "supreme",
limit: 99999,
pid: "price_1M5VoGGGoUDRyc3j6xhQou6D",
@@ -31,12 +33,14 @@ export const getLicensePriceList = () => {
case 0: {
p.title = i18n.t("price.pro.title");
p.desc = i18n.t("price.pro.desc");
p.price = i18n.t("price.pro.price");
}
break;
// supreme
case 1: {
p.title = i18n.t("price.supreme.title");
p.desc = i18n.t("price.supreme.desc");
p.price = i18n.t("price.supreme.price");
}
break;
@@ -51,6 +55,7 @@ export const getLicensePriceList = () => {
...ps,
{
type: "payment",
price: "1",
title: "Test VoceChat Enterprise",
limit: 99999,
pid: "price_1LkQGpGGoUDRyc3jGTh3GYHw",
@@ -58,6 +63,7 @@ export const getLicensePriceList = () => {
},
{
title: "VoceChat Pro",
price: "1",
limit: 100,
pid: "price_1MMNNCGGoUDRyc3jSIGIsb3C",
desc: "test subscription price",
@@ -1,15 +1,17 @@
import { FC, useState } from "react";
import { ChangeEvent, FC, useState } from "react";
import toast from "react-hot-toast";
import Modal from "../../../common/component/Modal";
import StyledModal from "../../../common/component/styled/Modal";
import Button from "../../../common/component/styled/Button";
import StyledRadio from "../../../common/component/styled/Radio";
import Input from "../../../common/component/styled/Input";
import { useGetLicensePaymentUrlMutation } from "../../../app/services/server";
import { getLicensePriceList } from "../../../app/config";
import { useTranslation } from "react-i18next";
import dayjs from "dayjs";
import { PriceSubscriptionDuration, PriceType } from "../../../types/common";
import Tippy from "@tippyjs/react";
// import { LicenseMetadata, RenewLicense } from "../../types/server";
interface Props {
@@ -41,20 +43,26 @@ const LicensePriceListModal: FC<Props> = ({ closeModal }) => {
const { t } = useTranslation("setting");
const { t: ct } = useTranslation();
const [getUrl, { isLoading, isSuccess }] = useGetLicensePaymentUrlMutation();
const [host, setHost] = useState(location.host);
const [popUpVisible, setPopUpVisible] = useState(false);
const [selectPrice, setSelectPrice] = useState(
`${LicensePriceList[0].pid}|${LicensePriceList[0].limit}|${LicensePriceList[0].type}|${LicensePriceList[0].sub_dur || ""}`
);
const handleRenew = async () => {
const [priceId, user_limit, type, sub_dur = "month"] = selectPrice.split("|") as [string, string, PriceType, PriceSubscriptionDuration];
const metadata = {
user_limit: Number(user_limit),
expire: type == "subscription" ? getExpireDay(sub_dur) : getExpireDay("year"),
// 本地,则*通配符
domain: host.startsWith("localhost") ? "*" : host
};
console.log(metadata);
// return;
const resp = await getUrl({
type,
priceId,
metadata: {
user_limit: Number(user_limit),
expire: type == "subscription" ? getExpireDay(sub_dur) : getExpireDay("year"),
// 本地,则*通配符
domain: location.host.startsWith("localhost") ? "*" : location.host
},
metadata,
cancel_url: location.href,
success_url: `${location.origin}/#/cb/payment_success`
});
@@ -69,9 +77,16 @@ const LicensePriceListModal: FC<Props> = ({ closeModal }) => {
const handlePriceSelect = (price: string) => {
setSelectPrice(price);
};
const handleUpdateHost = (evt: ChangeEvent<HTMLInputElement>) => {
setHost(evt.target.value);
};
const togglePopUpVisible = () => {
setPopUpVisible(prev => !prev);
};
return (
<Modal id="modal-modal">
<StyledModal
className="!min-w-[480px]"
title={t("license.renew")}
description={t("license.renew_select")}
buttons={
@@ -79,14 +94,42 @@ const LicensePriceListModal: FC<Props> = ({ closeModal }) => {
<Button onClick={closeModal} className="ghost">
{ct("action.cancel")}
</Button>
<Button disabled={isLoading || isSuccess} onClick={handleRenew} >
{isLoading ? "Initialize Payment Url" : isSuccess ? "Redirecting" : t("license.renew")}
</Button>
<Tippy
visible={popUpVisible}
interactive
placement="top-end"
offset={[0, -40]}
trigger="click"
content={
<div className="p-3 rounded-lg border border-solid border-gray-200 flex flex-col items-start gap-3 w-[340px] bg-white shadow shadow-gray-200 drop-shadow-xl">
<div className="text-gray-500 text-sm">
{t("license.tip_domain")}
</div>
<Input value={host} onChange={handleUpdateHost} />
<div className="flex justify-between items-center w-full mt-4">
<span className="text-xs text-orange-500"> {t("license.tip_port")}</span>
<div className="flex gap-3">
<Button className="cancel mini" onClick={togglePopUpVisible}>
{ct("action.cancel")}
</Button>
<Button className="mini" disabled={isLoading || isSuccess} onClick={handleRenew}>
{isLoading ? "Initialize Payment Url" : isSuccess ? "Redirecting" : t("license.tip_confirm")}
</Button>
</div>
</div>
</div>
}
>
<Button onClick={togglePopUpVisible}>
{t("license.renew")}
</Button>
</Tippy>
</>
}
>
<StyledRadio
options={LicensePriceList.map(({ title, desc }) => `${title} [${desc}]`)}
options={LicensePriceList.map(({ title, desc, price }) => `${title} [${desc}][${price}]`)}
values={LicensePriceList.map(({ pid, limit, type = "payment", sub_dur = "month" }) => `${pid}|${limit}|${type}|${sub_dur}`)}
value={selectPrice}
onChange={(v) => {
@@ -6,6 +6,7 @@ import LicensePriceListModal from "./LicensePriceListModal";
import clsx from "clsx";
import { useTranslation } from "react-i18next";
import UpdateLicenseModal from "./UpdateLicenseModal";
import Tippy from "@tippyjs/react";
interface ItemProps extends HTMLAttributes<HTMLSpanElement> { label: string, data?: string | number | string[], foldable?: boolean }
const Item = ({ label, data, foldable = false, ...rest }: ItemProps) => {
@@ -25,6 +26,7 @@ const Item = ({ label, data, foldable = false, ...rest }: ItemProps) => {
};
export default function License() {
const { t, i18n } = useTranslation("setting");
const { t: ct } = useTranslation();
const { license: licenseInfo, reachLimit, upsertLicense, upserting, upserted } = useLicense();
const [modalVisible, setModalVisible] = useState(false);
const [updateVisible, setUpdateVisible] = useState(false);
+1 -1
View File
@@ -4,7 +4,7 @@ import Logins from "./config/Logins";
import ConfigFirebase from "./config/Firebase";
import ConfigSMTP from "./config/SMTP";
import APIConfig from "./APIConfig";
import License from "./License/License";
import License from "./License";
import Widget from "./Widget";
import BotConfig from "./BotConfig";
import APIDocument from "./APIDocument";
+1
View File
@@ -6,6 +6,7 @@ export type PriceType = "subscription" | "payment";
export type PriceSubscriptionDuration = "month" | "quarter" | "year";
export type Price = {
title: string,
price: string,
limit: number,
pid: string,
desc: string,