Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
4 changes: 3 additions & 1 deletion app/components/Modal.tsx
Original file line number Diff line number Diff line change
@@ -1,11 +1,13 @@
import { ReactNode } from "react";

import CheckIcon from "@/app/components/icons/CheckIcon";
import SpinnerIcon from "@/app/components/icons/SpinnerIcon";
import XIcon from "@/app/components/icons/XIcon";

interface ModalProps {
title: string;
type: "loading" | "success" | "error";
message: string;
message: ReactNode;
onClose?: () => void;
}

Expand Down
33 changes: 30 additions & 3 deletions app/dashboard/coverage/handlers/handle-create-prs.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,16 @@
import { getCreditBalance } from "@/app/actions/supabase/owners/get-credit-balance";
import { PRResponse } from "@/app/dashboard/coverage/types";
import { CREDIT_PRICING } from "@/config/pricing";
import { Installation } from "@/types/github";
import { Tables } from "@/types/supabase";
import { fetchWithTiming } from "@/utils/fetch";

export type InsufficientCreditsInfo = {
balance: number;
required: number;
numPRs: number;
};

/**
* Handle creating GitHub PRs for selected coverage items
*/
Expand All @@ -12,26 +21,44 @@ export async function handleCreatePRs({
currentRepoName,
accessToken,
hasLabel = false,
ownerId,
installations,
setCoverageData,
setSelectedRows,
setActionSuccess,
setError,
setIsCreatingPRs,
setInsufficientCredits,
}: {
selectedRows: number[];
coverageData: Tables<"coverages">[];
currentOwnerName: string;
currentRepoName: string;
accessToken: string;
hasLabel?: boolean;
ownerId: number;
installations: Installation[] | undefined;
setCoverageData: (fn: (prev: Tables<"coverages">[]) => Tables<"coverages">[]) => void;
setSelectedRows: (rows: number[]) => void;
setActionSuccess: (success: boolean) => void;
setError: (error: string) => void;
setIsCreatingPRs: (loading: boolean) => void;
setInsufficientCredits: (info: InsufficientCreditsInfo | null) => void;
}) {
if (selectedRows.length === 0) return;

// Pre-flight credit check: skip for subscription users
const currentInstallation = installations?.find((inst) => inst.owner_id === ownerId);
if (!currentInstallation?.hasActiveSubscription) {
const balance = await getCreditBalance(ownerId);
const required = CREDIT_PRICING.PER_PR.AMOUNT_USD * selectedRows.length;
if (balance < required) {
console.log("Insufficient credits:", { balance, required, numPRs: selectedRows.length });
setInsufficientCredits({ balance, required, numPRs: selectedRows.length });
return;
}
}

setIsCreatingPRs(true);
try {
const selectedCoverages = coverageData.filter((item) => selectedRows.includes(item.id));
Expand All @@ -48,7 +75,7 @@ export async function handleCreatePRs({
accessToken,
hasLabel,
}),
}
},
);

// Update local state with PR URLs
Expand All @@ -57,7 +84,7 @@ export async function handleCreatePRs({
const pr = prs.find((p) => p.coverageId === item.id);
if (pr) return { ...item, github_issue_url: pr.prUrl };
return item;
})
}),
);

setSelectedRows([]);
Expand All @@ -67,7 +94,7 @@ export async function handleCreatePRs({
setError(
typeof error === "object" && error !== null && "message" in error
? String(error.message)
: "Failed to create PRs"
: "Failed to create PRs",
);
} finally {
setIsCreatingPRs(false);
Expand Down
31 changes: 29 additions & 2 deletions app/dashboard/coverage/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ import LoadingSpinner from "@/app/components/LoadingSpinner";
import Modal from "@/app/components/Modal";
import Toast from "@/app/components/Toast";
import RepositorySelector from "@/app/settings/components/RepositorySelector";
import { CREDIT_PRICING } from "@/config/pricing";
import { RELATIVE_URLS } from "@/config/urls";
import { STORAGE_KEYS } from "@/lib/constants";
import { safeLocalStorage } from "@/lib/local-storage";
Expand All @@ -37,7 +38,7 @@ import {
} from "./constants/filter-options";
import { SYNC_MESSAGES } from "./constants/sync-messages";
import { fetchCoverageData } from "./handlers/fetch-coverage-data";
import { handleCreatePRs } from "./handlers/handle-create-prs";
import { handleCreatePRs, InsufficientCreditsInfo } from "./handlers/handle-create-prs";
import { handleSelectAll } from "./handlers/handle-select-all";
import { handleSelectRow } from "./handlers/handle-select-row";
import { handleSort } from "./handlers/handle-sort";
Expand All @@ -55,6 +56,7 @@ export default function CoveragePage() {
organizations,
accessToken,
currentInstallationId,
installations,
userId,
userLogin,
userName,
Expand Down Expand Up @@ -96,6 +98,9 @@ export default function CoveragePage() {
const [isTogglingExclusion, setIsTogglingExclusion] = useState(false);
const [isSettingUpWorkflow, setIsSettingUpWorkflow] = useState(false);
const [showSetupModal, setShowSetupModal] = useState(false);
const [insufficientCredits, setInsufficientCredits] = useState<InsufficientCreditsInfo | null>(
null,
);

// Load sort settings from localStorage
useEffect(() => {
Expand Down Expand Up @@ -383,7 +388,7 @@ export default function CoveragePage() {
selectedRows={selectedRows}
isCreatingPRs={isCreatingPRs}
onCreatePRs={(hasLabel) => {
if (!currentOwnerName || !currentRepoName || !accessToken) {
if (!currentOwnerName || !currentRepoName || !accessToken || !currentOwnerId) {
setError("Missing required repository information");
return;
}
Expand All @@ -395,11 +400,14 @@ export default function CoveragePage() {
currentRepoName,
accessToken,
hasLabel,
ownerId: currentOwnerId,
installations,
setCoverageData,
setSelectedRows,
setActionSuccess,
setError,
setIsCreatingPRs,
setInsufficientCredits,
});
}}
onToggleExclusion={handleToggleExclusion}
Expand Down Expand Up @@ -487,6 +495,25 @@ export default function CoveragePage() {
/>
)}

{insufficientCredits && (
<Modal
title="Insufficient Credits"
type="error"
message={
<>
Your balance is ${insufficientCredits.balance.toFixed(2)}, but creating{" "}
{insufficientCredits.numPRs} PR{insufficientCredits.numPRs === 1 ? "" : "s"} requires
${insufficientCredits.required.toFixed(2)} (${CREDIT_PRICING.PER_PR.AMOUNT_USD}/PR).{" "}
<a href="/dashboard/credits" className="text-pink-600 hover:underline font-medium">
Purchase credits
</a>{" "}
to continue.
</>
}
onClose={() => setInsufficientCredits(null)}
/>
)}

{/* If no data exists: show sync status or last sync time */}
{!isLoadingDB && coverageData.length === 0 && gitHubSyncStatus && (
<Modal
Expand Down
Loading