mirror of
https://github.com/arsvendg/Stirling-PDF.git
synced 2026-07-15 11:00:47 +02:00
Add frontend autoformatting and set CI to require formatted code for all languages (#6052)
# Description of Changes Changes the strategy for autoformatting to reject PRs if they are not formatted correctly instead of allowing them to merge and then spawning a new PR to fix the formatting. The old strategy just caused more work for us because we'd have to manually approve the followup PR and get it merged, which required 2 reviewers so in practice it rarely got done and just meant everyone's PRs ended up containing reformatting for unrelated files, which makes code review unnecessarily difficult. If the PR's code is not formatted correctly after this PR, a comment will be added automatically to tell the author how to run the formatter script to fix their code so it can go in. This also enables autoformatting for the frontend code, using Prettier. I've enabled it for pretty much everything in the frontend folder, other than 3rd party files and files it doesn't make sense for. I also excluded Markdown because it sounds likely to be more annoying to have to autoformat the Markdown in the frontend folder but nowhere else. Open to changing this though if people disagree. > [!note] > > Advice to reviewers: The first commit contains all of the actual logic I've introduced (CI changes, Prettier config, etc.) > The second commit is just the reformatting of the entire frontend folder. > The first commit needs proper review, the second one just give it a spot-check that it's doing what you'd expect.
This commit is contained in:
@@ -1,6 +1,6 @@
|
||||
import React, { createContext, useContext, useState, ReactNode, useCallback } from 'react';
|
||||
import { SaaSStripeCheckout } from '@app/components/shared/billing/SaaSStripeCheckout';
|
||||
import { useSaaSBilling } from '@app/contexts/SaasBillingContext';
|
||||
import React, { createContext, useContext, useState, ReactNode, useCallback } from "react";
|
||||
import { SaaSStripeCheckout } from "@app/components/shared/billing/SaaSStripeCheckout";
|
||||
import { useSaaSBilling } from "@app/contexts/SaasBillingContext";
|
||||
|
||||
interface SaaSCheckoutContextType {
|
||||
opened: boolean;
|
||||
@@ -14,7 +14,7 @@ const SaaSCheckoutContext = createContext<SaaSCheckoutContextType | undefined>(u
|
||||
export const useSaaSCheckout = () => {
|
||||
const context = useContext(SaaSCheckoutContext);
|
||||
if (!context) {
|
||||
throw new Error('useSaaSCheckout must be used within SaaSCheckoutProvider');
|
||||
throw new Error("useSaaSCheckout must be used within SaaSCheckoutProvider");
|
||||
}
|
||||
return context;
|
||||
};
|
||||
@@ -23,9 +23,7 @@ interface SaaSCheckoutProviderProps {
|
||||
children: ReactNode;
|
||||
}
|
||||
|
||||
export const SaaSCheckoutProvider: React.FC<SaaSCheckoutProviderProps> = ({
|
||||
children
|
||||
}) => {
|
||||
export const SaaSCheckoutProvider: React.FC<SaaSCheckoutProviderProps> = ({ children }) => {
|
||||
const [opened, setOpened] = useState(false);
|
||||
const [selectedPlan, setSelectedPlan] = useState<string | null>(null);
|
||||
|
||||
@@ -46,11 +44,11 @@ export const SaaSCheckoutProvider: React.FC<SaaSCheckoutProviderProps> = ({
|
||||
// Internal success handler - automatically refreshes billing context
|
||||
const handleCheckoutSuccess = useCallback(async () => {
|
||||
// Wait for webhooks to process (2 seconds)
|
||||
await new Promise(resolve => setTimeout(resolve, 2000));
|
||||
await new Promise((resolve) => setTimeout(resolve, 2000));
|
||||
try {
|
||||
await refreshBilling();
|
||||
} catch (error) {
|
||||
console.error('[SaaSCheckoutContext] Failed to refresh billing after checkout:', error);
|
||||
console.error("[SaaSCheckoutContext] Failed to refresh billing after checkout:", error);
|
||||
}
|
||||
}, [refreshBilling]);
|
||||
|
||||
@@ -64,12 +62,7 @@ export const SaaSCheckoutProvider: React.FC<SaaSCheckoutProviderProps> = ({
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
<SaaSStripeCheckout
|
||||
opened={opened}
|
||||
onClose={closeCheckout}
|
||||
planId={selectedPlan}
|
||||
onSuccess={handleCheckoutSuccess}
|
||||
/>
|
||||
<SaaSStripeCheckout opened={opened} onClose={closeCheckout} planId={selectedPlan} onSuccess={handleCheckoutSuccess} />
|
||||
</SaaSCheckoutContext.Provider>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { createContext, useContext, useEffect, useState, ReactNode, useCallback } from 'react';
|
||||
import apiClient from '@app/services/apiClient';
|
||||
import { authService } from '@app/services/authService';
|
||||
import { connectionModeService } from '@app/services/connectionModeService';
|
||||
import { createContext, useContext, useEffect, useState, ReactNode, useCallback } from "react";
|
||||
import apiClient from "@app/services/apiClient";
|
||||
import { authService } from "@app/services/authService";
|
||||
import { connectionModeService } from "@app/services/connectionModeService";
|
||||
|
||||
/**
|
||||
* Desktop implementation of SaaS Team Context
|
||||
@@ -91,7 +91,7 @@ export function SaaSTeamProvider({ children }: { children: ReactNode }) {
|
||||
const checkAccess = async () => {
|
||||
const mode = await connectionModeService.getCurrentMode();
|
||||
const auth = await authService.isAuthenticated();
|
||||
setIsSaasMode(mode === 'saas');
|
||||
setIsSaasMode(mode === "saas");
|
||||
setIsAuthenticated(auth);
|
||||
};
|
||||
|
||||
@@ -105,7 +105,7 @@ export function SaaSTeamProvider({ children }: { children: ReactNode }) {
|
||||
// Subscribe to auth changes
|
||||
useEffect(() => {
|
||||
const unsubscribe = authService.subscribeToAuth((status) => {
|
||||
setIsAuthenticated(status === 'authenticated');
|
||||
setIsAuthenticated(status === "authenticated");
|
||||
});
|
||||
return unsubscribe;
|
||||
}, []);
|
||||
@@ -113,16 +113,16 @@ export function SaaSTeamProvider({ children }: { children: ReactNode }) {
|
||||
const fetchMyTeams = useCallback(async () => {
|
||||
// CRITICAL: Only fetch if in SaaS mode and authenticated
|
||||
if (!isSaasMode || !isAuthenticated) {
|
||||
console.log('[SaaSTeamContext] Skipping team fetch - not in SaaS mode or not authenticated');
|
||||
console.log("[SaaSTeamContext] Skipping team fetch - not in SaaS mode or not authenticated");
|
||||
return null;
|
||||
}
|
||||
|
||||
try {
|
||||
const response = await apiClient.get<Team[]>('/api/v1/team/my', { suppressErrorToast: true });
|
||||
const response = await apiClient.get<Team[]>("/api/v1/team/my", { suppressErrorToast: true });
|
||||
setTeams(response.data);
|
||||
|
||||
const activeTeam = response.data[0];
|
||||
console.log('[SaaSTeamContext] Current team set:', {
|
||||
console.log("[SaaSTeamContext] Current team set:", {
|
||||
teamId: activeTeam?.teamId,
|
||||
name: activeTeam?.name,
|
||||
isPersonal: activeTeam?.isPersonal,
|
||||
@@ -131,39 +131,47 @@ export function SaaSTeamProvider({ children }: { children: ReactNode }) {
|
||||
setCurrentTeam(activeTeam || null);
|
||||
return activeTeam || null;
|
||||
} catch (error) {
|
||||
console.error('[SaaSTeamContext] Failed to fetch teams:', error);
|
||||
console.error("[SaaSTeamContext] Failed to fetch teams:", error);
|
||||
return null;
|
||||
}
|
||||
}, [isSaasMode, isAuthenticated]);
|
||||
|
||||
const fetchTeamMembers = useCallback(async (teamId: number) => {
|
||||
// CRITICAL: Only fetch if in SaaS mode and authenticated
|
||||
if (!isSaasMode || !isAuthenticated) {
|
||||
console.log('[SaaSTeamContext] Skipping members fetch - not in SaaS mode or not authenticated');
|
||||
return;
|
||||
}
|
||||
const fetchTeamMembers = useCallback(
|
||||
async (teamId: number) => {
|
||||
// CRITICAL: Only fetch if in SaaS mode and authenticated
|
||||
if (!isSaasMode || !isAuthenticated) {
|
||||
console.log("[SaaSTeamContext] Skipping members fetch - not in SaaS mode or not authenticated");
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const response = await apiClient.get<TeamMember[]>(`/api/v1/team/${teamId}/members`, { suppressErrorToast: true });
|
||||
setTeamMembers(response.data);
|
||||
} catch (error) {
|
||||
console.error('[SaaSTeamContext] Failed to fetch team members:', error);
|
||||
}
|
||||
}, [isSaasMode, isAuthenticated]);
|
||||
try {
|
||||
const response = await apiClient.get<TeamMember[]>(`/api/v1/team/${teamId}/members`, { suppressErrorToast: true });
|
||||
setTeamMembers(response.data);
|
||||
} catch (error) {
|
||||
console.error("[SaaSTeamContext] Failed to fetch team members:", error);
|
||||
}
|
||||
},
|
||||
[isSaasMode, isAuthenticated],
|
||||
);
|
||||
|
||||
const fetchTeamInvitations = useCallback(async (teamId?: number) => {
|
||||
// CRITICAL: Only fetch if in SaaS mode and authenticated
|
||||
if (!isSaasMode || !isAuthenticated || !teamId) {
|
||||
return;
|
||||
}
|
||||
const fetchTeamInvitations = useCallback(
|
||||
async (teamId?: number) => {
|
||||
// CRITICAL: Only fetch if in SaaS mode and authenticated
|
||||
if (!isSaasMode || !isAuthenticated || !teamId) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const response = await apiClient.get<TeamInvitation[]>(`/api/v1/team/${teamId}/invitations`, { suppressErrorToast: true });
|
||||
setTeamInvitations(response.data);
|
||||
} catch (error) {
|
||||
console.error('[SaaSTeamContext] Failed to fetch team invitations:', error);
|
||||
}
|
||||
}, [isSaasMode, isAuthenticated]);
|
||||
try {
|
||||
const response = await apiClient.get<TeamInvitation[]>(`/api/v1/team/${teamId}/invitations`, {
|
||||
suppressErrorToast: true,
|
||||
});
|
||||
setTeamInvitations(response.data);
|
||||
} catch (error) {
|
||||
console.error("[SaaSTeamContext] Failed to fetch team invitations:", error);
|
||||
}
|
||||
},
|
||||
[isSaasMode, isAuthenticated],
|
||||
);
|
||||
|
||||
const fetchReceivedInvitations = useCallback(async () => {
|
||||
// CRITICAL: Only fetch if in SaaS mode and authenticated
|
||||
@@ -171,14 +179,14 @@ export function SaaSTeamProvider({ children }: { children: ReactNode }) {
|
||||
return;
|
||||
}
|
||||
|
||||
console.log('[SaaSTeamContext] Fetching received team invitations');
|
||||
console.log("[SaaSTeamContext] Fetching received team invitations");
|
||||
|
||||
try {
|
||||
const response = await apiClient.get<TeamInvitation[]>('/api/v1/team/invitations/pending', { suppressErrorToast: true });
|
||||
console.log('[SaaSTeamContext] Received invitations response:', response.data);
|
||||
const response = await apiClient.get<TeamInvitation[]>("/api/v1/team/invitations/pending", { suppressErrorToast: true });
|
||||
console.log("[SaaSTeamContext] Received invitations response:", response.data);
|
||||
setReceivedInvitations(response.data);
|
||||
} catch (error) {
|
||||
console.error('[SaaSTeamContext] Failed to fetch received invitations:', error);
|
||||
console.error("[SaaSTeamContext] Failed to fetch received invitations:", error);
|
||||
}
|
||||
}, [isSaasMode, isAuthenticated]);
|
||||
|
||||
@@ -214,18 +222,18 @@ export function SaaSTeamProvider({ children }: { children: ReactNode }) {
|
||||
}, [currentTeam, isSaasMode, isAuthenticated, fetchTeamMembers, fetchTeamInvitations]);
|
||||
|
||||
const inviteUser = async (email: string) => {
|
||||
if (!currentTeam) throw new Error('No current team');
|
||||
if (!isSaasMode) throw new Error('Not in SaaS mode');
|
||||
if (!currentTeam) throw new Error("No current team");
|
||||
if (!isSaasMode) throw new Error("Not in SaaS mode");
|
||||
|
||||
await apiClient.post('/api/v1/team/invite', {
|
||||
await apiClient.post("/api/v1/team/invite", {
|
||||
teamId: currentTeam.teamId,
|
||||
email
|
||||
email,
|
||||
});
|
||||
await fetchTeamInvitations(currentTeam.teamId);
|
||||
};
|
||||
|
||||
const acceptInvitation = async (token: string) => {
|
||||
if (!isSaasMode) throw new Error('Not in SaaS mode');
|
||||
if (!isSaasMode) throw new Error("Not in SaaS mode");
|
||||
|
||||
await apiClient.post(`/api/v1/team/invitations/${token}/accept`);
|
||||
await fetchReceivedInvitations();
|
||||
@@ -234,14 +242,14 @@ export function SaaSTeamProvider({ children }: { children: ReactNode }) {
|
||||
};
|
||||
|
||||
const rejectInvitation = async (token: string) => {
|
||||
if (!isSaasMode) throw new Error('Not in SaaS mode');
|
||||
if (!isSaasMode) throw new Error("Not in SaaS mode");
|
||||
|
||||
await apiClient.post(`/api/v1/team/invitations/${token}/reject`);
|
||||
await fetchReceivedInvitations();
|
||||
};
|
||||
|
||||
const cancelInvitation = async (invitationId: number) => {
|
||||
if (!isSaasMode) throw new Error('Not in SaaS mode');
|
||||
if (!isSaasMode) throw new Error("Not in SaaS mode");
|
||||
|
||||
await apiClient.delete(`/api/v1/team/invitations/${invitationId}`);
|
||||
if (currentTeam) {
|
||||
@@ -250,8 +258,8 @@ export function SaaSTeamProvider({ children }: { children: ReactNode }) {
|
||||
};
|
||||
|
||||
const removeMember = async (memberId: number) => {
|
||||
if (!currentTeam) throw new Error('No current team');
|
||||
if (!isSaasMode) throw new Error('Not in SaaS mode');
|
||||
if (!currentTeam) throw new Error("No current team");
|
||||
if (!isSaasMode) throw new Error("Not in SaaS mode");
|
||||
|
||||
await apiClient.delete(`/api/v1/team/${currentTeam.teamId}/members/${memberId}`);
|
||||
await refreshTeams();
|
||||
@@ -259,8 +267,8 @@ export function SaaSTeamProvider({ children }: { children: ReactNode }) {
|
||||
};
|
||||
|
||||
const leaveTeam = async () => {
|
||||
if (!currentTeam) throw new Error('No current team');
|
||||
if (!isSaasMode) throw new Error('Not in SaaS mode');
|
||||
if (!currentTeam) throw new Error("No current team");
|
||||
if (!isSaasMode) throw new Error("Not in SaaS mode");
|
||||
|
||||
await apiClient.post(`/api/v1/team/${currentTeam.teamId}/leave`);
|
||||
await refreshTeams();
|
||||
@@ -269,7 +277,7 @@ export function SaaSTeamProvider({ children }: { children: ReactNode }) {
|
||||
|
||||
const refreshTeams = useCallback(async () => {
|
||||
if (!isSaasMode || !isAuthenticated) {
|
||||
console.log('[SaaSTeamContext] Skipping refresh - not in SaaS mode or not authenticated');
|
||||
console.log("[SaaSTeamContext] Skipping refresh - not in SaaS mode or not authenticated");
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -288,23 +296,25 @@ export function SaaSTeamProvider({ children }: { children: ReactNode }) {
|
||||
const isPersonalTeam = currentTeam?.isPersonal ?? true;
|
||||
|
||||
return (
|
||||
<SaaSTeamContext.Provider value={{
|
||||
currentTeam,
|
||||
teams,
|
||||
teamMembers,
|
||||
teamInvitations,
|
||||
receivedInvitations,
|
||||
isTeamLeader,
|
||||
isPersonalTeam,
|
||||
loading,
|
||||
inviteUser,
|
||||
acceptInvitation,
|
||||
rejectInvitation,
|
||||
cancelInvitation,
|
||||
removeMember,
|
||||
leaveTeam,
|
||||
refreshTeams
|
||||
}}>
|
||||
<SaaSTeamContext.Provider
|
||||
value={{
|
||||
currentTeam,
|
||||
teams,
|
||||
teamMembers,
|
||||
teamInvitations,
|
||||
receivedInvitations,
|
||||
isTeamLeader,
|
||||
isPersonalTeam,
|
||||
loading,
|
||||
inviteUser,
|
||||
acceptInvitation,
|
||||
rejectInvitation,
|
||||
cancelInvitation,
|
||||
removeMember,
|
||||
leaveTeam,
|
||||
refreshTeams,
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
</SaaSTeamContext.Provider>
|
||||
);
|
||||
@@ -313,7 +323,7 @@ export function SaaSTeamProvider({ children }: { children: ReactNode }) {
|
||||
export function useSaaSTeam() {
|
||||
const context = useContext(SaaSTeamContext);
|
||||
if (context === undefined) {
|
||||
throw new Error('useSaaSTeam must be used within a SaaSTeamProvider');
|
||||
throw new Error("useSaaSTeam must be used within a SaaSTeamProvider");
|
||||
}
|
||||
return context;
|
||||
}
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import { createContext, useContext, useEffect, useState, ReactNode, useCallback, useRef, useMemo } from 'react';
|
||||
import { saasBillingService, BillingStatus, PlanPrice } from '@app/services/saasBillingService';
|
||||
import { authService } from '@app/services/authService';
|
||||
import { connectionModeService } from '@app/services/connectionModeService';
|
||||
import { useSaaSTeam } from '@app/contexts/SaaSTeamContext';
|
||||
import type { TierLevel } from '@app/types/billing';
|
||||
import { createContext, useContext, useEffect, useState, ReactNode, useCallback, useRef, useMemo } from "react";
|
||||
import { saasBillingService, BillingStatus, PlanPrice } from "@app/services/saasBillingService";
|
||||
import { authService } from "@app/services/authService";
|
||||
import { connectionModeService } from "@app/services/connectionModeService";
|
||||
import { useSaaSTeam } from "@app/contexts/SaaSTeamContext";
|
||||
import type { TierLevel } from "@app/types/billing";
|
||||
|
||||
// How long plan pricing is considered fresh. Lives at module level so both the
|
||||
// provider and usePlanPricing (the consumer hook) can reference it.
|
||||
@@ -26,8 +26,8 @@ interface SaasBillingContextType {
|
||||
// Billing Status
|
||||
billingStatus: BillingStatus | null;
|
||||
tier: TierLevel;
|
||||
subscription: BillingStatus['subscription'];
|
||||
usage: BillingStatus['meterUsage'];
|
||||
subscription: BillingStatus["subscription"];
|
||||
usage: BillingStatus["meterUsage"];
|
||||
isTrialing: boolean;
|
||||
trialDaysRemaining?: number;
|
||||
price?: number;
|
||||
@@ -57,7 +57,7 @@ interface SaasBillingContextType {
|
||||
|
||||
const SaasBillingContext = createContext<SaasBillingContextType>({
|
||||
billingStatus: null,
|
||||
tier: 'free',
|
||||
tier: "free",
|
||||
subscription: null,
|
||||
usage: null,
|
||||
isTrialing: false,
|
||||
@@ -84,7 +84,7 @@ export function SaasBillingProvider({ children }: { children: ReactNode }) {
|
||||
const [plans, setPlans] = useState<Map<string, PlanPrice>>(new Map());
|
||||
const [plansLoading, setPlansLoading] = useState(false);
|
||||
const [plansError, setPlansError] = useState<string | null>(null);
|
||||
const [loading, setLoading] = useState(false); // Start false (lazy load)
|
||||
const [loading, setLoading] = useState(false); // Start false (lazy load)
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
// lastFetchTimeRef is the source of truth for cache logic (always current, no stale closure).
|
||||
// lastFetchTimeValue mirrors it as state purely to drive re-renders and expose through context.
|
||||
@@ -113,7 +113,7 @@ export function SaasBillingProvider({ children }: { children: ReactNode }) {
|
||||
const checkAccess = async () => {
|
||||
const mode = await connectionModeService.getCurrentMode();
|
||||
const auth = await authService.isAuthenticated();
|
||||
setIsSaasMode(mode === 'saas');
|
||||
setIsSaasMode(mode === "saas");
|
||||
setIsAuthenticated(auth);
|
||||
};
|
||||
|
||||
@@ -127,7 +127,7 @@ export function SaasBillingProvider({ children }: { children: ReactNode }) {
|
||||
// Subscribe to auth changes
|
||||
useEffect(() => {
|
||||
const unsubscribe = authService.subscribeToAuth((status) => {
|
||||
setIsAuthenticated(status === 'authenticated');
|
||||
setIsAuthenticated(status === "authenticated");
|
||||
});
|
||||
return unsubscribe;
|
||||
}, []);
|
||||
@@ -151,7 +151,7 @@ export function SaasBillingProvider({ children }: { children: ReactNode }) {
|
||||
|
||||
// Cache check: Skip if fresh data exists (<5 min old)
|
||||
const now = Date.now();
|
||||
if (billingStatusRef.current && lastFetchTimeRef.current && (now - lastFetchTimeRef.current) < 300000) {
|
||||
if (billingStatusRef.current && lastFetchTimeRef.current && now - lastFetchTimeRef.current < 300000) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -168,8 +168,8 @@ export function SaasBillingProvider({ children }: { children: ReactNode }) {
|
||||
setLastFetchTimeValue(now);
|
||||
setError(null);
|
||||
} catch (err) {
|
||||
console.error('[SaasBillingContext] Failed to fetch billing:', err);
|
||||
setError(err instanceof Error ? err.message : 'Failed to fetch billing');
|
||||
console.error("[SaasBillingContext] Failed to fetch billing:", err);
|
||||
setError(err instanceof Error ? err.message : "Failed to fetch billing");
|
||||
// Don't clear billing status on error (preserve existing data)
|
||||
} finally {
|
||||
setLoading(false);
|
||||
@@ -192,14 +192,14 @@ export function SaasBillingProvider({ children }: { children: ReactNode }) {
|
||||
setPlansError(null);
|
||||
|
||||
try {
|
||||
const priceMap = await saasBillingService.getAvailablePlans('usd');
|
||||
const priceMap = await saasBillingService.getAvailablePlans("usd");
|
||||
setPlans(priceMap);
|
||||
const now = Date.now();
|
||||
plansLastFetchTimeRef.current = now;
|
||||
setPlansLastFetchTimeValue(now);
|
||||
} catch (err) {
|
||||
console.error('[SaasBillingContext] Failed to fetch plans:', err);
|
||||
setPlansError(err instanceof Error ? err.message : 'Failed to fetch plans');
|
||||
console.error("[SaasBillingContext] Failed to fetch plans:", err);
|
||||
setPlansError(err instanceof Error ? err.message : "Failed to fetch plans");
|
||||
} finally {
|
||||
plansFetchInProgressRef.current = false;
|
||||
setPlansLoading(false);
|
||||
@@ -272,7 +272,7 @@ export function SaasBillingProvider({ children }: { children: ReactNode }) {
|
||||
const customEvent = e as CustomEvent<{ creditsRemaining: number }>;
|
||||
const newBalance = customEvent.detail?.creditsRemaining;
|
||||
|
||||
if (typeof newBalance === 'number' && billingStatus) {
|
||||
if (typeof newBalance === "number" && billingStatus) {
|
||||
// Update credit balance in billing status without full refresh
|
||||
const updated = { ...billingStatus, creditBalance: newBalance };
|
||||
billingStatusRef.current = updated;
|
||||
@@ -280,68 +280,76 @@ export function SaasBillingProvider({ children }: { children: ReactNode }) {
|
||||
|
||||
// Dispatch exhausted event if credits hit 0
|
||||
if (newBalance <= 0 && (billingStatus.creditBalance ?? 0) > 0) {
|
||||
window.dispatchEvent(new CustomEvent('credits:exhausted', {
|
||||
detail: { previousBalance: billingStatus.creditBalance ?? 0, currentBalance: newBalance }
|
||||
}));
|
||||
window.dispatchEvent(
|
||||
new CustomEvent("credits:exhausted", {
|
||||
detail: { previousBalance: billingStatus.creditBalance ?? 0, currentBalance: newBalance },
|
||||
}),
|
||||
);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
window.addEventListener('credits:updated', handleCreditsUpdated);
|
||||
window.addEventListener("credits:updated", handleCreditsUpdated);
|
||||
return () => {
|
||||
window.removeEventListener('credits:updated', handleCreditsUpdated);
|
||||
window.removeEventListener("credits:updated", handleCreditsUpdated);
|
||||
};
|
||||
}, [billingStatus]);
|
||||
|
||||
const contextValue = useMemo(() => ({
|
||||
billingStatus,
|
||||
tier: isManagedTeamMember ? 'team' : (billingStatus?.tier ?? 'free'),
|
||||
subscription: billingStatus?.subscription ?? null,
|
||||
usage: billingStatus?.meterUsage ?? null,
|
||||
isTrialing: billingStatus?.isTrialing ?? false,
|
||||
trialDaysRemaining: billingStatus?.trialDaysRemaining,
|
||||
price: plans.get('team')?.price,
|
||||
currency: plans.get('team')?.currency,
|
||||
creditBalance: billingStatus?.creditBalance ?? 0,
|
||||
plans,
|
||||
plansLoading,
|
||||
plansError,
|
||||
plansLastFetchTime: plansLastFetchTimeValue,
|
||||
isManagedTeamMember,
|
||||
loading: loading || teamLoading,
|
||||
error,
|
||||
lastFetchTime: lastFetchTimeValue,
|
||||
refreshBilling,
|
||||
refreshCredits: refreshBilling,
|
||||
refreshPlans,
|
||||
openBillingPortal,
|
||||
}), [
|
||||
billingStatus, isManagedTeamMember, plans, plansLoading, plansError,
|
||||
plansLastFetchTimeValue, loading, teamLoading, error, lastFetchTimeValue,
|
||||
refreshBilling, refreshPlans, openBillingPortal,
|
||||
]);
|
||||
|
||||
return (
|
||||
<SaasBillingContext.Provider value={contextValue}>
|
||||
{children}
|
||||
</SaasBillingContext.Provider>
|
||||
const contextValue = useMemo(
|
||||
() => ({
|
||||
billingStatus,
|
||||
tier: isManagedTeamMember ? "team" : (billingStatus?.tier ?? "free"),
|
||||
subscription: billingStatus?.subscription ?? null,
|
||||
usage: billingStatus?.meterUsage ?? null,
|
||||
isTrialing: billingStatus?.isTrialing ?? false,
|
||||
trialDaysRemaining: billingStatus?.trialDaysRemaining,
|
||||
price: plans.get("team")?.price,
|
||||
currency: plans.get("team")?.currency,
|
||||
creditBalance: billingStatus?.creditBalance ?? 0,
|
||||
plans,
|
||||
plansLoading,
|
||||
plansError,
|
||||
plansLastFetchTime: plansLastFetchTimeValue,
|
||||
isManagedTeamMember,
|
||||
loading: loading || teamLoading,
|
||||
error,
|
||||
lastFetchTime: lastFetchTimeValue,
|
||||
refreshBilling,
|
||||
refreshCredits: refreshBilling,
|
||||
refreshPlans,
|
||||
openBillingPortal,
|
||||
}),
|
||||
[
|
||||
billingStatus,
|
||||
isManagedTeamMember,
|
||||
plans,
|
||||
plansLoading,
|
||||
plansError,
|
||||
plansLastFetchTimeValue,
|
||||
loading,
|
||||
teamLoading,
|
||||
error,
|
||||
lastFetchTimeValue,
|
||||
refreshBilling,
|
||||
refreshPlans,
|
||||
openBillingPortal,
|
||||
],
|
||||
);
|
||||
|
||||
return <SaasBillingContext.Provider value={contextValue}>{children}</SaasBillingContext.Provider>;
|
||||
}
|
||||
|
||||
export function useSaaSBilling() {
|
||||
const context = useContext(SaasBillingContext);
|
||||
if (context === undefined) {
|
||||
throw new Error('useSaaSBilling must be used within SaasBillingProvider');
|
||||
throw new Error("useSaaSBilling must be used within SaasBillingProvider");
|
||||
}
|
||||
|
||||
// Lazy fetch: Trigger fetch on first access (after team context loads)
|
||||
// Note: context.loading includes teamLoading, so this waits for team to load
|
||||
useEffect(() => {
|
||||
const needsFetch =
|
||||
context.billingStatus === null &&
|
||||
context.lastFetchTime === null &&
|
||||
!context.loading &&
|
||||
!context.isManagedTeamMember; // Managed members don't need billing data
|
||||
context.billingStatus === null && context.lastFetchTime === null && !context.loading && !context.isManagedTeamMember; // Managed members don't need billing data
|
||||
|
||||
if (needsFetch) {
|
||||
context.refreshBilling();
|
||||
@@ -360,9 +368,7 @@ export function usePlanPricing() {
|
||||
const { plans, plansLoading, plansError, plansLastFetchTime, refreshPlans } = useContext(SaasBillingContext);
|
||||
|
||||
useEffect(() => {
|
||||
const isFresh =
|
||||
plansLastFetchTime !== null &&
|
||||
Date.now() - plansLastFetchTime < PLANS_CACHE_TTL_MS;
|
||||
const isFresh = plansLastFetchTime !== null && Date.now() - plansLastFetchTime < PLANS_CACHE_TTL_MS;
|
||||
|
||||
if (!isFresh) {
|
||||
refreshPlans();
|
||||
|
||||
Reference in New Issue
Block a user