Skip to content
Open
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
23 changes: 23 additions & 0 deletions app/api/auth/tiktok/callback/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import { NextRequest, NextResponse } from "next/server";

/**
* GET /api/auth/tiktok/callback
*
* Proxy endpoint that forwards TikTok OAuth callback to Composio.
* TikTok requires redirect URIs on verified domains, so we use our domain
* and forward the OAuth params to Composio's callback handler.
*/
export async function GET(request: NextRequest) {
const searchParams = request.nextUrl.searchParams;

// Forward all query params to Composio's callback
const composioCallbackUrl = new URL("https://backend.composio.dev/api/v1/auth-apps/add");

// Copy all params from TikTok's callback
searchParams.forEach((value, key) => {
composioCallbackUrl.searchParams.set(key, value);
});

// Redirect to Composio to complete the OAuth flow
return NextResponse.redirect(composioCallbackUrl.toString());
}
34 changes: 34 additions & 0 deletions components/ArtistConnectorSuccessBanner.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
import { CheckCircle } from "lucide-react";

interface ArtistConnectorSuccessBannerProps {
show: boolean;
toolkit: string | null;
}

/**
* Human-readable names for toolkit slugs.
*/
const TOOLKIT_NAMES: Record<string, string> = {
tiktok: "TikTok",
};

/**
* Success banner shown after completing an artist connector OAuth flow.
*/
export function ArtistConnectorSuccessBanner({
show,
toolkit,
}: ArtistConnectorSuccessBannerProps) {
if (!show || !toolkit) return null;

const toolkitName = TOOLKIT_NAMES[toolkit] || toolkit;

return (
<div className="fixed top-4 left-1/2 -translate-x-1/2 z-50 flex items-center gap-2 px-4 py-3 rounded-lg bg-green-50 dark:bg-green-950/80 border border-green-200 dark:border-green-800 shadow-lg animate-in slide-in-from-top-2">
<CheckCircle className="h-5 w-5 text-green-600 dark:text-green-400" />
<span className="text-sm font-medium text-green-800 dark:text-green-200">
{toolkitName} connected successfully!
</span>
</div>
);
}
163 changes: 163 additions & 0 deletions components/ArtistSetting/ConnectionsTab/TikTokCard.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,163 @@
"use client";

import { useState } from "react";
import { Loader2, MoreVertical, RefreshCw, Unlink, CheckCircle } from "lucide-react";
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuTrigger,
} from "@/components/ui/dropdown-menu";
import {
AlertDialog,
AlertDialogAction,
AlertDialogCancel,
AlertDialogContent,
AlertDialogDescription,
AlertDialogFooter,
AlertDialogHeader,
AlertDialogTitle,
} from "@/components/ui/alert-dialog";
import { ArtistConnectorInfo } from "@/hooks/useArtistConnectors";
import { cn } from "@/lib/utils";

interface TikTokCardProps {
connector: ArtistConnectorInfo | undefined;
onConnect: () => Promise<string | null>;
onDisconnect: (connectedAccountId: string) => Promise<boolean>;
}

const TikTokIcon = ({ className }: { className?: string }) => (
<svg
viewBox="0 0 24 24"
fill="currentColor"
className={cn("h-5 w-5", className)}
>
<path d="M19.59 6.69a4.83 4.83 0 0 1-3.77-4.25V2h-3.45v13.67a2.89 2.89 0 0 1-5.2 1.74 2.89 2.89 0 0 1 2.31-4.64 2.93 2.93 0 0 1 .88.13V9.4a6.84 6.84 0 0 0-1-.05A6.33 6.33 0 0 0 5 20.1a6.34 6.34 0 0 0 10.86-4.43v-7a8.16 8.16 0 0 0 4.77 1.52v-3.4a4.85 4.85 0 0 1-1-.1z" />
</svg>
);

