feat: license upgrade confirm
This commit is contained in:
@@ -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": {
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -60,11 +60,13 @@
|
||||
"price": {
|
||||
"pro": {
|
||||
"title": "VoceChat 专业版",
|
||||
"desc": "用户数限制: 100人"
|
||||
"desc": "用户数限制: 100人",
|
||||
"price": "¥12/年"
|
||||
},
|
||||
"supreme": {
|
||||
"title": "VoceChat 终极版",
|
||||
"desc": "用户数限制: 无限制"
|
||||
"desc": "用户数限制: 无限制",
|
||||
"price": "¥3500/年"
|
||||
}
|
||||
},
|
||||
"server_update": {
|
||||
|
||||
@@ -85,7 +85,10 @@
|
||||
"back_home": "回到首页",
|
||||
"tip_renewing": "正在更新证书,请勿关闭窗口!",
|
||||
"tip_renewed": "证书更新成功!",
|
||||
"tip_renew_error": "无效的Stripe ID"
|
||||
"tip_renew_error": "无效的Stripe ID",
|
||||
"tip_domain": "证书与域名以及端口相绑定,请确认或修改:",
|
||||
"tip_port": "端口号80,443,可不填",
|
||||
"tip_confirm": "开始支付"
|
||||
},
|
||||
"bot": {
|
||||
"add_api_key": "新增API Key",
|
||||
|
||||
@@ -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);
|
||||
@@ -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";
|
||||
|
||||
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user