Skip to content

Commit b82b19e

Browse files
author
FalkWolsky
committed
Subscription Detail Page
1 parent f41b3e3 commit b82b19e

File tree

4 files changed

+313
-12
lines changed

4 files changed

+313
-12
lines changed

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

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -291,6 +291,57 @@ export const createCheckoutLink = async (customer: StripeCustomer, priceId: stri
291291
}
292292
};
293293

294+
// Function to get subscription details from Stripe
295+
export const getSubscriptionDetails = async (subscriptionId: string) => {
296+
const apiBody = {
297+
path: "webhook/secure/get-subscription-details",
298+
method: "post",
299+
data: { "subscriptionId": subscriptionId },
300+
headers: lcHeaders,
301+
};
302+
try {
303+
const result = await SubscriptionApi.secureRequest(apiBody);
304+
return result?.data;
305+
} catch (error) {
306+
console.error("Error fetching subscription details:", error);
307+
throw error;
308+
}
309+
};
310+
311+
// Function to get invoice documents from Stripe
312+
export const getInvoices = async (subscriptionId: string) => {
313+
const apiBody = {
314+
path: "webhook/secure/get-subscription-invoices",
315+
method: "post",
316+
data: { "subscriptionId": subscriptionId },
317+
headers: lcHeaders,
318+
};
319+
try {
320+
const result = await SubscriptionApi.secureRequest(apiBody);
321+
return result?.data?.data ?? [];
322+
} catch (error) {
323+
console.error("Error fetching invoices:", error);
324+
throw error;
325+
}
326+
};
327+
328+
// Function to get a customer Portal Session from Stripe
329+
export const getCustomerPortalSession = async (customerId: string) => {
330+
const apiBody = {
331+
path: "webhook/secure/create-customer-portal-session",
332+
method: "post",
333+
data: { "customerId": customerId },
334+
headers: lcHeaders,
335+
};
336+
try {
337+
const result = await SubscriptionApi.secureRequest(apiBody);
338+
return result?.data;
339+
} catch (error) {
340+
console.error("Error fetching invoices:", error);
341+
throw error;
342+
}
343+
};
344+
294345
// Hooks
295346

