Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Subscription handling #1197

Open
wants to merge 19 commits into
base: dev
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions client/packages/lowcoder/src/api/configApi.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,10 @@ class ConfigApi extends Api {
}
return Api.get(authConfigURL);
}

static fetchDeploymentId(): AxiosPromise<ConfigResponse> {
return Api.get(`${ConfigApi.configURL}/deploymentId`);
}
}

export default ConfigApi;
123 changes: 115 additions & 8 deletions client/packages/lowcoder/src/api/subscriptionApi.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,13 @@
import Api from "api/api";
import axios, { AxiosInstance, AxiosRequestConfig } from "axios";
import { useSelector } from "react-redux";
import { useDispatch, useSelector } from "react-redux";
import { getUser, getCurrentUser } from "redux/selectors/usersSelectors";
import { useEffect, useState } from "react";
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 {
Expand All @@ -17,6 +21,7 @@ export interface CustomerAddress {

export interface LowcoderNewCustomer {
hostname: string;
hostId: string;
email: string;
orgId: string;
userId: string;
Expand All @@ -28,13 +33,15 @@ export interface LowcoderNewCustomer {

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;
Expand Down Expand Up @@ -213,6 +220,13 @@ export const searchCustomersSubscriptions = async (Customer: LowcoderSearchCusto
else if (result.data.success == "false" && result.data.reason == "customerNotFound") {
return [];
}
else if (result.data.success == "false" && result.data.reason == "userSubscriptionNotFound") {
return [];
}
else if (result.data.success == "false" && result.data.reason == "orgSubscriptionNotFound") {
return [];
}
return [];
} catch (error) {
console.error("Error searching customer:", error);
throw error;
Expand Down Expand Up @@ -291,8 +305,81 @@ export const createCheckoutLink = async (customer: StripeCustomer, priceId: stri
}
};

// Function to get subscription details from Stripe
export const getSubscriptionDetails = async (subscriptionId: string) => {
const apiBody = {
path: "webhook/secure/get-subscription-details",
method: "post",
data: { "subscriptionId": subscriptionId },
headers: lcHeaders,
};
try {
const result = await SubscriptionApi.secureRequest(apiBody);
return result?.data;
} catch (error) {
console.error("Error fetching subscription details:", error);
throw error;
}
};

// Function to get invoice documents from Stripe
export const getInvoices = async (subscriptionId: string) => {
const apiBody = {
path: "webhook/secure/get-subscription-invoices",
method: "post",
data: { "subscriptionId": subscriptionId },
headers: lcHeaders,
};
try {
const result = await SubscriptionApi.secureRequest(apiBody);
return result?.data?.data ?? [];
} catch (error) {
console.error("Error fetching invoices:", error);
throw error;
}
};

// Function to get a customer Portal Session from Stripe
export const getCustomerPortalSession = async (customerId: string) => {
const apiBody = {
path: "webhook/secure/create-customer-portal-session",
method: "post",
data: { "customerId": customerId },
headers: lcHeaders,
};
try {
const result = await SubscriptionApi.secureRequest(apiBody);
return result?.data;
} catch (error) {
console.error("Error fetching invoices:", error);
throw error;
}
};

// Hooks

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<number>(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;
};

export const InitializeSubscription = () => {
const [customer, setCustomer] = useState<StripeCustomer | null>(null);
const [isCreatingCustomer, setIsCreatingCustomer] = useState<boolean>(false); // Track customer creation
Expand Down Expand Up @@ -338,22 +425,29 @@ export const InitializeSubscription = () => {
},
]);


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,
Expand All @@ -380,8 +474,10 @@ export const InitializeSubscription = () => {
}
};

initializeCustomer();
}, []);
if (Boolean(deploymentId)) {
initializeCustomer();
}
}, [deploymentId]);

