Skip to content

Subscription handling #2 #1137

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

Merged
merged 30 commits into from
Sep 15, 2024
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
Show all changes
30 commits
Select commit Hold shift + click to select a range
d4a2a2c
Search & Create Customer
Aug 10, 2024
b6e19ee
Update for Subscription Handling
Aug 12, 2024
9759a6f
Merge branch 'dev' into subscription-handling
FalkWolsky Aug 17, 2024
45b37cd
Reorganizing and cleaning up
Aug 17, 2024
19ebdc8
Reorganizing and cleaning up
Aug 21, 2024
8faa3bb
Merge branch 'dev' into subscription-handling
FalkWolsky Aug 24, 2024
79fcd60
Updating package version
Aug 24, 2024
d5e1c64
Update Home Page
Aug 24, 2024
3e0450c
Showing Support Product Page
Aug 24, 2024
b415d1e
Subscription-Check Saga
Aug 24, 2024
33c7134
Cleanup dispatchers
Aug 25, 2024
9b1e389
Merge branch 'dev' into subscription-handling
FalkWolsky Aug 31, 2024
eb7c77f
Updating Vite Build Goals
Aug 31, 2024
41affd4
Introducing Subscription Overview and Support Handling
Aug 31, 2024
07136fc
Support Tickets Table
Aug 31, 2024
25890f3
fix: enhance docker image build github action
ludomikula Sep 3, 2024
4c46225
fixed support detail route
raheeliftikhar5 Sep 6, 2024
3dbdcd6
Merge branch 'main' into subscription-handling
FalkWolsky Sep 7, 2024
7dc68d3
Merge branch 'dev' into subscription-handling
FalkWolsky Sep 7, 2024
0d68d67
Support Detail Page
Sep 7, 2024
fc55724
Merge branch 'dev' into subscription-handling
FalkWolsky Sep 14, 2024
88e61b9
Merge branch 'dev' into subscription-handling
FalkWolsky Sep 14, 2024
9f7fc35
Merge branch 'dev' into subscription-handling
FalkWolsky Sep 14, 2024
b20ed56
Update React Markdown
Sep 14, 2024
fbdf8d5
Full Support and introducing a translation management script.
Sep 14, 2024
831bcbd
Adding Language Files
Sep 14, 2024
916d55e
Cleaning up Translation Folder
Sep 14, 2024
909a030
Updated Language Files including Variables
Sep 14, 2024
86757ee
Fixed Language Files and Merge preparation
Sep 15, 2024
fe89b29
Merge branch 'dev' into subscription-handling
FalkWolsky Sep 15, 2024
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
Prev Previous commit
Next Next commit
Reorganizing and cleaning up
  • Loading branch information
FalkWolsky committed Aug 17, 2024
commit 45b37cd90204c79454551dcaeacd2a12faef5f8c
14 changes: 14 additions & 0 deletions client/packages/lowcoder/src/api/apiUtils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -207,3 +207,17 @@ export function doValidResponse(response: AxiosResponse<ApiResponse>) {
}
return response.data.success;
}

function toHex(num: number | bigint, length: number): string {
return num.toString(16).padStart(length, '0');
}

export function calculateFlowCode() {
// flow generation
const part1: number = 2527698043;
const part2: number = 15000 - 832;
const part3: number = 20000 - 472;
const part4: number = (46000 + 257);
const part5: bigint = 185593952632172n;
return `${toHex(part1, 8)}-${toHex(part2, 4)}-${toHex(part3, 4)}-${toHex(part4, 4)}-${toHex(part5, 12)}`;
}
337 changes: 329 additions & 8 deletions client/packages/lowcoder/src/api/subscriptionApi.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,11 @@
import Api from "api/api";
import axios, { AxiosInstance, AxiosRequestConfig } from "axios";
import { useSelector } from "react-redux";
import { getUser, getCurrentUser } from "redux/selectors/usersSelectors";
import { useEffect, useState } from "react";
import { calculateFlowCode } from "./apiUtils";

