diff --git a/client/packages/lowcoder/src/api/subscriptionApi.ts b/client/packages/lowcoder/src/api/subscriptionApi.ts index 1f376d159..b0fe54419 100644 --- a/client/packages/lowcoder/src/api/subscriptionApi.ts +++ b/client/packages/lowcoder/src/api/subscriptionApi.ts @@ -1,128 +1,16 @@ import Api from "api/api"; import axios, { AxiosInstance, AxiosRequestConfig } from "axios"; import { useDispatch, useSelector } from "react-redux"; -import { getUser, getCurrentUser } from "redux/selectors/usersSelectors"; import { useEffect, useState} from "react"; import { calculateFlowCode } from "./apiUtils"; -import { getDeploymentId } from "@lowcoder-ee/redux/selectors/configSelectors"; import { fetchOrgUsersAction } from "redux/reduxActions/orgActions"; import { getOrgUsers } from "redux/selectors/orgSelectors"; import { AppState } from "@lowcoder-ee/redux/reducers"; - -// Interfaces -export interface CustomerAddress { - line1: string; - line2: string; - city: string; - state: string; - country: string; - postalCode: string; -} - -export interface LowcoderNewCustomer { - hostname: string; - hostId: string; - email: string; - orgId: string; - userId: string; - userName: string; - type: string; - companyName: string; - address?: CustomerAddress; -} - -export interface LowcoderSearchCustomer { - hostname: string; - hostId: string; - email: string; - orgId: string; - userId: string; -} - -interface LowcoderMetadata { - lowcoder_host: string; - lowcoder_hostId: string; - lowcoder_orgId: string; - lowcoder_type: string; - lowcoder_userId: string; -} - -export interface StripeCustomer { - id: string; - object: string; - address?: object | null; - balance: number; - created: number; - currency: string | null; - default_source: string | null; - delinquent: boolean; - description: string | null; - discount: string | null; - email: string; - invoice_prefix: string; - invoice_settings: object | null; - livemode: boolean; - metadata: LowcoderMetadata; - name: string; - phone: string | null; - preferred_locales: string[]; - shipping: string | null; - tax_exempt: string; - test_clock: string | null; -} - -export interface Pricing { - type: string; - amount: string; -} - -export interface Product { - title?: string; - description?: string; - image?: string; - pricingType: string; - product: string; - activeSubscription: boolean; - accessLink: string; - subscriptionId: string; - checkoutLink: string; - checkoutLinkDataLoaded?: boolean; - type?: string; - quantity_entity?: string; -} - -export interface SubscriptionItem { - id: string; - object: string; - plan: { - id: string; - product: string; - }; - quantity: number; -} - -export interface Subscription { - id: string; - collection_method: string; - current_period_end: number; - current_period_start: number; - product: string; - currency: string; - interval: string; - tiers_mode: string; - status: string; - start_date: number; - quantity: number; - billing_scheme: string; - price: string; -} - -export interface SubscriptionsData { - subscriptions: Subscription[]; - subscriptionDataLoaded: boolean; - subscriptionDataError: boolean; - loading: boolean; -} +import type { + LowcoderNewCustomer, + LowcoderSearchCustomer, + StripeCustomer, +} from "@lowcoder-ee/constants/subscriptionConstants"; export type ResponseType = { response: any; @@ -274,7 +162,7 @@ export const getProducts = async () => { }; try { const result = await SubscriptionApi.secureRequest(apiBody); - return result?.data as any; + return result?.data?.data as any[]; } catch (error) { console.error("Error fetching product:", error); throw error; @@ -380,244 +268,4 @@ export const useOrgUserCount = (orgId: string) => { return userCount; }; -export const InitializeSubscription = () => { - const [customer, setCustomer] = useState(null); - const [isCreatingCustomer, setIsCreatingCustomer] = useState(false); // Track customer creation - const [customerDataError, setCustomerDataError] = useState(false); - const [subscriptions, setSubscriptions] = useState([]); - const [subscriptionDataLoaded, setSubscriptionDataLoaded] = useState(false); - const [subscriptionDataError, setSubscriptionDataError] = useState(false); - const [checkoutLinkDataLoaded, setCheckoutLinkDataLoaded] = useState(false); - const [checkoutLinkDataError, setCheckoutLinkDataError] = useState(false); - const [products, setProducts] = useState([ - { - pricingType: "Monthly, per User", - activeSubscription: false, - accessLink: "1PhH38DDlQgecLSfSukEgIeV", - product: "QW8L3WPMiNjQjI", - subscriptionId: "", - checkoutLink: "", - checkoutLinkDataLoaded: false, - type: "org", - quantity_entity: "orgUser", - }, - { - pricingType: "Monthly, per User", - activeSubscription: false, - accessLink: "1Pf65wDDlQgecLSf6OFlbsD5", - product: "QW8MpIBHxieKXd", - checkoutLink: "", - checkoutLinkDataLoaded: false, - subscriptionId: "", - type: "user", - quantity_entity: "singleItem", - }, - { - pricingType: "Monthly, per User", - activeSubscription: false, - accessLink: "1PttHIDDlQgecLSf0XP27tXt", - product: "QlQ7cdOh8Lv4dy", - subscriptionId: "", - checkoutLink: "", - checkoutLinkDataLoaded: false, - type: "org", - quantity_entity: "singleItem", - }, - ]); - - - const user = useSelector(getUser); - const currentUser = useSelector(getCurrentUser); - const deploymentId = useSelector(getDeploymentId); - const currentOrg = user.orgs.find(org => org.id === user.currentOrgId); - const orgID = user.currentOrgId; - const domain = window.location.protocol + "//" + window.location.hostname + (window.location.port ? ':' + window.location.port : ''); - const admin = user.orgRoleMap.get(orgID) === "admin" ? "admin" : "member"; - const dispatch = useDispatch(); - - const userCount = useOrgUserCount(orgID); - - const subscriptionSearchCustomer: LowcoderSearchCustomer = { - hostname: domain, - hostId: deploymentId, - email: currentUser.email, - orgId: orgID, - userId: user.id, - }; - - const subscriptionNewCustomer: LowcoderNewCustomer = { - hostname: domain, - hostId: deploymentId, - email: currentUser.email, - orgId: orgID, - userId: user.id, - userName: user.username, - type: admin, - companyName: currentOrg?.name || "Unknown", - }; - - useEffect(() => { - const initializeCustomer = async () => { - try { - setIsCreatingCustomer(true); - const existingCustomer = await searchCustomer(subscriptionSearchCustomer); - if (existingCustomer != null) { - setCustomer(existingCustomer); - } else { - const newCustomer = await createCustomer(subscriptionNewCustomer); - setCustomer(newCustomer); - } - } catch (error) { - setCustomerDataError(true); - } finally { - setIsCreatingCustomer(false); - } - }; - - if (Boolean(deploymentId)) { - initializeCustomer(); - } - }, [deploymentId]); - - useEffect(() => { - const fetchSubscriptions = async () => { - if (customer) { - try { - const subs = await searchSubscriptions(customer.id); - setSubscriptions(subs); - setSubscriptionDataLoaded(true); - } catch (error) { - setSubscriptionDataError(true); - } - } - }; - - fetchSubscriptions(); - }, [customer]); - - useEffect(() => { - const prepareCheckout = async () => { - if (subscriptionDataLoaded && userCount > 0) { // Ensure user count is available - try { - console.log("Total Users in Organization:", userCount); - - const updatedProducts = await Promise.all( - products.map(async (product) => { - const matchingSubscription = subscriptions.find( - (sub) => sub.plan.id === "price_" + product.accessLink - ); - - if (matchingSubscription) { - return { - ...product, - activeSubscription: true, - checkoutLinkDataLoaded: true, - subscriptionId: matchingSubscription.id.substring(4), - }; - } else { - // Use the user count to set the quantity for checkout link - const checkoutLink = await createCheckoutLink(customer!, product.accessLink, userCount); - return { - ...product, - activeSubscription: false, - checkoutLink: checkoutLink ? checkoutLink.url : "", - checkoutLinkDataLoaded: true, - }; - } - }) - ); - - setProducts(updatedProducts); - } catch (error) { - setCheckoutLinkDataError(true); - } - } - }; - - prepareCheckout(); - }, [subscriptionDataLoaded, userCount]); - - return { - customer, - isCreatingCustomer, - customerDataError, - subscriptions, - subscriptionDataLoaded, - subscriptionDataError, - checkoutLinkDataLoaded, - checkoutLinkDataError, - products, - admin, - }; -}; - -export enum SubscriptionProducts { - SUPPORT = "QW8L3WPMiNjQjI", - MEDIAPACKAGE = 'QW8MpIBHxieKXd', - AZUREAPIS = 'premium', - GOOGLEAPIS = 'enterprise', - AWSAPIS = 'enterprise-global', - PRIVATECLOUD = 'private-cloud', - MATRIXCLOUD = 'matrix-cloud', - AGORATOKENSERVER = 'agora-tokenserver', - SIGNALSERVER = 'signal-server', - DATABASE = 'database', - STORAGE = 'storage', - IOSAPP = 'ios-app', - ANDROIDAPP = 'android-app', - AUDITLOG = 'audit-log', - APPLOG = 'app-log', - ENVIRONMENTS = 'environments', - GITREPOS = 'git-repos', -} - -export const CheckSubscriptions = () => { - const [subscriptions, setSubscriptions] = useState([]); - const [subscriptionDataLoaded, setSubscriptionDataLoaded] = useState(false); - const [subscriptionDataError, setSubscriptionDataError] = useState(false); - const [loading, setLoading] = useState(true); - - const user = useSelector(getUser); - const currentUser = useSelector(getCurrentUser); - const deploymentId = useSelector(getDeploymentId); - const orgID = user.currentOrgId; - const domain = window.location.protocol + "//" + window.location.hostname + (window.location.port ? ':' + window.location.port : ''); - - const subscriptionSearchCustomer: LowcoderSearchCustomer = { - hostname: domain, - hostId: deploymentId, - email: currentUser.email, - orgId: orgID, - userId: user.id, - }; - - useEffect(() => { - const fetchCustomerAndSubscriptions = async () => { - try { - const subs = await searchCustomersSubscriptions(subscriptionSearchCustomer); - setSubscriptions(subs); - setSubscriptionDataLoaded(true); - } catch (error) { - setSubscriptionDataError(true); - } finally { - setLoading(false); - } - }; - if ( - Boolean(currentUser.email) - && Boolean(orgID) - && Boolean(user.id) - && Boolean(deploymentId) - ) - fetchCustomerAndSubscriptions(); - }, [subscriptionSearchCustomer]); - - return { - subscriptions, - subscriptionDataLoaded, - subscriptionDataError, - loading, - }; -}; - export default SubscriptionApi; diff --git a/client/packages/lowcoder/src/constants/reduxActionConstants.ts b/client/packages/lowcoder/src/constants/reduxActionConstants.ts index 1702e0184..8f0bf2f54 100644 --- a/client/packages/lowcoder/src/constants/reduxActionConstants.ts +++ b/client/packages/lowcoder/src/constants/reduxActionConstants.ts @@ -227,6 +227,8 @@ export const ReduxActionErrorTypes = { CREATE_APP_SNAPSHOT_ERROR: "CREATE_APP_SNAPSHOT_ERROR", FETCH_APP_SNAPSHOTS_ERROR: "FETCH_APP_SNAPSHOTS_ERROR", FETCH_APP_SNAPSHOT_DSL_ERROR: "FETCH_APP_SNAPSHOT_DSL_ERROR", + + FETCH_DATASOURCE_ERROR: "FETCH_DATASOURCE_ERROR", }; export type ReduxActionType = typeof ReduxActionTypes[keyof typeof ReduxActionTypes]; diff --git a/client/packages/lowcoder/src/constants/subscriptionConstants.ts b/client/packages/lowcoder/src/constants/subscriptionConstants.ts new file mode 100644 index 000000000..99c4ecb64 --- /dev/null +++ b/client/packages/lowcoder/src/constants/subscriptionConstants.ts @@ -0,0 +1,154 @@ + +export enum SubscriptionProductsEnum { + SUPPORT = "QW8L3WPMiNjQjI", + MEDIAPACKAGE = 'QW8MpIBHxieKXd', + AZUREAPIS = 'premium', + GOOGLEAPIS = 'enterprise', + AWSAPIS = 'enterprise-global', + PRIVATECLOUD = 'private-cloud', + MATRIXCLOUD = 'matrix-cloud', + AGORATOKENSERVER = 'agora-tokenserver', + SIGNALSERVER = 'signal-server', + DATABASE = 'database', + STORAGE = 'storage', + IOSAPP = 'ios-app', + ANDROIDAPP = 'android-app', + AUDITLOG = 'audit-log', + APPLOG = 'app-log', + ENVIRONMENTS = 'environments', + GITREPOS = 'git-repos', +} + +export const InitSubscriptionProducts = [ + { + pricingType: "Monthly, per User", + activeSubscription: false, + accessLink: "1PhH38DDlQgecLSfSukEgIeV", + product: SubscriptionProductsEnum.SUPPORT, + subscriptionId: "", + checkoutLink: "", + checkoutLinkDataLoaded: false, + type: "org", + quantity_entity: "orgUser", + }, + { + pricingType: "Monthly, per User", + activeSubscription: false, + accessLink: "1Pf65wDDlQgecLSf6OFlbsD5", + product: SubscriptionProductsEnum.MEDIAPACKAGE, + checkoutLink: "", + checkoutLinkDataLoaded: false, + subscriptionId: "", + type: "user", + quantity_entity: "singleItem", + }, + { + pricingType: "Monthly, per User", + activeSubscription: false, + accessLink: "1PttHIDDlQgecLSf0XP27tXt", + product: "QlQ7cdOh8Lv4dy", + subscriptionId: "", + checkoutLink: "", + checkoutLinkDataLoaded: false, + type: "org", + quantity_entity: "singleItem", + }, +] + +export interface Subscription { + id: string; + collection_method: string; + current_period_end: number; + current_period_start: number; + product: string; + currency: string; + interval: string; + tiers_mode: string; + status: string; + start_date: number; + quantity: number; + billing_scheme: string; + price: string; +} + +export interface SubscriptionProduct { + title?: string; + description?: string; + image?: string; + pricingType: string; + product: string; + activeSubscription: boolean; + accessLink: string; + subscriptionId: string; + checkoutLink: string; + checkoutLinkDataLoaded?: boolean; + type?: string; + quantity_entity?: string; +} + +export interface CustomerAddress { + line1: string; + line2: string; + city: string; + state: string; + country: string; + postalCode: string; +} + +export interface LowcoderNewCustomer { + hostname: string; + hostId: string; + email: string; + orgId: string; + userId: string; + userName: string; + type: string; + companyName: string; + address?: CustomerAddress; +} + +export interface LowcoderSearchCustomer { + hostname: string; + hostId: string; + email: string; + orgId: string; + userId: string; +} + +interface LowcoderMetadata { + lowcoder_host: string; + lowcoder_hostId: string; + lowcoder_orgId: string; + lowcoder_type: string; + lowcoder_userId: string; +} + +export interface StripeCustomer { + id: string; + object: string; + address?: object | null; + balance: number; + created: number; + currency: string | null; + default_source: string | null; + delinquent: boolean; + description: string | null; + discount: string | null; + email: string; + invoice_prefix: string; + invoice_settings: object | null; + livemode: boolean; + metadata: LowcoderMetadata; + name: string; + phone: string | null; + preferred_locales: string[]; + shipping: string | null; + tax_exempt: string; + test_clock: string | null; +} + +export interface Pricing { + type: string; + amount: string; +} + diff --git a/client/packages/lowcoder/src/pages/ApplicationV2/HomeLayout.tsx b/client/packages/lowcoder/src/pages/ApplicationV2/HomeLayout.tsx index fd5c2df16..e69792bbd 100644 --- a/client/packages/lowcoder/src/pages/ApplicationV2/HomeLayout.tsx +++ b/client/packages/lowcoder/src/pages/ApplicationV2/HomeLayout.tsx @@ -307,8 +307,6 @@ export function HomeLayout(props: HomeLayoutProps) { const { breadcrumb = [], elements = [], localMarketplaceApps = [], globalMarketplaceApps = [], mode } = props; - console.log("HomeLayout props: ", props); - const categoryOptions = [ { label: {trans("home.allCategories")}, value: 'All' }, ...Object.entries(ApplicationCategoriesEnum).map(([key, value]) => ({ diff --git a/client/packages/lowcoder/src/pages/ApplicationV2/index.tsx b/client/packages/lowcoder/src/pages/ApplicationV2/index.tsx index 0581c97eb..3a24ad9b7 100644 --- a/client/packages/lowcoder/src/pages/ApplicationV2/index.tsx +++ b/client/packages/lowcoder/src/pages/ApplicationV2/index.tsx @@ -34,7 +34,7 @@ import { EnterpriseIcon, UserIcon, } from "lowcoder-design"; -import React, { useCallback, useEffect, useState } from "react"; +import React, { useCallback, useEffect, useState, useMemo } from "react"; import { fetchAllApplications, fetchHomeData } from "redux/reduxActions/applicationActions"; import { fetchSubscriptionsAction } from "redux/reduxActions/subscriptionActions"; import { getHomeOrg, normalAppListSelector } from "redux/selectors/applicationSelector"; @@ -66,13 +66,14 @@ import { Support } from "pages/support"; // import { messageInstance } from "lowcoder-design/src/components/GlobalInstances"; import { isEE } from "util/envUtils"; import { getSubscriptions } from 'redux/selectors/subscriptionSelectors'; -import { SubscriptionProducts } from '@lowcoder-ee/api/subscriptionApi'; +import { SubscriptionProductsEnum } from '@lowcoder-ee/constants/subscriptionConstants'; import { ReduxActionTypes } from '@lowcoder-ee/constants/reduxActionConstants'; // adding App Editor, so we can show Apps inside the Admin Area import AppEditor from "../editor/AppEditor"; import { set } from "lodash"; import { fetchDeploymentIdAction } from "@lowcoder-ee/redux/reduxActions/configActions"; +import { getDeploymentId } from "@lowcoder-ee/redux/selectors/configSelectors"; const TabLabel = styled.div` font-weight: 500; @@ -166,18 +167,28 @@ export default function ApplicationHome() { const orgHomeId = "root"; const isSelfHost = window.location.host !== 'app.lowcoder.cloud'; const subscriptions = useSelector(getSubscriptions); + const deploymentId = useSelector(getDeploymentId); const isOrgAdmin = org?.createdBy == user.id ? true : false; useEffect(() => { if (user.currentOrgId) { - dispatch(fetchSubscriptionsAction()); dispatch(fetchDeploymentIdAction()); } dispatch(fetchHomeData({})); }, [user.currentOrgId]); - const supportSubscription = subscriptions.some(sub => sub.product === SubscriptionProducts.SUPPORT && sub.status === 'active'); + useEffect(() => { + if(Boolean(deploymentId)) { + dispatch(fetchSubscriptionsAction()) + } + }, [deploymentId]); + + const supportSubscription = useMemo(() => { + return subscriptions.some( + sub => sub.product === SubscriptionProductsEnum.SUPPORT && sub.status === 'active' + ); + }, [subscriptions]) useEffect(() => { if (!org) { diff --git a/client/packages/lowcoder/src/pages/datasource/datasourceList.tsx b/client/packages/lowcoder/src/pages/datasource/datasourceList.tsx index b89c822b6..87fb7ec08 100644 --- a/client/packages/lowcoder/src/pages/datasource/datasourceList.tsx +++ b/client/packages/lowcoder/src/pages/datasource/datasourceList.tsx @@ -2,7 +2,7 @@ import styled from "styled-components"; import { EditPopover, PointIcon, Search, TacoButton } from "lowcoder-design"; import React, { useState } from "react"; import { useDispatch, useSelector } from "react-redux"; -import { getDataSource, getDataSourceTypesMap } from "../../redux/selectors/datasourceSelectors"; +import { getDataSource, getDataSourceLoading, getDataSourceTypesMap } from "../../redux/selectors/datasourceSelectors"; import { deleteDatasource } from "../../redux/reduxActions/datasourceActions"; import { isEmpty } from "lodash"; import history from "../../util/history"; @@ -16,6 +16,7 @@ import { trans } from "../../i18n"; import { DatasourcePermissionDialog } from "../../components/PermissionDialog/DatasourcePermissionDialog"; import DataSourceIcon from "components/DataSourceIcon"; import { Helmet } from "react-helmet"; +import LoadingOutlined from "@ant-design/icons/LoadingOutlined"; const DatasourceWrapper = styled.div` display: flex; @@ -105,6 +106,7 @@ export const DatasourceList = () => { const [isCreateFormShow, showCreateForm] = useState(false); const [shareDatasourceId, setShareDatasourceId] = useState(undefined); const datasource = useSelector(getDataSource); + const datasourceLoading = useSelector(getDataSourceLoading); const plugins = useSelector(getDataSourceTypesMap); return ( @@ -145,7 +147,10 @@ export const DatasourceList = () => { + }} rowClassName={(record: any) => (!record.edit ? "datasource-can-not-edit" : "")} tableLayout={"auto"} scroll={{ x: "100%" }} diff --git a/client/packages/lowcoder/src/pages/setting/subscriptions/index.tsx b/client/packages/lowcoder/src/pages/setting/subscriptions/index.tsx index 5699c6125..e052fb6b7 100644 --- a/client/packages/lowcoder/src/pages/setting/subscriptions/index.tsx +++ b/client/packages/lowcoder/src/pages/setting/subscriptions/index.tsx @@ -7,17 +7,19 @@ import SubscriptionCancel from './subscriptionCancel'; import SubscriptionError from './subscriptionError'; import SubscriptionDetail from './subscriptionDetail'; import SubscriptionInfo from './subscriptionInfo'; +import { SubscriptionContextProvider } from '@lowcoder-ee/util/context/SubscriptionContext'; export const Subscription = () => { - return ( - - - - - - - - + + + + + + + + + + ); }; diff --git a/client/packages/lowcoder/src/pages/setting/subscriptions/productCard.tsx b/client/packages/lowcoder/src/pages/setting/subscriptions/productCard.tsx index 2a4fa9cbb..a9d6d1d0a 100644 --- a/client/packages/lowcoder/src/pages/setting/subscriptions/productCard.tsx +++ b/client/packages/lowcoder/src/pages/setting/subscriptions/productCard.tsx @@ -77,7 +77,9 @@ export const ProductCard: React.FC = ({ } + cover={ + {title} + } actions={[ , activeSubscription ? ( diff --git a/client/packages/lowcoder/src/pages/setting/subscriptions/subscriptionDetail.tsx b/client/packages/lowcoder/src/pages/setting/subscriptions/subscriptionDetail.tsx index 29e0f12d7..5687668f2 100644 --- a/client/packages/lowcoder/src/pages/setting/subscriptions/subscriptionDetail.tsx +++ b/client/packages/lowcoder/src/pages/setting/subscriptions/subscriptionDetail.tsx @@ -8,6 +8,7 @@ import history from "util/history"; import { SUBSCRIPTION_SETTING } from "constants/routesURL"; import { getProduct, getSubscriptionDetails, getInvoices } from "api/subscriptionApi"; import { Skeleton, Timeline, Card, Descriptions, Table, Typography, Button } from "antd"; +import { useSubscriptionContext } from "@lowcoder-ee/util/context/SubscriptionContext"; const { Text } = Typography; @@ -43,14 +44,20 @@ export function SubscriptionDetail() { const [subscription, setSubscription] = useState(null); const [invoices, setInvoices] = useState([]); const [loading, setLoading] = useState(true); + const { subscriptionProducts } = useSubscriptionContext(); useEffect(() => { const fetchData = async () => { setLoading(true); try { - // Fetch product details - const productData = await getProduct(productId); - setProduct(productData); + // Fetch product details if not found + const product = subscriptionProducts.find(p => p.id === `prod_${productId}`); + if (Boolean(product)) { + setProduct(product); + } else { + const productData = await getProduct(productId); + setProduct(productData); + } // Fetch enriched subscription details, including usage records const subscriptionDetails = await getSubscriptionDetails(subscriptionId); diff --git a/client/packages/lowcoder/src/pages/setting/subscriptions/subscriptionInfo.tsx b/client/packages/lowcoder/src/pages/setting/subscriptions/subscriptionInfo.tsx index 699cc3911..bfdc75f7b 100644 --- a/client/packages/lowcoder/src/pages/setting/subscriptions/subscriptionInfo.tsx +++ b/client/packages/lowcoder/src/pages/setting/subscriptions/subscriptionInfo.tsx @@ -12,6 +12,8 @@ import { CheckCircleOutlined } from '@ant-design/icons'; import { Level1SettingPageContent } from "../styled"; import { TacoMarkDown } from "lowcoder-design"; import ProductDescriptions, {Translations} from "./ProductDescriptions"; +import { SubscriptionProductsEnum } from "@lowcoder-ee/constants/subscriptionConstants"; +import { useSubscriptionContext } from "@lowcoder-ee/util/context/SubscriptionContext"; const { Meta } = Card; @@ -34,6 +36,7 @@ const useProduct = (productId: string) => { const [product, setProduct] = useState(null); const [loading, setLoading] = useState(true); const [error, setError] = useState(null); + const { subscriptionProducts } = useSubscriptionContext(); useEffect(() => { const fetchProduct = async () => { @@ -48,10 +51,15 @@ const useProduct = (productId: string) => { } }; - if (productId) { + const product = subscriptionProducts.find(p => p.id === `prod_${productId}`); + if (Boolean(product)) { + setLoading(false); + setProduct(product); + } + else if (productId && !Boolean(product)) { fetchProduct(); } - }, [productId]); + }, [productId, subscriptionProducts]); return { product, loading, error }; }; @@ -66,10 +74,10 @@ const useMarkdown = (productId: string | null, userLanguage: string) => { let descriptionContent : Translations | false; switch (productId) { - case "QW8L3WPMiNjQjI": + case SubscriptionProductsEnum.SUPPORT: descriptionContent = ProductDescriptions["SupportProduct"]; break; - case "QW8MpIBHxieKXd": + case SubscriptionProductsEnum.MEDIAPACKAGE: descriptionContent = ProductDescriptions["MediaPackageProduct"]; break; default: @@ -114,7 +122,9 @@ export function SubscriptionInfo() { } + cover={ + {product.name} + } actions={[]} > (null); - const [loading, setLoading] = useState(true); - const [error, setError] = useState(null); const customerId = customer?.id; // Get the customer ID from subscription details - - // Handle Customer Portal Session Redirect - const handleCustomerPortalRedirect = async () => { - try { - if (!customerId) { - message.error("Customer ID not available for the subscription."); - return; - } - - // Get the Customer Portal session URL - const portalSession = await getCustomerPortalSession(customerId); - if (portalSession && portalSession.url) { - // Redirect to the Stripe Customer Portal - window.open(portalSession.url, '_blank', 'noopener,noreferrer'); - } else { - message.error("Failed to generate customer portal session link."); - } - } catch (error) { - console.error("Error redirecting to customer portal:", error); - message.error("An error occurred while redirecting to the customer portal."); + // Handle Customer Portal Session Redirect + const handleCustomerPortalRedirect = async () => { + try { + if (!customerId) { + message.error("Customer ID not available for the subscription."); + return; } - }; - useEffect(() => { - const fetchProducts = async () => { - try { - const productData = await getProducts(); - setSubscriptionProducts(productData); - // console.log("productData", productData); - } catch (err) { - setError("Failed to fetch product."); - // console.error(err); - } finally { - setLoading(false); + // Get the Customer Portal session URL + const portalSession = await getCustomerPortalSession(customerId); + if (portalSession && portalSession.url) { + // Redirect to the Stripe Customer Portal + window.open(portalSession.url, '_blank', 'noopener,noreferrer'); + } else { + message.error("Failed to generate customer portal session link."); } - }; - - fetchProducts(); - }, []); + } catch (error) { + console.error("Error redirecting to customer portal:", error); + message.error("An error occurred while redirecting to the customer portal."); + } + }; return ( @@ -110,7 +89,7 @@ export function SubscriptionSetting() { return true; }) .map((product, index) => { - const productData = subscriptionProducts?.data.find((p: { id: string; }) => p.id === ("prod_" + product?.product)); + const productData = subscriptionProducts?.find((p: { id: string; }) => p.id === ("prod_" + product?.product)); const imageUrl = productData && productData.images.length > 0 ? productData.images[0] : null; return ( @@ -130,11 +109,11 @@ export function SubscriptionSetting() { } )} {/* Manage Subscription Button */} - - - {trans("subscription.manageSubscription")} - - + + + {trans("subscription.manageSubscription")} + + ) : (
Loading...
diff --git a/client/packages/lowcoder/src/pages/support/supportOverview.tsx b/client/packages/lowcoder/src/pages/support/supportOverview.tsx index 86e3ca5b7..9f87cfd94 100644 --- a/client/packages/lowcoder/src/pages/support/supportOverview.tsx +++ b/client/packages/lowcoder/src/pages/support/supportOverview.tsx @@ -1,7 +1,7 @@ import styled from "styled-components"; import { trans } from "i18n"; import { searchCustomerTickets, createTicket } from "@lowcoder-ee/api/supportApi"; -import { useState, useEffect } from 'react'; +import { useState, useEffect, useMemo } from 'react'; import { Helmet } from "react-helmet"; import { useUserDetails } from "./useUserDetails"; import StepModal from "components/StepModal"; @@ -14,6 +14,7 @@ import { Input } from "antd"; import ReactQuill from "react-quill"; import 'react-quill/dist/quill.snow.css'; import { Spin } from "antd"; +import LoadingOutlined from "@ant-design/icons/LoadingOutlined"; const SupportWrapper = styled.div` @@ -190,15 +191,16 @@ export function SupportOverview() { } }; - const filteredTickets = supportTickets.filter((ticket: any) => { - if (searchValue) { + const filteredTickets = useMemo(() => { + if (!Boolean(searchValue)) return supportTickets; + + return supportTickets.filter((ticket: any) => { return ( - ticket.title.toLowerCase().includes(searchValue.trim().toLowerCase()) || - ticket.description.toLowerCase().includes(searchValue.trim().toLowerCase()) - ); - } - return true; - }); + ticket.title?.toLowerCase().includes(searchValue.trim().toLowerCase()) || + ticket.description?.toLowerCase().includes(searchValue.trim().toLowerCase()) + ) + }); + }, [searchValue, supportTickets]); const handleCreateTicket = async () => { if (summary.length > 150) { @@ -273,8 +275,9 @@ export function SupportOverview() { buttonType={summary ? "primary" : "normal"} onClick={handleCreateTicket} disabled={isSubmitting || !summary} + loading={isSubmitting} > - {isSubmitting ? : trans("support.createTicketSubmit")} + {trans("support.createTicketSubmit")}
{trans("support.createTicketInfoText")}
@@ -304,7 +307,10 @@ export function SupportOverview() { + }} rowClassName="datasource-can-not-edit" tableLayout={"auto"} scroll={{ x: "100%" }} diff --git a/client/packages/lowcoder/src/redux/reducers/entitiyReducers/datasourceReducer.ts b/client/packages/lowcoder/src/redux/reducers/entitiyReducers/datasourceReducer.ts index e034a5444..46d215644 100644 --- a/client/packages/lowcoder/src/redux/reducers/entitiyReducers/datasourceReducer.ts +++ b/client/packages/lowcoder/src/redux/reducers/entitiyReducers/datasourceReducer.ts @@ -1,5 +1,5 @@ import { createReducer } from "util/reducerUtils"; -import { ReduxAction, ReduxActionTypes } from "constants/reduxActionConstants"; +import { ReduxAction, ReduxActionErrorTypes, ReduxActionTypes } from "constants/reduxActionConstants"; import { DatasourceInfo, DatasourceStructure } from "api/datasourceApi"; import { Datasource } from "@lowcoder-ee/constants/datasourceConstants"; import { DatasourcePermissionInfo } from "../../../api/datasourcePermissionApi"; @@ -13,15 +13,34 @@ export interface DatasourceDataState { data: DatasourceInfo[]; structure: Record; permissionInfo: Record; + loadingStates: { + fetchingDatasources?: boolean; + fetchingStructure?: boolean; + }; } const initialState: DatasourceDataState = { data: [], structure: {}, permissionInfo: {}, + loadingStates: { + fetchingDatasources: false, + fetchingStructure: false, + } }; const datasourceReducer = createReducer(initialState, { + [ReduxActionTypes.FETCH_DATASOURCE_INIT]: ( + state: DatasourceDataState + ): DatasourceDataState => { + return { + ...state, + loadingStates: { + ...state.loadingStates, + fetchingDatasources: true, + }, + }; + }, [ReduxActionTypes.FETCH_DATASOURCE_SUCCESS]: ( state: DatasourceDataState, action: ReduxAction @@ -29,9 +48,23 @@ const datasourceReducer = createReducer(initialState, { return { ...state, data: action.payload, + loadingStates: { + ...state.loadingStates, + fetchingDatasources: false, + }, + }; + }, + [ReduxActionErrorTypes.FETCH_DATASOURCE_ERROR]: ( + state: DatasourceDataState, + ): DatasourceDataState => { + return { + ...state, + loadingStates: { + ...state.loadingStates, + fetchingDatasources: false, + }, }; }, - [ReduxActionTypes.FETCH_DATASOURCE_STRUCTURE_SUCCESS]: ( state: DatasourceDataState, action: ReduxAction> diff --git a/client/packages/lowcoder/src/redux/reducers/uiReducers/subscriptionReducer.ts b/client/packages/lowcoder/src/redux/reducers/uiReducers/subscriptionReducer.ts index 219ec27b8..7186e7c4f 100644 --- a/client/packages/lowcoder/src/redux/reducers/uiReducers/subscriptionReducer.ts +++ b/client/packages/lowcoder/src/redux/reducers/uiReducers/subscriptionReducer.ts @@ -1,10 +1,10 @@ +import { Subscription } from "@lowcoder-ee/constants/subscriptionConstants"; import { ReduxAction, ReduxActionErrorTypes, ReduxActionTypes, } from "constants/reduxActionConstants"; import { createReducer } from "util/reducerUtils"; -import { Subscription } from "api/subscriptionApi"; const initialState: SubscriptionsReduxState = { subscriptions: [], @@ -12,7 +12,7 @@ const initialState: SubscriptionsReduxState = { fetchingSubscriptions: false, fetchSubscriptionsFinished: false, }, - error: "", + error: undefined, }; const subscriptionReducer = createReducer(initialState, { @@ -22,6 +22,7 @@ const subscriptionReducer = createReducer(initialState, { ...state.loadingStates, fetchingSubscriptions: true, }, + error: undefined, }), [ReduxActionTypes.FETCH_SUBSCRIPTIONS_SUCCESS]: ( @@ -35,6 +36,7 @@ const subscriptionReducer = createReducer(initialState, { fetchingSubscriptions: false, fetchSubscriptionsFinished: true, }, + error: undefined, }), [ReduxActionErrorTypes.FETCH_SUBSCRIPTIONS_ERROR]: ( @@ -57,7 +59,7 @@ export interface SubscriptionsReduxState { fetchingSubscriptions: boolean; fetchSubscriptionsFinished: boolean; }; - error: string; + error?: string; } export default subscriptionReducer; diff --git a/client/packages/lowcoder/src/redux/reduxActions/subscriptionActions.ts b/client/packages/lowcoder/src/redux/reduxActions/subscriptionActions.ts index 21753a081..875da41be 100644 --- a/client/packages/lowcoder/src/redux/reduxActions/subscriptionActions.ts +++ b/client/packages/lowcoder/src/redux/reduxActions/subscriptionActions.ts @@ -1,5 +1,5 @@ -import { ReduxActionTypes } from "constants/reduxActionConstants"; -import { Subscription } from 'api/subscriptionApi'; +import { Subscription } from "@lowcoder-ee/constants/subscriptionConstants"; +import { ReduxActionErrorTypes, ReduxActionTypes } from "constants/reduxActionConstants"; // Action Creators export const fetchSubscriptionsAction = () => ({ @@ -12,6 +12,6 @@ export const fetchSubscriptionsSuccess = (subscriptions: Subscription[]) => ({ }); export const fetchSubscriptionsError = (error: string) => ({ - type: ReduxActionTypes.FETCH_SUBSCRIPTIONS_FAILURE, + type: ReduxActionErrorTypes.FETCH_SUBSCRIPTIONS_ERROR, payload: { error }, }); \ No newline at end of file diff --git a/client/packages/lowcoder/src/redux/sagas/datasourceSagas.ts b/client/packages/lowcoder/src/redux/sagas/datasourceSagas.ts index 4ce365bce..3b757f50e 100644 --- a/client/packages/lowcoder/src/redux/sagas/datasourceSagas.ts +++ b/client/packages/lowcoder/src/redux/sagas/datasourceSagas.ts @@ -1,5 +1,6 @@ import { EvaluationReduxAction, + ReduxActionErrorTypes, ReduxActionTypes, ReduxActionWithCallbacks, } from "constants/reduxActionConstants"; @@ -33,6 +34,9 @@ export function* fetchDatasourceSaga(action: EvaluationReduxAction) { - try { - - // wait for deploymentId to be available - yield take(ReduxActionTypes.FETCH_DEPLOYMENT_ID_SUCCESS); - const user: User = yield select(getUser); const currentUser: CurrentUser = yield select(getCurrentUser); const orgID = user.currentOrgId; const domain = `${window.location.protocol}//${window.location.hostname}${window.location.port ? `:${window.location.port}` : ''}`; - - let hostIdenticator : string = yield select(getDeploymentId); - - // Poll until deploymentId is available - while (!hostIdenticator) { - yield delay(100); // wait for 100ms - hostIdenticator = yield select(getDeploymentId); - } + const hostIdenticator: string = yield select(getDeploymentId); const subscriptionSearchCustomer: LowcoderSearchCustomer = { hostname: domain, diff --git a/client/packages/lowcoder/src/redux/selectors/datasourceSelectors.ts b/client/packages/lowcoder/src/redux/selectors/datasourceSelectors.ts index a781f0fa2..ea354b45a 100644 --- a/client/packages/lowcoder/src/redux/selectors/datasourceSelectors.ts +++ b/client/packages/lowcoder/src/redux/selectors/datasourceSelectors.ts @@ -6,6 +6,10 @@ export const getDataSource = (state: AppState) => { return state.entities.datasource.data; }; +export const getDataSourceLoading = (state: AppState) => { + return state.entities.datasource.loadingStates.fetchingDatasources; +}; + export const getDataSourceTypes = (state: AppState) => { return state.entities.plugins.data; }; diff --git a/client/packages/lowcoder/src/redux/selectors/subscriptionSelectors.ts b/client/packages/lowcoder/src/redux/selectors/subscriptionSelectors.ts index d23a3ec45..8ad2a2ded 100644 --- a/client/packages/lowcoder/src/redux/selectors/subscriptionSelectors.ts +++ b/client/packages/lowcoder/src/redux/selectors/subscriptionSelectors.ts @@ -1,5 +1,5 @@ +import { Subscription } from "@lowcoder-ee/constants/subscriptionConstants"; import { AppState } from "redux/reducers"; -import { Subscription } from "api/subscriptionApi"; export const getSubscriptions = (state: AppState) : Subscription[] => { return state.ui.subscriptions.subscriptions; @@ -9,6 +9,10 @@ export const checkSubscriptionsLoading = (state: AppState) : boolean => { return state.ui.subscriptions.loadingStates.fetchingSubscriptions; }; -export const checkSubscriptionsError = (state: AppState) : string | null => { +export const getFetchSubscriptionsFinished = (state: AppState) : boolean => { + return state.ui.subscriptions.loadingStates.fetchSubscriptionsFinished; +}; + +export const getSubscriptionsError = (state: AppState) : string | undefined => { return state.ui.subscriptions.error; -}; \ No newline at end of file +}; \ No newline at end of file diff --git a/client/packages/lowcoder/src/util/context/SubscriptionContext.tsx b/client/packages/lowcoder/src/util/context/SubscriptionContext.tsx new file mode 100644 index 000000000..0e761bc27 --- /dev/null +++ b/client/packages/lowcoder/src/util/context/SubscriptionContext.tsx @@ -0,0 +1,191 @@ +import { createCheckoutLink, createCustomer, getProducts, searchCustomer, useOrgUserCount } from "@lowcoder-ee/api/subscriptionApi"; +import { StripeCustomer, SubscriptionProduct, InitSubscriptionProducts, LowcoderSearchCustomer, LowcoderNewCustomer, Subscription } from "@lowcoder-ee/constants/subscriptionConstants"; +import { getDeploymentId } from "@lowcoder-ee/redux/selectors/configSelectors"; +import { getFetchSubscriptionsFinished, getSubscriptions, getSubscriptionsError } from "@lowcoder-ee/redux/selectors/subscriptionSelectors"; +import { getCurrentUser, getUser } from "@lowcoder-ee/redux/selectors/usersSelectors"; +import { createContext, ReactNode, useContext, useEffect, useState } from "react"; +import { useSelector } from "react-redux"; + +export interface SubscriptionContextType { + products: SubscriptionProduct[]; + subscriptionProducts: any[], + customer?: StripeCustomer; + isCreatingCustomer: boolean; + customerDataError: boolean; + subscriptionDataError?: string; + checkoutLinkDataError: boolean; + subscriptionDataLoaded: boolean; + checkoutLinkDataLoaded: boolean; + subscriptionProductsLoading: boolean; + subscriptions: Subscription[], + admin: "admin" | "member", +} + +const SubscriptionContext = createContext({ + products: [], + subscriptionProducts: [], + customer: undefined, + isCreatingCustomer: false, + customerDataError: false, + subscriptionDataError: undefined, + checkoutLinkDataError: false, + subscriptionDataLoaded: false, + checkoutLinkDataLoaded: false, + subscriptionProductsLoading: false, + subscriptions: [], + admin: "member", +}); + +export const SubscriptionContextProvider = (props: { + children: ReactNode, +}) => { + const [customer, setCustomer] = useState(); + const [isCreatingCustomer, setIsCreatingCustomer] = useState(false); // Track customer creation + const [customerDataError, setCustomerDataError] = useState(false); + const [checkoutLinkDataLoaded, setCheckoutLinkDataLoaded] = useState(false); + const [checkoutLinkDataError, setCheckoutLinkDataError] = useState(false); + const [products, setProducts] = useState(InitSubscriptionProducts); + const [productsLoaded, setProductsLoaded] = useState(false); + const [subscriptionProducts, setSubscriptionProducts] = useState([]); + const [subscriptionProductsLoading, setSubscriptionProductsLoading] = useState(false); + + const user = useSelector(getUser); + const currentUser = useSelector(getCurrentUser); + const deploymentId = useSelector(getDeploymentId); + const subscriptions = useSelector(getSubscriptions); + const subscriptionDataLoaded = useSelector(getFetchSubscriptionsFinished); + const subscriptionDataError = useSelector(getSubscriptionsError); + + const currentOrg = user.orgs.find(org => org.id === user.currentOrgId); + const orgID = user.currentOrgId; + const domain = window.location.protocol + "//" + window.location.hostname + (window.location.port ? ':' + window.location.port : ''); + const admin = user.orgRoleMap.get(orgID) === "admin" ? "admin" : "member"; + + const userCount = useOrgUserCount(orgID); + + const subscriptionSearchCustomer: LowcoderSearchCustomer = { + hostname: domain, + hostId: deploymentId, + email: currentUser.email, + orgId: orgID, + userId: user.id, + }; + + const subscriptionNewCustomer: LowcoderNewCustomer = { + hostname: domain, + hostId: deploymentId, + email: currentUser.email, + orgId: orgID, + userId: user.id, + userName: user.username, + type: admin, + companyName: currentOrg?.name || "Unknown", + }; + + useEffect(() => { + const fetchProducts = async () => { + try { + const productData = await getProducts(); + setSubscriptionProducts(productData); + } catch (err) { + // setError("Failed to fetch product."); + console.error("Failed to fetch product.", err); + } finally { + setSubscriptionProductsLoading(false); + } + }; + + if (!Boolean(subscriptionProducts.length)) { + fetchProducts(); + } + }, [subscriptionProducts]); + + useEffect(() => { + const initializeCustomer = async () => { + try { + setIsCreatingCustomer(true); + const existingCustomer = await searchCustomer(subscriptionSearchCustomer); + if (existingCustomer != null) { + setCustomer(existingCustomer); + } else { + const newCustomer = await createCustomer(subscriptionNewCustomer); + setCustomer(newCustomer); + } + } catch (error) { + setCustomerDataError(true); + } finally { + setIsCreatingCustomer(false); + } + }; + + if (Boolean(deploymentId) && !customer) { + initializeCustomer(); + } + }, [deploymentId, customer]); + + useEffect(() => { + const prepareCheckout = async () => { + if (subscriptionDataLoaded && userCount > 0) { // Ensure user count is available + try { + console.log("Total Users in Organization:", userCount); + + const updatedProducts = await Promise.all( + products.map(async (product) => { + const matchingSubscription = subscriptions.find( + (sub) => sub.price === product.accessLink + ); + + if (matchingSubscription) { + return { + ...product, + activeSubscription: true, + checkoutLinkDataLoaded: true, + subscriptionId: matchingSubscription.id, + }; + } else { + // Use the user count to set the quantity for checkout link + const checkoutLink = await createCheckoutLink(customer!, product.accessLink, userCount); + return { + ...product, + activeSubscription: false, + checkoutLink: checkoutLink ? checkoutLink.url : "", + checkoutLinkDataLoaded: true, + }; + } + }) + ); + setProducts(updatedProducts); + setProductsLoaded(true); + setCheckoutLinkDataError(false); + } catch (error) { + setCheckoutLinkDataError(true); + } + } + }; + + if (!productsLoaded && customer) { + prepareCheckout(); + } + }, [subscriptionDataLoaded, customer, userCount]); + + return ( + + {props.children} + + ) +} + +export const useSubscriptionContext = () => useContext(SubscriptionContext); diff --git a/client/packages/lowcoder/src/util/hooks.ts b/client/packages/lowcoder/src/util/hooks.ts index 57fa2baa4..3c3870aab 100644 --- a/client/packages/lowcoder/src/util/hooks.ts +++ b/client/packages/lowcoder/src/util/hooks.ts @@ -9,7 +9,7 @@ import React, { useRef, useState, } from "react"; -import { shallowEqual, useSelector } from "react-redux"; +import { shallowEqual, useDispatch, useSelector } from "react-redux"; import { useLocation, useParams } from "react-router-dom"; import { DATASOURCE_URL, QUERY_LIBRARY_URL } from "../constants/routesURL"; import { AuthSearchParams } from "constants/authConstants"; @@ -26,6 +26,9 @@ import { CompAction, changeChildAction } from "lowcoder-core"; import { ThemeDetail } from "@lowcoder-ee/api/commonSettingApi"; import { uniq } from "lodash"; import { constantColors } from "components/colorSelect/colorUtils"; +import { AppState } from "@lowcoder-ee/redux/reducers"; +import { getOrgUsers } from "@lowcoder-ee/redux/selectors/orgSelectors"; +import { fetchOrgUsersAction } from "@lowcoder-ee/redux/reduxActions/orgActions"; export const ForceViewModeContext = React.createContext(false); @@ -256,4 +259,26 @@ export function useThemeColors(allowGradient?: boolean) { currentTheme, defaultTheme, ]); -} \ No newline at end of file +} + +export const useOrgUserCount = (orgId: string) => { + const dispatch = useDispatch(); + const orgUsers = useSelector((state: AppState) => getOrgUsers(state)); // Use selector to get orgUsers from state + const [userCount, setUserCount] = useState(0); + + useEffect(() => { + // Dispatch action to fetch organization users + if (orgId) { + dispatch(fetchOrgUsersAction(orgId)); + } + }, [dispatch, orgId]); + + useEffect(() => { + // Update user count when orgUsers state changes + if (orgUsers && orgUsers.length > 0) { + setUserCount(orgUsers.length); + } + }, [orgUsers]); + + return userCount; +};