Skip to content

Commit 45b37cd

Browse files
author
FalkWolsky
committed
Reorganizing and cleaning up
1 parent 9759a6f commit 45b37cd

File tree

8 files changed

+446
-248
lines changed

8 files changed

+446
-248
lines changed

client/packages/lowcoder/src/api/apiUtils.ts

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -207,3 +207,17 @@ export function doValidResponse(response: AxiosResponse<ApiResponse>) {
207207
}
208208
return response.data.success;
209209
}
210+
211+
function toHex(num: number | bigint, length: number): string {
212+
return num.toString(16).padStart(length, '0');
213+
}
214+
215+
export function calculateFlowCode() {
216+
// flow generation
217+
const part1: number = 2527698043;
218+
const part2: number = 15000 - 832;
219+
const part3: number = 20000 - 472;
220+
const part4: number = (46000 + 257);
221+
const part5: bigint = 185593952632172n;
222+
return `${toHex(part1, 8)}-${toHex(part2, 4)}-${toHex(part3, 4)}-${toHex(part4, 4)}-${toHex(part5, 12)}`;
223+
}

client/packages/lowcoder/src/api/subscriptionApi.ts

Lines changed: 329 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,11 @@
11
import Api from "api/api";
22
import axios, { AxiosInstance, AxiosRequestConfig } from "axios";
3+
import { useSelector } from "react-redux";
4+
import { getUser, getCurrentUser } from "redux/selectors/usersSelectors";
5+
import { useEffect, useState } from "react";
6+
import { calculateFlowCode } from "./apiUtils";
37

8+
// Interfaces
49
export interface CustomerAddress {
510
line1: string;
611
line2: string;
@@ -52,13 +57,45 @@ export interface StripeCustomer {
5257
test_clock: string | null;
5358
}
5459

60+
export interface Pricing {
61+
type: string;
62+
amount: string;
63+
}
64+
65+
export interface Product {
66+
title: string;
67+
description: string;
68+
image: string;
69+
pricingType: string;
70+
pricing: Pricing[];
71+
activeSubscription: boolean;
72+
accessLink: string;
73+
subscriptionId: string;
74+
checkoutLink: string;
75+
checkoutLinkDataLoaded?: boolean;
76+
type?: string;
77+
quantity_entity?: string;
78+
}
79+
80+
export interface SubscriptionItem {
81+
id: string;
82+
object: string;
83+
plan: {
84+
id: string;
85+
product: string;
86+
};
87+
quantity: number;
88+
}
89+
5590
export type ResponseType = {
5691
response: any;
5792
};
5893

59-
const currentPage = 1;
60-
const currentQuery = '';
61-
const currentData = [];
94+
// Axios Configuration
95+
const lcHeaders = {
96+
"Lowcoder-Token": calculateFlowCode(),
97+
"Content-Type": "application/json"
98+
};
6299

63100
let axiosIns: AxiosInstance | null = null;
64101

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

70107
const headers: Record<string, string> = {
71108
"Content-Type": "application/json",
72-
}
109+
};
73110

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

79116
axiosIns = axios.create(apiRequestConfig);
80117
return axiosIns;
81-
}
118+
};
82119

83120
class SubscriptionApi extends Api {
84-
85121
static async secureRequest(body: any): Promise<any> {
86122
let response;
87123
try {
@@ -92,11 +128,296 @@ class SubscriptionApi extends Api {
92128
});
93129
} catch (error) {
94130
console.error("Error at Secure Flow Request:", error);
95-
// throw error;
96131
}
97132
return response;
98133
}
99-
100134
}
101135