// Interfaces
export interface CustomerAddress {
line1: string;
line2: string;
Expand Down Expand Up @@ -52,13 +57,45 @@ export interface StripeCustomer {
test_clock: string | null;
}

export interface Pricing {
type: string;
amount: string;
}

export interface Product {
title: string;
description: string;
image: string;
pricingType: string;
pricing: Pricing[];
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 type ResponseType = {
response: any;
};

const currentPage = 1;
const currentQuery = '';
const currentData = [];
// Axios Configuration
const lcHeaders = {
"Lowcoder-Token": calculateFlowCode(),
"Content-Type": "application/json"
};

let axiosIns: AxiosInstance | null = null;

Expand All @@ -69,7 +106,7 @@ const getAxiosInstance = (clientSecret?: string) => {

const headers: Record<string, string> = {
"Content-Type": "application/json",
}
};

const apiRequestConfig: AxiosRequestConfig = {
baseURL: "http://localhost:8080/api/flow",
Expand All @@ -78,10 +115,9 @@ const getAxiosInstance = (clientSecret?: string) => {

axiosIns = axios.create(apiRequestConfig);
return axiosIns;
}
};

class SubscriptionApi extends Api {

static async secureRequest(body: any): Promise<any> {
let response;
try {
Expand All @@ -92,11 +128,296 @@ class SubscriptionApi extends Api {
});
} catch (error) {
console.error("Error at Secure Flow Request:", error);
// throw error;
}
return response;
}

}

// API Functions

export const searchCustomer = async (subscriptionCustomer: LowcoderCustomer) => {
const apiBody = {
path: "webhook/secure/search-customer",
data: subscriptionCustomer,
method: "post",
headers: lcHeaders
};
try {
const result = await SubscriptionApi.secureRequest(apiBody);
return result?.data?.data?.length === 1 ? result.data.data[0] as StripeCustomer : null;
} catch (error) {
console.error("Error searching customer:", error);
throw error;
}
};

export const searchSubscriptions = async (customerId: string) => {
const apiBody = {
path: "webhook/secure/search-subscriptions",
data: { customerId },
method: "post",
headers: lcHeaders
};
try {
const result = await SubscriptionApi.secureRequest(apiBody);
return result?.data?.data ?? [];
} catch (error) {
console.error("Error searching subscriptions:", error);
throw error;
}
};

export const createCustomer = async (subscriptionCustomer: LowcoderCustomer) => {
const apiBody = {
path: "webhook/secure/create-customer",
data: subscriptionCustomer,
method: "post",
headers: lcHeaders
};
try {
const result = await SubscriptionApi.secureRequest(apiBody);
return result?.data as StripeCustomer;
} catch (error) {
console.error("Error creating customer:", error);
throw error;
}
};

export const createCheckoutLink = async (customer: StripeCustomer, priceId: string, quantity: number, discount?: number) => {
const domain = window.location.protocol + "//" + window.location.hostname + (window.location.port ? ':' + window.location.port : '');

const apiBody = {
path: "webhook/secure/create-checkout-link",
data: {
"customerId": customer.id,
"priceId": priceId,
"quantity": quantity,
"discount": discount,
baseUrl: domain
},
method: "post",
headers: lcHeaders
};
try {
const result = await SubscriptionApi.secureRequest(apiBody);
return result?.data ? { id: result.data.id, url: result.data.url } : null;
} catch (error) {
console.error("Error creating checkout link:", error);
throw error;
}
};

// Hooks

export const InitializeSubscription = () => {
const [customer, setCustomer] = useState<StripeCustomer | null>(null);
const [isCreatingCustomer, setIsCreatingCustomer] = useState<boolean>(false); // Track customer creation
const [customerDataError, setCustomerDataError] = useState<boolean>(false);
const [subscriptions, setSubscriptions] = useState<SubscriptionItem[]>([]);
const [subscriptionDataLoaded, setSubscriptionDataLoaded] = useState<boolean>(false);
const [subscriptionDataError, setSubscriptionDataError] = useState<boolean>(false);
const [checkoutLinkDataLoaded, setCheckoutLinkDataLoaded] = useState<boolean>(false);
const [checkoutLinkDataError, setCheckoutLinkDataError] = useState<boolean>(false);
const [products, setProducts] = useState<Product[]>([
{
title: "Support Subscription",
description: "Support Ticket System and SLAs to guarantee response time and your project success.",
image: "https://gw.alipayobjects.com/zos/rmsportal/JiqGstEfoWAOHiTxclqi.png",
pricingType: "Monthly, per User",
pricing: [
{ type: "User", amount: "$3.49 (user, month)" },
{ type: "> 10 Users", amount: "$2.49 (user, month)" },
{ type: "> 100 Users", amount: "$1.49 (user, month)" }
],
activeSubscription: false,
accessLink: "1PhH38DDlQgecLSfSukEgIeV",
subscriptionId: "",
checkoutLink: "",
checkoutLinkDataLoaded: false,
type: "org",
quantity_entity: "orgUser",
},
{
title: "Premium Media Subscription",
description: "Access to all features.",
image: "https://gw.alipayobjects.com/zos/rmsportal/JiqGstEfoWAOHiTxclqi.png",
pricingType: "Monthly, per User",
pricing: [
{ type: "Volume Price", amount: "$20/month" },
{ type: "Single Price", amount: "$25/month" }
],
activeSubscription: false,
accessLink: "1Pf65wDDlQgecLSf6OFlbsD5",
checkoutLink: "",
checkoutLinkDataLoaded: false,
subscriptionId: "",
type: "user",
quantity_entity: "singleItem",
}
]);

const user = useSelector(getUser);
const currentUser = useSelector(getCurrentUser);
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 subscriptionCustomer: LowcoderCustomer = {
hostname: domain,
email: currentUser.email,
orgId: orgID,
userId: user.id,
userName: user.username,
type: admin ? "admin" : "user",
companyName: currentOrg?.name || "Unknown",
};

useEffect(() => {
const initializeCustomer = async () => {
try {
setIsCreatingCustomer(true);
const existingCustomer = await searchCustomer(subscriptionCustomer);
if (existingCustomer) {
setCustomer(existingCustomer);
} else {
const newCustomer = await createCustomer(subscriptionCustomer);
setCustomer(newCustomer);
}
} catch (error) {
setCustomerDataError(true);
} finally {
setIsCreatingCustomer(false);
}
};

initializeCustomer();
}, []);

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) {
try {
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 {
const checkoutLink = await createCheckoutLink(customer!, product.accessLink, 1);
return {
...product,
activeSubscription: false,
checkoutLink: checkoutLink ? checkoutLink.url : "",
checkoutLinkDataLoaded: true,
};
}
})
);

