Support deep link for Dyad Pro (#25)

This commit is contained in:
Will Chen
2025-04-26 10:58:11 -07:00
committed by GitHub
parent 4848b2f085
commit eda2f9206d
6 changed files with 174 additions and 30 deletions

View File

@@ -8,11 +8,31 @@ import { Button } from "@/components/ui/button";
import logo from "../../assets/logo_transparent.png";
import { providerSettingsRoute } from "@/routes/settings/providers/$provider";
import { cn } from "@/lib/utils";
import { useDeepLink } from "@/contexts/DeepLinkContext";
import { useEffect, useState } from "react";
import { DyadProSuccessDialog } from "@/components/DyadProSuccessDialog";
export const TitleBar = () => {
const [selectedAppId] = useAtom(selectedAppIdAtom);
const { apps } = useLoadApps();
const { navigate } = useRouter();
const { settings } = useSettings();
const { settings, refreshSettings } = useSettings();
const [isSuccessDialogOpen, setIsSuccessDialogOpen] = useState(false);
const showDyadProSuccessDialog = () => {
setIsSuccessDialogOpen(true);
};
const { lastDeepLink } = useDeepLink();
useEffect(() => {
const handleDeepLink = async () => {
if (lastDeepLink?.type === "dyad-pro-return") {
await refreshSettings();
showDyadProSuccessDialog();
}
};
handleDeepLink();
}, [lastDeepLink]);
// Get selected app name
const selectedApp = apps.find((app) => app.id === selectedAppId);
@@ -30,37 +50,44 @@ export const TitleBar = () => {
const isDyadProEnabled = settings?.enableDyadPro;
return (
<div className="@container z-11 w-full h-11 bg-(--sidebar) absolute top-0 left-0 app-region-drag flex items-center">
<div className="pl-20"></div>
<img src={logo} alt="Dyad Logo" className="w-6 h-6 mr-2" />
<Button
variant="outline"
size="sm"
className={`hidden @md:block no-app-region-drag text-sm font-medium ${
selectedApp ? "cursor-pointer" : ""
}`}
onClick={handleAppClick}
>
{displayText}
</Button>
{isDyadPro && (
<>
<div className="@container z-11 w-full h-11 bg-(--sidebar) absolute top-0 left-0 app-region-drag flex items-center">
<div className="pl-20"></div>
<img src={logo} alt="Dyad Logo" className="w-6 h-6 mr-2" />
<Button
onClick={() => {
navigate({
to: providerSettingsRoute.id,
params: { provider: "auto" },
});
}}
variant="outline"
className={cn(
"ml-4 no-app-region-drag h-7 bg-indigo-600 text-white dark:bg-indigo-600 dark:text-white",
!isDyadProEnabled && "bg-zinc-600 dark:bg-zinc-600"
)}
size="sm"
className={`hidden @md:block no-app-region-drag text-sm font-medium ${
selectedApp ? "cursor-pointer" : ""
}`}
onClick={handleAppClick}
>
{isDyadProEnabled ? "Dyad Pro" : "Dyad Pro (disabled)"}
{displayText}
</Button>
)}
</div>
{isDyadPro && (
<Button
onClick={() => {
navigate({
to: providerSettingsRoute.id,
params: { provider: "auto" },
});
}}
variant="outline"
className={cn(
"ml-4 no-app-region-drag h-7 bg-indigo-600 text-white dark:bg-indigo-600 dark:text-white",
!isDyadProEnabled && "bg-zinc-600 dark:bg-zinc-600"
)}
size="sm"
>
{isDyadProEnabled ? "Dyad Pro" : "Dyad Pro (disabled)"}
</Button>
)}
</div>
<DyadProSuccessDialog
isOpen={isSuccessDialogOpen}
onClose={() => setIsSuccessDialogOpen(false)}
/>
</>
);
};

View File

@@ -12,10 +12,10 @@ export default function RootLayout({
}) {
return (
<>
<TitleBar />
<ThemeProvider>
<DeepLinkProvider>
<SidebarProvider>
<TitleBar />
<AppSidebar />
<div className="flex h-screenish w-full overflow-x-hidden mt-12 mb-4 mr-4 border-t border-l border-border rounded-lg bg-background">
{children}

View File

@@ -0,0 +1,52 @@
import {
Dialog,
DialogContent,
DialogHeader,
DialogTitle,
DialogFooter,
} from "@/components/ui/dialog";
import { Button } from "@/components/ui/button";
import { CheckCircle, Sparkles } from "lucide-react";
interface DyadProSuccessDialogProps {
isOpen: boolean;
onClose: () => void;
}
export function DyadProSuccessDialog({
isOpen,
onClose,
}: DyadProSuccessDialogProps) {
return (
<Dialog open={isOpen} onOpenChange={onClose}>
<DialogContent className="max-w-md">
<DialogHeader>
<DialogTitle className="flex items-center gap-2 text-xl">
<div className="flex h-10 w-10 items-center justify-center rounded-full bg-green-100 dark:bg-green-900">
<CheckCircle className="h-6 w-6 text-green-600 dark:text-green-400" />
</div>
<span>Dyad Pro Enabled</span>
</DialogTitle>
</DialogHeader>
<div className="py-4">
<p className="mb-4 text-base">
Congrats! Dyad Pro is now enabled in the app.
</p>
<div className="flex items-center gap-2 mb-3">
<Sparkles className="h-5 w-5 text-indigo-500" />
<p className="text-sm">You have access to leading AI models.</p>
</div>
<p className="text-sm text-muted-foreground">
You can click the Pro button at the top to access the settings at
any time.
</p>
</div>
<DialogFooter className="flex justify-end gap-2">
<Button onClick={onClose} variant="outline">
OK
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
);
}

View File

@@ -94,6 +94,12 @@ export const ExperimentsSchema = z.object({
});
export type Experiments = z.infer<typeof ExperimentsSchema>;
export const DyadProBudgetSchema = z.object({
budgetResetAt: z.string(),
maxBudget: z.number(),
});
export type DyadProBudget = z.infer<typeof DyadProBudgetSchema>;
/**
* Zod schema for user settings
*/
@@ -108,7 +114,7 @@ export const UserSettingsSchema = z.object({
telemetryUserId: z.string().optional(),
hasRunBefore: z.boolean().optional(),
enableDyadPro: z.boolean().optional(),
dyadProBudget: DyadProBudgetSchema.optional(),
experiments: ExperimentsSchema.optional(),
// DEPRECATED.
runtimeMode: RuntimeModeSchema.optional(),

View File

@@ -8,6 +8,7 @@ import { updateElectronApp } from "update-electron-app";
import log from "electron-log";
import { readSettings, writeSettings } from "./main/settings";
import { handleSupabaseOAuthReturn } from "./supabase_admin/supabase_return_handler";
import { handleDyadProReturn } from "./main/pro";
log.errorHandler.startCatching();
log.eventLogger.startLogging();
@@ -185,6 +186,32 @@ function handleDeepLinkReturn(url: string) {
});
return;
}
// dyad://dyad-pro-return?key=123&budget_reset_at=2025-05-26T16:31:13.492000Z&max_budget=100
if (parsed.hostname === "dyad-pro-return") {
const apiKey = parsed.searchParams.get("key");
// UTC time
// budget_reset_at: '2025-05-26T16:31:13.492000Z'
const budgetResetAt = parsed.searchParams.get("budget_reset_at");
const maxBudget = Number(parsed.searchParams.get("max_budget"));
if (!apiKey) {
dialog.showErrorBox(
"Invalid URL",
"Expected key, budget_reset_at, and max_budget"
);
return;
}
handleDyadProReturn({
apiKey,
budgetResetAt,
maxBudget,
});
// Send message to renderer to trigger re-render
mainWindow?.webContents.send("deep-link-received", {
type: parsed.hostname,
url,
});
return;
}
dialog.showErrorBox("Invalid deep link URL", url);
}

32
src/main/pro.ts Normal file
View File

@@ -0,0 +1,32 @@
import { readSettings, writeSettings } from "./settings";
export function handleDyadProReturn({
apiKey,
budgetResetAt,
maxBudget,
}: {
apiKey: string;
budgetResetAt: string | null | undefined;
maxBudget: number | null | undefined;
}) {
const settings = readSettings();
writeSettings({
providerSettings: {
...settings.providerSettings,
auto: {
...settings.providerSettings.auto,
apiKey: {
value: apiKey,
},
},
},
dyadProBudget:
budgetResetAt && maxBudget
? {
budgetResetAt,
maxBudget,
}
: undefined,
enableDyadPro: true,
});
}