Skip to content

Commit fe5d483

Browse files
committed
feat: implement marketplace deep links and real app installation
- Add syft://marketplace/{appId}?action=install deep link support - Integrate real installApp API throughout marketplace components - Update marketplace API to populate installed field from listApps - Add automatic installation when action=install parameter present - Implement consistent toast notifications with app icons - Add install/uninstall functionality to marketplace cards and detail page - Support auto-refresh of installation status across marketplace UI - Add uninstall functionality to marketplace detail page - Change action button in marketplace list app card to open installed app
1 parent 87ea9ea commit fe5d483

File tree

13 files changed

+337
-81
lines changed

13 files changed

+337
-81
lines changed

src-frontend/app/layout.tsx

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -47,8 +47,12 @@ function MainLayout({ children }: { children: React.ReactNode }) {
4747
logger: (message: string) => Promise<void>,
4848
) {
4949
const original = console[fnName];
50-
console[fnName] = (message) => {
51-
original(message);
50+
console[fnName] = (...args) => {
51+
original(...args);
52+
// Convert all arguments to strings and join them
53+
const message = args
54+
.map((arg) => (typeof arg === "string" ? arg : JSON.stringify(arg)))
55+
.join(" ");
5256
logger(message);
5357
};
5458
}

src-frontend/components/deep-link-router.tsx

Lines changed: 29 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -7,27 +7,32 @@ import { parseDeepLink } from "@/lib/deep-links/parser";
77
import { createDeepLinkRouter } from "@/lib/deep-links/router";
88
import { createWorkspaceHandler } from "@/lib/deep-links/handlers/workspace";
99
import { createAppsHandler } from "@/lib/deep-links/handlers/apps";
10+
import { createMarketplaceHandler } from "@/lib/deep-links/handlers/marketplace";
11+
12+
const INITIAL_DEEP_LINKS_PROCESSED_KEY = "syft_initial_deep_links_processed";
1013

1114
export function DeepLinkRouter() {
1215
const router = useRouter();
1316

1417
useEffect(() => {
1518
const handleDeepLink = async () => {
1619
if (typeof window !== "undefined" && window.__TAURI__) {
17-
const { onOpenUrl } = window.__TAURI__.deepLink;
20+
const { onOpenUrl, getCurrent } = window.__TAURI__.deepLink;
1821

1922
// Create handlers with dependencies
2023
const workspaceHandler = createWorkspaceHandler({ router });
2124
const appsHandler = createAppsHandler({ router });
25+
const marketplaceHandler = createMarketplaceHandler({ router });
2226

2327
// Create the router
2428
const deepLinkRouter = createDeepLinkRouter({
2529
workspaceHandler,
2630
appsHandler,
31+
marketplaceHandler,
2732
});
2833

29-
await onOpenUrl(async (urls) => {
30-
console.log("Deep link received:", urls);
34+
const processUrls = async (urls: string[]) => {
35+
console.log("Processing deep links:", urls);
3136

3237
for (const url of urls) {
3338
try {
@@ -56,7 +61,27 @@ export function DeepLinkRouter() {
5661
});
5762
}
5863
}
59-
});
64+
};
65+
66+
// Check for initial URLs that launched the app (run once per session)
67+
const hasProcessed = sessionStorage.getItem(
68+
INITIAL_DEEP_LINKS_PROCESSED_KEY,
69+
);
70+
if (!hasProcessed) {
71+
sessionStorage.setItem(INITIAL_DEEP_LINKS_PROCESSED_KEY, "true");
72+
try {
73+
const initialUrls = await getCurrent();
74+
if (initialUrls && initialUrls.length > 0) {
75+
console.log("Processing initial launch URLs:", initialUrls);
76+
await processUrls(initialUrls);
77+
}
78+
} catch (error) {
79+
console.log("No initial URLs or failed to get them:", error);
80+
}
81+
}
82+
83+
// Listen for new deep link events while app is running
84+
await onOpenUrl(processUrls);
6085
}
6186
};
6287

src-frontend/components/marketplace/app-card.tsx

Lines changed: 17 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,8 @@
11
"use client";
22

33
import { motion } from "framer-motion";
4-
import { useState } from "react";
4+
import { useState, useEffect } from "react";
5+
import { useRouter } from "next/navigation";
56
import { Star } from "lucide-react";
67
import { Badge } from "@/components/ui/badge";
78
import { Button } from "@/components/ui/button";
@@ -11,13 +12,18 @@ import { InstallConfirmationDialog } from "./install-confirmation-dialog";
1112
interface AppCardProps {
1213
app: MarketplaceApp;
1314
onClick: (appId: string) => void;
14-
onActionClick?: (appId: string) => void;
15+
onAppInstalled?: () => void;
1516
}
1617

17-
export function AppCard({ app, onClick, onActionClick }: AppCardProps) {
18-
const [isProcessing, setIsProcessing] = useState(false);
18+
export function AppCard({ app, onClick, onAppInstalled }: AppCardProps) {
19+
const router = useRouter();
1920
const [isInstalled, setIsInstalled] = useState(app.installed);
2021
const [showInstallDialog, setShowInstallDialog] = useState(false);
22+
23+
// Sync with app.installed prop when it changes
24+
useEffect(() => {
25+
setIsInstalled(app.installed);
26+
}, [app.installed]);
2127
return (
2228
<>
2329
<motion.div
@@ -58,35 +64,21 @@ export function AppCard({ app, onClick, onActionClick }: AppCardProps) {
5864
size="sm"
5965
className={
6066
isInstalled
61-
? "transition-colors hover:border-red-500 hover:text-red-500"
67+
? "hover:border-primary hover:text-primary transition-colors"
6268
: ""
6369
}
64-
disabled={isProcessing}
6570
onClick={(e) => {
6671
e.stopPropagation();
6772
if (isInstalled) {
68-
if (onActionClick) {
69-
setIsProcessing(true);
70-
// Simulate installation/uninstallation process
71-
setTimeout(() => {
72-
setIsInstalled(!isInstalled);
73-
setIsProcessing(false);
74-
onActionClick(app.id);
75-
}, 2000);
76-
}
73+
// Navigate to the apps page to open the installed app
74+
router.push(`/apps?id=${app.id}`);
7775
} else {
7876
// Show install confirmation dialog
7977
setShowInstallDialog(true);
8078
}
8179
}}
8280
>
83-
{isProcessing
84-
? isInstalled
85-
? "Uninstalling..."
86-
: "Installing..."
87-
: isInstalled
88-
? "Uninstall"
89-
: "Install"}
81+
{isInstalled ? "Open App" : "Install"}
9082
</Button>
9183
</div>
9284
</div>
@@ -107,9 +99,9 @@ export function AppCard({ app, onClick, onActionClick }: AppCardProps) {
10799
onOpenChange={setShowInstallDialog}
108100
app={app}
109101
onConfirm={() => {
110-
setIsInstalled(!isInstalled);
111-
if (onActionClick) {
112-
onActionClick(app.id);
102+
setIsInstalled(true);
103+
if (onAppInstalled) {
104+
onAppInstalled();
113105
}
114106
setShowInstallDialog(false);
115107
}}

src-frontend/components/marketplace/app-detail.tsx

Lines changed: 110 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
"use client";
22

3-
import { useState, useEffect } from "react";
3+
import { useState, useEffect, useCallback } from "react";
44
import { ReviewDialog } from "./review-dialog";
55
import { ChevronLeft, Star, ExternalLink } from "lucide-react";
66
import ReactMarkdown from "react-markdown";
@@ -10,13 +10,15 @@ import { Button } from "@/components/ui/button";
1010
import { Badge } from "@/components/ui/badge";
1111
import { Toolbar } from "@/components/ui/toolbar";
1212
import { type MarketplaceApp, getMarketplaceApp } from "@/lib/api/marketplace";
13+
import { installApp, uninstallApp } from "@/lib/api/apps";
14+
import { toast } from "@/hooks/use-toast";
1315
import { useBreadcrumbStore } from "@/stores";
1416
import { MarketplaceBreadcrumb } from "./marketplace-breadcrumb";
1517

1618
import { getAssetPath } from "@/lib/utils";
1719
import Image from "next/image";
1820
import remarkGfm from "remark-gfm";
19-
import { useRouter } from "next/navigation";
21+
import { useRouter, useSearchParams } from "next/navigation";
2022
import { Separator } from "@radix-ui/react-separator";
2123

2224
interface AppDetailProps {
@@ -62,13 +64,102 @@ interface AppDetailProps {
6264

6365
export function AppDetail({ appId, onBack }: AppDetailProps) {
6466
const router = useRouter();
67+
const searchParams = useSearchParams();
6568
const [app, setApp] = useState<MarketplaceApp | null>(null);
6669
const [isLoading, setIsLoading] = useState(true);
6770
const [isProcessing, setIsProcessing] = useState(false);
71+
const [isUninstalling, setIsUninstalling] = useState(false);
6872
const [isInstalled, setIsInstalled] = useState(false);
6973
const [reviewDialogOpen, setReviewDialogOpen] = useState(false);
7074
const { setBreadcrumb, clearBreadcrumb } = useBreadcrumbStore();
7175

76+
const handleInstall = useCallback(async () => {
77+
if (!app || !app.repository) {
78+
toast({
79+
icon: "❌",
80+
title: "Installation Failed",
81+
description: "App repository URL is not available",
82+
variant: "destructive",
83+
});
84+
return;
85+
}
86+
87+
setIsProcessing(true);
88+
89+
try {
90+
// Show loading toast
91+
toast({
92+
icon: app.icon || "📦",
93+
title: "Installing app",
94+
description: `Installing ${app.name}...`,
95+
});
96+
97+
// Install app using repository URL
98+
await installApp({
99+
repoURL: app.repository,
100+
branch: app.branch || "main",
101+
force: false,
102+
});
103+
104+
// Success toast
105+
toast({
106+
icon: "🎉",
107+
title: "App Installed!",
108+
description: `${app.name} has been successfully installed.`,
109+
variant: "default",
110+
});
111+
112+
setIsInstalled(true);
113+
} catch (error) {
114+
console.error("Failed to install app:", error);
115+
116+
toast({
117+
icon: "❌",
118+
title: "Installation Failed",
119+
description:
120+
error instanceof Error ? error.message : "Unknown error occurred",
121+
variant: "destructive",
122+
});
123+
} finally {
124+
setIsProcessing(false);
125+
}
126+
}, [app]);
127+
128+
const handleUninstall = useCallback(async () => {
129+
if (!app) return;
130+
131+
setIsUninstalling(true);
132+
const appName = app.name;
133+
134+
try {
135+
toast({
136+
icon: "🗑️",
137+
title: "Uninstalling...",
138+
description: `Uninstalling ${appName}...`,
139+
});
140+
141+
await uninstallApp(app.id);
142+
143+
toast({
144+
icon: "🗑️",
145+
title: "App uninstalled",
146+
description: `${appName} has been uninstalled.`,
147+
});
148+
149+
setIsInstalled(false);
150+
} catch (error) {
151+
toast({
152+
icon: "❌",
153+
title: "Uninstall Failed",
154+
description:
155+
error instanceof Error ? error.message : "Unknown error occurred",
156+
variant: "destructive",
157+
});
158+
} finally {
159+
setIsUninstalling(false);
160+
}
161+
}, [app]);
162+
72163
useEffect(() => {
73164
const fetchApp = async () => {
74165
try {
@@ -85,21 +176,19 @@ export function AppDetail({ appId, onBack }: AppDetailProps) {
85176
fetchApp();
86177
}, [appId]);
87178

179+
useEffect(() => {
180+
const action = searchParams.get("action");
181+
if (action === "install" && app && !isInstalled && !isProcessing) {
182+
// Automatically trigger installation
183+
handleInstall();
184+
}
185+
}, [searchParams, app, isInstalled, isProcessing, handleInstall]);
186+
88187
useEffect(() => {
89188
setBreadcrumb(<MarketplaceBreadcrumb app={app} />);
90189
return () => clearBreadcrumb();
91190
}, [setBreadcrumb, clearBreadcrumb, app]);
92191

93-
const handleInstall = () => {
94-
setIsProcessing(true);
95-
96-
// Simulate installation
97-
setTimeout(() => {
98-
setIsProcessing(false);
99-
setIsInstalled(true);
100-
}, 1500);
101-
};
102-
103192
if (isLoading) {
104193
return (
105194
<div className="flex h-full items-center justify-center">
@@ -133,11 +222,19 @@ export function AppDetail({ appId, onBack }: AppDetailProps) {
133222
<Button
134223
variant="default"
135224
onClick={() => {
136-
router.push(`/apps/${app.id}`);
225+
router.push(`/apps/?id=${app.id}`);
137226
}}
138227
>
139228
Open App
140229
</Button>
230+
<Button
231+
variant="outline"
232+
className="border-destructive text-destructive hover:text-destructive hover:bg-none"
233+
onClick={handleUninstall}
234+
disabled={isUninstalling}
235+
>
236+
{isUninstalling ? "Uninstalling..." : "Uninstall App"}
237+
</Button>
141238
</div>
142239
)}
143240
</Toolbar>

0 commit comments

Comments
 (0)