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. - -
- {!isGuestUser && ( -
- -
- - {os.auth.user?.user.email_verified ? ( - - ) : ( - - )} -
- {!os.auth.user?.user.email_verified && ( -
- {verificationStatus === "unverified" ? ( - <> - Unverified -{" "} - - - ) : ( - "Pending - Check your email for verification link" - )} -
- )} -
- )} -
- - - {billingStatus?.current_period_end && ( -
- {billingStatus.payment_provider === "subscription_pass" || - billingStatus.payment_provider === "zaprite" - ? "Expires on " - : "Renews on "} - {new Date(Number(billingStatus.current_period_end) * 1000).toLocaleDateString( - undefined, - { - year: "numeric", - month: "long", - day: "numeric" - } - )} -
- )} -
-
- - {(isEmailUser || isGuestUser) && ( - - - - )} - -
-
- - - -
- {(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 ( - - - !open && setShowAboutMenu(false)}> -
- - - {billingStatus ? `${billingStatus.product_name} Plan` : "Loading..."} - - - - - - -
- -
-
- {teamStatus?.team_name || "Maple AI"} - - - - - - Profile - - - {showUpgrade && ( - - - - Upgrade your plan - - - )} - {showManage && ( - - - {isPortalLoading ? "Loading..." : "Manage Subscription"} - - )} - {isTeamPlan && ( - setIsTeamDialogOpen(true)}> -
-
- - Manage Team -
- {showTeamSetupAlert && ( - - Setup Required - - )} -
-
- )} - {showApiManagement && ( - setIsApiKeyDialogOpen(true)}> - - API Management - - )} - - - - Delete History - - -
- - - { - e.preventDefault(); - setShowAboutMenu(true); - }} - onSelect={(e) => { - e.preventDefault(); - }} - > - - About Us - - - - - - Log out - -
- - {/* About Us Submenu */} -
- - { - e.preventDefault(); - setShowAboutMenu(false); - }} - onSelect={(e) => { - e.preventDefault(); - }} - > - - Back - - - - - - - - About Maple - - - { - e.preventDefault(); - handleOpenExternalUrl("https://opensecret.cloud/privacy"); - }} - onSelect={(e) => { - e.preventDefault(); - }} - > - - Privacy Policy - - { - e.preventDefault(); - handleOpenExternalUrl("https://opensecret.cloud/terms"); - }} - onSelect={(e) => { - e.preventDefault(); - }} - > - - Terms of Service - - { - e.preventDefault(); - handleOpenExternalUrl("mailto:support@opensecret.cloud"); - }} - onSelect={(e) => { - e.preventDefault(); - }} - > - - Contact Us - - -
-
-
- - - - -
-
-
- ); -} 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 ( - - - - User Preferences - - Customize your default system prompt for AI conversations. - - -
- {error && ( - - {error} - - )} - {success && ( - - Preferences saved successfully. - - )} -
- -