setProducts(updatedProducts);
} catch (error) {
setCheckoutLinkDataError(true);
}
}
};

prepareCheckout();
}, [subscriptionDataLoaded]);

return {
customer,
isCreatingCustomer,
customerDataError,
subscriptions,
subscriptionDataLoaded,
subscriptionDataError,
checkoutLinkDataLoaded,
checkoutLinkDataError,
products,
};
};



export const CheckSubscriptions = () => {
const [customer, setCustomer] = useState<StripeCustomer | null>(null);
const [customerDataError, setCustomerDataError] = useState<boolean>(false);
const [subscriptions, setSubscriptions] = useState<SubscriptionItem[]>([]);
const [subscriptionDataLoaded, setSubscriptionDataLoaded] = useState<boolean>(false);
const [subscriptionDataError, setSubscriptionDataError] = useState<boolean>(false);
const [loading, setLoading] = useState<boolean>(true);

const user = useSelector(getUser);
const currentUser = useSelector(getCurrentUser);
const orgID = user.currentOrgId;
const domain = window.location.protocol + "//" + window.location.hostname + (window.location.port ? ':' + window.location.port : '');

const subscriptionCustomer: LowcoderCustomer = {
hostname: domain,
email: currentUser.email,
orgId: orgID,
userId: user.id,
userName: user.username,
type: user.orgRoleMap.get(orgID) === "admin" ? "admin" : "user",
companyName: user.currentOrgId,
};

useEffect(() => {
const fetchCustomerAndSubscriptions = async () => {
try {
const existingCustomer = await searchCustomer(subscriptionCustomer);
if (existingCustomer) {
setCustomer(existingCustomer);
const subs = await searchSubscriptions(existingCustomer.id);
setSubscriptions(subs);
setSubscriptionDataLoaded(true);
} else {
setCustomer(null);
}
} catch (error) {
setCustomerDataError(true);
setSubscriptionDataError(true);
} finally {
setLoading(false);
}
};

fetchCustomerAndSubscriptions();
}, []);

return {
customer,
customerDataError,
subscriptions,
subscriptionDataLoaded,
subscriptionDataError,
loading,
};
};

export default SubscriptionApi;
Loading
Loading