diff --git a/app/package.json b/app/package.json index 1cab49d..d40f964 100644 --- a/app/package.json +++ b/app/package.json @@ -16,6 +16,7 @@ "@radix-ui/react-dialog": "^1.0.4", "@radix-ui/react-dropdown-menu": "^2.0.5", "@radix-ui/react-label": "^2.0.2", + "@radix-ui/react-select": "^1.2.2", "@radix-ui/react-separator": "^1.0.3", "@radix-ui/react-slot": "^1.0.2", "@radix-ui/react-switch": "^1.0.3", diff --git a/app/pnpm-lock.yaml b/app/pnpm-lock.yaml index c5811e2..3a23a62 100644 --- a/app/pnpm-lock.yaml +++ b/app/pnpm-lock.yaml @@ -20,6 +20,9 @@ dependencies: '@radix-ui/react-label': specifier: ^2.0.2 version: 2.0.2(@types/react-dom@18.2.7)(@types/react@18.2.15)(react-dom@18.2.0)(react@18.2.0) + '@radix-ui/react-select': + specifier: ^1.2.2 + version: 1.2.2(@types/react-dom@18.2.7)(@types/react@18.2.15)(react-dom@18.2.0)(react@18.2.0) '@radix-ui/react-separator': specifier: ^1.0.3 version: 1.0.3(@types/react-dom@18.2.7)(@types/react@18.2.15)(react-dom@18.2.0)(react@18.2.0) @@ -1675,6 +1678,12 @@ packages: '@nodelib/fs.scandir': 2.1.5 fastq: 1.15.0 + /@radix-ui/number@1.0.1: + resolution: {integrity: sha512-T5gIdVO2mmPW3NNhjNgEP3cqMXjXL9UbO0BzWcXfvdBs+BohbQxvd/K5hSVKmn9/lbTdsQVKbUcP5WLCwvUbBg==} + dependencies: + '@babel/runtime': 7.22.11 + dev: false + /@radix-ui/primitive@1.0.1: resolution: {integrity: sha512-yQ8oGX2GVsEYMWGxcovu1uGWPCxV5BFfeeYxqPmuAzUyLT9qmaMXSAhXpb0WrspIeqYzdJpkh2vHModJPgRIaw==} dependencies: @@ -2140,6 +2149,47 @@ packages: react-dom: 18.2.0(react@18.2.0) dev: false + /@radix-ui/react-select@1.2.2(@types/react-dom@18.2.7)(@types/react@18.2.15)(react-dom@18.2.0)(react@18.2.0): + resolution: {integrity: sha512-zI7McXr8fNaSrUY9mZe4x/HC0jTLY9fWNhO1oLWYMQGDXuV4UCivIGTxwioSzO0ZCYX9iSLyWmAh/1TOmX3Cnw==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 + react-dom: ^16.8 || ^17.0 || ^18.0 + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + dependencies: + '@babel/runtime': 7.22.11 + '@radix-ui/number': 1.0.1 + '@radix-ui/primitive': 1.0.1 + '@radix-ui/react-collection': 1.0.3(@types/react-dom@18.2.7)(@types/react@18.2.15)(react-dom@18.2.0)(react@18.2.0) + '@radix-ui/react-compose-refs': 1.0.1(@types/react@18.2.15)(react@18.2.0) + '@radix-ui/react-context': 1.0.1(@types/react@18.2.15)(react@18.2.0) + '@radix-ui/react-direction': 1.0.1(@types/react@18.2.15)(react@18.2.0) + '@radix-ui/react-dismissable-layer': 1.0.4(@types/react-dom@18.2.7)(@types/react@18.2.15)(react-dom@18.2.0)(react@18.2.0) + '@radix-ui/react-focus-guards': 1.0.1(@types/react@18.2.15)(react@18.2.0) + '@radix-ui/react-focus-scope': 1.0.3(@types/react-dom@18.2.7)(@types/react@18.2.15)(react-dom@18.2.0)(react@18.2.0) + '@radix-ui/react-id': 1.0.1(@types/react@18.2.15)(react@18.2.0) + '@radix-ui/react-popper': 1.1.2(@types/react-dom@18.2.7)(@types/react@18.2.15)(react-dom@18.2.0)(react@18.2.0) + '@radix-ui/react-portal': 1.0.3(@types/react-dom@18.2.7)(@types/react@18.2.15)(react-dom@18.2.0)(react@18.2.0) + '@radix-ui/react-primitive': 1.0.3(@types/react-dom@18.2.7)(@types/react@18.2.15)(react-dom@18.2.0)(react@18.2.0) + '@radix-ui/react-slot': 1.0.2(@types/react@18.2.15)(react@18.2.0) + '@radix-ui/react-use-callback-ref': 1.0.1(@types/react@18.2.15)(react@18.2.0) + '@radix-ui/react-use-controllable-state': 1.0.1(@types/react@18.2.15)(react@18.2.0) + '@radix-ui/react-use-layout-effect': 1.0.1(@types/react@18.2.15)(react@18.2.0) + '@radix-ui/react-use-previous': 1.0.1(@types/react@18.2.15)(react@18.2.0) + '@radix-ui/react-visually-hidden': 1.0.3(@types/react-dom@18.2.7)(@types/react@18.2.15)(react-dom@18.2.0)(react@18.2.0) + '@types/react': 18.2.15 + '@types/react-dom': 18.2.7 + aria-hidden: 1.2.3 + react: 18.2.0 + react-dom: 18.2.0(react@18.2.0) + react-remove-scroll: 2.5.5(@types/react@18.2.15)(react@18.2.0) + dev: false + /@radix-ui/react-separator@1.0.3(@types/react-dom@18.2.7)(@types/react@18.2.15)(react-dom@18.2.0)(react@18.2.0): resolution: {integrity: sha512-itYmTy/kokS21aiV5+Z56MZB54KrhPgn6eHDKkFeOLR34HMN2s8PaN47qZZAGnvupcjxHaFZnW4pQEh0BvvVuw==} peerDependencies: diff --git a/app/src/App.tsx b/app/src/App.tsx index ead83f8..433f20e 100644 --- a/app/src/App.tsx +++ b/app/src/App.tsx @@ -5,7 +5,7 @@ import { Button } from "./components/ui/button.tsx"; import router from "./router.ts"; import I18nProvider from "./components/I18nProvider.tsx"; import ProjectLink from "./components/ProjectLink.tsx"; -import { Cloud, Menu } from "lucide-react"; +import {BadgeCent, Boxes, CalendarPlus, Cloud, Menu} from "lucide-react"; import { Provider, useDispatch, useSelector } from "react-redux"; import { toggleMenu } from "./store/menu.ts"; import store from "./store/index.ts"; @@ -30,7 +30,9 @@ import { useTranslation } from "react-i18next"; import Quota from "./routes/Quota.tsx"; import { openDialog as openQuotaDialog, quotaSelector } from "./store/quota.ts"; import { openDialog as openPackageDialog } from "./store/package.ts"; +import { openDialog as openSub } from "./store/subscription.ts"; import Package from "./routes/Package.tsx"; +import Subscription from "./routes/Subscription.tsx"; function Settings() { const { t } = useTranslation(); @@ -56,9 +58,15 @@ function Settings() { {quota} dispatch(openQuotaDialog())}> + {t("quota")} + dispatch(openSub())}> + + {t("sub.title")} + dispatch(openPackageDialog())}> + {t("pkg.title")} @@ -126,6 +134,7 @@ function App() { + ); } diff --git a/app/src/assets/main.less b/app/src/assets/main.less index 3dc2709..b5df54b 100644 --- a/app/src/assets/main.less +++ b/app/src/assets/main.less @@ -70,3 +70,26 @@ strong { flex-direction: row; align-items: center; } + +.flex-dialog { + border-radius: var(--radius) !important; + max-height: calc(100vh - 2rem) !important; + overflow-x: hidden; + overflow-y: auto; + scrollbar-width: none; + -webkit-overflow-scrolling: touch; + touch-action: pan-y; + outline: 0; + + @media (max-width: 520px) { + & { + margin-top: 1rem; + margin-bottom: 1rem; + transform: translate(var(--tw-translate-x), calc(var(--tw-translate-y) - 1rem)) !important; + } + } +} + +.cent { + font-weight: normal !important; +} diff --git a/app/src/assets/quota.less b/app/src/assets/quota.less index 2714245..2ece261 100644 --- a/app/src/assets/quota.less +++ b/app/src/assets/quota.less @@ -36,21 +36,6 @@ .quota-dialog { max-width: min(90vw, 1044px) !important; - border-radius: var(--radius) !important; - max-height: calc(100vh - 2rem) !important; - overflow-x: hidden; - overflow-y: auto; - scrollbar-width: none; - -webkit-overflow-scrolling: touch; - touch-action: pan-y; - - @media (max-width: 520px) { - & { - margin-top: 1rem; - margin-bottom: 1rem; - transform: translate(var(--tw-translate-x), calc(var(--tw-translate-y) - 1rem)) !important; - } - } } .amount-container { @@ -224,6 +209,14 @@ flex-direction: row; align-items: center; gap: 4px; + + svg { + flex-shrink: 0; + } + } + + .info { + margin-top: 6px; } } diff --git a/app/src/assets/subscription.less b/app/src/assets/subscription.less new file mode 100644 index 0000000..660eb02 --- /dev/null +++ b/app/src/assets/subscription.less @@ -0,0 +1,109 @@ +.sub-dialog { + max-width: min(90vw, 844px) !important; +} + +.sub-wrapper { + display: flex; + flex-direction: column; + margin: 18px 4px !important; + margin-bottom: 32px !important; + align-items: center; +} + +.date { + display: flex; + flex-direction: row; + align-items: center; + color: hsl(45, 100%, 50%); + padding: 8px 16px; + margin-top: 16px; + border-radius: var(--radius); + border: 1px solid hsl(var(--border)); + + svg { + flex-shrink: 0; + transform: translateY(1px); + } +} + +.plan-wrapper { + display: flex; + flex-direction: row; + flex-wrap: wrap; + gap: 12px; + justify-content: center; + margin-top: 24px; +} + +.plan { + display: flex; + flex-direction: column; + gap: 6px; + padding: 36px 64px; + border-radius: var(--radius); + border: 2px solid hsl(var(--border)); + color: hsl(var(--text)); + min-width: 294px; + + &.pro { + border-color: hsl(var(--text-secondary)); + } + + .title { + text-align: center; + font-size: 16px; + margin: 4px; + } + + .price { + font-size: 18px; + font-weight: bold; + text-align: center; + margin: 2px; + } + + .desc { + display: flex; + flex-direction: column; + align-items: center; + gap: 6px; + margin: 24px 0; + + div { + display: flex; + flex-direction: row; + align-items: center; + + svg { + flex-shrink: 0; + transform: translateY(1px); + } + } + } + + .action { + margin-top: auto !important; + } +} + +.upgrade-wrapper { + margin: 24px auto 8px; + + .price { + font-size: 14px; + margin-top: 12px; + text-align: center; + transform: translateY(12px); + } +} + +@media (max-width: 460px) { + .plan { + scale: .95; + min-width: 0 !important; + } + + .plan-wrapper { + gap: 8px; + } +} diff --git a/app/src/components/ui/select.tsx b/app/src/components/ui/select.tsx new file mode 100644 index 0000000..c55b92a --- /dev/null +++ b/app/src/components/ui/select.tsx @@ -0,0 +1,119 @@ +import * as React from "react" +import * as SelectPrimitive from "@radix-ui/react-select" +import { Check, ChevronDown } from "lucide-react" + +import { cn } from "./lib/utils" + +const Select = SelectPrimitive.Root + +const SelectGroup = SelectPrimitive.Group + +const SelectValue = SelectPrimitive.Value + +const SelectTrigger = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, children, ...props }, ref) => ( + + {children} + + + + +)) +SelectTrigger.displayName = SelectPrimitive.Trigger.displayName + +const SelectContent = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, children, position = "popper", ...props }, ref) => ( + + + + {children} + + + +)) +SelectContent.displayName = SelectPrimitive.Content.displayName + +const SelectLabel = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)) +SelectLabel.displayName = SelectPrimitive.Label.displayName + +const SelectItem = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, children, ...props }, ref) => ( + + + + + + + + {children} + +)) +SelectItem.displayName = SelectPrimitive.Item.displayName + +const SelectSeparator = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)) +SelectSeparator.displayName = SelectPrimitive.Separator.displayName + +export { + Select, + SelectGroup, + SelectValue, + SelectTrigger, + SelectContent, + SelectLabel, + SelectItem, + SelectSeparator, +} diff --git a/app/src/conf.ts b/app/src/conf.ts index 4fb314d..52400bc 100644 --- a/app/src/conf.ts +++ b/app/src/conf.ts @@ -18,3 +18,5 @@ export function login() { axios.defaults.baseURL = rest_api; axios.defaults.headers.post["Content-Type"] = "application/json"; + +console.debug(`chatnio application (version: ${version})`); diff --git a/app/src/conversation/addition.ts b/app/src/conversation/addition.ts index cf506ff..5bda693 100644 --- a/app/src/conversation/addition.ts +++ b/app/src/conversation/addition.ts @@ -11,6 +11,17 @@ type PackageResponse = { teenager: boolean; }; +type SubscriptionResponse = { + status: boolean; + is_subscribed: boolean; + expired: number; +} + +type BuySubscriptionResponse = { + status: boolean; + error: string; +} + export async function buyQuota(quota: number): Promise { try { const resp = await axios.post(`/buy`, { quota }); @@ -24,6 +35,9 @@ export async function buyQuota(quota: number): Promise { export async function getPackage(): Promise { try { const resp = await axios.get(`/package`); + if (resp.data.status === false) { + return { status: false, cert: false, teenager: false }; + } return { status: resp.data.status, cert: resp.data.data.cert, @@ -34,3 +48,32 @@ export async function getPackage(): Promise { return { status: false, cert: false, teenager: false }; } } + +export async function getSubscription(): Promise { + try { + const resp = await axios.get(`/subscription`); + if (resp.data.status === false) { + return { status: false, is_subscribed: false, expired: 0 }; + } + return { + status: resp.data.status, + is_subscribed: resp.data.data.is_subscribed, + expired: resp.data.data.expired, + }; + } catch (e) { + console.debug(e); + return { status: false, is_subscribed: false, expired: 0 }; + } +} + +export async function buySubscription( + month: number, +): Promise { + try { + const resp = await axios.post(`/subscription`, { month }); + return resp.data as BuySubscriptionResponse; + } catch (e) { + console.debug(e); + return { status: false, error: "network error" }; + } +} diff --git a/app/src/i18n.ts b/app/src/i18n.ts index f40dd98..71b68d6 100644 --- a/app/src/i18n.ts +++ b/app/src/i18n.ts @@ -75,6 +75,7 @@ const resources = { failed: "Purchase failed", "failed-prompt": "Failed to purchase points. Please make sure you have enough balance, you will soon jump to deeptrain wallet to pay balance.", + "gpt4-tip": "Tip: web searching feature may consume more input points", }, pkg: { title: "Packages", @@ -91,6 +92,43 @@ const resources = { false: "Not Received", }, }, + sub: { + title: "Subscription", + "dialog-title": "Subscription Plan", + free: "Free", + "free-price": "Free Forever", + pro: "Pro", + "pro-price": "8 CNY/Month", + "free-gpt3": "GPT-3.5 Free Forever", + "free-dalle": "5 free quotas per day", + "free-web": "web searching feature", + "free-conversation": "conversation storage", + "free-api": "API calls", + "pro-gpt4": "GPT-4 10 requests per day", + "pro-dalle": "50 quotas per day", + "pro-service": "Priority Service Support", + "pro-thread": "Concurrency Increase", + "current": "Current Plan", + "upgrade": "Upgrade", + "renew": "Renew", + "cannot-select": "Cannot Select", + "select-time": "Select Subscription Time", + "price": "Price {{price}} CNY", + "expired": "Your Pro subscription will expire in {{expired}} days", + time: { + 1: "1 Month", + 3: "3 Months", + 6: "6 Months", + 12: "1 Year", + }, + "success": "Subscribe success", + "success-prompt": "You have successfully subscribed to {{month}} months of Pro.", + "failed": "Subscribe failed", + "failed-prompt": "Failed to subscribe, please make sure you have enough balance, you will soon jump to deeptrain wallet to pay balance.", + }, + "cancel": "Cancel", + "confirm": "Confirm", + "percent": "{{cent}}0%", }, }, cn: { @@ -157,6 +195,7 @@ const resources = { failed: "购买失败", "failed-prompt": "购买点数失败。请确保您有足够的余额,您即将跳转到 deeptrain 钱包支付余额。", + "gpt4-tip": "提示:web 联网版功能可能会带来更多的输入点数消耗", }, pkg: { title: "礼包", @@ -172,6 +211,43 @@ const resources = { false: "无法领取", }, }, + sub: { + title: "订阅", + "dialog-title": "订阅计划", + free: "免费版", + "free-price": "永久免费", + pro: "专业版", + "pro-price": "8 元/月", + "free-gpt3": "GPT-3.5 永久免费", + "free-dalle": "每日 5 次免费绘图", + "free-web": "联网搜索功能", + "free-conversation": "对话存储记录", + "free-api": "API 调用", + "pro-gpt4": "GPT-4 每日请求 10 次", + "pro-dalle": "每日 50 次绘图", + "pro-service": "优先服务支持", + "pro-thread": "并发数提升", + "current": "当前计划", + "upgrade": "升级", + "renew": "续费", + "cannot-select": "无法选择", + "select-time": "选择订阅时间", + "price": "价格 {{price}} 元", + "expired": "您的专业版订阅还有 {{expired}} 天到期", + time: { + 1: "1个月", + 3: "3个月", + 6: "半年", + 12: "1年", + }, + "success": "订阅成功", + "success-prompt": "您已成功订阅 {{month}} 月专业版。", + "failed": "订阅失败", + "failed-prompt": "订阅失败,请确保您有足够的余额,您即将跳转到 deeptrain 钱包支付余额。", + }, + "cancel": "取消", + "confirm": "确认", + "percent": "{{cent}}折", }, }, }; diff --git a/app/src/main.tsx b/app/src/main.tsx index 6b868bc..482663b 100644 --- a/app/src/main.tsx +++ b/app/src/main.tsx @@ -5,9 +5,7 @@ import "./conf.ts"; import "./i18n.ts"; import "./assets/main.less"; import "./assets/globals.less"; -import {version} from "./conf.ts"; - -console.debug(`chatnio application (version: ${version})`); +import "./conf.ts"; ReactDOM.createRoot(document.getElementById("root")!).render( diff --git a/app/src/routes/Auth.tsx b/app/src/routes/Auth.tsx index 691e031..b0f45ee 100644 --- a/app/src/routes/Auth.tsx +++ b/app/src/routes/Auth.tsx @@ -1,7 +1,7 @@ import { useToast } from "../components/ui/use-toast.ts"; import { useLocation } from "react-router-dom"; import { ToastAction } from "../components/ui/toast.tsx"; -import { login, tokenField } from "../conf.ts"; +import { login } from "../conf.ts"; import { useEffect } from "react"; import Loader from "../components/Loader.tsx"; import "../assets/auth.less"; @@ -16,23 +16,24 @@ function Auth() { const { t } = useTranslation(); const dispatch = useDispatch(); const search = new URLSearchParams(useLocation().search); - const token = (search.get(tokenField) || "").trim(); - - if (!token.length) { - toast({ - title: t("invalid-token"), - description: t("invalid-token-prompt"), - action: ( - - {t("try-again")} - - ), - }); - - setTimeout(login, 2500); - } + const token = (search.get("token") || "").trim(); useEffect(() => { + if (!token.length) { + toast({ + title: t("invalid-token"), + description: t("invalid-token-prompt"), + action: ( + + {t("try-again")} + + ), + }); + + setTimeout(login, 2500); + return; + } + axios .post("/login", { token }) .then((res) => { diff --git a/app/src/routes/Quota.tsx b/app/src/routes/Quota.tsx index 6a2d9cf..3d9fb86 100644 --- a/app/src/routes/Quota.tsx +++ b/app/src/routes/Quota.tsx @@ -20,7 +20,7 @@ import { Cloud, ExternalLink, HardDriveDownload, - HardDriveUpload, + HardDriveUpload, Info, Plus, } from "lucide-react"; import { Input } from "../components/ui/input.tsx"; @@ -88,7 +88,7 @@ function Quota() { open={open} onOpenChange={(state: boolean) => dispatch(setDialog(state))} > - + {t("buy.choose")} @@ -275,6 +275,12 @@ function Quota() { 4.3 / 1k token +
+
+ + {t("buy.gpt4-tip")} +
+
diff --git a/app/src/routes/Subscription.tsx b/app/src/routes/Subscription.tsx new file mode 100644 index 0000000..4acc1df --- /dev/null +++ b/app/src/routes/Subscription.tsx @@ -0,0 +1,185 @@ +import { + dialogSelector, + expiredSelector, + isSubscribedSelector, refreshSubscription, + refreshSubscriptionTask, + setDialog +} from "../store/subscription.ts"; +import {Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle, DialogTrigger} from "../components/ui/dialog.tsx"; +import {useDispatch, useSelector} from "react-redux"; +import {useTranslation} from "react-i18next"; +import {useToast} from "../components/ui/use-toast.ts"; +import React, {useEffect} from "react"; +import "../assets/subscription.less"; +import { + Calendar, + Compass, + Globe, + Image, + ImagePlus, + LifeBuoy, + MessageSquare, + MessagesSquare, Plus, + ServerCrash, Webhook +} from "lucide-react"; +import {Button} from "../components/ui/button.tsx"; +import {Select, SelectContent, SelectItem, SelectTrigger, SelectValue} from "../components/ui/select.tsx"; +import {Badge} from "../components/ui/badge.tsx"; +import {buySubscription} from "../conversation/addition.ts"; + +function calc_prize(month: number): number { + if (month >= 12) { + return 8 * month * 0.9; + } + return 8 * month; +} + +type UpgradeProps = { + children: React.ReactNode; +} + +async function callBuyAction(t: any, toast: any, month: number): Promise { + const res = await buySubscription(month); + if (res.status) { + toast({ + title: t("sub.success"), + description: t("sub.success-prompt", { + month, + }), + }); + } else { + toast({ + title: t("sub.failed"), + description: t("sub.failed-prompt"), + }); + setTimeout(() => { + window.open( + "https://deeptrain.lightxi.com/home/wallet", + ); + }, 2000); + } + return res.status; +} +function Upgrade({ children }: UpgradeProps) { + const { t } = useTranslation(); + const [open, setOpen] = React.useState(false); + const [month, setMonth] = React.useState(1); + const dispatch = useDispatch(); + const { toast } = useToast(); + + return ( + + {children} + + + { t('sub.select-time') } + +
+ +

{ t('sub.price', { price: calc_prize(month) }) }

+
+ + + + +
+
+ ) +} + +function Subscription() { + const { t } = useTranslation(); + const open = useSelector(dialogSelector); + const subscription = useSelector(isSubscribedSelector); + const expired = useSelector(expiredSelector); + const dispatch = useDispatch(); + useEffect(() => { + refreshSubscriptionTask(dispatch); + }, []); + + return ( + dispatch(setDialog(state))} + > + + + {t("sub.dialog-title")} + +
+ { + subscription && ( +
+ + {t("sub.expired", { expired })} +
+ ) + } +
+
+
{t("sub.free")}
+
{t("sub.free-price")}
+
+
{t("sub.free-gpt3")}
+
{t("sub.free-dalle")}
+
{t("sub.free-web")}
+
{t("sub.free-conversation")}
+
{t("sub.free-api")}
+
+ +
+
+
{t("sub.pro")}
+
{t("sub.pro-price")}
+
+
{t("sub.pro-gpt4")}
+
{t("sub.pro-dalle")}
+
{t("sub.pro-service")}
+
{t("sub.pro-thread")}
+
+ + + +
+
+
+
+
+
+
+ ) +} + +export default Subscription; diff --git a/app/src/store/index.ts b/app/src/store/index.ts index dc0d64c..0a87213 100644 --- a/app/src/store/index.ts +++ b/app/src/store/index.ts @@ -4,6 +4,7 @@ import authReducer from "./auth"; import chatReducer from "./chat"; import quotaReducer from "./quota"; import packageReducer from "./package"; +import subscriptionReducer from "./subscription"; const store = configureStore({ reducer: { @@ -12,6 +13,7 @@ const store = configureStore({ chat: chatReducer, quota: quotaReducer, package: packageReducer, + subscription: subscriptionReducer, }, }); diff --git a/app/src/store/subscription.ts b/app/src/store/subscription.ts new file mode 100644 index 0000000..82f2c44 --- /dev/null +++ b/app/src/store/subscription.ts @@ -0,0 +1,57 @@ +import { createSlice } from "@reduxjs/toolkit"; +import {getSubscription} from "../conversation/addition.ts"; + +export const subscriptionSlice = createSlice({ + name: "subscription", + initialState: { + dialog: false, + is_subscribed: false, + expired: 0, + }, + reducers: { + toggleDialog: (state) => { + state.dialog = !state.dialog; + }, + setDialog: (state, action) => { + state.dialog = action.payload as boolean; + }, + openDialog: (state) => { + state.dialog = true; + }, + closeDialog: (state) => { + state.dialog = false; + }, + updateSubscription: (state, action) => { + state.is_subscribed = action.payload.is_subscribed; + state.expired = action.payload.expired; + }, + }, +}); + +export const { + toggleDialog, + setDialog, + openDialog, + closeDialog, + updateSubscription, +} = subscriptionSlice.actions; +export default subscriptionSlice.reducer; + +export const dialogSelector = (state: any): boolean => state.subscription.dialog; +export const isSubscribedSelector = (state: any): boolean => state.subscription.is_subscribed; +export const expiredSelector = (state: any): number => state.subscription.expired; + +export const refreshSubscription = async (dispatch: any) => { + const current = new Date().getTime(); //@ts-ignore + if (window.hasOwnProperty("subscription") && current - window.subscription < 2500) + return; //@ts-ignore + window.subscription = current; + + const response = await getSubscription(); + if (response.status) dispatch(updateSubscription(response)); +}; + +export const refreshSubscriptionTask = (dispatch: any) => { + setInterval(() => refreshSubscription(dispatch), 20000); + refreshSubscription(dispatch).then(); +};