Files
ColdBreeze-chat-web/src/routes/setting/License/LicensePriceListModal.tsx
T
Will Sheng c8199da34e 增加VoceSpace视频会议功能 (#275)
* vocespace video conf page

[feat]: get and update vocespace config

[feat]: get and update vocespace config

[feat]: vocespace setting box in home page

remove zip

send vocespace link

[feat]: vocespace license upgrade

* docs(readme): add Chinese readme and update package mananger to pnpm

* docs: Remove outdated contact information and completed feature requests (#272)

* feat: 增加passkey支持 (#270)

* chore: add .pnpm-store to .gitignore

* feat: implement passkey authentication and management features

- Added passkey login and registration endpoints to the auth service.
- Introduced PasskeyManagement component for user passkey management.
- Updated localization files to include passkey-related strings in English and Chinese.
- Created utility functions for handling passkey operations in WebAuthn.
- Adjusted login page to support passkey login flow.
- Modified configuration for local development server.

* feat: enhance passkey management with modal input and localization updates

- Added a modal for entering passkey names during registration in the PasskeyManagement component.
- Updated English and Chinese localization files to include new strings for passkey name input.
- Improved user experience by validating passkey name input before registration.

* feat: enhance passkey login experience with localization updates

- Added new localization strings for passkey authentication errors and status messages in English and Chinese.
- Updated the login page to utilize localized strings for improved user feedback during passkey login attempts.

* feat: add name field to UserPasskey interface for enhanced user identification

* feat: integrate passkey support across login and settings

- Added passkey option to login configuration and updated related components to conditionally render passkey login button.
- Enhanced MyAccount and Logins components to manage passkey settings and display passkey management options based on configuration.
- Updated English and Chinese localization files to include new strings for passkey functionality.

* feat: added server version check for passkey

* chore: update version to 0.9.30 in package.json

* chore: update version to 0.9.31 in package.json and add magic link authentication strings in English and Chinese

* i18n

[feat]: undeploy or vocespace.com

[fix]: theme fit for ui

[feat]: ip or alert

version v0.9.32

[fix]: use version compare for feat

[fix]: api hook inner useEffect

---------

Co-authored-by: haorwen <haorwen@qq.com>
2025-12-18 23:33:24 +08:00

201 lines
6.8 KiB
TypeScript

import { ChangeEvent, FC, useState } from "react";
import toast from "react-hot-toast";
import { useTranslation } from "react-i18next";
import Tippy from "@tippyjs/react";
import dayjs from "dayjs";
import * as linkify from "linkifyjs";
import { getLicensePriceList } from "@/app/config";
import { useGetLicensePaymentUrlMutation } from "@/app/services/server";
import { PriceSubscriptionDuration, PriceType } from "@/types/common";
import Modal from "@/components/Modal";
import Button from "@/components/styled/Button";
import Input from "@/components/styled/Input";
import StyledModal from "@/components/styled/Modal";
import StyledRadio from "@/components/styled/Radio";
// import { LicenseMetadata, RenewLicense } from "@/types/server";
interface Props {
closeModal: () => void;
// domain: string;
}
const getExpireDay = (sub_dur: PriceSubscriptionDuration) => {
const currDate = dayjs();
// 默认10年
let res = currDate;
switch (sub_dur) {
case "year":
res = currDate.add(100, "year");
break;
case "month":
res = currDate.add(1, "month");
break;
case "quarter":
res = currDate.add(3, "month");
break;
default:
break;
}
return res.format("YYYY-MM-DD");
};
const LicensePriceList = getLicensePriceList();
const LicensePriceListModal: FC<Props> = ({ closeModal }) => {
const { t } = useTranslation("setting");
const { t: ct } = useTranslation();
const [getUrl, { isLoading, isSuccess }] = useGetLicensePaymentUrlMutation();
const [host, setHost] = useState(location.hostname);
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 hostPrefixed = `https://${host}`;
if (!linkify.test(hostPrefixed)) {
toast.error("Invalid Host");
return;
}
if (new URL(hostPrefixed).port !== "" || host.endsWith(":443")) {
toast.error(t("license.tip_port"));
return;
}
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,
cancel_url: location.href,
success_url: `${location.origin}/#/cb/payment_success`,
});
if ("error" in resp) {
toast.error("Payment link initialized failed!");
return;
}
console.log("aaaa", resp.data);
// todo
location.href = resp.data.session_url;
};
const handlePriceSelect = (price: string) => {
console.log(price);
setSelectPrice(price);
};
const handleUpdateHost = (evt: ChangeEvent<HTMLInputElement>) => {
setHost(evt.target.value);
};
const togglePopUpVisible = () => {
setPopUpVisible((prev) => !prev);
};
const handleTalk = () => {
window.open("https://buy.stripe.com/bJeaEX9ex2PUer2aLe6c00O", "_blank");
};
const isBooking = selectPrice.includes("booking");
return (
<Modal id="modal-modal">
<StyledModal
footer={
<div className="text-sm text-gray-400 dark:text-gray-100 mt-3">
{t("vocespace.prerequisite.4")}
</div>
}
// className="!min-w-[480px]"
title={t("license.renew")}
description={t("license.renew_select")}
buttons={
<>
<Button onClick={closeModal} className="ghost">
{ct("action.cancel")}
</Button>
{isBooking ? (
<Button
className="text-sm text-white bg-primary-400 break-keep shadow rounded-lg px-3.5 py-2.5 md:hover:bg-primary-500 active:bg-primary-500 disabled:bg-gray-300"
onClick={handleTalk}
>
{t("license.renew")}
</Button>
) : (
<Tippy
visible={popUpVisible}
interactive
placement="top-end"
offset={[-50, -40]}
trigger="click"
content={
<div className="p-3 rounded-lg border border-solid border-gray-200 dark:border-gray-900 flex flex-col items-start gap-3 w-[430px] bg-white dark:bg-gray-800 shadow shadow-gray-200 dark:shadow-gray-900 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 text-left">
{" "}
{t("license.tip_port")}
</span>
<div className="flex gap-3 whitespace-nowrap">
<Button className="mini cancel" 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}
className="text-sm text-white bg-primary-400 break-keep shadow rounded-lg px-3.5 py-2.5 md:hover:bg-primary-500 active:bg-primary-500 disabled:bg-gray-300"
>
{" "}
{t("license.renew")}
</button>
</Tippy>
)}
</>
}
>
<StyledRadio
options={LicensePriceList.map(
({ title, desc, price }) =>
`${title} ${desc ? `[${desc}]` : ""}${price ? `[${price}]` : ""}`
)}
values={LicensePriceList.map(
({ pid, limit, type = "payment", sub_dur = "month" }) =>
`${pid}|${limit}|${type}|${sub_dur}`
)}
value={selectPrice}
onChange={(v) => {
handlePriceSelect(v);
}}
/>
</StyledModal>
</Modal>
);
};
export default LicensePriceListModal;