296347
export const InitializeSubscription = () => {

client/packages/lowcoder/src/i18n/locales/en.ts

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2257,6 +2257,56 @@ export const en = {
22572257
"AppUsage": "Global App Usage",
22582258
},
22592259

2260+
"subscription": {
2261+
"details": "Subscription Details",
2262+
"productDetails": "Product Details",
2263+
"productName": "Product Name",
2264+
"productDescription": "Product Description",
2265+
"productPrice": "Product Price",
2266+
"subscriptionDetails": "Subscription Details",
2267+
"status": "Status",
2268+
"startDate": "Start Date",
2269+
"currentPeriodEnd": "Current Period End",
2270+
"customerId": "Customer ID",
2271+
"subscriptionItems": "Subscription Items",
2272+
"itemId": "Item ID",
2273+
"plan": "Plan",
2274+
"quantity": "Quantity",
2275+
"product": "Product",
2276+
"invoices": "Invoices",
2277+
"invoiceNumber": "Invoice Number",
2278+
"date": "Date",
2279+
"amount": "Amount",
2280+
"link": "Link",
2281+
"viewInvoice": "View Invoice",
2282+
"downloadPDF": "Download PDF",
2283+
"billingReason": "Billing Reason",
2284+
"subscriptionCycle": "monthly Subscription",
2285+
"customer": "Customer",
2286+
"links": "Links",
2287+
"paid": "Paid",
2288+
"unpaid": "Unpaid",
2289+
"noInvoices": "No invoices available",
2290+
"costVolumeDevelopment": "Cost/Volume Development",
2291+
"noUsageRecords": "No usage records available",
2292+
"itemDescription": "Item Description",
2293+
"periodStart": "Period Start",
2294+
"periodEnd": "Period End",
2295+
"billingReason.subscription_cycle": "Subscription Cycle",
2296+
"billingReason.subscription_create": "Subscription Creation",
2297+
"billingReason.manual": "Manual Billing",
2298+
"billingReason.upcoming": "Upcoming Billing",
2299+
"billingReason.subscription_threshold": "Subscription Threshold",
2300+
"billingReason.subscription_update": "Subscription Update",
2301+
"backToSubscriptions": "Back to Subscriptions",
2302+
"manageSubscription" : "Manage Your Subscription",
2303+
},
2304+
"subscriptionError": {
2305+
"fetchProductDetails": "Error fetching product details.",
2306+
"fetchSubscriptionDetails": "Error fetching subscription details.",
2307+
"fetchInvoices": "Error fetching invoices."
2308+
},
2309+
22602310

22612311
// thirteenth part
22622312

client/packages/lowcoder/src/pages/setting/subscriptions/subscriptionDetail.tsx

Lines changed: 196 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,29 +1,106 @@
1+
import React, { useEffect, useState } from "react";
12
import { ArrowIcon } from "lowcoder-design";
23
import styled from "styled-components";
34
import { trans } from "i18n";
45
import { useParams } from "react-router-dom";
56
import { HeaderBack } from "../permission/styledComponents";
67
import history from "util/history";
78
import { SUBSCRIPTION_SETTING } from "constants/routesURL";
8-
import { getProduct } from '@lowcoder-ee/api/subscriptionApi';
9+
import { getProduct, getSubscriptionDetails, getInvoices, getCustomerPortalSession } from "api/subscriptionApi";
10+
import { Skeleton, Timeline, Card, Descriptions, Table, Typography, Button, message } from "antd";
911

10-
const FieldWrapper = styled.div`
11-
margin-bottom: 32px;
12-
width: 408px;
13-
margin-top: 40px;
14-
`;
12+
const { Text } = Typography;
1513

1614
const Wrapper = styled.div`
1715
padding: 32px 24px;
1816
`;
1917

18+
const InvoiceLink = styled.a`
19+
color: #1d39c4;
20+
&:hover {
21+
text-decoration: underline;
22+
}
23+
`;
24+
25+
const CardWrapper = styled(Card)`
26+
width: 100%;
27+
margin-bottom: 24px;
28+
`;
29+
30+
const TimelineWrapper = styled.div`
31+
margin-top: 24px;
32+
`;
33+
34+
const ManageSubscriptionButton = styled(Button)`
35+
margin-top: 24px;
36+
`;
37+
2038
export function SubscriptionDetail() {
2139
const { subscriptionId } = useParams<{ subscriptionId: string }>();
2240
const { productId } = useParams<{ productId: string }>();
2341

24-
const product = getProduct(productId);
42+
const [product, setProduct] = useState<any>(null);
43+
const [subscription, setSubscription] = useState<any>(null);
44+
const [invoices, setInvoices] = useState<any[]>([]);
45+
const [loading, setLoading] = useState<boolean>(true);
46+
47+
useEffect(() => {
48+
const fetchData = async () => {
49+
setLoading(true);
50+
try {
51+
// Fetch product details
52+
const productData = await getProduct(productId);
53+
setProduct(productData);
54+
55+
// Fetch enriched subscription details, including usage records
56+
const subscriptionDetails = await getSubscriptionDetails(subscriptionId);
57+
setSubscription(subscriptionDetails);
2558

26-
console.log("product", product);
59+
// Fetch invoices separately using the previous function
60+
const invoiceData = await getInvoices(subscriptionId);
61+
setInvoices(invoiceData);
62+
} catch (error) {
63+
console.error("Error loading subscription details:", error);
64+
} finally {
65+
setLoading(false);
66+
}
67+
};
68+
69+
fetchData();
70+
}, [subscriptionId, productId]);
71+
72+
if (loading) {
73+
return <Skeleton style={{ margin: "40px" }} active paragraph={{ rows: 8 }} />;
74+
}
75+
76+
// Extracting data from the enriched response
77+
const subscriptionDetails = subscription ? subscription[0] : {};
78+
const usageRecords = subscription ? subscription[1]?.data || [] : [];
79+
80+
const statusColor = subscriptionDetails?.status === "active" ? "green" : "red";
81+
const customerId = subscriptionDetails?.customer; // Get the customer ID from subscription details
82+
83+
// Handle Customer Portal Session Redirect
84+
const handleCustomerPortalRedirect = async () => {
85+
try {
86+
if (!customerId) {
87+
message.error("Customer ID not available for the subscription.");
88+
return;
89+
}
90+
91+
// Get the Customer Portal session URL
92+
const portalSession = await getCustomerPortalSession(customerId);
93+
if (portalSession && portalSession.url) {
94+
// Redirect to the Stripe Customer Portal
95+
window.location.href = portalSession.url;
96+
} else {
97+
message.error("Failed to generate customer portal session link.");
98+
}
99+
} catch (error) {
100+
console.error("Error redirecting to customer portal:", error);
101+
message.error("An error occurred while redirecting to the customer portal.");
102+
}
103+
};
27104

28105
return (
29106
<Wrapper>
@@ -32,10 +109,118 @@ export function SubscriptionDetail() {
32109
{trans("settings.subscription")}
33110
</span>
34111
<ArrowIcon />
112+
<span>{trans("subscription.details")}</span>
35113
</HeaderBack>
36-
<div>
37-
<h1>{`Subscription ID: ${subscriptionId}`}</h1>
38-
</div>
114+
115+
{/* Subscription Details Card */}
116+
<CardWrapper title={trans("subscription.subscriptionDetails")} style={{ marginTop: "40px" }}>
117+
<Descriptions bordered column={2}>
118+
<Descriptions.Item label={trans("subscription.productName")}>
119+
{product?.name || "N/A"}
120+
</Descriptions.Item>
121+
<Descriptions.Item contentStyle={{ color: statusColor }} label={trans("subscription.status")}>
122+
{subscriptionDetails?.status || "N/A"}
123+
</Descriptions.Item>
124+
<Descriptions.Item label={trans("subscription.startDate")}>
125+
{new Date(subscriptionDetails?.start_date * 1000).toLocaleDateString() || "N/A"}
126+
</Descriptions.Item>
127+
<Descriptions.Item label={trans("subscription.currentPeriodEnd")}>
128+
{new Date(subscriptionDetails?.current_period_end * 1000).toLocaleDateString() || "N/A"}
129+
</Descriptions.Item>
130+
</Descriptions>
131+
</CardWrapper>
132+
133+
{/* Invoice Information Card */}
134+
{invoices?.length > 0 ? (
135+
invoices.map((invoice: any) => (
136+
<CardWrapper key={invoice.id} title={`${trans("subscription.invoiceNumber")} - ${invoice.number}`}>
137+
{/* Invoice Summary */}
138+
<Descriptions bordered size="small" column={1}>
139+
<Descriptions.Item label={trans("subscription.customer")}>
140+
{invoice.customer_name || invoice.customer_email}
141+
</Descriptions.Item>
142+
<Descriptions.Item label={trans("subscription.billingReason")}>
143+
{invoice.billing_reason === "subscription_cycle" ? trans("subscription.subscriptionCycle") : "N/A"}
144+
</Descriptions.Item>
145+
<Descriptions.Item label={trans("subscription.status")}>
146+
<Text style={{ color: invoice.status === "paid" ? "green" : "red" }}>
147+
{invoice.status.charAt(0).toUpperCase() + invoice.status.slice(1)}
148+
</Text>
149+
</Descriptions.Item>
150+
<Descriptions.Item label={trans("subscription.links")}>
151+
<InvoiceLink href={invoice.hosted_invoice_url} target="_blank" rel="noopener noreferrer">
152+
{trans("subscription.viewInvoice")}
153+
</InvoiceLink>{" "}
154+
|{" "}
155+
<InvoiceLink href={invoice.invoice_pdf} target="_blank" rel="noopener noreferrer">
156+
{trans("subscription.downloadPDF")}
157+
</InvoiceLink>
158+
</Descriptions.Item>
159+
</Descriptions>
160+
161+
{/* Line Items Table */}
162+
<Table
163+
style={{ marginTop: "16px" }}
164+
dataSource={invoice.lines.data.filter((lineItem: any) => lineItem.amount !== 0)} // Filter out line items with amount = 0
165+
pagination={false}
166+
rowKey={(lineItem) => lineItem.id}
167+
columns={[
168+
{
169+
title: trans("subscription.itemDescription"),
170+
dataIndex: "description",
171+
key: "description",
172+
},
173+
{
174+
title: trans("subscription.amount"),
175+
dataIndex: "amount",
176+
key: "amount",
177+
render: (amount: number) => `${(amount / 100).toFixed(2)} ${invoice.currency?.toUpperCase()}`,
178+
},
179+
{
180+
title: trans("subscription.periodStart"),
181+
dataIndex: ["period", "start"],
182+
key: "period_start",
183+
render: (start: number) => new Date(start * 1000).toLocaleDateString(),
184+
},
185+
{
186+
title: trans("subscription.periodEnd"),
187+
dataIndex: ["period", "end"],
188+
key: "period_end",
189+
render: (end: number) => new Date(end * 1000).toLocaleDateString(),
190+
},
191+
]}
192+
/>
193+
</CardWrapper>
194+
))
195+
) : (
196+
<CardWrapper title={trans("subscription.invoices")}>
197+
<p>{trans("subscription.noInvoices")}</p>
198+
</CardWrapper>
199+
)}
200+
201+
{/* Cost/Volume Development Timeline */}
202+
<CardWrapper title={trans("subscription.costVolumeDevelopment")}>
203+
<TimelineWrapper>
204+
<Timeline>
205+
{usageRecords?.length > 0 ? (
206+
usageRecords.map((record: any, index: number) => (
207+
<Timeline.Item key={index} color={record.total_usage > 0 ? "green" : "gray"}>
208+
{`Usage for ${record.total_usage} units on ${new Date(record.period.start * 1000).toLocaleDateString()}`}
209+
</Timeline.Item>
210+
))
211+
) : (
212+
<Timeline.Item color="gray">{trans("subscription.noUsageRecords")}</Timeline.Item>
213+
)}
214+
</Timeline>
215+
</TimelineWrapper>
216+
</CardWrapper>
217+
218+
{/* Manage Subscription Button */}
219+
<CardWrapper title={trans("subscription.manageSubscription")} style={{marginBottom : "60px"}}>
220+
<ManageSubscriptionButton type="primary" onClick={handleCustomerPortalRedirect}>
221+
{trans("subscription.manageSubscription")}
222+
</ManageSubscriptionButton>
223+
</CardWrapper>
39224
</Wrapper>
40225
);
41226
}

client/packages/lowcoder/src/pages/support/supportOverview.tsx

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -95,6 +95,17 @@ const toolbarOptions = [
9595
[{ 'align': [] }] // Text alignment
9696
];
9797

98+
const getStatusColor = (statusName: string) => {
99+
if (statusName.toLowerCase() === "to do") {
100+
return "red";
101+
} else if (statusName.toLowerCase().includes("in progress")) {
102+
return "orange";
103+
} else if (statusName.toLowerCase() === "done") {
104+
return "green";
105+
}
106+
return "#8b8fa3"; // Default color if no match
107+
};
108+
98109

99110
function formatDateToMinute(dateString: string): string {
100111
// Create a Date object from the string
@@ -336,7 +347,11 @@ export function SupportOverview() {
336347
ellipsis: true,
337348
width: "220px",
338349
sorter: (a: any, b: any) => a.status.name.localeCompare(b.status.name),
339-
render: (status: any) => <SubColumnCell>{status.name}</SubColumnCell>,
350+
render: (status: any) => (
351+
<SubColumnCell style={{ color: getStatusColor(status.name) }}>
352+
{status.name}
353+
</SubColumnCell>
354+
),
340355
},
341356
{
342357
title: trans("support.updatedTime"),

0 commit comments

Comments
 (0)