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
65 changes: 49 additions & 16 deletions frontend/bun.lock

Large diffs are not rendered by default.

1 change: 1 addition & 0 deletions frontend/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@
"@opensecret/react": "1.5.0",
"@radix-ui/react-alert-dialog": "^1.1.1",
"@radix-ui/react-avatar": "^1.1.0",
"@radix-ui/react-checkbox": "^1.3.3",
"@radix-ui/react-dialog": "^1.1.1",
"@radix-ui/react-dropdown-menu": "^2.1.1",
"@radix-ui/react-label": "^2.1.0",
Expand Down
73 changes: 38 additions & 35 deletions frontend/src/components/AccountDialog.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -36,8 +36,9 @@ export function AccountDialog() {
);
const { billingStatus } = useLocalState();

// Check if user is an email user
// 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 {
Expand All @@ -56,41 +57,43 @@ export function AccountDialog() {
<DialogDescription>Change your email or upgrade your plan.</DialogDescription>
</DialogHeader>
<form className="grid gap-4 py-4">
<div className="grid gap-2">
<Label htmlFor="email">Email</Label>
<div className="flex items-center gap-2">
<Input
id="email"
name="email"
type="email"
value={os.auth.user?.user.email}
disabled
/>
{os.auth.user?.user.email_verified ? (
<CheckCircle className="dark:text-green-500 text-green-700" />
) : (
<XCircle className="dark:text-red-500 text-red-700" />
)}
</div>
{!os.auth.user?.user.email_verified && (
<div className="text-sm text-muted-foreground">
{verificationStatus === "unverified" ? (
<>
Unverified -{" "}
<button
type="button"
onClick={handleResendVerification}
className="text-primary hover:underline focus:outline-none"
>
Resend verification email
</button>
</>
{!isGuestUser && (
<div className="grid gap-2">
<Label htmlFor="email">Email</Label>
<div className="flex items-center gap-2">
<Input
id="email"
name="email"
type="email"
value={os.auth.user?.user.email}
disabled
/>
{os.auth.user?.user.email_verified ? (
<CheckCircle className="dark:text-green-500 text-green-700" />
) : (
"Pending - Check your email for verification link"
<XCircle className="dark:text-red-500 text-red-700" />
)}
</div>
)}
</div>
{!os.auth.user?.user.email_verified && (
<div className="text-sm text-muted-foreground">
{verificationStatus === "unverified" ? (
<>
Unverified -{" "}
<button
type="button"
onClick={handleResendVerification}
className="text-primary hover:underline focus:outline-none"
>
Resend verification email
</button>
</>
) : (
"Pending - Check your email for verification link"
)}
</div>
)}
</div>
)}
<div className="grid gap-2">
<Label htmlFor="plan">Plan</Label>
<Select disabled>
Expand Down Expand Up @@ -120,7 +123,7 @@ export function AccountDialog() {
>
User Preferences
</Button>
{isEmailUser && (
{(isEmailUser || isGuestUser) && (
<DialogTrigger asChild>
<Button
onClick={(e) => {
Expand Down Expand Up @@ -153,7 +156,7 @@ export function AccountDialog() {
</Button>
</DialogFooter>
</DialogContent>
{isEmailUser && (
{(isEmailUser || isGuestUser) && (
<ChangePasswordDialog open={isChangePasswordOpen} onOpenChange={setIsChangePasswordOpen} />
)}
<DeleteAccountDialog open={isDeleteAccountOpen} onOpenChange={setIsDeleteAccountOpen} />
Expand Down
147 changes: 147 additions & 0 deletions frontend/src/components/GuestCredentialsDialog.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,147 @@
import { useState } from "react";
import { Button } from "@/components/ui/button";
import {
Dialog,
DialogContent,
DialogDescription,
DialogFooter,
DialogHeader,
DialogTitle
} from "@/components/ui/dialog";
import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
import { AlertTriangle, Copy, CheckCircle } from "lucide-react";
import { Checkbox } from "@/components/ui/checkbox";

interface GuestCredentialsDialogProps {
open: boolean;
onOpenChange: (open: boolean) => void;
uuid: string;
onContinue: () => void;
}

export function GuestCredentialsDialog({
open,
onOpenChange,
uuid,
onContinue
}: GuestCredentialsDialogProps) {
const [hasBackedUp, setHasBackedUp] = useState(false);
const [uuidCopied, setUuidCopied] = useState(false);

const handleCopyUuid = async () => {
try {
await navigator.clipboard.writeText(uuid);
setUuidCopied(true);
setTimeout(() => setUuidCopied(false), 2000);
} catch (error) {
console.error("Failed to copy UUID:", error);
}
};

const handleContinue = () => {
if (hasBackedUp) {
setHasBackedUp(false);
setUuidCopied(false);
onContinue();
}
};

return (
<Dialog open={open} onOpenChange={onOpenChange}>
<DialogContent
className="sm:max-w-[500px] [&>button]:hidden"
onInteractOutside={(e) => e.preventDefault()}
onEscapeKeyDown={(e) => e.preventDefault()}
>
<DialogHeader>
<DialogTitle className="flex items-center gap-2 text-amber-600 dark:text-amber-500">
<AlertTriangle className="w-5 h-5" />
Save Your Anonymous Account ID
</DialogTitle>
<DialogDescription className="text-base font-medium">
This is your ONLY chance to see your Account ID. Save it now!
</DialogDescription>
</DialogHeader>

<div className="space-y-4 py-4">
{/* Critical Warning Banner */}
<div className="rounded-lg border-2 border-red-500 bg-red-500/10 p-4">
<div className="flex items-start gap-3">
<AlertTriangle className="w-5 h-5 text-red-500 flex-shrink-0 mt-0.5" />
<div className="space-y-2 text-sm">
<p className="font-semibold text-red-600 dark:text-red-500">
Critical: Save your Account ID immediately!
</p>
<p>
Your Account ID will <strong>never be shown again</strong> after you close this
dialog. If you lose it, you will <strong>permanently lose access</strong> to your
account. We cannot recover it for you.
</p>
</div>
</div>
</div>

{/* UUID Display */}
<div className="space-y-2">
<Label htmlFor="uuid" className="text-base font-medium">
Your Account ID
</Label>
<div className="flex gap-2">
<Input id="uuid" value={uuid} readOnly className="font-mono text-sm" />
<Button
type="button"
variant="outline"
size="icon"
onClick={handleCopyUuid}
className="flex-shrink-0"
>
{uuidCopied ? (
<CheckCircle className="w-4 h-4 text-green-500" />
) : (
<Copy className="w-4 h-4" />
)}
</Button>
</div>
<p className="text-xs text-muted-foreground">
You will need this Account ID along with your password to sign in.
</p>
</div>

{/* Backup Instructions */}
<div className="rounded-lg border border-border bg-muted/50 p-4 space-y-2">
<p className="text-sm font-medium">How to save your Account ID:</p>
<ul className="text-sm space-y-1 list-disc list-inside text-muted-foreground">
<li>Copy it to a password manager (recommended)</li>
<li>Write it down on paper and store it safely</li>
<li>Save it in a secure note or encrypted file</li>
<li>Take a screenshot and store it securely</li>
</ul>
</div>

{/* Confirmation Checkbox */}
<div className="flex items-start space-x-3 pt-2">
<Checkbox
id="backed-up"
checked={hasBackedUp}
onCheckedChange={(checked: boolean) => setHasBackedUp(checked === true)}
/>
<Label
htmlFor="backed-up"
className="text-sm font-medium leading-relaxed cursor-pointer"
>
I have securely saved my Account ID and understand I cannot recover my account without
it
</Label>
</div>
</div>

<DialogFooter>
<Button onClick={handleContinue} disabled={!hasBackedUp} className="w-full sm:w-auto">
Continue to Payment
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
);
}
72 changes: 72 additions & 0 deletions frontend/src/components/GuestPaymentWarningDialog.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
import { Button } from "@/components/ui/button";
import {
Dialog,
DialogContent,
DialogDescription,
DialogHeader,
DialogTitle
} from "@/components/ui/dialog";
import { useNavigate } from "@tanstack/react-router";
import { useOpenSecret } from "@opensecret/react";
import { AlertTriangle, LogOut, CreditCard } from "lucide-react";

interface GuestPaymentWarningDialogProps {
open: boolean;
onOpenChange: (open: boolean) => void;
}

export function GuestPaymentWarningDialog({ open, onOpenChange }: GuestPaymentWarningDialogProps) {
const navigate = useNavigate();
const os = useOpenSecret();

const handleGoToPricing = () => {
navigate({ to: "/pricing" });
};

const handleLogout = async () => {
await os.signOut();
};

return (
<Dialog open={open} onOpenChange={onOpenChange}>
<DialogContent
className="sm:max-w-[425px] [&>button]:hidden"
onInteractOutside={(e) => e.preventDefault()}
onEscapeKeyDown={(e) => e.preventDefault()}
>
<DialogHeader>
<DialogTitle className="flex items-center gap-2 text-amber-600 dark:text-amber-500">
<AlertTriangle className="w-5 h-5" />
Subscription Required
</DialogTitle>
<DialogDescription>
Anonymous accounts require a paid subscription to use Maple AI.
</DialogDescription>
</DialogHeader>

<div className="space-y-4 py-4">
<div className="rounded-lg border border-amber-500/50 bg-amber-500/10 p-4 space-y-3">
<p className="text-sm font-medium">
Your anonymous account is not activated yet and cannot use the chat feature.
</p>
<p className="text-sm text-muted-foreground">
To start chatting with Maple AI, you need to subscribe to a paid plan. Anonymous
accounts must pay for a full year using Bitcoin.
</p>
</div>

<div className="space-y-2">
<Button onClick={handleGoToPricing} className="w-full gap-2">
<CreditCard className="w-4 h-4" />
View Pricing & Subscribe
</Button>
<Button variant="outline" onClick={handleLogout} className="w-full gap-2">
<LogOut className="w-4 h-4" />
Log Out
</Button>
</div>
</div>
</DialogContent>
</Dialog>
);
}
Loading
Loading