From c39b6a2c4f5ee89a3c899f7327670cd5ad744b68 Mon Sep 17 00:00:00 2001
From: zhujingyang <72259332+zjy365@users.noreply.github.com>
Date: Mon, 25 Sep 2023 10:45:09 +0800
Subject: [PATCH] feat: license desktop page (#3969)
* feat license desktop page
Signed-off-by: jingyang <3161362058@qq.com>
* delete log
---------
Signed-off-by: jingyang <3161362058@qq.com>
---
frontend/desktop/{.env => .env.template} | 6 +-
frontend/desktop/public/icons/download.svg | 3 +
frontend/desktop/public/icons/empty.svg | 5 +
frontend/desktop/public/icons/license.svg | 3 +
frontend/desktop/public/icons/pay_wechat.svg | 1 +
frontend/desktop/public/icons/shell_coin.svg | 3 +
frontend/desktop/public/icons/stripe.svg | 3 +
frontend/desktop/public/icons/token.svg | 10 +
frontend/desktop/public/icons/wechat.svg | 4 +
.../desktop/public/locales/en/common.json | 11 +-
.../public/locales/zh-Hans/common.json | 43 ---
.../desktop/public/locales/zh/common.json | 11 +-
.../src/components/LangSelect/simple.tsx | 43 +++
.../components/signin/auth/useLanguage.tsx | 12 +-
.../components/signin/auth/useProtocol.tsx | 2 +-
.../src/components/user_menu/index.tsx | 33 ++-
frontend/desktop/src/hooks/useBonusBox.tsx | 76 ++++++
.../pages/api/license/createLicenseRecord.ts | 31 +++
.../src/pages/api/license/getLicenseRecord.ts | 29 ++
frontend/desktop/src/pages/api/license/pay.ts | 83 ++++++
.../desktop/src/pages/api/license/result.ts | 32 +++
.../desktop/src/pages/api/platform/getEnv.ts | 13 +-
frontend/desktop/src/pages/api/price/bonus.ts | 28 ++
frontend/desktop/src/pages/api/price/index.ts | 52 ++++
.../src/pages/api/system/getSystemConfig.ts | 1 -
.../license/components/CurrencySymbol.tsx | 22 ++
.../pages/license/components/OuterLink.tsx | 35 +++
.../pages/license/components/Pagination.tsx | 118 ++++++++
.../src/pages/license/components/Recharge.tsx | 258 ++++++++++++++++++
.../src/pages/license/components/Record.tsx | 123 +++++++++
.../license/components/WechatPayment.tsx | 60 ++++
frontend/desktop/src/pages/license/index.tsx | 75 +++++
.../src/services/backend/db/license.ts | 72 +++++
frontend/desktop/src/services/enable.ts | 8 +
frontend/desktop/src/styles/globals.scss | 19 ++
frontend/desktop/src/types/index.ts | 2 +
frontend/desktop/src/types/license.ts | 63 +++++
frontend/desktop/src/types/system.ts | 3 +
frontend/desktop/src/types/valuation.ts | 18 ++
frontend/desktop/src/utils/format.ts | 16 ++
frontend/providers/license/.env.template | 4 +-
.../license/deploy/manifests/appcr.yaml.tmpl | 6 +-
.../license/deploy/manifests/deploy.yaml.tmpl | 25 +-
.../deploy/manifests/ingress.yaml.tmpl | 4 +-
frontend/providers/license/public/favicon.ico | Bin 4286 -> 4286 bytes
.../license/public/icons/license-bg.svg | 17 --
.../license/public/icons/license-sealos.svg | 21 ++
.../license/public/locales/en/common.json | 7 +-
.../license/public/locales/zh/common.json | 9 +-
frontend/providers/license/public/logo.svg | 4 +-
.../src/components/FileSelect/index.tsx | 5 +-
.../src/components/Pagination/index.tsx | 2 +-
.../providers/license/src/constants/theme.ts | 1 -
frontend/providers/license/src/pages/_app.tsx | 2 +-
.../src/pages/api/license/getLicense.tsx | 8 +-
.../license/src/pages/api/platform/getEnv.ts | 5 +-
.../providers/license/src/pages/index.tsx | 220 ++++++++++-----
.../providers/license/src/utils/crypto.ts | 7 +
.../license/src/utils/downloadFIle.ts | 14 +
.../providers/license/src/utils/json2Yaml.ts | 14 +-
frontend/providers/license/src/utils/user.ts | 3 +-
61 files changed, 1612 insertions(+), 196 deletions(-)
rename frontend/desktop/{.env => .env.template} (88%)
create mode 100644 frontend/desktop/public/icons/download.svg
create mode 100644 frontend/desktop/public/icons/empty.svg
create mode 100644 frontend/desktop/public/icons/license.svg
create mode 100644 frontend/desktop/public/icons/pay_wechat.svg
create mode 100644 frontend/desktop/public/icons/shell_coin.svg
create mode 100644 frontend/desktop/public/icons/stripe.svg
create mode 100644 frontend/desktop/public/icons/token.svg
create mode 100644 frontend/desktop/public/icons/wechat.svg
delete mode 100644 frontend/desktop/public/locales/zh-Hans/common.json
create mode 100644 frontend/desktop/src/components/LangSelect/simple.tsx
create mode 100644 frontend/desktop/src/hooks/useBonusBox.tsx
create mode 100644 frontend/desktop/src/pages/api/license/createLicenseRecord.ts
create mode 100644 frontend/desktop/src/pages/api/license/getLicenseRecord.ts
create mode 100644 frontend/desktop/src/pages/api/license/pay.ts
create mode 100644 frontend/desktop/src/pages/api/license/result.ts
create mode 100644 frontend/desktop/src/pages/api/price/bonus.ts
create mode 100644 frontend/desktop/src/pages/api/price/index.ts
create mode 100644 frontend/desktop/src/pages/license/components/CurrencySymbol.tsx
create mode 100644 frontend/desktop/src/pages/license/components/OuterLink.tsx
create mode 100644 frontend/desktop/src/pages/license/components/Pagination.tsx
create mode 100644 frontend/desktop/src/pages/license/components/Recharge.tsx
create mode 100644 frontend/desktop/src/pages/license/components/Record.tsx
create mode 100644 frontend/desktop/src/pages/license/components/WechatPayment.tsx
create mode 100644 frontend/desktop/src/pages/license/index.tsx
create mode 100644 frontend/desktop/src/services/backend/db/license.ts
create mode 100644 frontend/desktop/src/types/license.ts
create mode 100644 frontend/desktop/src/types/valuation.ts
create mode 100644 frontend/providers/license/public/icons/license-sealos.svg
create mode 100644 frontend/providers/license/src/utils/crypto.ts
create mode 100644 frontend/providers/license/src/utils/downloadFIle.ts
diff --git a/frontend/desktop/.env b/frontend/desktop/.env.template
similarity index 88%
rename from frontend/desktop/.env
rename to frontend/desktop/.env.template
index 3de62b3b9f2..53db89856ce 100644
--- a/frontend/desktop/.env
+++ b/frontend/desktop/.env.template
@@ -20,4 +20,8 @@ SEALOS_CLOUD_DOMAIN="cloud.sealos.io"
# WECHAT_ENABLED=
# GITHUB_ENABLED=
# PASSWORD_ENABLED=
-# SMS_ENABLED=
\ No newline at end of file
+# SMS_ENABLED=
+
+# costcenter
+STRIPE_ENABLED=
+STRIPE_PUB=
\ No newline at end of file
diff --git a/frontend/desktop/public/icons/download.svg b/frontend/desktop/public/icons/download.svg
new file mode 100644
index 00000000000..8aed8eea5b5
--- /dev/null
+++ b/frontend/desktop/public/icons/download.svg
@@ -0,0 +1,3 @@
+
diff --git a/frontend/desktop/public/icons/empty.svg b/frontend/desktop/public/icons/empty.svg
new file mode 100644
index 00000000000..f213a801161
--- /dev/null
+++ b/frontend/desktop/public/icons/empty.svg
@@ -0,0 +1,5 @@
+
diff --git a/frontend/desktop/public/icons/license.svg b/frontend/desktop/public/icons/license.svg
new file mode 100644
index 00000000000..9893206ace4
--- /dev/null
+++ b/frontend/desktop/public/icons/license.svg
@@ -0,0 +1,3 @@
+
diff --git a/frontend/desktop/public/icons/pay_wechat.svg b/frontend/desktop/public/icons/pay_wechat.svg
new file mode 100644
index 00000000000..ecab7b8e6a8
--- /dev/null
+++ b/frontend/desktop/public/icons/pay_wechat.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/frontend/desktop/public/icons/shell_coin.svg b/frontend/desktop/public/icons/shell_coin.svg
new file mode 100644
index 00000000000..759c769a0a0
--- /dev/null
+++ b/frontend/desktop/public/icons/shell_coin.svg
@@ -0,0 +1,3 @@
+
diff --git a/frontend/desktop/public/icons/stripe.svg b/frontend/desktop/public/icons/stripe.svg
new file mode 100644
index 00000000000..219cbed9b96
--- /dev/null
+++ b/frontend/desktop/public/icons/stripe.svg
@@ -0,0 +1,3 @@
+
diff --git a/frontend/desktop/public/icons/token.svg b/frontend/desktop/public/icons/token.svg
new file mode 100644
index 00000000000..b0dd76f35a8
--- /dev/null
+++ b/frontend/desktop/public/icons/token.svg
@@ -0,0 +1,10 @@
+
diff --git a/frontend/desktop/public/icons/wechat.svg b/frontend/desktop/public/icons/wechat.svg
new file mode 100644
index 00000000000..b713640cbdf
--- /dev/null
+++ b/frontend/desktop/public/icons/wechat.svg
@@ -0,0 +1,4 @@
+
diff --git a/frontend/desktop/public/locales/en/common.json b/frontend/desktop/public/locales/en/common.json
index 87fff97cd87..e4642f2bff4 100644
--- a/frontend/desktop/public/locales/en/common.json
+++ b/frontend/desktop/public/locales/en/common.json
@@ -79,5 +79,14 @@
"Dissovle Tips": "Dissolving the team will clear all resources. Are you sure you want to disband",
"Enter Confirm.": "Please enter {{value}} to confirm",
"Accept Invitation": "Accept Invitation",
- "Recive Tips": "{{managerName}} invite you join in {{teamName}} as {{role}}"
+ "Recive Tips": "{{managerName}} invite you join in {{teamName}} as {{role}}",
+ "pay with stripe": "Pay With Stripe",
+ "pay with wechat": "Pay With Wechat",
+ "License Buy": "License Buy",
+ "Purchase History": "Purchase History",
+ "Purchase License": "Purchase License",
+ "Remaining Time": "Remaining Time: ",
+ "Please read and agree to the agreement": "Please read and agree to the agreement",
+ "Purchase Link Error": "Purchase Link Error",
+ "You have not purchased the License": "You have not purchased the License"
}
\ No newline at end of file
diff --git a/frontend/desktop/public/locales/zh-Hans/common.json b/frontend/desktop/public/locales/zh-Hans/common.json
deleted file mode 100644
index 66aecacbec8..00000000000
--- a/frontend/desktop/public/locales/zh-Hans/common.json
+++ /dev/null
@@ -1,43 +0,0 @@
-{
- "More Apps": "更多应用",
- "Message Center": "消息中心",
- "Have Read": "已读",
- "Unread": "未读",
- "Read All": "全部已读",
- "Username": "用户名",
- "Log In": "登录",
- "Loading": "加载中",
- "Log Out": "退出账号",
- "From": "来自",
- "Balance": "余额",
- "verify code tips": "6位验证码",
- "phone number tips": "手机号码",
- "Password Login":"密码登录",
- "Password":"密码",
- "Verify password": "确认密码",
- "Verification Code Login":"手机号登录",
- "password tips": "密码为8位以上字符",
- "username tips":"用户名为3-16位的英文或数字的字符",
- "Invalid username or password": "用户名或密码错误",
- "Invalid phone number":"无效的手机号码",
- "Invalid verification code": "无效的验证码",
- "Get code failed": "获取验证码失败",
- "Read and agree": "请阅读并同意下方协议",
- "agree policy": "我已阅读并同意",
- "and": "和",
- "Service Agreement": "服务协议",
- "Privacy Policy": "隐私政策",
- "Get Code": "获取验证码",
- "Bonus": "赠",
- "Payment Result": "支付结果",
- "Payment Successful": "支付成功",
- "In Payment": "支付中 ...",
- "Recharge Amount": "充值金额",
- "Select Amount": "选择金额",
- "View Discount Rules": "查看优惠规则",
- "Payment Status": "支付状态",
- "Scan with WeChat": "微信扫码支付",
- "Charge": "充值",
- "Order Number": "订单号",
- "Confirm": "确认"
-}
diff --git a/frontend/desktop/public/locales/zh/common.json b/frontend/desktop/public/locales/zh/common.json
index ffabefbf910..47f45e1c335 100644
--- a/frontend/desktop/public/locales/zh/common.json
+++ b/frontend/desktop/public/locales/zh/common.json
@@ -73,5 +73,14 @@
"Dissovle Tips": "解散团队会清空所有资源,确定要解散吗?",
"Enter Confirm.": "请输入 {{value}} 确认",
"Accept Invitation": "接受邀请",
- "Recive Tips": "{{managerName}} 邀请你到 {{teamName}} 当 {{role}}"
+ "Recive Tips": "{{managerName}} 邀请你到 {{teamName}} 当 {{role}}",
+ "pay with wechat": "微信支付",
+ "pay with stripe": "Stripe 支付",
+ "License Buy": "License 购买",
+ "Purchase History": "购买记录",
+ "Purchase License": "购买 License",
+ "Remaining Time": "剩余激活时间: ",
+ "Please read and agree to the agreement": "请阅读并同意协议",
+ "Purchase Link Error": "购买链接错误",
+ "You have not purchased the License": "您还没有购买 License"
}
\ No newline at end of file
diff --git a/frontend/desktop/src/components/LangSelect/simple.tsx b/frontend/desktop/src/components/LangSelect/simple.tsx
new file mode 100644
index 00000000000..75cd047114a
--- /dev/null
+++ b/frontend/desktop/src/components/LangSelect/simple.tsx
@@ -0,0 +1,43 @@
+import { setCookie } from '@/utils/cookieUtils';
+import { Flex, FlexProps } from '@chakra-ui/react';
+import { useTranslation } from 'next-i18next';
+import { EVENT_NAME } from 'sealos-desktop-sdk';
+import { masterApp } from 'sealos-desktop-sdk/master';
+
+export default function LangSelectSimple(props: FlexProps) {
+ const { t, i18n } = useTranslation();
+
+ return (
+ {
+ masterApp?.sendMessageToAll({
+ apiName: 'event-bus',
+ eventName: EVENT_NAME.CHANGE_I18N,
+ data: {
+ currentLanguage: i18n?.language === 'en' ? 'zh' : 'en'
+ }
+ });
+ setCookie('NEXT_LOCALE', i18n?.language === 'en' ? 'zh' : 'en', {
+ expires: 30,
+ sameSite: 'None',
+ secure: true
+ });
+ i18n?.changeLanguage(i18n?.language === 'en' ? 'zh' : 'en');
+ }}
+ >
+ {i18n?.language === 'en' ? 'En' : '中'}
+
+ );
+}
diff --git a/frontend/desktop/src/components/signin/auth/useLanguage.tsx b/frontend/desktop/src/components/signin/auth/useLanguage.tsx
index 910bcbab7f4..2a953dec0dc 100644
--- a/frontend/desktop/src/components/signin/auth/useLanguage.tsx
+++ b/frontend/desktop/src/components/signin/auth/useLanguage.tsx
@@ -1,4 +1,5 @@
import LangSelect from '@/components/LangSelect';
+import LangSelectSimple from '@/components/LangSelect/simple';
import { Box, Flex, UseDisclosureReturn } from '@chakra-ui/react';
import { I18n } from 'next-i18next';
@@ -14,19 +15,12 @@ const Language = ({ disclosure, i18n }: LanguageType) => {
cursor={'pointer'}
gap={'16px'}
>
-
- disclosure.onOpen()}>{i18n?.language === 'en' ? 'en' : '中'}
-
-
+ />
);
};
diff --git a/frontend/desktop/src/components/signin/auth/useProtocol.tsx b/frontend/desktop/src/components/signin/auth/useProtocol.tsx
index d34f5ca560f..36574dab25c 100644
--- a/frontend/desktop/src/components/signin/auth/useProtocol.tsx
+++ b/frontend/desktop/src/components/signin/auth/useProtocol.tsx
@@ -1,4 +1,4 @@
-import { Checkbox, Flex, Link, Text } from '@chakra-ui/react';
+import { Checkbox, Flex, Link, Text, TextProps } from '@chakra-ui/react';
import { useTranslation } from 'next-i18next';
import { useState } from 'react';
diff --git a/frontend/desktop/src/components/user_menu/index.tsx b/frontend/desktop/src/components/user_menu/index.tsx
index 7ee95d09508..8eac640810d 100644
--- a/frontend/desktop/src/components/user_menu/index.tsx
+++ b/frontend/desktop/src/components/user_menu/index.tsx
@@ -1,10 +1,9 @@
import Account from '@/components/account';
import Notification from '@/components/notification';
import useSessionStore from '@/stores/session';
-import { Box, Flex, Image, useDisclosure } from '@chakra-ui/react';
-import { i18n } from 'next-i18next';
+import { Box, Flex, FlexProps, Image, useDisclosure } from '@chakra-ui/react';
import { useState } from 'react';
-import LangSelect from '../LangSelect';
+import LangSelectSimple from '../LangSelect/simple';
import Iconfont from '../iconfont';
enum UserMenuKeys {
@@ -13,25 +12,31 @@ enum UserMenuKeys {
Account
}
-export default function Index() {
+export default function Index(props: { userMenuStyleProps?: FlexProps }) {
const [notificationAmount, setNotificationAmount] = useState(0);
const accountDisclosure = useDisclosure();
const showDisclosure = useDisclosure();
const switchLangDisclosure = useDisclosure();
const userInfo = useSessionStore((state) => state.getSession());
if (!userInfo) return null;
+
+ const {
+ userMenuStyleProps = {
+ alignItems: 'center',
+ position: 'absolute',
+ top: '42px',
+ right: '42px',
+ cursor: 'pointer',
+ gap: '16px'
+ }
+ } = props;
+
const buttonList: {
click?: () => void;
button: JSX.Element;
content: JSX.Element;
key: UserMenuKeys;
}[] = [
- {
- key: UserMenuKeys.LangSelect,
- button: {i18n?.language === 'en' ? 'en' : '中'},
- click: () => switchLangDisclosure.onOpen(),
- content:
- },
{
key: UserMenuKeys.Notification,
button: (
@@ -63,7 +68,13 @@ export default function Index() {
}
];
return (
-
+
+
{buttonList.map((item, index) => (
+ request.get<
+ any,
+ ApiResp<{
+ steps: string;
+ ratios: string;
+ }>
+ >('/api/price/bonus'),
+ {}
+ );
+
+ const { ratios, steps } = useMemo(() => {
+ return {
+ ratios: (bonuses?.data?.ratios || '').split(',').map((v) => +v),
+ steps: (bonuses?.data?.steps || '').split(',').map((v) => +v)
+ };
+ }, [bonuses?.data]);
+
+ const BonusBox = useCallback(
+ () => (
+
+ {steps.map((amount, index) => (
+ {
+ e.preventDefault();
+ setSelectAmountIndex(index);
+ }}
+ >
+
+
+
+ {amount}
+
+
+
+ ))}
+
+ ),
+ [selectAmountIndex, steps]
+ );
+
+ return {
+ BonusBox,
+ selectAmount: steps[selectAmountIndex]
+ };
+}
diff --git a/frontend/desktop/src/pages/api/license/createLicenseRecord.ts b/frontend/desktop/src/pages/api/license/createLicenseRecord.ts
new file mode 100644
index 00000000000..86aa73f8a32
--- /dev/null
+++ b/frontend/desktop/src/pages/api/license/createLicenseRecord.ts
@@ -0,0 +1,31 @@
+import { authSession } from '@/services/backend/auth';
+import { createLicenseRecord } from '@/services/backend/db/license';
+import { jsonRes } from '@/services/backend/response';
+import { LicensePayload } from '@/types';
+import type { NextApiRequest, NextApiResponse } from 'next';
+
+export default async function handler(req: NextApiRequest, resp: NextApiResponse) {
+ try {
+ const payload = await authSession(req.headers);
+ if (!payload) return jsonRes(resp, { code: 401, message: 'token verify error' });
+
+ const { token, orderID, amount, quota, paymentMethod } = req.body as LicensePayload;
+
+ const record = {
+ uid: payload.user.nsid,
+ amount: amount,
+ token: token,
+ orderID: orderID,
+ quota: quota,
+ paymentMethod: paymentMethod
+ };
+
+ const result = await createLicenseRecord(record);
+
+ return jsonRes(resp, {
+ data: result
+ });
+ } catch (error) {
+ jsonRes(resp, { code: 500, data: error });
+ }
+}
diff --git a/frontend/desktop/src/pages/api/license/getLicenseRecord.ts b/frontend/desktop/src/pages/api/license/getLicenseRecord.ts
new file mode 100644
index 00000000000..cf18b889010
--- /dev/null
+++ b/frontend/desktop/src/pages/api/license/getLicenseRecord.ts
@@ -0,0 +1,29 @@
+import { authSession } from '@/services/backend/auth';
+import { createLicenseRecord, getLicenseRecordsByUid } from '@/services/backend/db/license';
+import { jsonRes } from '@/services/backend/response';
+import { LicensePayload } from '@/types';
+import type { NextApiRequest, NextApiResponse } from 'next';
+
+export default async function handler(req: NextApiRequest, resp: NextApiResponse) {
+ try {
+ const payload = await authSession(req.headers);
+ if (!payload) return jsonRes(resp, { code: 401, message: 'token verify error' });
+
+ const { page = 1, pageSize = 10 } = req.body as {
+ page: number;
+ pageSize: number;
+ };
+
+ const result = await getLicenseRecordsByUid({
+ uid: payload.user.nsid,
+ page: page,
+ pageSize: pageSize
+ });
+
+ return jsonRes(resp, {
+ data: result
+ });
+ } catch (error) {
+ jsonRes(resp, { code: 500, data: error });
+ }
+}
diff --git a/frontend/desktop/src/pages/api/license/pay.ts b/frontend/desktop/src/pages/api/license/pay.ts
new file mode 100644
index 00000000000..11efd922ca1
--- /dev/null
+++ b/frontend/desktop/src/pages/api/license/pay.ts
@@ -0,0 +1,83 @@
+import { authSession } from '@/services/backend/auth';
+import { ApplyYaml } from '@/services/backend/kubernetes/user';
+import { jsonRes } from '@/services/backend/response';
+import { LicensePaymentForm } from '@/types';
+import yaml from 'js-yaml';
+import type { NextApiRequest, NextApiResponse } from 'next';
+
+export const generateLicenseCrd = (form: LicensePaymentForm) => {
+ const paymentCrd = {
+ apiVersion: 'infostream.sealos.io/v1',
+ kind: 'Payment',
+ metadata: {
+ name: form.paymentName,
+ namespace: form.namespace
+ },
+ spec: {
+ userID: form.userId,
+ amount: form.amount, // weixin
+ paymentMethod: form.paymentMethod,
+ service: {
+ amt: form.quota, // Actual value
+ hid: form.hashID
+ }
+ }
+ };
+ try {
+ const result = yaml.dump(paymentCrd);
+ return result;
+ } catch (error) {
+ throw error;
+ }
+};
+
+export default async function handler(req: NextApiRequest, resp: NextApiResponse) {
+ try {
+ const payload = await authSession(req.headers);
+ if (!payload) return jsonRes(resp, { code: 401, message: 'token verify error' });
+
+ const { amount, paymentMethod, hid, quota } = req.body as {
+ amount: number;
+ paymentMethod: 'wechat' | 'stripe';
+ hid: string;
+ quota: number;
+ };
+ console.log(amount, quota, paymentMethod, hid);
+ if (!hid) {
+ return jsonRes(resp, {
+ code: 400,
+ message: 'Missing hid parameter'
+ });
+ }
+ if (amount <= 0) {
+ return jsonRes(resp, {
+ code: 400,
+ message: 'Amount cannot be less than 0'
+ });
+ }
+
+ const paymentName = crypto.randomUUID();
+ const form: LicensePaymentForm = {
+ namespace: payload.user.nsid,
+ paymentName: paymentName,
+ userId: payload.user.k8s_username,
+ amount: amount,
+ quota: quota,
+ paymentMethod: paymentMethod,
+ hashID: hid
+ };
+
+ const LicenseCrd = generateLicenseCrd(form);
+
+ const res = await ApplyYaml(payload.kc, LicenseCrd);
+ return jsonRes(resp, {
+ data: {
+ paymentName: paymentName,
+ extra: res[0]
+ }
+ });
+ } catch (error) {
+ console.log(error);
+ jsonRes(resp, { code: 500, data: error });
+ }
+}
diff --git a/frontend/desktop/src/pages/api/license/result.ts b/frontend/desktop/src/pages/api/license/result.ts
new file mode 100644
index 00000000000..a51198f2d9b
--- /dev/null
+++ b/frontend/desktop/src/pages/api/license/result.ts
@@ -0,0 +1,32 @@
+import { authSession } from '@/services/backend/auth';
+import { GetCRD } from '@/services/backend/kubernetes/user';
+import { jsonRes } from '@/services/backend/response';
+import type { NextApiRequest, NextApiResponse } from 'next';
+
+export default async function handler(req: NextApiRequest, resp: NextApiResponse) {
+ try {
+ const payload = await authSession(req.headers);
+ if (!payload) return jsonRes(resp, { code: 401, message: 'token verify error' });
+
+ const { paymentName } = req.query as {
+ paymentName: string;
+ };
+
+ if (typeof paymentName !== 'string' || paymentName === '') {
+ return jsonRes(resp, { code: 400, message: 'payment name cannot be empty' });
+ }
+
+ const paymentM = {
+ group: 'infostream.sealos.io',
+ version: 'v1',
+ namespace: payload.user.nsid,
+ plural: 'payments'
+ };
+ const paymentDesc = await GetCRD(payload.kc, paymentM, paymentName);
+ console.log(paymentDesc?.body?.status, 'payment');
+ return jsonRes(resp, { data: paymentDesc?.body?.status });
+ } catch (error) {
+ console.log(error);
+ jsonRes(resp, { code: 500, data: error });
+ }
+}
diff --git a/frontend/desktop/src/pages/api/platform/getEnv.ts b/frontend/desktop/src/pages/api/platform/getEnv.ts
index 572ae64efd6..455c83e8e0f 100644
--- a/frontend/desktop/src/pages/api/platform/getEnv.ts
+++ b/frontend/desktop/src/pages/api/platform/getEnv.ts
@@ -5,7 +5,10 @@ import {
enableWechat,
enablePassword,
enableSms,
- enableGoogle
+ enableGoogle,
+ enableStripe,
+ enableWechatRecharge,
+ enableLicense
} from '@/services/enable';
export default async function handler(req: NextApiRequest, res: NextApiResponse) {
@@ -20,6 +23,9 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
const needSms = enableSms();
const needGoogle = enableGoogle();
const callback_url = process.env.CALLBACK_URL;
+ const stripeEnabled = enableStripe();
+ const wechatEnabledRecharge = enableWechatRecharge();
+ const licenseEnabled = enableLicense();
jsonRes(res, {
data: {
@@ -34,7 +40,10 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
needSms,
needGithub,
needWechat,
- needGoogle
+ needGoogle,
+ stripeEnabled,
+ wechatEnabledRecharge,
+ licenseEnabled
}
});
}
diff --git a/frontend/desktop/src/pages/api/price/bonus.ts b/frontend/desktop/src/pages/api/price/bonus.ts
new file mode 100644
index 00000000000..62cadd7c6ae
--- /dev/null
+++ b/frontend/desktop/src/pages/api/price/bonus.ts
@@ -0,0 +1,28 @@
+import { authSession } from '@/services/backend/auth';
+import { GetConfigMap } from '@/services/backend/kubernetes/user';
+import { jsonRes } from '@/services/backend/response';
+import { enableRecharge } from '@/services/enable';
+import type { NextApiRequest, NextApiResponse } from 'next';
+
+export default async function handler(req: NextApiRequest, resp: NextApiResponse) {
+ try {
+ const payload = await authSession(req.headers);
+ if (!payload) {
+ return jsonRes(resp, { code: 401, message: 'token verify error' });
+ }
+ const result = await GetConfigMap(payload.kc, 'sealos', 'recharge-gift');
+ if (!result.body.data) {
+ return jsonRes(resp, { code: 404, message: 'not found' });
+ }
+ return jsonRes(resp, {
+ code: 200,
+ data: {
+ ratios: result.body.data.ratios,
+ steps: result.body.data.steps
+ }
+ });
+ } catch (error) {
+ console.log(error);
+ jsonRes(resp, { code: 500, message: 'get price error' });
+ }
+}
diff --git a/frontend/desktop/src/pages/api/price/index.ts b/frontend/desktop/src/pages/api/price/index.ts
new file mode 100644
index 00000000000..5586297b69a
--- /dev/null
+++ b/frontend/desktop/src/pages/api/price/index.ts
@@ -0,0 +1,52 @@
+import { authSession } from '@/services/backend/auth';
+import type { NextApiRequest, NextApiResponse } from 'next';
+import * as yaml from 'js-yaml';
+import { jsonRes } from '@/services/backend/response';
+import { CRDMeta, ValuationBillingRecord, ValuationData } from '@/types';
+import { ApplyYaml, GetCRD } from '@/services/backend/kubernetes/user';
+import { IncomingMessage } from 'http';
+
+export default async function handler(req: NextApiRequest, resp: NextApiResponse) {
+ try {
+ const payload = await authSession(req.headers);
+ if (!payload) {
+ return jsonRes(resp, { code: 401, message: 'token verify error' });
+ }
+ const namespace = payload.user.nsid;
+ const name = 'prices';
+ const crdSchema = {
+ apiVersion: `account.sealos.io/v1`,
+ kind: 'PriceQuery',
+ metadata: {
+ name,
+ namespace
+ },
+ spec: {}
+ };
+ const meta: CRDMeta = {
+ group: 'account.sealos.io',
+ version: 'v1',
+ namespace,
+ plural: 'pricequeries'
+ };
+ try {
+ await ApplyYaml(payload.kc, yaml.dump(crdSchema));
+ await new Promise((resolve) => setTimeout(() => resolve(), 1000));
+ } finally {
+ const crd = (await GetCRD(payload.kc, meta, name)) as {
+ response: IncomingMessage;
+ body: ValuationData;
+ };
+ const billingRecords = crd?.body?.status?.billingRecords || [];
+ return jsonRes<{ billingRecords: ValuationBillingRecord[] }>(resp, {
+ code: 200,
+ data: {
+ billingRecords
+ }
+ });
+ }
+ } catch (error) {
+ console.log(error);
+ jsonRes(resp, { code: 500, message: 'get price error' });
+ }
+}
diff --git a/frontend/desktop/src/pages/api/system/getSystemConfig.ts b/frontend/desktop/src/pages/api/system/getSystemConfig.ts
index 2b08d072f4b..87f78b8d1d1 100644
--- a/frontend/desktop/src/pages/api/system/getSystemConfig.ts
+++ b/frontend/desktop/src/pages/api/system/getSystemConfig.ts
@@ -22,7 +22,6 @@ export async function getSystemConfig(): Promise {
const res = JSON.parse(readFileSync(filename, 'utf-8'));
return res;
} catch (error) {
- console.log('get system config error, set default');
return defaultConfig;
}
}
diff --git a/frontend/desktop/src/pages/license/components/CurrencySymbol.tsx b/frontend/desktop/src/pages/license/components/CurrencySymbol.tsx
new file mode 100644
index 00000000000..f784def928f
--- /dev/null
+++ b/frontend/desktop/src/pages/license/components/CurrencySymbol.tsx
@@ -0,0 +1,22 @@
+import { Text, Icon } from '@chakra-ui/react';
+export default function currencysymbol({
+ type = 'shellCoin',
+ ...props
+}: {
+ type?: 'shellCoin' | 'cny' | 'usd';
+} & Pick[0], 'w' | 'h' | 'color'>) {
+ return type === 'shellCoin' ? (
+
+
+
+ ) : type === 'cny' ? (
+ ¥
+ ) : (
+ $
+ );
+}
diff --git a/frontend/desktop/src/pages/license/components/OuterLink.tsx b/frontend/desktop/src/pages/license/components/OuterLink.tsx
new file mode 100644
index 00000000000..415ac13d707
--- /dev/null
+++ b/frontend/desktop/src/pages/license/components/OuterLink.tsx
@@ -0,0 +1,35 @@
+import { Flex, Img, Link } from '@chakra-ui/react';
+import { useTranslation } from 'next-i18next';
+
+export default function Index({ text, href }: { text: string; href?: string }) {
+ const { t } = useTranslation();
+ return (
+
+
+
+ {text}
+
+
+ );
+}
diff --git a/frontend/desktop/src/pages/license/components/Pagination.tsx b/frontend/desktop/src/pages/license/components/Pagination.tsx
new file mode 100644
index 00000000000..064132bc699
--- /dev/null
+++ b/frontend/desktop/src/pages/license/components/Pagination.tsx
@@ -0,0 +1,118 @@
+import { Flex, Text, Image, FlexProps, Icon, Box } from '@chakra-ui/react';
+import { useState } from 'react';
+type PaginationProps = {
+ totalItems: number;
+ itemsPerPage: number;
+ onPageChange: any;
+};
+
+export default function Pagination({ totalItems, itemsPerPage, onPageChange }: PaginationProps) {
+ const totalPage = Math.ceil(totalItems / itemsPerPage);
+ const [currentPage, setCurrentPage] = useState(1);
+
+ const goToPage = (page: number) => {
+ if (page >= 1 && page <= totalPage) {
+ setCurrentPage(page);
+ onPageChange(page);
+ }
+ };
+
+ const handlePrevPage = () => {
+ goToPage(currentPage - 1);
+ };
+
+ const handleNextPage = () => {
+ goToPage(currentPage + 1);
+ };
+
+ const buttonStyle: FlexProps = {
+ justifyContent: 'center',
+ alignItems: 'center',
+ w: '24px',
+ h: '24px',
+ borderRadius: '50%'
+ };
+
+ return (
+
+ Total:
+ {totalItems}
+
+
+
+
+
+
+ goToPage(1)}
+ >
+
+
+
+
+
+
+ {currentPage}/{totalPage}
+
+
+
+
+
+
+
+ goToPage(totalPage)}
+ >
+
+
+
+
+
+ {itemsPerPage}
+ /Page
+
+ );
+}
diff --git a/frontend/desktop/src/pages/license/components/Recharge.tsx b/frontend/desktop/src/pages/license/components/Recharge.tsx
new file mode 100644
index 00000000000..8a7832e13c3
--- /dev/null
+++ b/frontend/desktop/src/pages/license/components/Recharge.tsx
@@ -0,0 +1,258 @@
+import useBonusBox from '@/hooks/useBonusBox';
+import { useCustomToast } from '@/hooks/useCustomToast';
+import request from '@/services/request';
+import { ApiResp, LicensePayStatus, LicensePayload, Payment, SystemEnv } from '@/types';
+import { deFormatMoney } from '@/utils/format';
+import {
+ Button,
+ Checkbox,
+ Flex,
+ Image,
+ Link,
+ Modal,
+ ModalCloseButton,
+ ModalContent,
+ ModalHeader,
+ ModalOverlay,
+ Text,
+ useDisclosure
+} from '@chakra-ui/react';
+import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query';
+import { useTranslation } from 'next-i18next';
+import { useRouter } from 'next/router';
+import { useEffect, useState } from 'react';
+import WechatPayment from './WechatPayment';
+
+export default function RechargeComponent() {
+ const { t } = useTranslation();
+ const [isAgree, setIsAgree] = useState(false);
+ const [isInvalid, setIsInvalid] = useState(false);
+ const { BonusBox, selectAmount } = useBonusBox();
+ const { isOpen, onOpen, onClose } = useDisclosure();
+ // 整个流程跑通需要状态管理, 0 初始态, 1 创建支付单, 2 支付中, 3 支付成功
+ const [complete, setComplete] = useState<0 | 1 | 2 | 3>(0);
+ // 0 是微信,1 是stripe
+ const [payType, setPayType] = useState<'wechat' | 'stripe'>('wechat');
+ const [paymentName, setPaymentName] = useState('');
+ const queryClient = useQueryClient();
+ const { toast } = useCustomToast();
+ const { query } = useRouter();
+ const [hid, setHid] = useState(''); // license key
+
+ // handle hid
+ useEffect(() => {
+ if (query?.hid && typeof query.hid === 'string') {
+ const decodedHid = decodeURIComponent(query.hid);
+ setHid(decodedHid);
+ } else {
+ toast({
+ status: 'error',
+ title: 'Purchase Link Error',
+ isClosable: true,
+ position: 'top'
+ });
+ }
+ }, []);
+
+ const onModalClose = () => {
+ setComplete(0);
+ onClose();
+ };
+
+ const handleWechatConfirm = () => {
+ if (isAgree) {
+ setComplete(1);
+ createPaymentLicense.mutate();
+ onOpen();
+ } else {
+ toast({
+ status: 'error',
+ title: t('Please read and agree to the agreement'),
+ isClosable: true,
+ position: 'top'
+ });
+ }
+ };
+
+ const { data: platformEnv } = useQuery(['getPlatformEnv'], () =>
+ request>('/api/platform/getEnv')
+ );
+
+ const createPaymentLicense = useMutation(
+ () =>
+ request.post>('/api/license/pay', {
+ amount: deFormatMoney(selectAmount), //weixin
+ quota: selectAmount,
+ paymentMethod: payType,
+ hid: hid
+ }),
+ {
+ onSuccess(data) {
+ console.log(data);
+ setPaymentName((data?.data?.paymentName as string).trim());
+ setComplete(2);
+ },
+ onError(err: any) {
+ toast({
+ status: 'error',
+ title: err?.message || '',
+ isClosable: true,
+ position: 'top'
+ });
+ setComplete(0);
+ }
+ }
+ );
+
+ const createLicenseRecord = useMutation(
+ (payload: LicensePayload) =>
+ request.post('/api/license/createLicenseRecord', payload),
+ {
+ onSuccess(data) {
+ console.log(data);
+ queryClient.invalidateQueries(['getLicenseActive']);
+ },
+ onError(err: any) {
+ console.log(err);
+ }
+ }
+ );
+
+ const { data } = useQuery(
+ ['getLicenseResult', paymentName],
+ () =>
+ request>('/api/license/result', {
+ params: {
+ paymentName: paymentName
+ }
+ }),
+ {
+ refetchInterval: complete === 2 ? 1000 : false,
+ enabled: complete === 2 && !!paymentName,
+ cacheTime: 0,
+ staleTime: 0,
+ onSuccess(data) {
+ if (data?.data?.status === 'Completed') {
+ onModalClose();
+ toast({
+ status: 'success',
+ title: t('Payment Successful'),
+ isClosable: true,
+ position: 'top'
+ });
+ createLicenseRecord.mutate({
+ uid: '',
+ amount: deFormatMoney(selectAmount),
+ quota: selectAmount,
+ token: data.data?.token,
+ orderID: data?.data?.tradeNO,
+ paymentMethod: 'wechat'
+ });
+ }
+ }
+ }
+ );
+
+ return (
+
+
+ {t('Purchase License')}
+
+
+ {t('Select Amount')}
+
+
+
+ {
+ setIsInvalid(false);
+ setIsAgree(e.target.checked);
+ }}
+ />
+
+ {t('agree policy')}
+
+ {t('Service Agreement')}
+
+ {t('and')}
+
+ {t('Privacy Policy')}
+
+
+ {/* */}
+
+
+ {/* {platformEnv?.data?.stripeEnabled && (
+
+ )} */}
+
+
+
+
+
+
+ 充值金额
+
+
+
+
+
+
+ );
+}
diff --git a/frontend/desktop/src/pages/license/components/Record.tsx b/frontend/desktop/src/pages/license/components/Record.tsx
new file mode 100644
index 00000000000..a46110992f9
--- /dev/null
+++ b/frontend/desktop/src/pages/license/components/Record.tsx
@@ -0,0 +1,123 @@
+import request from '@/services/request';
+import { ApiResp, LicenseRecord } from '@/types';
+import download from '@/utils/downloadFIle';
+import { formatMoney, getRemainingTime } from '@/utils/format';
+import { Box, Flex, Image, Text } from '@chakra-ui/react';
+import { useQuery } from '@tanstack/react-query';
+import { useTranslation } from 'next-i18next';
+import { useState } from 'react';
+import Pagination from './Pagination';
+
+export default function History() {
+ const { t } = useTranslation();
+ const [page, setPage] = useState(1);
+ const [pageSize, setPageSize] = useState(10);
+
+ const { data } = useQuery(['getLicenseActive', page, pageSize], () =>
+ request.post>(
+ '/api/license/getLicenseRecord',
+ {
+ page,
+ pageSize
+ }
+ )
+ );
+
+ const downloadToken = (token: string) => {
+ const result = Buffer.from(token, 'binary').toString('base64');
+ download('token.txt', result);
+ };
+
+ return (
+
+
+ {t('Purchase History')}
+
+ {data?.data?.records && data?.data?.records?.length > 0 ? (
+
+ {data?.data?.records.map((item) => (
+
+
+
+
+
+ License
+
+
+
+ {formatMoney(item.amount)}
+
+
+
+ {t('Remaining Time')} {getRemainingTime(item.exp)}
+
+
+
+
+
+
+ Token
+
+ downloadToken(item.token)}
+ src={'/icons/download.svg'}
+ w={'20px'}
+ h={'20px'}
+ alt="download"
+ >
+
+
+ ))}
+
+ ) : (
+
+
+
+ {t('You have not purchased the License')}
+
+
+ )}
+
+
+ {}}
+ />
+
+
+ );
+}
diff --git a/frontend/desktop/src/pages/license/components/WechatPayment.tsx b/frontend/desktop/src/pages/license/components/WechatPayment.tsx
new file mode 100644
index 00000000000..8faacec5885
--- /dev/null
+++ b/frontend/desktop/src/pages/license/components/WechatPayment.tsx
@@ -0,0 +1,60 @@
+import { Flex, Box, Text } from '@chakra-ui/react';
+import { useTranslation } from 'next-i18next';
+import { QRCodeSVG } from 'qrcode.react';
+
+export default function WechatPayment(props: {
+ complete: number;
+ codeURL?: string;
+ tradeNO?: string;
+}) {
+ const { t } = useTranslation();
+ return (
+
+
+
+ {t('Scan with WeChat')}
+
+ {props.complete === 2 && !!props.codeURL ? (
+
+ ) : (
+ waiting...
+ )}
+
+
+ {t('Order Number')}: {props.tradeNO || ''}
+
+
+ {t('Payment Result')}:{props.complete === 3 ? t('Payment Successful') : t('In Payment')}
+
+
+
+
+ );
+}
diff --git a/frontend/desktop/src/pages/license/index.tsx b/frontend/desktop/src/pages/license/index.tsx
new file mode 100644
index 00000000000..cd46ad14460
--- /dev/null
+++ b/frontend/desktop/src/pages/license/index.tsx
@@ -0,0 +1,75 @@
+import LangSelectSimple from '@/components/LangSelect/simple';
+import { Flex, Image, Text } from '@chakra-ui/react';
+import { useTranslation } from 'next-i18next';
+import { serverSideTranslations } from 'next-i18next/serverSideTranslations';
+import RechargeComponent from './components/Recharge';
+import LicenseRecord from './components/Record';
+import { useRouter } from 'next/router';
+import request from '@/services/request';
+import { useQuery } from '@tanstack/react-query';
+import { ApiResp, SystemEnv } from '@/types';
+
+export default function LicensePage() {
+ const { t } = useTranslation();
+ const router = useRouter();
+ const goHome = () => router.replace('/');
+
+ const { data: platformEnv } = useQuery(
+ ['getPlatformEnv'],
+ () => request>('/api/platform/getEnv'),
+ {
+ onSuccess(data) {
+ console.log(data.data?.licenseEnabled, 'licenseEnabled');
+ if (!data.data?.licenseEnabled) {
+ goHome();
+ }
+ }
+ }
+ );
+
+ return (
+
+
+
+
+ Sealos
+
+
+ | {t('License Buy')}
+
+
+
+
+
+
+
+
+ );
+}
+
+export async function getServerSideProps({ req, res, locales }: any) {
+ const local = req?.cookies?.NEXT_LOCALE || 'en';
+
+ return {
+ props: {
+ ...(await serverSideTranslations(local, undefined, null, locales || []))
+ }
+ };
+}
diff --git a/frontend/desktop/src/services/backend/db/license.ts b/frontend/desktop/src/services/backend/db/license.ts
new file mode 100644
index 00000000000..89a59485170
--- /dev/null
+++ b/frontend/desktop/src/services/backend/db/license.ts
@@ -0,0 +1,72 @@
+import { LicensePayload, LicenseRecord } from '@/types';
+import { connectToDatabase } from './mongodb';
+
+async function connectLicenseRecordCollection() {
+ const client = await connectToDatabase();
+ const collection = client.db().collection('licenseRecord');
+ return collection;
+}
+
+export async function createLicenseRecord({
+ uid,
+ amount,
+ token,
+ orderID,
+ quota,
+ paymentMethod
+}: LicensePayload) {
+ const collection = await connectLicenseRecordCollection();
+
+ const now = Math.floor(Date.now() / 1000); // Get current timestamp in seconds
+ const oneDayInSeconds = 24 * 60 * 60; // One day in seconds
+
+ const record: LicenseRecord = {
+ uid: uid,
+ token: token,
+ orderID: orderID,
+ paymentMethod: paymentMethod,
+ service: {
+ quota: quota
+ },
+ iat: now, // Store the current timestamp as iat
+ exp: now + oneDayInSeconds, // Set expiration to one day from now (in seconds)
+ amount: amount
+ };
+
+ const result = await collection.insertOne(record);
+
+ return result;
+}
+
+export async function getLicenseRecordsByUid({
+ uid,
+ page,
+ pageSize
+}: {
+ uid: string;
+ page: number;
+ pageSize: number;
+}) {
+ const collection = await connectLicenseRecordCollection();
+
+ const skip = (page - 1) * pageSize;
+
+ const query = { uid: uid };
+ const options = {
+ skip: skip,
+ limit: pageSize
+ };
+
+ // Find records for the specified uid, skip records based on pagination, and limit the result to pageSize
+ const records = await collection.find(query, options).sort({ iat: -1 }).toArray();
+
+ // Calculate the total count of records for the given uid
+ const totalCount = await collection.countDocuments(query);
+
+ const result = {
+ records: records,
+ total: totalCount
+ };
+
+ return result;
+}
diff --git a/frontend/desktop/src/services/enable.ts b/frontend/desktop/src/services/enable.ts
index a97ed5078f5..322f9e35ae4 100644
--- a/frontend/desktop/src/services/enable.ts
+++ b/frontend/desktop/src/services/enable.ts
@@ -22,3 +22,11 @@ export const enableGoogle = () =>
export const enableRecharge = () => {
return process.env.RECHARGE_ENABLED === 'true';
};
+// costcenter
+export const enableStripe = () =>
+ process.env['STRIPE_ENABLED'] === 'true' && !!process.env['STRIPE_PUB'];
+export const enableWechatRecharge = () => process.env['WECHAT_ENABLED'] === 'true';
+// license
+export const enableLicense = () => {
+ return process.env.LICENSE_ENABLED === 'true';
+};
diff --git a/frontend/desktop/src/styles/globals.scss b/frontend/desktop/src/styles/globals.scss
index 9a24db4d163..c59ed76cd89 100644
--- a/frontend/desktop/src/styles/globals.scss
+++ b/frontend/desktop/src/styles/globals.scss
@@ -52,3 +52,22 @@ input:-webkit-autofill {
background-image: none;
color: #000;
}
+
+div {
+ &::-webkit-scrollbar-thumb,
+ &::-webkit-scrollbar-thumb {
+ background: transparent !important;
+ border-radius: 10px;
+ transition: 1s;
+ }
+ &:hover {
+ &::-webkit-scrollbar-thumb,
+ &::-webkit-scrollbar-thumb {
+ background: rgba(189, 193, 197, 0.5) !important;
+ }
+ &::-webkit-scrollbar-thumb:hover,
+ &::-webkit-scrollbar-thumb:hover {
+ background: rgba(189, 193, 197, 1) !important;
+ }
+ }
+}
diff --git a/frontend/desktop/src/types/index.ts b/frontend/desktop/src/types/index.ts
index 7ac651a2620..d0f327f1e38 100644
--- a/frontend/desktop/src/types/index.ts
+++ b/frontend/desktop/src/types/index.ts
@@ -6,6 +6,8 @@ export * from './crd';
export * from './payment';
export * from './system';
export * from './login';
+export * from './valuation';
+export * from './license';
declare global {
var mongodb: MongoClient | null;
diff --git a/frontend/desktop/src/types/license.ts b/frontend/desktop/src/types/license.ts
new file mode 100644
index 00000000000..69d699c2f71
--- /dev/null
+++ b/frontend/desktop/src/types/license.ts
@@ -0,0 +1,63 @@
+export type LicenseCrd = {
+ apiVersion: 'infostream.sealos.io/v1';
+ kind: 'Payment';
+ metadata: {
+ name: string;
+ namespace: string;
+ };
+ spec: {
+ userID: string;
+ amount: number;
+ paymentMethod: 'wechat' | 'stripe';
+ service: {
+ amt: number;
+ objType: string; // 'external,internal'
+ };
+ };
+ status: {
+ tradeNO: string;
+ codeURL: string;
+ status: 'Created' | 'Completed';
+ token: string;
+ };
+};
+
+export type LicensePaymentForm = {
+ paymentName: string;
+ namespace: string;
+ userId: string;
+ amount: number;
+ paymentMethod: 'wechat' | 'stripe';
+ hashID: string;
+ quota: number;
+};
+
+export type LicensePayStatus = {
+ tradeNO: string;
+ codeURL: string;
+ status: 'Created' | 'Completed';
+ token: string;
+};
+
+export type LicenseRecord = {
+ _id?: string;
+ uid: string; // user id
+ token: string; // license token
+ orderID: string; // order number
+ paymentMethod: 'wechat' | 'stripe';
+ service: {
+ quota: number; // 额度
+ };
+ iat: number; // 签发日期
+ exp: number; // 有效期
+ amount: number; // 消费金额
+};
+
+export type LicensePayload = {
+ uid: string;
+ amount: number;
+ token: string;
+ orderID: string;
+ quota: number;
+ paymentMethod: 'wechat' | 'stripe';
+};
diff --git a/frontend/desktop/src/types/system.ts b/frontend/desktop/src/types/system.ts
index 741b872a3e4..3fa09f8bbba 100644
--- a/frontend/desktop/src/types/system.ts
+++ b/frontend/desktop/src/types/system.ts
@@ -23,4 +23,7 @@ export type LoginProps = {
export type SystemEnv = {
SEALOS_CLOUD_DOMAIN: string;
+ stripeEnabled: boolean;
+ wechatEnabledRecharge: boolean;
+ licenseEnabled: boolean;
} & LoginProps;
diff --git a/frontend/desktop/src/types/valuation.ts b/frontend/desktop/src/types/valuation.ts
new file mode 100644
index 00000000000..da4244ceb53
--- /dev/null
+++ b/frontend/desktop/src/types/valuation.ts
@@ -0,0 +1,18 @@
+export type ValuationStandard = {
+ name: string;
+ unit: string;
+ price: string;
+};
+export type ValuationBillingRecord = {
+ price: number;
+ resourceType: string;
+};
+export type ValuationData = {
+ apiVersion: 'account.sealos.io/v1';
+ kind: 'PriceQuery';
+ metadata: any;
+ spec: {};
+ status: {
+ billingRecords: ValuationBillingRecord[];
+ };
+};
diff --git a/frontend/desktop/src/utils/format.ts b/frontend/desktop/src/utils/format.ts
index a44903afac6..5f24f135ef4 100644
--- a/frontend/desktop/src/utils/format.ts
+++ b/frontend/desktop/src/utils/format.ts
@@ -36,3 +36,19 @@ export const parseOpenappQuery = (openapp: string) => {
appQuery
};
};
+
+export const getRemainingTime = (expirationTime: number) => {
+ const currentTime = Math.floor(Date.now() / 1000);
+
+ if (currentTime >= expirationTime) {
+ return 'expired';
+ }
+
+ const remainingTimeInSeconds = expirationTime - currentTime;
+ const hours = Math.floor(remainingTimeInSeconds / 3600);
+ const minutes = Math.floor((remainingTimeInSeconds % 3600) / 60);
+ const seconds = remainingTimeInSeconds % 60;
+
+ const formattedTime = `${hours}小时${minutes}分钟`;
+ return formattedTime;
+};
diff --git a/frontend/providers/license/.env.template b/frontend/providers/license/.env.template
index b86f5b79196..98609357cd8 100644
--- a/frontend/providers/license/.env.template
+++ b/frontend/providers/license/.env.template
@@ -1,3 +1,5 @@
NEXT_PUBLIC_MOCK_USER=
SEALOS_DOMAIN="cloud.sealos.io"
-MONGODB_URI=
\ No newline at end of file
+MONGODB_URI=
+# Same as desktop important
+PASSWORD_SALT=
\ No newline at end of file
diff --git a/frontend/providers/license/deploy/manifests/appcr.yaml.tmpl b/frontend/providers/license/deploy/manifests/appcr.yaml.tmpl
index 485790ea584..4b611e0c98b 100644
--- a/frontend/providers/license/deploy/manifests/appcr.yaml.tmpl
+++ b/frontend/providers/license/deploy/manifests/appcr.yaml.tmpl
@@ -14,8 +14,4 @@ spec:
name: license
type: iframe
displayType: normal
- i18n:
- zh:
- name: 定时任务
- zh-Hans:
- name: 定时任务
+
diff --git a/frontend/providers/license/deploy/manifests/deploy.yaml.tmpl b/frontend/providers/license/deploy/manifests/deploy.yaml.tmpl
index 6dabee4b810..739783fbce3 100644
--- a/frontend/providers/license/deploy/manifests/deploy.yaml.tmpl
+++ b/frontend/providers/license/deploy/manifests/deploy.yaml.tmpl
@@ -1,15 +1,8 @@
apiVersion: v1
-kind: Namespace
-metadata:
- labels:
- app: license-frontend
- name: license-frontend
----
-apiVersion: v1
kind: ConfigMap
metadata:
name: license-frontend-config
- namespace: license-frontend
+ namespace: sealos
data:
config.yaml: |-
addr: :3000
@@ -18,7 +11,7 @@ apiVersion: apps/v1
kind: Deployment
metadata:
name: license-frontend
- namespace: license-frontend
+ namespace: sealos
spec:
replicas: 1
selector:
@@ -39,6 +32,16 @@ spec:
env:
- name: SEALOS_DOMAIN
value: {{ .cloudDomain }}
+ - name: MONGODB_URI
+ valueFrom:
+ secretKeyRef:
+ key: mongodb_uri
+ name: license-frontend-secret
+ - name: PASSWORD_SALT
+ valueFrom:
+ secretKeyRef:
+ key: password_salt
+ name: license-frontend-secret
securityContext:
runAsNonRoot: true
runAsUser: 1001
@@ -54,7 +57,7 @@ spec:
cpu: 10m
memory: 128Mi
# do not modify this image, it is used for CI/CD
- image: ghcr.io/labring/sealos-license-frontend:latest
+ image: zhujingyang/sealos-license-frontend:1.0.1
imagePullPolicy: Always
volumeMounts:
- name: license-frontend-volume
@@ -71,7 +74,7 @@ metadata:
labels:
app: license-frontend
name: license-frontend
- namespace: license-frontend
+ namespace: sealos
spec:
ports:
- name: http
diff --git a/frontend/providers/license/deploy/manifests/ingress.yaml.tmpl b/frontend/providers/license/deploy/manifests/ingress.yaml.tmpl
index ecc46f16604..affa394e9d3 100644
--- a/frontend/providers/license/deploy/manifests/ingress.yaml.tmpl
+++ b/frontend/providers/license/deploy/manifests/ingress.yaml.tmpl
@@ -14,7 +14,7 @@ metadata:
add_header Cache-Control "public";
}
name: license-frontend
- namespace: license-frontend
+ namespace: sealos
spec:
rules:
- host: license.{{ .cloudDomain }}
@@ -30,4 +30,4 @@ spec:
tls:
- hosts:
- license.{{ .cloudDomain }}
- secretName: {{ .certSecretName }}
+ secretName: wildcard-cert
diff --git a/frontend/providers/license/public/favicon.ico b/frontend/providers/license/public/favicon.ico
index 1519d186a3d48771d05d94204d364d6514159a88..0587dabd2325c5ca594c306b155461b6f37ea15a 100644
GIT binary patch
literal 4286
zcmdUyTZmOv7{}LPJPLza$)L{EjN|OJ&szI-&R7(Vr71$G2tiNw){EeiLKl6qn-BVC
zL{HUA6f&YwFA+iY5-6zMkU}3sx*!o4QCiu}e*c--vYO7YHQ6(C4!^V3Uf(+F|E+J`
z_A;iycX-(7*Noq4%u-{F2a3#kQ2E3M+lPxMnJk$5-`rXwvX!yPV`~Z@)k|Vc;#(#K
z=bXuS?ktW?R-ije`qrf;^YquFy^MSwP5AM!S!mjBq^Ircp+mc)scn#=9TR8bo}sPO
z+X4J}6n59lPTIe-?QM_r^Efu`A=jX62mAAFe^asTq*%H2ef=^BBAc-%dS$O8Pu}
zHT4|&Ri#fq;%E3@Kiob8vaNv=ptg^~3eY@01RGi7vY&xaTlwqIw+Heha^Sm=Oxo-N
zwDr+c`)?G^4iqjPWL^)$a`abYJ3D0$eW=n`{4c`!3ckUvaaY?%(7rc!_672a`(E@n
zkp7qchrV8KUi$0N)41zDuib>c_McB+91aKQS>yd1O!w;uW$WM-cpdiNguY_>M0<2#
zUQrA+VSZ5zHjB<3O~!pYwjUtW*S`5|k;MH)VpqVDB8^v4=HM;Rc!u?>#$Ww=2{yr&
zfX(C&!X*gX`;7H+ckWw@NXM{kD}3C_TTw50ohjM}!unPE@>d$)b)h^X!uF2li5;Y_
z*4`KJJ-o;HvX*o09`ffQtY4)c)}5Z0Z<-%vOur<*m2nv%{Q%1PI%oFhF6twF4#F5z
z>FdAm5X-&rR2QDc{yxZ$bF8`hb01mq+5<~{`FAk$(QTx=Asn+Redb^MWRWJ#BJ~UY
zMdB6^TlsJWr1vUlpWFi%;1~E99)wZ&97?~GR_W`^c$4+B6Lxh2``b{SEojB7=Fv~^Uuu+ol+WGLf9lWS`*lqllR3lu)%ww1skK@stS5U{){1&E7n#1^C1%xVB7L)k>tNmYk4_RbxPzFq#k~Xl
zjqAqDNN>N%3*G-}zWYtwEv*?I;SPv?ivFzMKY8bGG3R{C8q2+Ru0L1#%#{z){u=}v
B)A9fS
literal 4286
zcmbW5Ym8l08HU&K%ycqCnbz74u}){s+4uW7Gt<%*T3Sj=DoRDMR9d--!K4Z05<|EY
zEeQz$666O7pe2y-2QMfnKfD`53}^+k788vL6^sN#tcsC~Ql5A1vu93cPD^1=p83|^
zd+qgp-?y$a#?<&}G>m?mhZY($VvLD^M`k_fxxIMz_;5-2p!e@Q2uwxc#
zl;f^v_Psk(C-zO21SZ2S564Uqxuz94mfou6(~NoQi63?WlQ#
zej2)_R;%@N!|G4_;G2_|oH7%C;??kBG5+P)O#G>ufg-3`>o{rB>7P0p7BMVg|0WA-)D&$^bzAN
zb{(_YSyjWAF}}YVo&v>r0PVeSDY;*frvAv}igy3qkM3t&$AgU7G5YCr>U|L&hUefF
z_#SM{1Ao|DX~-*c$|u*&*y^vE%8i)FVpGI{nap_~`X3^$9ek%)mET{J%Nww18U;gn
z;`JqkUyGZ|Oze%DAUsKJGERd2EDneHb~AOq0qT?gz(e@9hIR*)W6CCHZt8NzM=gVc
z1E!sZW@Paqjq~;JZIJ&OSJ%=nPT)4`E#?#PGd6uQwQ|z7(TWnFa)t>l|~
z;csvl)E64Jj`{#=3f2c*3HO%3F_G7-(NBwce--E(ji=kv$d8AHM{4aXGMiVCpQO}p
zw==V1`Etfg0KQ2AzmCm_e>dMf4gUq{{~t6aKZ@>rh6mv9@Hm|9
z2C1sPq|c?iBa8!tX(SROL@Tyaw{)n;;(+Lg!m+hq=ECs&8iN&A!*#
ztaZh^cQgdRp|caXyv!i`W>7L=$HGW
zY!P|$(O6GA_kiZT+R(c91Gt!XOB?87yJJ^b`uBqR_Gr4*bL~-&F`hSInZMH4xuTkb
z?g^CTgW}OR{yu0-EQL~+bt6sUvRpoUFZwUCKHdt6)=8mw3VbLn>u2PiKzRe~g6**M
zRO;k0zGh96=7~x2_WRY>x^O-$s^X!2XixNB?Cs?}w}?q}R!3m$r1~K-}%s>}|zpJtYW!==yj%Z2L7fZW
zgK&iY9LUjlQhUdv{}kWsOhRvT%x~AU
zb_StN>1J-s($5@Iv>N90FLPcf8bev^o`=rQ;7{-v?54eQ)6n}kJK5_xO9Fq(%oA$w-8E8)yp(lCBLTBFPucejzJAJUH22kq(4f!3>MLHnfUnAR)i
zxZQI#haZNkVJ{p=Blo)r=fN;`2CycIonG=Lj+>SU>m_5OixO<(XsKJ*K{m
zpYt7-w%K!717ct26Aw;>(_kGCr?tA*x$DCdt=yh<@yARq;qSpyI3rCqkvnV@7ss%7
zj~`tp8MXCjvzjxkk@kGB56hUg=Tnu%Gw^fx)Rqf(nZ$3{@Q9yyLsQ1h8a5rj{5kfe
zGh%gKq+R)-^;Y|o##=A6U+rAGVRKFTpSb7>=1+kp?MQbP$5v2KJX*j3j4zXLSq
zweQGQe2wy*uy*YkGx}3gQzqlwI$K-!$8*tMw+}V$b>`4HU^ZHdl%E0gZT=O%^hK}h
hbz=q&@h<{#kP3T_0k7WEwTIfYKZnCSv+ObB?*M`h2cG}{
diff --git a/frontend/providers/license/public/icons/license-bg.svg b/frontend/providers/license/public/icons/license-bg.svg
index 08977cb1d5b..b6de3cbc6f7 100644
--- a/frontend/providers/license/public/icons/license-bg.svg
+++ b/frontend/providers/license/public/icons/license-bg.svg
@@ -23,13 +23,6 @@
-
-
-
-
-
-
-
@@ -53,16 +46,6 @@
-
-
-
-
-
-
-
-
-
-
diff --git a/frontend/providers/license/public/icons/license-sealos.svg b/frontend/providers/license/public/icons/license-sealos.svg
new file mode 100644
index 00000000000..c63dd2757cf
--- /dev/null
+++ b/frontend/providers/license/public/icons/license-sealos.svg
@@ -0,0 +1,21 @@
+
diff --git a/frontend/providers/license/public/locales/en/common.json b/frontend/providers/license/public/locales/en/common.json
index e19bcfe5ce8..4504e44379d 100644
--- a/frontend/providers/license/public/locales/en/common.json
+++ b/frontend/providers/license/public/locales/en/common.json
@@ -4,6 +4,9 @@
"Activation Record": "Activation Record",
"Purchase License": "Purchase License",
"Please go to": "Please go to",
- "Buy": "Buy",
- "Activation Time": "Activation time: "
+ "Purchase": "purchase a license.",
+ "Activation Time": "Activation time: ",
+ "Go to the message center to see the results": "Go to the message center to see the results",
+ "token does not exist": "token does not exist",
+ "No Record": "No Record"
}
\ No newline at end of file
diff --git a/frontend/providers/license/public/locales/zh/common.json b/frontend/providers/license/public/locales/zh/common.json
index 06fc0cddef1..18ec9e4b9a6 100644
--- a/frontend/providers/license/public/locales/zh/common.json
+++ b/frontend/providers/license/public/locales/zh/common.json
@@ -3,7 +3,10 @@
"Activate License": "激活 License",
"Activation Record": "激活记录",
"Purchase License": "购买 License",
- "Please go to": "请前往",
- "Buy": "购买",
- "Activation Time": "激活时间: "
+ "Please go to": "请到",
+ "Purchase": "购买 license",
+ "Activation Time": "激活时间: ",
+ "Go to the message center to see the results": "前往消息中心查看结果",
+ "token does not exist": "token 不存在",
+ "No Record": "暂无记录"
}
\ No newline at end of file
diff --git a/frontend/providers/license/public/logo.svg b/frontend/providers/license/public/logo.svg
index d826088c31e..956fd4ac3f9 100644
--- a/frontend/providers/license/public/logo.svg
+++ b/frontend/providers/license/public/logo.svg
@@ -1,3 +1,3 @@
-