From e4d10aae3670fcacd1784a1930e3a52750d298ba Mon Sep 17 00:00:00 2001
From: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
Date: Wed, 25 Feb 2026 17:48:29 +0000
Subject: [PATCH 09/15] fix: prevent premature activeTab reset before
billingStatus loads
Co-Authored-By: unknown <>
---
frontend/src/components/settings/SettingsPage.tsx | 10 +++++++++-
1 file changed, 9 insertions(+), 1 deletion(-)
diff --git a/frontend/src/components/settings/SettingsPage.tsx b/frontend/src/components/settings/SettingsPage.tsx
index 60c0b69d..19bd1e17 100644
--- a/frontend/src/components/settings/SettingsPage.tsx
+++ b/frontend/src/components/settings/SettingsPage.tsx
@@ -164,12 +164,20 @@ export function SettingsPage({ initialTab, creditsSuccess }: SettingsPageProps)
});
// Ensure activeTab is always a visible tab (prevent showing hidden tab content)
+ // Also re-apply initialTab when it becomes visible (e.g., after billingStatus loads)
useEffect(() => {
+ if (initialTab && isValidTab(initialTab)) {
+ const isInitialTabNowVisible = visibleTabs.some((tab) => tab.id === initialTab);
+ if (isInitialTabNowVisible) {
+ setActiveTab(initialTab);
+ return;
+ }
+ }
const isTabVisible = visibleTabs.some((tab) => tab.id === activeTab);
if (!isTabVisible && visibleTabs.length > 0) {
setActiveTab(visibleTabs[0].id);
}
- }, [activeTab, visibleTabs]);
+ }, [activeTab, visibleTabs, initialTab]);
return (
From d8fed73237eb7b46cbe2abfae0c79ac7e09481b0 Mon Sep 17 00:00:00 2001
From: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
Date: Wed, 25 Feb 2026 18:51:16 +0000
Subject: [PATCH 10/15] perf: memoize visibleTabs to prevent useEffect firing
every render
Co-Authored-By: unknown <>
---
.../src/components/settings/SettingsPage.tsx | 18 +++++++++++-------
1 file changed, 11 insertions(+), 7 deletions(-)
diff --git a/frontend/src/components/settings/SettingsPage.tsx b/frontend/src/components/settings/SettingsPage.tsx
index 19bd1e17..7f0a9ff5 100644
--- a/frontend/src/components/settings/SettingsPage.tsx
+++ b/frontend/src/components/settings/SettingsPage.tsx
@@ -1,4 +1,4 @@
-import { useState, useEffect, useCallback } from "react";
+import { useState, useEffect, useCallback, useMemo } from "react";
import {
User,
CreditCard,
@@ -156,12 +156,16 @@ export function SettingsPage({ initialTab, creditsSuccess }: SettingsPageProps)
}
}
- // Filter tabs based on user's plan
- const visibleTabs = TAB_CONFIG.filter((tab) => {
- if (tab.requiresTeam && !isTeamPlan) return false;
- if (tab.requiresApiAccess && !showApiManagement) return false;
- return true;
- });
+ // Filter tabs based on user's plan (memoized to prevent useEffect firing every render)
+ const visibleTabs = useMemo(
+ () =>
+ TAB_CONFIG.filter((tab) => {
+ if (tab.requiresTeam && !isTeamPlan) return false;
+ if (tab.requiresApiAccess && !showApiManagement) return false;
+ return true;
+ }),
+ [isTeamPlan, showApiManagement]
+ );
// Ensure activeTab is always a visible tab (prevent showing hidden tab content)
// Also re-apply initialTab when it becomes visible (e.g., after billingStatus loads)
From db77bcd35bfbd0f92d4e4e5b672aa55cf8aa8a92 Mon Sep 17 00:00:00 2001
From: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
Date: Wed, 25 Feb 2026 22:06:15 +0000
Subject: [PATCH 11/15] fix: use isTauri() for subscription portal link
handling (match master pattern)
Co-Authored-By: unknown <>
---
.../src/components/settings/SubscriptionSection.tsx | 13 ++++++++++---
1 file changed, 10 insertions(+), 3 deletions(-)
diff --git a/frontend/src/components/settings/SubscriptionSection.tsx b/frontend/src/components/settings/SubscriptionSection.tsx
index 5f3bdce9..ae8109d9 100644
--- a/frontend/src/components/settings/SubscriptionSection.tsx
+++ b/frontend/src/components/settings/SubscriptionSection.tsx
@@ -5,7 +5,7 @@ import { Badge } from "@/components/ui/badge";
import { ArrowUpCircle, Loader2, ExternalLink } from "lucide-react";
import { useLocalState } from "@/state/useLocalState";
import { getBillingService } from "@/billing/billingService";
-import { isMobile } from "@/utils/platform";
+import { isMobile, isTauri } from "@/utils/platform";
import { formatResetDate } from "@/utils/dateFormat";
export function SubscriptionSection() {
@@ -29,18 +29,25 @@ export function SubscriptionSection() {
const billingService = getBillingService();
const url = await billingService.getPortalUrl();
- if (isMobile()) {
+ // Use external browser for all Tauri platforms (mobile and desktop)
+ if (isTauri()) {
const { invoke } = await import("@tauri-apps/api/core");
await invoke("plugin:opener|open_url", { url })
.then(() => console.log("[Billing] Opened portal URL"))
.catch((err: Error) => {
console.error("[Billing] Failed to open browser:", err);
- alert("Failed to open browser. Please try again.");
+ if (isMobile()) {
+ alert("Failed to open browser. Please try again.");
+ } else {
+ // Fallback to window.open on desktop
+ window.open(url, "_blank");
+ }
});
await new Promise((resolve) => setTimeout(resolve, 300));
return;
}
+ // Web flow
window.open(url, "_blank");
} catch (error) {
console.error("Error fetching portal URL:", error);
From 453613184b052c90971ae9d142cbbe0a815bba27 Mon Sep 17 00:00:00 2001
From: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
Date: Wed, 25 Feb 2026 22:11:27 +0000
Subject: [PATCH 12/15] chore: remove dead code - old AccountMenu,
AccountDialog, PreferencesDialog, and dialog wrappers
These components are no longer imported anywhere after the settings page refactor:
- AccountMenu.tsx (replaced by SettingsPage + SettingsButton)
- AccountDialog.tsx (replaced by ProfileSection)
- PreferencesDialog.tsx (replaced by inline preferences in ProfileSection)
- TeamManagementDialog.tsx (replaced by TeamManagementSection)
- ApiKeyManagementDialog.tsx (replaced by ApiManagementSection)
Co-Authored-By: unknown <>
---
frontend/src/components/AccountDialog.tsx | 182 -------
frontend/src/components/AccountMenu.tsx | 461 ------------------
frontend/src/components/PreferencesDialog.tsx | 148 ------
.../apikeys/ApiKeyManagementDialog.tsx | 22 -
.../components/team/TeamManagementDialog.tsx | 58 ---
5 files changed, 871 deletions(-)
delete mode 100644 frontend/src/components/AccountDialog.tsx
delete mode 100644 frontend/src/components/AccountMenu.tsx
delete mode 100644 frontend/src/components/PreferencesDialog.tsx
delete mode 100644 frontend/src/components/apikeys/ApiKeyManagementDialog.tsx
delete mode 100644 frontend/src/components/team/TeamManagementDialog.tsx
diff --git a/frontend/src/components/AccountDialog.tsx b/frontend/src/components/AccountDialog.tsx
deleted file mode 100644
index 152c07f3..00000000
--- a/frontend/src/components/AccountDialog.tsx
+++ /dev/null
@@ -1,182 +0,0 @@
-import { useState } from "react";
-import { Button } from "@/components/ui/button";
-import {
- DialogContent,
- DialogDescription,
- DialogFooter,
- DialogHeader,
- DialogTitle,
- DialogTrigger
-} from "@/components/ui/dialog";
-import {
- Select,
- SelectContent,
- SelectGroup,
- SelectItem,
- SelectLabel,
- SelectTrigger,
- SelectValue
-} from "@/components/ui/select";
-import { Input } from "@/components/ui/input";
-import { Label } from "@/components/ui/label";
-import { useOpenSecret } from "@opensecret/react";
-import { CheckCircle, XCircle, Trash } from "lucide-react";
-import { ChangePasswordDialog } from "./ChangePasswordDialog";
-import { useLocalState } from "@/state/useLocalState";
-import { DeleteAccountDialog } from "./DeleteAccountDialog";
-import { PreferencesDialog } from "./PreferencesDialog";
-
-export function AccountDialog() {
- const os = useOpenSecret();
- const [isChangePasswordOpen, setIsChangePasswordOpen] = useState(false);
- const [isDeleteAccountOpen, setIsDeleteAccountOpen] = useState(false);
- const [isPreferencesOpen, setIsPreferencesOpen] = useState(false);
- const [verificationStatus, setVerificationStatus] = useState<"unverified" | "pending">(
- "unverified"
- );
- const { billingStatus } = useLocalState();
-
- // Check user login method
- const isEmailUser = os.auth.user?.user.login_method === "email";
- const isGuestUser = os.auth.user?.user.login_method?.toLowerCase() === "guest";
-
- const handleResendVerification = async () => {
- try {
- await os.requestNewVerificationEmail();
- setVerificationStatus("pending");
- } catch (error) {
- console.error("Failed to resend verification email:", error);
- }
- };
-
- return (
- <>
-
-
- Update Your Account
- Change your email or upgrade your plan.
-
-
-
-
-
-
- {(isEmailUser || isGuestUser) && (
-
- )}
-
-
- >
- );
-}
diff --git a/frontend/src/components/AccountMenu.tsx b/frontend/src/components/AccountMenu.tsx
deleted file mode 100644
index 5f8c0e75..00000000
--- a/frontend/src/components/AccountMenu.tsx
+++ /dev/null
@@ -1,461 +0,0 @@
-import {
- LogOut,
- Trash,
- User,
- CreditCard,
- ArrowUpCircle,
- Mail,
- Users,
- AlertCircle,
- Key,
- Info,
- Shield,
- FileText,
- ChevronLeft
-} from "lucide-react";
-import { isMobile, isTauri, isIOS } from "@/utils/platform";
-
-import { Button } from "@/components/ui/button";
-import {
- DropdownMenu,
- DropdownMenuContent,
- DropdownMenuGroup,
- DropdownMenuItem,
- DropdownMenuLabel,
- DropdownMenuSeparator,
- DropdownMenuTrigger
-} from "@/components/ui/dropdown-menu";
-import { useOpenSecret } from "@opensecret/react";
-import { useNavigate, useRouter } from "@tanstack/react-router";
-import { Dialog, DialogTrigger } from "./ui/dialog";
-import { AccountDialog } from "./AccountDialog";
-import { CreditUsage } from "./CreditUsage";
-import { Badge } from "./ui/badge";
-
-import {
- AlertDialog,
- AlertDialogAction,
- AlertDialogCancel,
- AlertDialogContent,
- AlertDialogDescription,
- AlertDialogFooter,
- AlertDialogHeader,
- AlertDialogTitle,
- AlertDialogTrigger
-} from "@/components/ui/alert-dialog";
-import { useLocalState } from "@/state/useLocalState";
-import { useQueryClient, useQuery } from "@tanstack/react-query";
-import { Link } from "@tanstack/react-router";
-import { getBillingService } from "@/billing/billingService";
-import { useState } from "react";
-import type { TeamStatus } from "@/types/team";
-import { TeamManagementDialog } from "@/components/team/TeamManagementDialog";
-import { ApiKeyManagementDialog } from "@/components/apikeys/ApiKeyManagementDialog";
-import packageJson from "../../package.json";
-
-function ConfirmDeleteDialog() {
- const { clearHistory } = useLocalState();
- const os = useOpenSecret();
- const queryClient = useQueryClient();
- const navigate = useNavigate();
-
- async function handleDeleteHistory() {
- // 1. Delete archived chats (KV)
- try {
- await clearHistory();
- console.log("History (KV) cleared");
- } catch (error) {
- console.error("Error clearing history:", error);
- // Continue to delete server conversations even if this fails
- }
-
- // 2. Delete server conversations (API) if any exist
- try {
- // Check if we have any conversations to delete
- const conversations = await os.listConversations({ limit: 1 });
- if (conversations.data && conversations.data.length > 0) {
- await os.deleteConversations();
- console.log("Server conversations deleted");
- }
- } catch (e) {
- console.error("Error deleting conversations:", e);
- }
-
- // Always refresh UI and navigate home
- queryClient.invalidateQueries({ queryKey: ["chatHistory"] });
- queryClient.invalidateQueries({ queryKey: ["conversations"] });
- queryClient.invalidateQueries({ queryKey: ["archivedChats"] });
- navigate({ to: "/" });
- }
-
- return (
-
-
- Are you sure?
- This will delete your entire chat history.
-
-
- Cancel
- Delete
-
-
- );
-}
-
-export function AccountMenu() {
- const os = useOpenSecret();
- const router = useRouter();
- const { billingStatus } = useLocalState();
- const [isPortalLoading, setIsPortalLoading] = useState(false);
- const [isTeamDialogOpen, setIsTeamDialogOpen] = useState(false);
- const [isApiKeyDialogOpen, setIsApiKeyDialogOpen] = useState(false);
- const [showAboutMenu, setShowAboutMenu] = useState(false);
-
- const hasStripeAccount = billingStatus?.stripe_customer_id !== null;
- const productName = billingStatus?.product_name || "";
- const isPro = productName.toLowerCase().includes("pro");
- const isMax = productName.toLowerCase().includes("max");
- const isStarter = productName.toLowerCase().includes("starter");
- const isTeamPlan = productName.toLowerCase().includes("team");
- const showUpgrade = !isMax && !isTeamPlan;
- const showManage = (isPro || isMax || isStarter || isTeamPlan) && hasStripeAccount;
-
- // Fetch team status if user has team plan
- const { data: teamStatus } = useQuery
({
- queryKey: ["teamStatus"],
- queryFn: async () => {
- const billingService = getBillingService();
- return await billingService.getTeamStatus();
- },
- enabled: isTeamPlan && !!os.auth.user && !!billingStatus
- });
-
- // Fetch products with version check for iOS to determine API Management availability
- const isIOSPlatform = isIOS();
- const { data: products } = useQuery({
- queryKey: ["products-version-check", isIOSPlatform],
- queryFn: async () => {
- try {
- const billingService = getBillingService();
- // Send version for iOS builds (App Store restrictions)
- if (isIOSPlatform) {
- const version = `v${packageJson.version}`;
- return await billingService.getProducts(version);
- }
- return await billingService.getProducts();
- } catch (error) {
- console.error("Error fetching products for version check:", error);
- return null;
- }
- },
- enabled: isIOSPlatform
- });
-
- // Show alert badge if user has team plan but hasn't created team yet
- const showTeamSetupAlert =
- isTeamPlan && teamStatus?.has_team_subscription && !teamStatus?.team_created;
-
- // Determine if API Management should be shown
- // On desktop/web/Android: always show
- // On iOS only: only show if version is approved (at least one product is available)
- const showApiManagement = (() => {
- if (!isIOSPlatform) {
- return true; // Always show on desktop/web/Android
- }
- // On iOS, check if version is approved
- // If products is null/undefined, default to false (hide until we know)
- if (!products) {
- return false;
- }
- // Show if at least one product is available (meaning version is approved)
- return products.some((product) => product.is_available !== false);
- })();
-
- const handleManageSubscription = async () => {
- if (!hasStripeAccount) return;
-
- try {
- setIsPortalLoading(true);
- const billingService = getBillingService();
- const url = await billingService.getPortalUrl();
-
- // Check if we're on any Tauri platform (mobile or desktop)
- if (isTauri()) {
- console.log(
- "[Billing] Tauri platform detected, using opener plugin to launch external browser for portal"
- );
-
- const { invoke } = await import("@tauri-apps/api/core");
-
- // Use the opener plugin directly for all Tauri platforms
- await invoke("plugin:opener|open_url", { url })
- .then(() => {
- console.log("[Billing] Successfully opened portal URL in external browser");
- })
- .catch((err: Error) => {
- console.error("[Billing] Failed to open external browser:", err);
- if (isMobile()) {
- alert("Failed to open browser. Please try again.");
- } else {
- // Fallback to window.open on desktop
- window.open(url, "_blank");
- }
- });
-
- // Add a small delay to ensure the browser has time to open
- await new Promise((resolve) => setTimeout(resolve, 300));
- return;
- }
-
- // Default browser opening for web platforms
- window.open(url, "_blank");
- } catch (error) {
- console.error("Error fetching portal URL:", error);
- } finally {
- setIsPortalLoading(false);
- }
- };
-
- const handleOpenExternalUrl = async (url: string) => {
- try {
- // Check if we're on any Tauri platform (mobile or desktop)
- const isInTauri = isTauri();
-
- if (isInTauri) {
- const { invoke } = await import("@tauri-apps/api/core");
- await invoke("plugin:opener|open_url", { url })
- .then(() => {
- console.log("[External Link] Successfully opened URL with Tauri opener");
- })
- .catch((err: Error) => {
- console.error("[External Link] Failed to open with Tauri opener:", err);
- // Fallback to window.open on desktop (may work), alert on mobile
- if (isMobile()) {
- alert("Failed to open link. Please try again.");
- } else {
- window.open(url, "_blank", "noopener,noreferrer");
- }
- });
- } else {
- // Default browser opening for web platform
- window.open(url, "_blank", "noopener,noreferrer");
- }
- } catch (error) {
- console.error("Error opening external URL:", error);
- // Fallback to window.open
- window.open(url, "_blank", "noopener,noreferrer");
- }
- };
-
- async function signOut() {
- try {
- // Try to clear billing token first
- try {
- getBillingService().clearToken();
- } catch (error) {
- console.error("Error clearing billing token:", error);
- // Fallback to direct session storage removal if billing service fails
- sessionStorage.removeItem("maple_billing_token");
- }
-
- // Sign out from OpenSecret
- await os.signOut();
-
- // Navigate after everything is done
- await router.invalidate();
- await router.navigate({ to: "/" });
- } catch (error) {
- console.error("Error during sign out:", error);
- // Force reload as last resort
- window.location.href = "/";
- }
- }
-
- return (
-
-
-
- );
-}
diff --git a/frontend/src/components/PreferencesDialog.tsx b/frontend/src/components/PreferencesDialog.tsx
deleted file mode 100644
index 6fb42da2..00000000
--- a/frontend/src/components/PreferencesDialog.tsx
+++ /dev/null
@@ -1,148 +0,0 @@
-import { useState, useEffect } from "react";
-import { useOpenSecret } from "@opensecret/react";
-import { Button } from "@/components/ui/button";
-import {
- Dialog,
- DialogContent,
- DialogDescription,
- DialogFooter,
- DialogHeader,
- DialogTitle
-} from "@/components/ui/dialog";
-import { Label } from "@/components/ui/label";
-import { Textarea } from "@/components/ui/textarea";
-import { Alert, AlertDescription } from "@/components/ui/alert";
-
-interface PreferencesDialogProps {
- open: boolean;
- onOpenChange: (open: boolean) => void;
-}
-
-export function PreferencesDialog({ open, onOpenChange }: PreferencesDialogProps) {
- const os = useOpenSecret();
- const [prompt, setPrompt] = useState("");
- const [instructionId, setInstructionId] = useState(null);
- const [isLoading, setIsLoading] = useState(false);
- const [isSaving, setIsSaving] = useState(false);
- const [error, setError] = useState(null);
- const [success, setSuccess] = useState(false);
-
- useEffect(() => {
- if (open) {
- loadPreferences();
- } else {
- setError(null);
- setSuccess(false);
- }
- }, [open]);
-
- const loadPreferences = async () => {
- setIsLoading(true);
- setError(null);
- try {
- const response = await os.listInstructions({ limit: 100 });
- const defaultInstruction = response.data.find((inst) => inst.is_default);
-
- if (defaultInstruction) {
- setInstructionId(defaultInstruction.id);
- setPrompt(defaultInstruction.prompt);
- } else {
- // No default instruction exists yet
- setInstructionId(null);
- setPrompt("");
- }
- } catch (error) {
- console.error("Failed to load preferences:", error);
- setError("Failed to load preferences. Please try again.");
- } finally {
- setIsLoading(false);
- }
- };
-
- const handleSubmit = async (e: React.FormEvent) => {
- e.preventDefault();
- setError(null);
- setSuccess(false);
-
- setIsSaving(true);
- try {
- if (instructionId) {
- // If prompt is empty, delete the instruction
- if (prompt.trim() === "") {
- await os.deleteInstruction(instructionId);
- setInstructionId(null);
- setPrompt("");
- } else {
- // Update existing instruction
- await os.updateInstruction(instructionId, {
- prompt: prompt
- });
- }
- } else {
- // Only create new instruction if prompt is not empty
- if (prompt.trim() !== "") {
- const newInstruction = await os.createInstruction({
- name: "User Preferences",
- prompt: prompt,
- is_default: true
- });
- setInstructionId(newInstruction.id);
- }
- }
- setSuccess(true);
- } catch (error) {
- console.error("Failed to save preferences:", error);
- setError("Failed to save preferences. Please try again.");
- } finally {
- setIsSaving(false);
- }
- };
-
- return (
-
- );
-}
diff --git a/frontend/src/components/apikeys/ApiKeyManagementDialog.tsx b/frontend/src/components/apikeys/ApiKeyManagementDialog.tsx
deleted file mode 100644
index 84448a40..00000000
--- a/frontend/src/components/apikeys/ApiKeyManagementDialog.tsx
+++ /dev/null
@@ -1,22 +0,0 @@
-import { Dialog, DialogContent } from "@/components/ui/dialog";
-import { ApiKeyDashboard } from "./ApiKeyDashboard";
-
-interface ApiKeyManagementDialogProps {
- open: boolean;
- onOpenChange: (open: boolean) => void;
- showCreditSuccessMessage?: boolean;
-}
-
-export function ApiKeyManagementDialog({
- open,
- onOpenChange,
- showCreditSuccessMessage = false
-}: ApiKeyManagementDialogProps) {
- return (
-
- );
-}
diff --git a/frontend/src/components/team/TeamManagementDialog.tsx b/frontend/src/components/team/TeamManagementDialog.tsx
deleted file mode 100644
index bb8709be..00000000
--- a/frontend/src/components/team/TeamManagementDialog.tsx
+++ /dev/null
@@ -1,58 +0,0 @@
-import { useState, useEffect } from "react";
-import { Dialog, DialogContent } from "@/components/ui/dialog";
-import { TeamSetupDialog } from "./TeamSetupDialog";
-import { TeamDashboard } from "./TeamDashboard";
-import type { TeamStatus } from "@/types/team";
-
-interface TeamManagementDialogProps {
- open: boolean;
- onOpenChange: (open: boolean) => void;
- teamStatus?: TeamStatus;
-}
-
-export function TeamManagementDialog({
- open,
- onOpenChange,
- teamStatus
-}: TeamManagementDialogProps) {
- const [showSetupDialog, setShowSetupDialog] = useState(false);
-
- // Determine if we should show the setup dialog
- const needsSetup = teamStatus?.has_team_subscription && !teamStatus?.team_created;
-
- // Check if team needs to be set up when dialog opens
- useEffect(() => {
- if (open && needsSetup) {
- setShowSetupDialog(true);
- } else if (!needsSetup) {
- // Reset state when team is created
- setShowSetupDialog(false);
- }
- }, [open, needsSetup]);
-
- const handleTeamCreated = () => {
- // Don't update state here - let the effect handle it when teamStatus updates
- // This prevents race conditions without setTimeout
- };
-
- // Show setup dialog if explicitly set or if team needs setup
- if (showSetupDialog && needsSetup) {
- return (
-
- );
- }
-
- // Otherwise show the team dashboard
- return (
-
- );
-}
From 581b46c1ef48d6db440dd8f8a07d753813b4780d Mon Sep 17 00:00:00 2001
From: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
Date: Wed, 25 Feb 2026 22:19:30 +0000
Subject: [PATCH 13/15] fix: clear credits_success URL param after processing
to prevent repeat flash on refresh
Co-Authored-By: unknown <>
---
frontend/src/components/settings/ApiManagementSection.tsx | 4 +++-
1 file changed, 3 insertions(+), 1 deletion(-)
diff --git a/frontend/src/components/settings/ApiManagementSection.tsx b/frontend/src/components/settings/ApiManagementSection.tsx
index 6ec0a6da..c569780f 100644
--- a/frontend/src/components/settings/ApiManagementSection.tsx
+++ b/frontend/src/components/settings/ApiManagementSection.tsx
@@ -46,10 +46,12 @@ export function ApiManagementSection({ creditsSuccess = false }: ApiManagementSe
if (creditsSuccess) {
setShowCreditSuccess(true);
queryClient.invalidateQueries({ queryKey: ["apiCreditBalance"] });
+ // Clear credits_success from URL to prevent repeat flash on refresh
+ navigate({ to: "/settings", search: { tab: "api" }, replace: true });
const timer = setTimeout(() => setShowCreditSuccess(false), 5000);
return () => clearTimeout(timer);
}
- }, [creditsSuccess, queryClient]);
+ }, [creditsSuccess, queryClient, navigate]);
// Check if user has API access
const isBillingLoading = billingStatus === null;
From 7a6183a6612f9a10af1882ba1712dc69db21d661 Mon Sep 17 00:00:00 2001
From: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
Date: Wed, 25 Feb 2026 22:27:57 +0000
Subject: [PATCH 14/15] fix: use loose inequality for hasStripeAccount to
handle undefined during loading
Co-Authored-By: unknown <>
---
frontend/src/components/settings/SubscriptionSection.tsx | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/frontend/src/components/settings/SubscriptionSection.tsx b/frontend/src/components/settings/SubscriptionSection.tsx
index ae8109d9..11ae4810 100644
--- a/frontend/src/components/settings/SubscriptionSection.tsx
+++ b/frontend/src/components/settings/SubscriptionSection.tsx
@@ -13,7 +13,7 @@ export function SubscriptionSection() {
const [isPortalLoading, setIsPortalLoading] = useState(false);
const productName = billingStatus?.product_name || "";
- const hasStripeAccount = billingStatus?.stripe_customer_id !== null;
+ const hasStripeAccount = billingStatus?.stripe_customer_id != null;
const isPro = productName.toLowerCase().includes("pro");
const isMax = productName.toLowerCase().includes("max");
const isStarter = productName.toLowerCase().includes("starter");
From dfb027cad8a016cd87f1fc09fc00f341def5e80a Mon Sep 17 00:00:00 2001
From: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
Date: Wed, 25 Feb 2026 22:35:50 +0000
Subject: [PATCH 15/15] fix: split credit success timer into separate useEffect
to prevent cleanup race condition
Co-Authored-By: unknown <>
---
frontend/src/components/settings/ApiManagementSection.tsx | 8 +++++++-
1 file changed, 7 insertions(+), 1 deletion(-)
diff --git a/frontend/src/components/settings/ApiManagementSection.tsx b/frontend/src/components/settings/ApiManagementSection.tsx
index c569780f..0bab75c0 100644
--- a/frontend/src/components/settings/ApiManagementSection.tsx
+++ b/frontend/src/components/settings/ApiManagementSection.tsx
@@ -48,10 +48,16 @@ export function ApiManagementSection({ creditsSuccess = false }: ApiManagementSe
queryClient.invalidateQueries({ queryKey: ["apiCreditBalance"] });
// Clear credits_success from URL to prevent repeat flash on refresh
navigate({ to: "/settings", search: { tab: "api" }, replace: true });
+ }
+ }, [creditsSuccess, queryClient, navigate]);
+
+ // Auto-hide credit success message after 5 seconds
+ useEffect(() => {
+ if (showCreditSuccess) {
const timer = setTimeout(() => setShowCreditSuccess(false), 5000);
return () => clearTimeout(timer);
}
- }, [creditsSuccess, queryClient, navigate]);
+ }, [showCreditSuccess]);
// Check if user has API access
const isBillingLoading = billingStatus === null;