diff --git a/.dockerignore b/.dockerignore
index b0a11682..352fa2dd 100644
--- a/.dockerignore
+++ b/.dockerignore
@@ -10,4 +10,4 @@ coverage
bun.lock
docs/
md/
-playwright.config.ts
+playwright.config.ts
\ No newline at end of file
diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index b55d9e31..90a058ee 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -31,8 +31,10 @@ jobs:
echo "BILLING_CURRENCY=USD" >> $GITHUB_ENV
echo "BYOB_ENCRYPTION_SECRET=test_secret_32_characters_long_123" >> $GITHUB_ENV
echo "NEXT_PUBLIC_APPWRITE_BYOB_TENANTS_ID=byob_tenants" >> $GITHUB_ENV
- echo "NEXT_PUBLIC_TRIAL_CREDIT_USD=30" >> $GITHUB_ENV
- echo "TRIAL_CREDIT_DAYS=60" >> $GITHUB_ENV
+ echo "NEXT_PUBLIC_ORG_TRIAL_CREDIT_USD=30" >> $GITHUB_ENV
+ echo "ORG_TRIAL_CREDIT_DAYS=60" >> $GITHUB_ENV
+ echo "NEXT_PUBLIC_PERSONAL_TRIAL_CREDIT_USD=10" >> $GITHUB_ENV
+ echo "PERSONAL_TRIAL_CREDIT_DAYS=30" >> $GITHUB_ENV
- name: Type check
run: npx tsc --noEmit
diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml
index 7bcb6ed5..a4a1d91b 100644
--- a/.github/workflows/deploy.yml
+++ b/.github/workflows/deploy.yml
@@ -155,9 +155,13 @@ jobs:
# Redis (server-side caching layer)
REDIS_URL: ${{ secrets.REDIS_URL }}
REDIS_PASSWORD: ${{ secrets.REDIS_PASSWORD }}
- # Trial Credits
- NEXT_PUBLIC_TRIAL_CREDIT_USD: ${{ vars.NEXT_PUBLIC_TRIAL_CREDIT_USD }}
- TRIAL_CREDIT_DAYS: ${{ vars.TRIAL_CREDIT_DAYS }}
+ # Trial Credits (Granular Config)
+ NEXT_PUBLIC_ORG_TRIAL_CREDIT_USD: ${{ vars.NEXT_PUBLIC_ORG_TRIAL_CREDIT_USD }}
+ ORG_TRIAL_CREDIT_USD: ${{ vars.ORG_TRIAL_CREDIT_USD }}
+ ORG_TRIAL_CREDIT_DAYS: ${{ vars.ORG_TRIAL_CREDIT_DAYS }}
+ NEXT_PUBLIC_PERSONAL_TRIAL_CREDIT_USD: ${{ vars.NEXT_PUBLIC_PERSONAL_TRIAL_CREDIT_USD }}
+ PERSONAL_TRIAL_CREDIT_USD: ${{ vars.PERSONAL_TRIAL_CREDIT_USD }}
+ PERSONAL_TRIAL_CREDIT_DAYS: ${{ vars.PERSONAL_TRIAL_CREDIT_DAYS }}
run: npm run build
- name: Clean up before transfer
@@ -298,8 +302,12 @@ jobs:
R2_PUBLIC_URL=${{ vars.R2_PUBLIC_URL }}
REDIS_URL=${{ secrets.REDIS_URL }}
REDIS_PASSWORD=${{ secrets.REDIS_PASSWORD }}
- NEXT_PUBLIC_TRIAL_CREDIT_USD=${{ vars.NEXT_PUBLIC_TRIAL_CREDIT_USD }}
- TRIAL_CREDIT_DAYS=${{ vars.TRIAL_CREDIT_DAYS }}
+ NEXT_PUBLIC_ORG_TRIAL_CREDIT_USD=${{ vars.NEXT_PUBLIC_ORG_TRIAL_CREDIT_USD }}
+ ORG_TRIAL_CREDIT_USD=${{ vars.ORG_TRIAL_CREDIT_USD }}
+ ORG_TRIAL_CREDIT_DAYS=${{ vars.ORG_TRIAL_CREDIT_DAYS }}
+ NEXT_PUBLIC_PERSONAL_TRIAL_CREDIT_USD=${{ vars.NEXT_PUBLIC_PERSONAL_TRIAL_CREDIT_USD }}
+ PERSONAL_TRIAL_CREDIT_USD=${{ vars.PERSONAL_TRIAL_CREDIT_USD }}
+ PERSONAL_TRIAL_CREDIT_DAYS=${{ vars.PERSONAL_TRIAL_CREDIT_DAYS }}
EOF
chmod 600 .env.local
diff --git a/src/app/(dashboard)/layout.tsx b/src/app/(dashboard)/layout.tsx
index 4466cfa7..477c497c 100644
--- a/src/app/(dashboard)/layout.tsx
+++ b/src/app/(dashboard)/layout.tsx
@@ -59,7 +59,8 @@ const DashboardContent = ({ children }: DashboardLayoutProps) => {
const isProfilePage = pathname === "/profile" || pathname.startsWith("/profile/");
const workspaceId = useWorkspaceId();
const isTaskDetailPage = /^\/workspaces\/[^\/]+\/tasks\/[^\/]+$/.test(pathname || "");
- const isMainDashboard = /^\/workspaces\/[^\/]+$/.test(pathname || "");
+const isWorkflowPage = /^\/workspaces\/[^\/]+\/spaces\/[^\/]+\/workflows\/[^\/]+$/.test(pathname || "");
+ const isMainDashboard = /^\/workspaces\/[^\/]+$/.test(pathname || "");
return (
@@ -93,14 +94,12 @@ const DashboardContent = ({ children }: DashboardLayoutProps) => {
-
-
- {children}
-
-
+
+ {children}
+
diff --git a/src/app/(dashboard)/workspaces/[workspaceId]/admin/usage/client.tsx b/src/app/(dashboard)/workspaces/[workspaceId]/admin/usage/client.tsx
index 451eee5b..dacd7bdf 100644
--- a/src/app/(dashboard)/workspaces/[workspaceId]/admin/usage/client.tsx
+++ b/src/app/(dashboard)/workspaces/[workspaceId]/admin/usage/client.tsx
@@ -33,6 +33,7 @@ import {
useGetUsageDashboard,
useExportUsage,
} from "@/features/usage/api";
+import { useGetBillingAccount } from "@/features/billing/api";
import {
UsageKPICards,
UsageCharts,
@@ -131,6 +132,13 @@ export function UsageDashboardClient() {
eventsOffset: fetchOffset,
});
+ // Fetch billing account for wallet balance
+ const { data: billingAccountData } = useGetBillingAccount({
+ organizationId: isOrg ? primaryOrganizationId : undefined,
+ userId: !isOrg ? user?.$id : undefined,
+ enabled: !!(isOrg ? primaryOrganizationId : user?.$id)
+ });
+
const isEventsLoading = isDashboardLoading;
const isSummaryLoading = isDashboardLoading;
const isAlertsLoading = isDashboardLoading;
@@ -367,6 +375,8 @@ export function UsageDashboardClient() {
isLoading={isSummaryLoading}
currency={currency}
exchangeRate={rate}
+ walletBalance={billingAccountData?.walletBalance}
+ walletCurrency={billingAccountData?.walletCurrency}
/>
{/* Charts */}
diff --git a/src/app/(dashboard)/workspaces/[workspaceId]/billing/client.tsx b/src/app/(dashboard)/workspaces/[workspaceId]/billing/client.tsx
index d8c425c3..382389fc 100644
--- a/src/app/(dashboard)/workspaces/[workspaceId]/billing/client.tsx
+++ b/src/app/(dashboard)/workspaces/[workspaceId]/billing/client.tsx
@@ -1,28 +1,196 @@
"use client";
+import { useState, useCallback } from "react";
import { useParams } from "next/navigation";
-import { Building2, CreditCard, TrendingUp, Calendar } from "lucide-react";
+import {
+ Building2,
+ CreditCard,
+ TrendingUp,
+ Calendar,
+ Wallet,
+ ArrowUpRight,
+ Plus,
+ Loader2,
+ ExternalLink,
+ FileText,
+ Clock
+} from "lucide-react";
+import { format, startOfMonth, endOfMonth, differenceInDays } from "date-fns";
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
import { Badge } from "@/components/ui/badge";
import { Button } from "@/components/ui/button";
import { Separator } from "@/components/ui/separator";
+import { Input } from "@/components/ui/input";
import { useAccountType } from "@/features/organizations/hooks/use-account-type";
import { useGetOrganizations } from "@/features/organizations/api/use-get-organizations";
-import { useGetUsageSummary } from "@/features/usage/api/use-get-usage-summary";
-import { useGetUsageEvents } from "@/features/usage/api/use-get-usage-events";
+import {
+ useGetUsageSummary,
+ useGetUsageEvents
+} from "@/features/usage/api";
import { WorkspaceUsageBreakdown } from "@/features/usage/components";
import { RedeemCouponCard } from "@/features/github-rewards/components/RedeemCouponCard";
+import {
+ useGetBillingAccount,
+ useSetupBilling,
+ useGetBillingStatus,
+ useGetInvoices
+} from "@/features/billing/api";
+import { useCurrent } from "@/features/auth/api/use-current";
+import { BillingAccountType, BillingStatus, BillingInvoice } from "@/features/billing/types";
+import { client } from "@/lib/rpc";
+import { toast } from "sonner";
+import { useQueryClient } from "@tanstack/react-query";
+import Script from "next/script";
+import { cn } from "@/lib/utils";
export const BillingDashboardClient = () => {
const params = useParams();
+ const queryClient = useQueryClient();
const workspaceId = params.workspaceId as string;
- const { isOrg, primaryOrganizationId, accountType } = useAccountType();
+ const { isOrg, isPersonal, primaryOrganizationId, accountType } = useAccountType();
+ const { data: user } = useCurrent();
const { data: organizations } = useGetOrganizations();
const { data: usageSummary, isLoading: usageLoading } = useGetUsageSummary({
workspaceId,
period: new Date().toISOString().slice(0, 7), // Current month YYYY-MM
});
+ // Billing hooks
+ const { data: billingAccountData } = useGetBillingAccount({
+ organizationId: isOrg ? primaryOrganizationId : undefined,
+ userId: isPersonal ? user?.$id : undefined,
+ enabled: !!(isOrg ? primaryOrganizationId : user?.$id)
+ });
+ const { mutateAsync: setupBilling } = useSetupBilling();
+
+ const [isAddingCredits, setIsAddingCredits] = useState(false);
+ const [isScriptLoaded, setIsScriptLoaded] = useState(false);
+ const [topupAmount, setTopupAmount] = useState("");
+
+ // Wallet balance
+ const walletBalance = billingAccountData?.walletBalance ?? 0;
+ const walletCurrency = billingAccountData?.walletCurrency ?? "USD";
+ const trialExpiresAt = billingAccountData?.trialExpiresAt;
+ const initialTrialAmount = billingAccountData?.initialTrialAmount ?? 0;
+
+ // Credit breakdown logic
+ // Trial credits are consumed first.
+ // Remaining Trial = min(current balance, initial trial amount)
+ // Real Credits = current balance - remaining trial
+ const trialBalance = Math.min(walletBalance, initialTrialAmount);
+ const realBalance = Math.max(0, walletBalance - trialBalance);
+
+ const formatCurrency = (amount: number) => {
+ return new Intl.NumberFormat("en-US", {
+ style: "currency",
+ currency: walletCurrency,
+ minimumFractionDigits: 2,
+ }).format(amount);
+ };
+
+ // Handler for adding credits
+ const handleAddCredits = useCallback(async () => {
+ const entityId = isOrg ? primaryOrganizationId : user?.$id;
+ if (!entityId) {
+ toast.error("Account ID is required");
+ return;
+ }
+
+ const amount = Number(topupAmount);
+ if (!amount || amount < 1) {
+ toast.error("Minimum top-up amount is $1");
+ return;
+ }
+
+ if (!isScriptLoaded) {
+ toast.error("Payment system not ready. Please refresh the page.");
+ return;
+ }
+
+ setIsAddingCredits(true);
+
+ try {
+ // Ensure billing account exists
+ if (!billingAccountData?.data) {
+ try {
+ await setupBilling({
+ json: {
+ type: isOrg ? BillingAccountType.ORG : BillingAccountType.PERSONAL,
+ organizationId: isOrg ? primaryOrganizationId : undefined,
+ userId: isPersonal ? user?.$id : undefined,
+ billingEmail: user?.email || undefined,
+ contactName: user?.name || "User",
+ }
+ });
+ } catch {
+ toast.error("Failed to initialize billing. Please contact support.");
+ setIsAddingCredits(false);
+ return;
+ }
+ }
+
+ // Create order
+ const orderResponse = await client.api.wallet["create-order"].$post({
+ json: {
+ amount,
+ organizationId: isOrg ? primaryOrganizationId : undefined,
+ userId: isPersonal ? user?.$id : undefined,
+ },
+ });
+
+ if (!orderResponse.ok) {
+ const errorData = await orderResponse.json().catch(() => ({}));
+ throw new Error((errorData as { error?: string }).error || "Failed to create order");
+ }
+
+ const orderResult = await orderResponse.json() as {
+ data: {
+ orderId: string;
+ paymentSessionId: string;
+ environment: string;
+ }
+ };
+ const orderData = orderResult.data;
+
+ const cashfree = (window as Window & {
+ Cashfree: (args: { mode: string }) => {
+ checkout: (args: { paymentSessionId: string; redirectTarget: string }) => Promise<{ error?: { message?: string } }>
+ }
+ }).Cashfree({
+ mode: orderData.environment || "sandbox"
+ });
+
+ const checkoutResult = await cashfree.checkout({
+ paymentSessionId: orderData.paymentSessionId,
+ redirectTarget: "_modal",
+ });
+
+ if (checkoutResult.error) {
+ if (!checkoutResult.error.message?.includes("user")) {
+ toast.error(`Payment error: ${checkoutResult.error.message}`);
+ }
+ } else {
+ // Verify
+ const verifyResponse = await client.api.wallet["verify-topup"].$post({
+ json: { cashfreeOrderId: orderData.orderId },
+ });
+
+ if (!verifyResponse.ok) {
+ toast.info("Payment is being processed. Wallet will be credited shortly.");
+ } else {
+ toast.success(`$${amount} added successfully!`);
+ queryClient.invalidateQueries({ queryKey: ["billing-account"] });
+ setTopupAmount("");
+ }
+ }
+ setIsAddingCredits(false);
+ } catch (error: unknown) {
+ const message = error instanceof Error ? error.message : "Failed to initialize payment";
+ toast.error(message);
+ setIsAddingCredits(false);
+ }
+ }, [isOrg, isPersonal, primaryOrganizationId, user, topupAmount, isScriptLoaded, billingAccountData, setupBilling, queryClient]);
+
// Fetch events for workspace breakdown
const { data: eventsData, isLoading: eventsLoading } = useGetUsageEvents({
workspaceId: isOrg ? undefined : workspaceId,
@@ -35,8 +203,23 @@ export const BillingDashboardClient = () => {
? organizations?.documents?.find((o: { $id: string }) => o.$id === primaryOrganizationId)
: null;
+ // Fetch invoices
+ const { data: invoicesDoc, isLoading: isInvoicesLoading } = useGetInvoices({
+ organizationId: isOrg ? primaryOrganizationId : undefined,
+ userId: isPersonal ? user?.$id : undefined,
+ limit: 10
+ });
+ const invoices = invoicesDoc?.documents || [];
+
+ // Billing status
+ const { data: billingStatus } = useGetBillingStatus({
+ organizationId: isOrg ? primaryOrganizationId : undefined,
+ userId: isPersonal ? user?.$id : undefined,
+ enabled: !!(isOrg ? primaryOrganizationId : user?.$id)
+ });
+
return (
-
+
{/* Header */}
@@ -98,9 +281,138 @@ export const BillingDashboardClient = () => {
+ {/* Wallet & Credits - PREMIUM ENHANCED SECTION */}
+
+ {/* Wallet Balance Display */}
+
+
+
+ {/* Animated shapes */}
+
+
+
+
+
+
+
+ Fairlx Wallet
+
+
+ {isOrg ? "Organization Funds" : "Personal Credits"}
+
+
+
+ Available balance for AI compute and automated tasks
+
+
+
+
+
+
+
Total Available
+
+
+ {new Intl.NumberFormat("en-US", {
+ style: "currency",
+ currency: walletCurrency,
+ minimumFractionDigits: 2,
+ }).format(walletBalance)}
+
+ {walletCurrency}
+
+
+ {initialTrialAmount > 0 && (
+
+
{formatCurrency(realBalance)} real
+
+
{formatCurrency(trialBalance)} trial
+
=
+
{formatCurrency(walletBalance)} total
+
+ )}
+
+ {trialExpiresAt && (
+
+
+ Trial credit expires in {differenceInDays(new Date(trialExpiresAt), new Date())} days
+
+ )}
+
+
+
+
+
+
Real-time usage active
+
+
+ * Balance updates instantly after compute operations
+
+
+
+
+
+
+ {/* Quick Top-up Card */}
+
+
+
+
+ Add Credits
+
+
+
+
+
+ $
+ setTopupAmount(e.target.value)}
+ disabled={isAddingCredits}
+ />
+
+
+ {[5, 10, 25, 50].map((amt) => (
+
+ ))}
+
+
+
+
+
+ Secure payment via UPI, Cards & Net Banking
+
+
+
+
+
{/* Usage Summary */}
-
+
Traffic Usage
@@ -111,13 +423,13 @@ export const BillingDashboardClient = () => {
{usageLoading ? "..." : `${usageSummary?.data?.trafficTotalGB?.toFixed(2) || 0} GB`}
-
+
This month
-
+
Storage Usage
@@ -133,7 +445,7 @@ export const BillingDashboardClient = () => {
-
+
Compute Units
@@ -155,6 +467,128 @@ export const BillingDashboardClient = () => {
workspaceId={workspaceId}
organizationId={isOrg ? primaryOrganizationId : undefined}
/>
+
+ {/* Billing Lifecycle & Invoices */}
+
+ {/* Billing Lifecycle */}
+
+
+
+
+ Billing Lifecycle
+
+ Your current billing period and status
+
+
+
+
+
+
+
+
+
+ Current Period
+ ACTIVE
+
+
+ Started on {startOfMonth(new Date()).toLocaleDateString("en-US", { month: "long", day: "numeric", year: "numeric" })}
+
+
+
+
+
+
+
+
+ Next Invoice
+ UPCOMING
+
+
+ Expected on {endOfMonth(new Date()).toLocaleDateString("en-US", { month: "long", day: "numeric", year: "numeric" })}
+
+
+
+
+
+
+
+
Account Status
+
+
+
{billingStatus?.status || "ACTIVE"}
+
+
+
+
+
+
+
+ {/* Invoice History */}
+
+
+
+
+
+ Invoice History
+
+ Recent billing statements
+
+
+
+
+
+ {isInvoicesLoading ? (
+
+
+ Fetching invoices...
+
+ ) : invoices.length === 0 ? (
+
+
+
No invoices generated yet
+
+ ) : (
+ invoices.map((invoice: BillingInvoice) => (
+
+
+
+
+
+
+
{invoice.invoiceId}
+
{format(new Date(invoice.$createdAt), "MMM dd, yyyy")}
+
+
+
+
+
+ {new Intl.NumberFormat("en-US", {
+ style: "currency",
+ currency: "USD",
+ }).format(invoice.totalCost)}
+
+
Paid
+
+
+
+
+ ))
+ )}
+
+
+
+
+
{/* Organization Workspaces Breakdown (ORG only) */}
{isOrg && primaryOrganizationId && (
{
{/* Upgrade CTA for Personal accounts */}
{!isOrg && (
-
-
-
-
-
Upgrade to Organization
-
- Create unlimited workspaces, invite team members, and get organization-level billing.
+
+
+
+
+
+
+
+
Scale to Organization
+
+ Collaborate with team members, manage multiple workspaces, and get unified billing for your entire team.
-
)}
+ {/* Scripts */}
+