const TikTokCard = ({ connector, onConnect, onDisconnect }: TikTokCardProps) => {
const [isConnecting, setIsConnecting] = useState(false);
const [isDisconnecting, setIsDisconnecting] = useState(false);
const [showDisconnectDialog, setShowDisconnectDialog] = useState(false);

const isConnected = connector?.isConnected ?? false;

const handleConnect = async () => {
setIsConnecting(true);
try {
const redirectUrl = await onConnect();
if (redirectUrl) {
window.location.href = redirectUrl;
}
} finally {
setIsConnecting(false);
}
};

const handleReconnect = async () => {
await handleConnect();
};

const handleDisconnect = async () => {
if (!connector?.connectedAccountId) return;

setIsDisconnecting(true);
try {
await onDisconnect(connector.connectedAccountId);
} finally {
setIsDisconnecting(false);
setShowDisconnectDialog(false);
}
};

return (
<>
<Card>
<CardHeader className="pb-3">
<div className="flex items-center gap-2">
<TikTokIcon className="text-black dark:text-white" />
<CardTitle className="text-base">TikTok</CardTitle>
</div>
<CardDescription>
Connect your TikTok account to enable AI access to your TikTok stats, profile info, and video data.
</CardDescription>
</CardHeader>
<CardContent>
{isConnecting ? (
<div className="flex items-center justify-center gap-2 px-4 py-2">
<Loader2 className="h-4 w-4 animate-spin" />
<span className="text-sm text-muted-foreground">Connecting...</span>
</div>
) : isConnected ? (
<div className="flex items-center justify-between">
<div className="flex items-center gap-2">
<span className="text-xs font-medium text-muted-foreground flex items-center gap-1">
Connected
<CheckCircle className="h-3.5 w-3.5 text-green-600 dark:text-green-500" />
</span>
</div>
<DropdownMenu>
<DropdownMenuTrigger asChild>
<button
className="p-1.5 rounded-md hover:bg-muted transition-colors"
title="TikTok options"
>
<MoreVertical className="h-4 w-4 text-muted-foreground" />
</button>
</DropdownMenuTrigger>
<DropdownMenuContent align="end">
<DropdownMenuItem onClick={handleReconnect} className="cursor-pointer">
<RefreshCw className="h-4 w-4 mr-2" />
Reconnect
</DropdownMenuItem>
<DropdownMenuItem
onClick={() => setShowDisconnectDialog(true)}
className="cursor-pointer text-red-600 dark:text-red-400"
>
<Unlink className="h-4 w-4 mr-2" />
Disconnect
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
</div>
) : (
<button
type="button"
onClick={handleConnect}
disabled={isConnecting}
className="w-full px-4 py-2 rounded-lg bg-black dark:bg-white text-white dark:text-black font-medium text-sm hover:opacity-90 transition-opacity disabled:opacity-50"
>
Connect TikTok
</button>
)}
</CardContent>
</Card>

<AlertDialog open={showDisconnectDialog} onOpenChange={setShowDisconnectDialog}>
<AlertDialogContent>
<AlertDialogHeader>
<AlertDialogTitle>Disconnect TikTok?</AlertDialogTitle>
<AlertDialogDescription>
This will remove the TikTok connection for this artist. The AI will no longer be able to access TikTok stats or data.
</AlertDialogDescription>
</AlertDialogHeader>
<AlertDialogFooter>
<AlertDialogCancel disabled={isDisconnecting}>Cancel</AlertDialogCancel>
<AlertDialogAction
onClick={handleDisconnect}
disabled={isDisconnecting}
className="bg-red-600 hover:bg-red-700"
>
{isDisconnecting ? "Disconnecting..." : "Disconnect"}
</AlertDialogAction>
</AlertDialogFooter>
</AlertDialogContent>
</AlertDialog>
</>
);
};

export default TikTokCard;
30 changes: 30 additions & 0 deletions components/ArtistSetting/ConnectionsTab/YouTubeCard.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
"use client";

import StandaloneYoutubeComponent from "../StandaloneYoutubeComponent";
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
import { Youtube } from "lucide-react";

interface YouTubeCardProps {
artistId: string;
}

const YouTubeCard = ({ artistId }: YouTubeCardProps) => {
return (
<Card>
<CardHeader className="pb-3">
<div className="flex items-center gap-2">
<Youtube className="h-5 w-5 text-red-600" />
<CardTitle className="text-base">YouTube</CardTitle>
</div>
<CardDescription>
Connect your YouTube channel to enable AI access to your channel data and analytics.
</CardDescription>
</CardHeader>
<CardContent>
<StandaloneYoutubeComponent artistAccountId={artistId} />
</CardContent>
</Card>
);
};

export default YouTubeCard;
66 changes: 66 additions & 0 deletions components/ArtistSetting/ConnectionsTab/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
"use client";

import { useArtistProvider } from "@/providers/ArtistProvider";
import { useArtistConnectors } from "@/hooks/useArtistConnectors";
import { Skeleton } from "@/components/ui/skeleton";
import YouTubeCard from "./YouTubeCard";
import TikTokCard from "./TikTokCard";

const ConnectionsTab = () => {
const { editableArtist } = useArtistProvider();
const artistId = editableArtist?.account_id;

const { connectors, isLoading, authorize, disconnect } = useArtistConnectors(artistId);

const tiktokConnector = connectors.find((c) => c.slug === "tiktok");

const handleTikTokConnect = async () => {
return authorize("tiktok");
};

const handleTikTokDisconnect = async (connectedAccountId: string) => {
return disconnect(connectedAccountId);
};

if (!artistId) {
return (
<div className="space-y-4">
<p className="text-sm text-muted-foreground">
No artist selected.
</p>
</div>
);
}

if (isLoading) {
return (
<div className="space-y-4">
<p className="text-sm text-muted-foreground">
Connect external accounts to enable additional AI capabilities.
</p>
<div className="grid gap-4">
<Skeleton className="h-32 w-full rounded-xl" />
<Skeleton className="h-32 w-full rounded-xl" />
</div>
</div>
);
}

return (
<div className="space-y-4">
<p className="text-sm text-muted-foreground">
Connect external accounts to enable additional AI capabilities.
</p>
<div className="grid gap-4">
<YouTubeCard artistId={artistId} />
<TikTokCard
connector={tiktokConnector}
onConnect={handleTikTokConnect}
onDisconnect={handleTikTokDisconnect}
/>
</div>
</div>
);
};

export default ConnectionsTab;
Loading