c8199da34e
* 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>
201 lines
6.8 KiB
TypeScript
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;
|