Unverified 提交 3e153fd3 authored 作者: Will Chen's avatar Will Chen 提交者: GitHub

Show setup banner for Pro trial (#2391)

#skip-bb <!-- This is an auto-generated description by cubic. --> --- ## Summary by cubic Show a setup banner and dialog to promote the Dyad Pro free trial during onboarding. Improves provider selection with clear logos and refreshed styles. - **New Features** - Added DyadProTrialDialog with feature highlights, Start Free Trial (opens checkout with trialCode=1PRO30), and Learn more. - Prominent “Start with Dyad Pro free trial” card in SetupBanner; click opens the trial dialog; marked as Recommended. - Added Google and OpenRouter assets and switched provider icons to real logos, including in OpenRouterSetupBanner. - **Refactors** - Updated SetupProviderCard styles and chips (new chipColor per variant); more neutral Google/OpenRouter variants. - Minor layout tweaks in SetupBanner and ProBanner for spacing and readability. <sup>Written for commit 3e3c2932b1189a52f4d332f6d3e8649afe03bfe1. Summary will update on new commits.</sup> <!-- End of auto-generated description by cubic. --> <!-- CURSOR_SUMMARY --> --- > [!NOTE] > **Low Risk** > Low risk UI-only onboarding changes that add a new modal and update links/assets; main risk is UX/analytics behavior changes around the Dyad Pro entry point and external URL targets. > > **Overview** > Adds a new `DyadProTrialDialog` modal to promote a Dyad Pro free trial, including feature highlights and CTAs that open external checkout/learn-more URLs. > > Updates `SetupBanner` to show a prominent “Start with Dyad Pro free trial” recommended card that opens the modal (instead of immediately navigating externally), refreshes Google/OpenRouter setup cards to use real logo assets, and slightly tweaks spacing. > > Refines `SetupProviderCard` styling by introducing per-variant `chipColor` and making Google/OpenRouter variants more neutral, plus a small spacing tweak in `ProBanner`’s “Already have Dyad Pro?” button. > > <sup>Written by [Cursor Bugbot](https://cursor.com/dashboard?tab=bugbot) for commit 3e3c2932b1189a52f4d332f6d3e8649afe03bfe1. This will update automatically on new commits. Configure [here](https://cursor.com/dashboard?tab=bugbot).</sup> <!-- /CURSOR_SUMMARY -->
上级 2fe32d27
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24">
<path fill="#4285F4" d="M22.56 12.25c0-.78-.07-1.53-.2-2.25H12v4.26h5.92c-.26 1.37-1.04 2.53-2.21 3.31v2.77h3.57c2.08-1.92 3.28-4.74 3.28-8.09z"/>
<path fill="#34A853" d="M12 23c2.97 0 5.46-.98 7.28-2.66l-3.57-2.77c-.98.66-2.23 1.06-3.71 1.06-2.86 0-5.29-1.93-6.16-4.53H2.18v2.84C3.99 20.53 7.7 23 12 23z"/>
<path fill="#FBBC05" d="M5.84 14.09c-.22-.66-.35-1.36-.35-2.09s.13-1.43.35-2.09V7.07H2.18C1.43 8.55 1 10.22 1 12s.43 3.45 1.18 4.93l2.85-2.22.81-.62z"/>
<path fill="#EA4335" d="M12 5.38c1.62 0 3.06.56 4.21 1.64l3.15-3.15C17.45 2.09 14.97 1 12 1 7.7 1 3.99 3.47 2.18 7.07l3.66 2.84c.87-2.6 3.3-4.53 6.16-4.53z"/>
</svg>
import { Dialog, DialogContent } from "@/components/ui/dialog";
import { Button } from "@/components/ui/button";
import { Sparkles, Check, Zap, Wand2, Cpu } from "lucide-react";
import { ipc } from "@/ipc/types";
interface DyadProTrialDialogProps {
isOpen: boolean;
onClose: () => void;
}
export function DyadProTrialDialog({
isOpen,
onClose,
}: DyadProTrialDialogProps) {
const handleStartTrial = () => {
ipc.system.openExternalUrl(
"https://academy.dyad.sh/redirect-to-checkout?trialCode=1PRO30&utm_source=dyad-app&utm_medium=app&utm_campaign=setup-dialog-v2",
);
onClose();
};
const handleLearnMore = () => {
ipc.system.openExternalUrl(
"https://www.dyad.sh/pro?utm_source=dyad-app&utm_medium=app&utm_campaign=setup-dialog-v2",
);
};
const features = [
{
icon: Zap,
title: "50 AI Credits",
description: "Start building right away",
},
{
icon: Cpu,
title: "Agent Mode",
description: "Automatically debug errors with AI",
},
{
icon: Wand2,
title: "Pro Features",
description: "AI themes, visual editing & more",
},
];
return (
<Dialog open={isOpen} onOpenChange={onClose}>
<DialogContent className="max-w-md overflow-hidden border-0 p-0 shadow-2xl">
{/* Header */}
<div className="relative bg-muted/50 px-6 pt-6">
{/* Subtle accent line */}
<div className="absolute inset-x-0 top-0 h-1 bg-gradient-to-r from-indigo-500 via-purple-500 to-violet-500" />
{/* Title */}
<div className="text-center">
<h2 className="text-xl font-semibold tracking-tight text-foreground">
Unlock Dyad Pro
</h2>
<p className="mt-1 text-sm text-muted-foreground">
Start your free 3-day trial today
</p>
</div>
</div>
{/* Content */}
<div className="px-6 pb-6">
{/* Features */}
<div className="space-y-3">
{features.map((feature) => (
<div
key={feature.title}
className="flex items-center gap-4 rounded-xl border border-border/50 bg-muted/30 p-3 transition-colors hover:bg-muted/50"
>
<div className="flex h-10 w-10 shrink-0 items-center justify-center rounded-lg bg-gradient-to-br from-indigo-500/10 to-purple-500/10 ring-1 ring-indigo-500/20">
<feature.icon className="h-5 w-5 text-indigo-600 dark:text-indigo-400" />
</div>
<div className="min-w-0 flex-1">
<p className="font-medium text-foreground">{feature.title}</p>
<p className="text-sm text-muted-foreground">
{feature.description}
</p>
</div>
<Check className="h-5 w-5 shrink-0 text-green-600 dark:text-green-500" />
</div>
))}
</div>
{/* CTA Buttons */}
<div className="mt-6 space-y-3">
<Button
onClick={handleStartTrial}
className="w-full bg-violet-600 py-5 text-base font-semibold text-white shadow-lg shadow-violet-500/30 transition-all hover:bg-violet-500 hover:shadow-xl hover:shadow-violet-500/40 active:scale-[0.98]"
>
<Sparkles className="mr-2 h-4 w-4" />
Start Free Trial
</Button>
<Button
variant="ghost"
onClick={handleLearnMore}
className="w-full text-muted-foreground hover:text-foreground"
>
Learn more about Pro
</Button>
</div>
{/* Fine print */}
<p className="mt-4 text-center text-xs text-muted-foreground">
Cancel anytime. Free trial for first-time customers only.
</p>
</div>
</DialogContent>
</Dialog>
);
}
...@@ -63,7 +63,7 @@ export function SetupDyadProButton() { ...@@ -63,7 +63,7 @@ export function SetupDyadProButton() {
<Button <Button
variant="outline" variant="outline"
size="lg" size="lg"
className="cursor-pointer w-full mt-4 bg-(--background-lighter) text-primary" className="cursor-pointer w-full bg-(--background-lighter) text-primary"
onClick={() => { onClick={() => {
ipc.system.openExternalUrl("https://academy.dyad.sh/settings"); ipc.system.openExternalUrl("https://academy.dyad.sh/settings");
}} }}
......
...@@ -2,7 +2,6 @@ import { useNavigate } from "@tanstack/react-router"; ...@@ -2,7 +2,6 @@ import { useNavigate } from "@tanstack/react-router";
import { import {
ChevronRight, ChevronRight,
GiftIcon, GiftIcon,
Sparkles,
CheckCircle, CheckCircle,
AlertCircle, AlertCircle,
XCircle, XCircle,
...@@ -29,9 +28,14 @@ import { useLanguageModelProviders } from "@/hooks/useLanguageModelProviders"; ...@@ -29,9 +28,14 @@ import { useLanguageModelProviders } from "@/hooks/useLanguageModelProviders";
import { useScrollAndNavigateTo } from "@/hooks/useScrollAndNavigateTo"; import { useScrollAndNavigateTo } from "@/hooks/useScrollAndNavigateTo";
// @ts-ignore // @ts-ignore
import logo from "../../assets/logo.svg"; import logo from "../../assets/logo.svg";
// @ts-ignore
import googleIcon from "../../assets/ai-logos/google-g-icon.svg";
// @ts-ignore
import openrouterLogo from "../../assets/ai-logos/openrouter-logo.png";
import { OnboardingBanner } from "./home/OnboardingBanner"; import { OnboardingBanner } from "./home/OnboardingBanner";
import { showError } from "@/lib/toast"; import { showError } from "@/lib/toast";
import { useSettings } from "@/hooks/useSettings"; import { useSettings } from "@/hooks/useSettings";
import { DyadProTrialDialog } from "./DyadProTrialDialog";
type NodeInstallStep = type NodeInstallStep =
| "install" | "install"
...@@ -64,6 +68,7 @@ export function SetupBanner() { ...@@ -64,6 +68,7 @@ export function SetupBanner() {
}, [setNodeSystemInfo, setNodeCheckError]); }, [setNodeSystemInfo, setNodeCheckError]);
const [showManualConfig, setShowManualConfig] = useState(false); const [showManualConfig, setShowManualConfig] = useState(false);
const [isSelectingPath, setIsSelectingPath] = useState(false); const [isSelectingPath, setIsSelectingPath] = useState(false);
const [showDyadProTrialDialog, setShowDyadProTrialDialog] = useState(false);
const { updateSettings } = useSettings(); const { updateSettings } = useSettings();
// Add handler for manual path selection // Add handler for manual path selection
...@@ -115,9 +120,7 @@ export function SetupBanner() { ...@@ -115,9 +120,7 @@ export function SetupBanner() {
}; };
const handleDyadProSetupClick = () => { const handleDyadProSetupClick = () => {
posthog.capture("setup-flow:ai-provider-setup:dyad:click"); posthog.capture("setup-flow:ai-provider-setup:dyad:click");
ipc.system.openExternalUrl( setShowDyadProTrialDialog(true);
"https://www.dyad.sh/pro?utm_source=dyad-app&utm_medium=app&utm_campaign=setup-banner",
);
}; };
const handleOtherProvidersClick = () => { const handleOtherProvidersClick = () => {
...@@ -176,7 +179,7 @@ export function SetupBanner() { ...@@ -176,7 +179,7 @@ export function SetupBanner() {
return ( return (
<> <>
<p className="text-xl font-medium text-zinc-700 dark:text-zinc-300 p-4"> <p className="text-xl font-medium text-zinc-700 dark:text-zinc-300 p-4 pt-6">
Setup Dyad Setup Dyad
</p> </p>
<OnboardingBanner <OnboardingBanner
...@@ -314,14 +317,26 @@ export function SetupBanner() { ...@@ -314,14 +317,26 @@ export function SetupBanner() {
<p className="text-[15px] mb-3"> <p className="text-[15px] mb-3">
Not sure what to do? Watch the Get Started video above ☝️ Not sure what to do? Watch the Get Started video above ☝️
</p> </p>
<div className="flex gap-2">
<SetupProviderCard
variant="dyad"
onClick={handleDyadProSetupClick}
tabIndex={isNodeSetupComplete ? 0 : -1}
leadingIcon={
<img src={logo} alt="Dyad Logo" className="w-6 h-6 mr-0.5" />
}
title="Start with Dyad Pro free trial"
subtitle="Unlock the full power of Dyad"
chip={<>Recommended</>}
/>
<div className="mt-2 flex gap-2">
<SetupProviderCard <SetupProviderCard
className="flex-1" className="flex-1"
variant="google" variant="google"
onClick={handleGoogleSetupClick} onClick={handleGoogleSetupClick}
tabIndex={isNodeSetupComplete ? 0 : -1} tabIndex={isNodeSetupComplete ? 0 : -1}
leadingIcon={ leadingIcon={
<Sparkles className="w-4 h-4 text-blue-600 dark:text-blue-400" /> <img src={googleIcon} alt="Google" className="w-4 h-4" />
} }
title="Setup Google Gemini API Key" title="Setup Google Gemini API Key"
chip={<>Free</>} chip={<>Free</>}
...@@ -333,26 +348,17 @@ export function SetupBanner() { ...@@ -333,26 +348,17 @@ export function SetupBanner() {
onClick={handleOpenRouterSetupClick} onClick={handleOpenRouterSetupClick}
tabIndex={isNodeSetupComplete ? 0 : -1} tabIndex={isNodeSetupComplete ? 0 : -1}
leadingIcon={ leadingIcon={
<Sparkles className="w-4 h-4 text-teal-600 dark:text-teal-400" /> <img
src={openrouterLogo}
alt="OpenRouter"
className="w-4 h-4"
/>
} }
title="Setup OpenRouter API Key" title="Setup OpenRouter API Key"
chip={<>Free</>} chip={<>Free</>}
/> />
</div> </div>
<SetupProviderCard
className="mt-2"
variant="dyad"
onClick={handleDyadProSetupClick}
tabIndex={isNodeSetupComplete ? 0 : -1}
leadingIcon={
<img src={logo} alt="Dyad Logo" className="w-6 h-6 mr-0.5" />
}
title="Setup Dyad Pro"
subtitle="Access all AI models with one plan"
chip={<>Recommended</>}
/>
<div <div
className="mt-2 p-3 bg-gray-50 dark:bg-gray-800/50 border border-gray-200 dark:border-gray-700 rounded-lg cursor-pointer hover:bg-gray-100 dark:hover:bg-gray-800/70 transition-colors" className="mt-2 p-3 bg-gray-50 dark:bg-gray-800/50 border border-gray-200 dark:border-gray-700 rounded-lg cursor-pointer hover:bg-gray-100 dark:hover:bg-gray-800/70 transition-colors"
onClick={handleOtherProvidersClick} onClick={handleOtherProvidersClick}
...@@ -380,6 +386,11 @@ export function SetupBanner() { ...@@ -380,6 +386,11 @@ export function SetupBanner() {
</AccordionItem> </AccordionItem>
</Accordion> </Accordion>
</div> </div>
<DyadProTrialDialog
isOpen={showDyadProTrialDialog}
onClose={() => setShowDyadProTrialDialog(false)}
/>
</> </>
); );
} }
...@@ -471,7 +482,7 @@ export const OpenRouterSetupBanner = ({ ...@@ -471,7 +482,7 @@ export const OpenRouterSetupBanner = ({
}} }}
tabIndex={0} tabIndex={0}
leadingIcon={ leadingIcon={
<Sparkles className="w-4 h-4 text-purple-600 dark:text-purple-400" /> <img src={openrouterLogo} alt="OpenRouter" className="w-4 h-4" />
} }
title="Setup OpenRouter API Key" title="Setup OpenRouter API Key"
chip={ chip={
......
...@@ -40,8 +40,7 @@ export function SetupProviderCard({ ...@@ -40,8 +40,7 @@ export function SetupProviderCard({
<div <div
className={cn( className={cn(
"absolute top-2 right-2 px-2 py-1 rounded-full text-xs font-semibold", "absolute top-2 right-2 px-2 py-1 rounded-full text-xs font-semibold",
styles.subtitleColor, styles.chipColor,
"bg-white/80 dark:bg-black/20 backdrop-blur-sm",
)} )}
> >
{chip} {chip}
...@@ -79,20 +78,24 @@ function getVariantStyles(variant: SetupProviderVariant) { ...@@ -79,20 +78,24 @@ function getVariantStyles(variant: SetupProviderVariant) {
case "google": case "google":
return { return {
container: container:
"bg-blue-50 dark:bg-blue-900/50 border-blue-200 dark:border-blue-700 hover:bg-blue-100 dark:hover:bg-blue-900/70", "bg-blue-50/50 dark:bg-blue-900/20 border-blue-200/50 dark:border-blue-800/30 hover:bg-blue-50 dark:hover:bg-blue-900/30",
iconWrapper: "bg-blue-100 dark:bg-blue-800", iconWrapper: "bg-blue-100/50 dark:bg-blue-800/30",
titleColor: "text-blue-800 dark:text-blue-300", titleColor: "text-zinc-700 dark:text-zinc-300",
subtitleColor: "text-blue-600 dark:text-blue-400", subtitleColor: "text-blue-500/70 dark:text-blue-400/70",
chevronColor: "text-blue-600 dark:text-blue-400", chipColor:
"text-zinc-600 dark:text-zinc-300 bg-white dark:bg-zinc-800 border border-zinc-200 dark:border-zinc-700",
chevronColor: "text-zinc-400 dark:text-zinc-500",
} as const; } as const;
case "openrouter": case "openrouter":
return { return {
container: container:
"bg-teal-50 dark:bg-teal-900/50 border-teal-200 dark:border-teal-700 hover:bg-teal-100 dark:hover:bg-teal-900/70", "bg-blue-50/50 dark:bg-blue-900/20 border-blue-200/50 dark:border-blue-800/30 hover:bg-blue-50 dark:hover:bg-blue-900/30",
iconWrapper: "bg-teal-100 dark:bg-teal-800", iconWrapper: "bg-blue-100/50 dark:bg-blue-800/30",
titleColor: "text-teal-800 dark:text-teal-300", titleColor: "text-zinc-700 dark:text-zinc-300",
subtitleColor: "text-teal-600 dark:text-teal-400", subtitleColor: "text-blue-500/70 dark:text-blue-400/70",
chevronColor: "text-teal-600 dark:text-teal-400", chipColor:
"text-zinc-600 dark:text-zinc-300 bg-white dark:bg-zinc-800 border border-zinc-200 dark:border-zinc-700",
chevronColor: "text-zinc-400 dark:text-zinc-500",
} as const; } as const;
case "dyad": case "dyad":
return { return {
...@@ -101,6 +104,8 @@ function getVariantStyles(variant: SetupProviderVariant) { ...@@ -101,6 +104,8 @@ function getVariantStyles(variant: SetupProviderVariant) {
iconWrapper: "bg-primary/5 dark:bg-violet-800", iconWrapper: "bg-primary/5 dark:bg-violet-800",
titleColor: "text-violet-800 dark:text-violet-300", titleColor: "text-violet-800 dark:text-violet-300",
subtitleColor: "text-violet-600 dark:text-violet-400", subtitleColor: "text-violet-600 dark:text-violet-400",
chipColor:
"text-violet-700 dark:text-violet-200 bg-violet-100 dark:bg-violet-900 border border-violet-200 dark:border-violet-700",
chevronColor: "text-violet-600 dark:text-violet-400", chevronColor: "text-violet-600 dark:text-violet-400",
} as const; } as const;
} }
......
Markdown 格式
0%
您添加了 0 到此讨论。请谨慎行事。
请先完成此评论的编辑!
注册 或者 后发表评论