useEffect(() => {
const fetchSubscriptions = async () => {
Expand All @@ -401,8 +497,10 @@ export const InitializeSubscription = () => {

useEffect(() => {
const prepareCheckout = async () => {
if (subscriptionDataLoaded) {
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(
Expand All @@ -417,7 +515,8 @@ export const InitializeSubscription = () => {
subscriptionId: matchingSubscription.id.substring(4),
};
} else {
const checkoutLink = await createCheckoutLink(customer!, product.accessLink, 1);
// Use the user count to set the quantity for checkout link
const checkoutLink = await createCheckoutLink(customer!, product.accessLink, userCount);
return {
...product,
activeSubscription: false,
Expand All @@ -436,7 +535,7 @@ export const InitializeSubscription = () => {
};

prepareCheckout();
}, [subscriptionDataLoaded]);
}, [subscriptionDataLoaded, userCount]);

return {
customer,
Expand Down Expand Up @@ -480,11 +579,13 @@ export const CheckSubscriptions = () => {

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,
Expand All @@ -502,6 +603,12 @@ export const CheckSubscriptions = () => {
setLoading(false);
}
};
if (
Boolean(currentUser.email)
&& Boolean(orgID)
&& Boolean(user.id)
&& Boolean(deploymentId)
)
fetchCustomerAndSubscriptions();
}, [subscriptionSearchCustomer]);

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -153,6 +153,8 @@ export const ReduxActionTypes = {
FETCH_SYS_CONFIG_INIT: "FETCH_SYS_CONFIG_INIT",
FETCH_SYS_CONFIG_SUCCESS: "FETCH_SYS_CONFIG_SUCCESS",
SET_EDITOR_EXTERNAL_STATE: "SET_EDITOR_EXTERNAL_STATE",
FETCH_DEPLOYMENT_ID_INIT: "FETCH_DEPLOYMENT_ID_INIT",
FETCH_DEPLOYMENT_ID_SUCCESS: "FETCH_DEPLOYMENT_ID_SUCCESS",

/* audit log */
FETCH_AUDIT_EVENT: "FETCH_AUDIT_EVENT",
Expand Down
2 changes: 2 additions & 0 deletions client/packages/lowcoder/src/constants/routesURL.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,9 @@ export const OAUTH_PROVIDER_DETAIL = "/setting/oauth-provider/detail";

export const PERMISSION_SETTING_DETAIL = `${PERMISSION_SETTING}/:groupId`;
export const ORGANIZATION_SETTING_DETAIL = `${ORGANIZATION_SETTING}/:orgId`;

export const SUBSCRIPTION_SUCCESS = `${SUBSCRIPTION_SETTING}/success`;
export const SUBSCRIPTION_CANCEL = `${SUBSCRIPTION_SETTING}/cancel`;
export const SUBSCRIPTION_ERROR = `${SUBSCRIPTION_SETTING}/error`;
export const SUBSCRIPTION_DETAIL = `${SUBSCRIPTION_SETTING}/details/:subscriptionId/:productId`;
export const SUBSCRIPTION_INFO = `${SUBSCRIPTION_SETTING}/info/:productId`;
Expand Down
56 changes: 56 additions & 0 deletions client/packages/lowcoder/src/i18n/locales/en.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2258,6 +2258,62 @@ export const en = {
"AppUsage": "Global App Usage",
},

"subscription": {
"details": "Subscription Details",
"productDetails": "Product Details",
"productName": "Product Name",
"productDescription": "Product Description",
"productPrice": "Product Price",
"subscriptionDetails": "Subscription Details",
"status": "Status",
"startDate": "Start Date",
"currentPeriodEnd": "Current Period End",
"customerId": "Customer ID",
"subscriptionItems": "Subscription Items",
"itemId": "Item ID",
"plan": "Plan",
"quantity": "Quantity",
"product": "Product",
"invoices": "Invoices",
"invoiceNumber": "Invoice Number",
"date": "Date",
"amount": "Amount",
"link": "Link",
"viewInvoice": "View Invoice",
"downloadPDF": "Download PDF",
"billingReason": "Billing Reason",
"subscriptionCycle": "monthly Subscription",
"customer": "Customer",
"links": "Links",
"paid": "Paid",
"unpaid": "Unpaid",
"noInvoices": "No invoices available",
"costVolumeDevelopment": "Cost/Volume Development",
"noUsageRecords": "No usage records available",
"itemDescription": "Item Description",
"periodStart": "Period Start",
"periodEnd": "Period End",
"billingReason.subscription_cycle": "Subscription Cycle",
"billingReason.subscription_create": "Subscription Creation",
"billingReason.manual": "Manual Billing",
"billingReason.upcoming": "Upcoming Billing",
"billingReason.subscription_threshold": "Subscription Threshold",
"billingReason.subscription_update": "Subscription Update",
"backToSubscriptions": "Back to Subscriptions",
"manageSubscription" : "Manage Your Subscriptions",
"subscriptionHelp" : "Subscription Help",
"subscriptionHelpDescription" : "If you have any questions, please contact us. We are happy to help you. service@lowcoder.cloud",
"success" : "Payment & Subscription Success",
"successTitle" : "Thank you for your trust in Lowcoder!",
"successThankYou" : "Your trust and decision to partner with us mean the world to our entire team. We want you to know that we are fully committed to delivering exceptional service, unparalleled support, and continuous value every step of the way. At Lowcoder, our customers are at the heart of everything we do. We are passionate about helping you achieve your goals, and we take pride in being more than just a platform. Whether it's providing quick solutions, building innovative features, or simply being there when you need us, we are dedicated to making your experience outstanding. Thank you for inspiring us to strive for excellence every day. We love what we do, and we couldn’t do it without incredible customers like you. Welcome aboard, and let’s create something extraordinary together!",
"successLowcoderTeam" : "The Lowcoder Team",
},
"subscriptionError": {
"fetchProductDetails": "Error fetching product details.",
"fetchSubscriptionDetails": "Error fetching subscription details.",
"fetchInvoices": "Error fetching invoices."
},


// thirteenth part

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -307,6 +307,8 @@ export function HomeLayout(props: HomeLayoutProps) {

const { breadcrumb = [], elements = [], localMarketplaceApps = [], globalMarketplaceApps = [], mode } = props;

console.log("HomeLayout props: ", props);

const categoryOptions = [
{ label: <FilterMenuItem>{trans("home.allCategories")}</FilterMenuItem>, value: 'All' },
...Object.entries(ApplicationCategoriesEnum).map(([key, value]) => ({
Expand Down
13 changes: 8 additions & 5 deletions client/packages/lowcoder/src/pages/ApplicationV2/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,6 @@ import {
FOLDER_URL_PREFIX,
FOLDERS_URL,
MARKETPLACE_URL,
// MODULE_APPLICATIONS_URL,
QUERY_LIBRARY_URL,
SETTING_URL,
SUPPORT_URL,
Expand Down Expand Up @@ -73,6 +72,7 @@ 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";

const TabLabel = styled.div`
font-weight: 500;
Expand Down Expand Up @@ -170,11 +170,14 @@ export default function ApplicationHome() {
const isOrgAdmin = org?.createdBy == user.id ? true : false;

useEffect(() => {
if (user.currentOrgId) {
dispatch(fetchSubscriptionsAction());
dispatch(fetchDeploymentIdAction());
}
dispatch(fetchHomeData({}));
dispatch(fetchSubscriptionsAction());
}, [user.currentOrgId]);

const supportSubscription = subscriptions.some(sub => sub.product === SubscriptionProducts.SUPPORT);
const supportSubscription = subscriptions.some(sub => sub.product === SubscriptionProducts.SUPPORT && sub.status === 'active');

useEffect(() => {
if (!org) {
Expand Down Expand Up @@ -296,7 +299,7 @@ export default function ApplicationHome() {
],
} : { items: [] },

/* supportSubscription ? {
supportSubscription ? {
items: [
{
text: <TabLabel>{trans("home.support")}</TabLabel>,
Expand All @@ -306,7 +309,7 @@ export default function ApplicationHome() {
icon: ({ selected, ...otherProps }) => selected ? <SupportIcon {...otherProps} width={"24px"}/> : <SupportIcon {...otherProps} width={"24px"}/>,
},
],
} : { items: [] }, */
} : { items: [] },

{
items: [
Expand Down
Loading
Loading