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
53 changes: 38 additions & 15 deletions components/ArtistSetting/ArtistConnectorsTab.tsx
Original file line number Diff line number Diff line change
@@ -1,8 +1,11 @@
"use client";

import { useMemo } from "react";
import { useQuery } from "@tanstack/react-query";
import { useConnectors } from "@/hooks/useConnectors";
import { ALLOWED_ARTIST_CONNECTORS } from "@/lib/composio/allowedArtistConnectors";
import { getArtistSocials } from "@/lib/api/artist/getArtistSocials";
import { matchSocialToConnector } from "@/lib/composio/matchSocialToConnector";
import { ConnectorCard } from "@/components/ConnectorsPage/ConnectorCard";
import { Loader2, Plug } from "lucide-react";

Expand All @@ -14,15 +17,29 @@ interface ArtistConnectorsTabProps {
* Connectors tab content for the Artist Settings modal.
* Reuses ConnectorCard from the user-level ConnectorsPage (DRY).
*/
export function ArtistConnectorsTab({ artistAccountId }: ArtistConnectorsTabProps) {
const config = useMemo(() => ({
accountId: artistAccountId,
allowedSlugs: [...ALLOWED_ARTIST_CONNECTORS],
callbackUrl: `${window.location.origin}${window.location.pathname}?artist_connected=true&artist_id=${artistAccountId}`,
}), [artistAccountId]);
export function ArtistConnectorsTab({
artistAccountId,
}: ArtistConnectorsTabProps) {
const config = useMemo(
() => ({
accountId: artistAccountId,
allowedSlugs: [...ALLOWED_ARTIST_CONNECTORS],
callbackUrl: `${window.location.origin}${window.location.pathname}?artist_connected=true&artist_id=${artistAccountId}`,
}),
[artistAccountId],
Comment on lines +23 to +29
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

🌐 Web query:

In Next.js App Router, can a "use client"component safely readwindowduring render, or must browser APIs be accessed after mount (for example inuseEffect or event handlers)?

💡 Result:

In the App Router, a "use client" component still participates in the initial server render to produce HTML (“Client Components and the RSC Payload are used to prerender HTML”) and is only made interactive after hydration. So it is not safe to read window (or other browser-only APIs) during render, because that render can run in a non-browser environment. Use browser APIs after mount (e.g., in useEffect/useLayoutEffect) or in event handlers (which only run in the browser). [1][2]

If you truly need code to run only in the browser even for the initial render, you must opt out of SSR for that component (e.g., next/dynamic with ssr: false). [1]

Sources
[1] Next.js Docs — Server and Client Components (Last updated Mar 31, 2026) (nextjs.org)
[2] React Docs — useEffect (“Effects only run on the client. They don’t run during server rendering.”) (react.dev)

Citations:


Move callback URL construction out of the render path

useMemo runs during the render phase, and Next.js client components are still prerendered on the server before hydration. Reading window.location here will throw in the server environment and block rendering. Build the callback URL in useEffect or derive it from Next.js router state instead.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@components/ArtistSetting/ArtistConnectorsTab.tsx` around lines 23 - 29, The
callback URL is being built from window.location inside the useMemo in
ArtistConnectorsTab (config -> callbackUrl), which runs during render and will
break server-side rendering; instead, stop reading window in the render path by
computing callbackUrl inside a useEffect (or derive it from Next.js router
state) and store it in state (e.g., callbackUrlState), then include that state
in the useMemo dependencies so config is created from artistAccountId,
ALLOWED_ARTIST_CONNECTORS and the callbackUrlState; update references to use the
new state-backed callback URL.

);
const { connectors, isLoading, error, authorize, disconnect } =
useConnectors(config);

const { data: socialsData } = useQuery({
queryKey: ["artistSocials", artistAccountId],
queryFn: () => getArtistSocials(artistAccountId),
enabled: !!artistAccountId,
staleTime: 1000 * 60 * 5,
});
Comment on lines +34 to +39
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This solution won't work currently. The getArtistSocials does not include data from artist connections. We should find a way to update the API to return the data you're expecting.

  • actual: API for getArtistSocials does not include connections in the response.
  • required: update the docs and API so getArtistSocials also returns the equivalent data from composio to augment the existing supabase data.


const socials = socialsData?.socials ?? [];

if (isLoading) {
return (
<div className="flex items-center justify-center py-12">
Expand Down Expand Up @@ -51,17 +68,23 @@ export function ArtistConnectorsTab({ artistAccountId }: ArtistConnectorsTabProp
return (
<div className="space-y-3 py-2">
<p className="text-sm text-muted-foreground">
Connect third-party services so the AI agent can take actions on behalf of this artist.
Connect third-party services so the AI agent can take actions on behalf
of this artist.
</p>
<div className="space-y-2">
{connectors.map((connector) => (
<ConnectorCard
key={connector.slug}
connector={connector}
onConnect={authorize}
onDisconnect={disconnect}
/>
))}
{connectors.map((connector) => {
const social = matchSocialToConnector(connector.slug, socials);
return (
<ConnectorCard
key={connector.slug}
connector={connector}
onConnect={authorize}
onDisconnect={disconnect}
socialAvatar={social?.avatar}
socialUsername={social?.username}
/>
);
})}
</div>
</div>
);
Expand Down
34 changes: 30 additions & 4 deletions components/ConnectorsPage/ConnectorCard.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@ interface ConnectorCardProps {
connector: ConnectorInfo;
onConnect: (slug: string) => Promise<string | null>;
onDisconnect: (connectedAccountId: string) => Promise<boolean>;
socialAvatar?: string | null;
socialUsername?: string | null;
}

/**
Expand All @@ -21,6 +23,8 @@ export function ConnectorCard({
connector,
onConnect,
onDisconnect,
socialAvatar,
socialUsername,
}: ConnectorCardProps) {
const { isConnecting, isDisconnecting, handleConnect, handleDisconnect } =
useConnectorHandlers({
Expand All @@ -30,19 +34,41 @@ export function ConnectorCard({
onDisconnect,
});
const meta = getConnectorMeta(connector.slug);
const showSocialProfile =
connector.isConnected && (socialAvatar || socialUsername);

return (
<div className="group flex items-center gap-4 p-4 rounded-xl border border-border bg-card transition-all duration-200 hover:border-muted-foreground/30 hover:shadow-sm">
<div className="shrink-0 p-2.5 rounded-xl transition-colors bg-muted/50 group-hover:bg-muted">
{getConnectorIcon(connector.slug, 22)}
<div className="shrink-0 relative">
{showSocialProfile && socialAvatar ? (
<div className="relative">
{/* eslint-disable-next-line @next/next/no-img-element */}
<img
src={socialAvatar}
alt={socialUsername || "Profile"}
width={40}
height={40}
className="rounded-xl object-cover"
/>
<div className="absolute -bottom-1 -right-1 p-0.5 rounded-md bg-card">
{getConnectorIcon(connector.slug, 14)}
</div>
</div>
) : (
<div className="p-2.5 rounded-xl transition-colors bg-muted/50 group-hover:bg-muted">
{getConnectorIcon(connector.slug, 22)}
</div>
)}
Comment on lines +42 to +61
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Open closed principle

  • actual: pfp component code added inline to ConnectorCard.
  • required: new component file for the ConnectorPfp.

</div>

<div className="flex-1 min-w-0">
<h3 className="font-medium text-foreground truncate">
{formatConnectorName(connector.name, connector.slug)}
</h3>
<p className="text-sm text-muted-foreground truncate">
{meta.description}
{showSocialProfile && socialUsername
? `@${socialUsername}`
: meta.description}
</p>
</div>

Expand All @@ -53,7 +79,7 @@ export function ConnectorCard({
onReconnect={handleConnect}
onDisconnect={handleDisconnect}
/>
) : (
) : (
<ConnectorEnableButton
isConnecting={isConnecting}
onClick={handleConnect}
Expand Down
18 changes: 18 additions & 0 deletions lib/composio/matchSocialToConnector.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import type { Social } from "@/lib/api/artist/getArtistSocials";

const CONNECTOR_SOCIAL_DOMAINS: Record<string, string> = {
instagram: "instagram.com",
tiktok: "tiktok.com",
};
Comment on lines +3 to +6
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

KISS - why is this mapping needed? doesn't the Social type already include a platform tag?


/**
* Find the matching social profile for a connector slug.
*/
export function matchSocialToConnector(
slug: string,
socials: Social[],
): Social | undefined {
const domain = CONNECTOR_SOCIAL_DOMAINS[slug];
if (!domain) return undefined;
return socials.find((s) => s.profile_url?.toLowerCase().includes(domain));
Copy link
Copy Markdown

@cubic-dev-ai cubic-dev-ai bot Apr 6, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2: Match against the URL hostname instead of string .includes() to avoid false-positive connector/social matches.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At lib/composio/matchSocialToConnector.ts, line 17:

<comment>Match against the URL hostname instead of string `.includes()` to avoid false-positive connector/social matches.</comment>

<file context>
@@ -0,0 +1,18 @@
+): Social | undefined {
+  const domain = CONNECTOR_SOCIAL_DOMAINS[slug];
+  if (!domain) return undefined;
+  return socials.find((s) => s.profile_url?.toLowerCase().includes(domain));
+}
</file context>
Fix with Cubic

Comment on lines +15 to +17
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Match on the hostname, not a substring.

includes(domain) also matches unrelated URLs like https://notinstagram.com/... or any URL that only mentions the platform in its path/query, so this can bind the wrong social record to a connector.

🔧 Safer match
 export function matchSocialToConnector(
   slug: string,
   socials: Social[],
 ): Social | undefined {
   const domain = CONNECTOR_SOCIAL_DOMAINS[slug];
   if (!domain) return undefined;
-  return socials.find((s) => s.profile_url?.toLowerCase().includes(domain));
+  return socials.find((social) => {
+    try {
+      const hostname = new URL(social.profile_url).hostname.toLowerCase();
+      return hostname === domain || hostname.endsWith(`.${domain}`);
+    } catch {
+      return false;
+    }
+  });
 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@lib/composio/matchSocialToConnector.ts` around lines 15 - 17, The current
matching uses profile_url?.toLowerCase().includes(domain) which can false-match
substrings; instead parse profile_url with the URL constructor inside a
try/catch and compare the URL.hostname to the expected domain from
CONNECTOR_SOCIAL_DOMAINS[slug]: accept exact equality or hostname.endsWith('.' +
domain) to allow subdomains (e.g., www.instagram.com), and return undefined on
parse errors or non-matches; update the socials.find logic that references
profile_url to perform this hostname-based check using slug and
CONNECTOR_SOCIAL_DOMAINS.

}
Loading