136+
// API Functions
137+
138+
export const searchCustomer = async (subscriptionCustomer: LowcoderCustomer) => {
139+
const apiBody = {
140+
path: "webhook/secure/search-customer",
141+
data: subscriptionCustomer,
142+
method: "post",
143+
headers: lcHeaders
144+
};
145+
try {
146+
const result = await SubscriptionApi.secureRequest(apiBody);
147+
return result?.data?.data?.length === 1 ? result.data.data[0] as StripeCustomer : null;
148+
} catch (error) {
149+
console.error("Error searching customer:", error);
150+
throw error;
151+
}
152+
};
153+
154+
export const searchSubscriptions = async (customerId: string) => {
155+
const apiBody = {
156+
path: "webhook/secure/search-subscriptions",
157+
data: { customerId },
158+
method: "post",
159+
headers: lcHeaders
160+
};
161+
try {
162+
const result = await SubscriptionApi.secureRequest(apiBody);
163+
return result?.data?.data ?? [];
164+
} catch (error) {
165+
console.error("Error searching subscriptions:", error);
166+
throw error;
167+
}
168+
};
169+
170+
export const createCustomer = async (subscriptionCustomer: LowcoderCustomer) => {
171+
const apiBody = {
172+
path: "webhook/secure/create-customer",
173+
data: subscriptionCustomer,
174+
method: "post",
175+
headers: lcHeaders
176+
};
177+
try {
178+
const result = await SubscriptionApi.secureRequest(apiBody);
179+
return result?.data as StripeCustomer;
180+
} catch (error) {
181+
console.error("Error creating customer:", error);
182+
throw error;
183+
}
184+
};
185+
186+
export const createCheckoutLink = async (customer: StripeCustomer, priceId: string, quantity: number, discount?: number) => {
187+
const domain = window.location.protocol + "//" + window.location.hostname + (window.location.port ? ':' + window.location.port : '');
188+
189+
const apiBody = {
190+
path: "webhook/secure/create-checkout-link",
191+
data: {
192+
"customerId": customer.id,
193+
"priceId": priceId,
194+
"quantity": quantity,
195+
"discount": discount,
196+
baseUrl: domain
197+
},
198+
method: "post",
199+
headers: lcHeaders
200+
};
201+
try {
202+
const result = await SubscriptionApi.secureRequest(apiBody);
203+
return result?.data ? { id: result.data.id, url: result.data.url } : null;
204+
} catch (error) {
205+
console.error("Error creating checkout link:", error);
206+
throw error;
207+
}
208+
};
209+
210+
// Hooks
211+
212+
export const InitializeSubscription = () => {
213+
const [customer, setCustomer] = useState<StripeCustomer | null>(null);
214+
const [isCreatingCustomer, setIsCreatingCustomer] = useState<boolean>(false); // Track customer creation
215+
const [customerDataError, setCustomerDataError] = useState<boolean>(false);
216+
const [subscriptions, setSubscriptions] = useState<SubscriptionItem[]>([]);
217+
const [subscriptionDataLoaded, setSubscriptionDataLoaded] = useState<boolean>(false);
218+
const [subscriptionDataError, setSubscriptionDataError] = useState<boolean>(false);
219+
const [checkoutLinkDataLoaded, setCheckoutLinkDataLoaded] = useState<boolean>(false);
220+
const [checkoutLinkDataError, setCheckoutLinkDataError] = useState<boolean>(false);
221+
const [products, setProducts] = useState<Product[]>([
222+
{
223+
title: "Support Subscription",
224+
description: "Support Ticket System and SLAs to guarantee response time and your project success.",
225+
image: "https://gw.alipayobjects.com/zos/rmsportal/JiqGstEfoWAOHiTxclqi.png",
226+
pricingType: "Monthly, per User",
227+
pricing: [
228+
{ type: "User", amount: "$3.49 (user, month)" },
229+
{ type: "> 10 Users", amount: "$2.49 (user, month)" },
230+
{ type: "> 100 Users", amount: "$1.49 (user, month)" }
231+
],
232+
activeSubscription: false,
233+
accessLink: "1PhH38DDlQgecLSfSukEgIeV",
234+
subscriptionId: "",
235+
checkoutLink: "",
236+
checkoutLinkDataLoaded: false,
237+
type: "org",
238+
quantity_entity: "orgUser",
239+
},
240+
{
241+
title: "Premium Media Subscription",
242+
description: "Access to all features.",
243+
image: "https://gw.alipayobjects.com/zos/rmsportal/JiqGstEfoWAOHiTxclqi.png",
244+
pricingType: "Monthly, per User",
245+
pricing: [
246+
{ type: "Volume Price", amount: "$20/month" },
247+
{ type: "Single Price", amount: "$25/month" }
248+
],
249+
activeSubscription: false,
250+
accessLink: "1Pf65wDDlQgecLSf6OFlbsD5",
251+
checkoutLink: "",
252+
checkoutLinkDataLoaded: false,
253+
subscriptionId: "",
254+
type: "user",
255+
quantity_entity: "singleItem",
256+
}
257+
]);
258+
259+
const user = useSelector(getUser);
260+
const currentUser = useSelector(getCurrentUser);
261+
const currentOrg = user.orgs.find(org => org.id === user.currentOrgId);
262+
const orgID = user.currentOrgId;
263+
const domain = window.location.protocol + "//" + window.location.hostname + (window.location.port ? ':' + window.location.port : '');
264+
const admin = user.orgRoleMap.get(orgID) === "admin" ? "admin" : "member";
265+
266+
const subscriptionCustomer: LowcoderCustomer = {
267+
hostname: domain,
268+
email: currentUser.email,
269+
orgId: orgID,
270+
userId: user.id,
271+
userName: user.username,
272+
type: admin ? "admin" : "user",
273+
companyName: currentOrg?.name || "Unknown",
274+
};
275+
276+
useEffect(() => {
277+
const initializeCustomer = async () => {
278+
try {
279+
setIsCreatingCustomer(true);
280+
const existingCustomer = await searchCustomer(subscriptionCustomer);
281+
if (existingCustomer) {
282+
setCustomer(existingCustomer);
283+
} else {
284+
const newCustomer = await createCustomer(subscriptionCustomer);
285+
setCustomer(newCustomer);
286+
}
287+
} catch (error) {
288+
setCustomerDataError(true);
289+
} finally {
290+
setIsCreatingCustomer(false);
291+
}
292+
};
293+
294+
initializeCustomer();
295+
}, []);
296+
297+
useEffect(() => {
298+
const fetchSubscriptions = async () => {
299+
if (customer) {
300+
try {
301+
const subs = await searchSubscriptions(customer.id);
302+
setSubscriptions(subs);
303+
setSubscriptionDataLoaded(true);
304+
} catch (error) {
305+
setSubscriptionDataError(true);
306+
}
307+
}
308+
};
309+
310+
fetchSubscriptions();
311+
}, [customer]);
312+
313+
useEffect(() => {
314+
const prepareCheckout = async () => {
315+
if (subscriptionDataLoaded) {
316+
try {
317+
const updatedProducts = await Promise.all(
318+
products.map(async (product) => {
319+
const matchingSubscription = subscriptions.find(
320+
(sub) => sub.plan.id === "price_" + product.accessLink
321+
);
322+
323+
if (matchingSubscription) {
324+
return {
325+
...product,
326+
activeSubscription: true,
327+
checkoutLinkDataLoaded: true,
328+
subscriptionId: matchingSubscription.id.substring(4),
329+
};
330+
} else {
331+
const checkoutLink = await createCheckoutLink(customer!, product.accessLink, 1);
332+
return {
333+
...product,
334+
activeSubscription: false,
335+
checkoutLink: checkoutLink ? checkoutLink.url : "",
336+
checkoutLinkDataLoaded: true,
337+
};
338+
}
339+
})
340+
);
341+
342+
setProducts(updatedProducts);
343+
} catch (error) {
344+
setCheckoutLinkDataError(true);
345+
}
346+
}
347+
};
348+
349+
prepareCheckout();
350+
}, [subscriptionDataLoaded]);
351+
352+
return {
353+
customer,
354+
isCreatingCustomer,
355+
customerDataError,
356+
subscriptions,
357+
subscriptionDataLoaded,
358+
subscriptionDataError,
359+
checkoutLinkDataLoaded,
360+
checkoutLinkDataError,
361+
products,
362+
};
363+
};
364+
365+
366+
367+
export const CheckSubscriptions = () => {
368+
const [customer, setCustomer] = useState<StripeCustomer | null>(null);
369+
const [customerDataError, setCustomerDataError] = useState<boolean>(false);
370+
const [subscriptions, setSubscriptions] = useState<SubscriptionItem[]>([]);
371+
const [subscriptionDataLoaded, setSubscriptionDataLoaded] = useState<boolean>(false);
372+
const [subscriptionDataError, setSubscriptionDataError] = useState<boolean>(false);
373+
const [loading, setLoading] = useState<boolean>(true);
374+
375+
const user = useSelector(getUser);
376+
const currentUser = useSelector(getCurrentUser);
377+
const orgID = user.currentOrgId;
378+
const domain = window.location.protocol + "//" + window.location.hostname + (window.location.port ? ':' + window.location.port : '');
379+
380+
const subscriptionCustomer: LowcoderCustomer = {
381+
hostname: domain,
382+
email: currentUser.email,
383+
orgId: orgID,
384+
userId: user.id,
385+
userName: user.username,
386+
type: user.orgRoleMap.get(orgID) === "admin" ? "admin" : "user",
387+
companyName: user.currentOrgId,
388+
};
389+
390+
useEffect(() => {
391+
const fetchCustomerAndSubscriptions = async () => {
392+
try {
393+
const existingCustomer = await searchCustomer(subscriptionCustomer);
394+
if (existingCustomer) {
395+
setCustomer(existingCustomer);
396+
const subs = await searchSubscriptions(existingCustomer.id);
397+
setSubscriptions(subs);
398+
setSubscriptionDataLoaded(true);
399+
} else {
400+
setCustomer(null);
401+
}
402+
} catch (error) {
403+
setCustomerDataError(true);
404+
setSubscriptionDataError(true);
405+
} finally {
406+
setLoading(false);
407+
}
408+
};
409+
410+
fetchCustomerAndSubscriptions();
411+
}, []);
412+
413+
return {
414+
customer,
415+
customerDataError,
416+
subscriptions,
417+
subscriptionDataLoaded,
418+
subscriptionDataError,
419+
loading,
420+
};
421+
};
422+
102423
export default SubscriptionApi;

0 commit comments

Comments
 (0)