-
Notifications
You must be signed in to change notification settings - Fork 10
feat: add Google Drive integration via Composio #1454
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,18 @@ | ||
| import { Loader } from "lucide-react"; | ||
|
|
||
| interface ComposioLoginLoadingProps { | ||
| toolkitName: string; | ||
| } | ||
|
|
||
| export function ComposioLoginLoading({ | ||
| toolkitName, | ||
| }: ComposioLoginLoadingProps) { | ||
| return ( | ||
| <div className="flex items-center gap-1 py-1 px-2 bg-muted/50 rounded-sm border border-border w-fit text-xs text-muted-foreground"> | ||
| <Loader className="h-3 w-3 animate-spin text-foreground" /> | ||
| <span>Initializing {toolkitName} login...</span> | ||
| </div> | ||
| ); | ||
| } | ||
|
|
||
| export default ComposioLoginLoading; |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,68 @@ | ||
| "use client"; | ||
|
|
||
| import React from "react"; | ||
| import { LucideIcon } from "lucide-react"; | ||
| import { Button } from "@/components/ui/button"; | ||
| import { useUserProvider } from "@/providers/UserProvder"; | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🧩 Analysis chain🏁 Script executed: # Find provider files with similar names
fd -i "userprov" providers/Repository: Recoupable-com/Recoup-Chat Length of output: 95 🏁 Script executed: # Also check the full path for the file in question
fd -t f "ComposioLoginResult.tsx"Repository: Recoupable-com/Recoup-Chat Length of output: 130 🏁 Script executed: # Search for UserProvider/UserProvder in providers directory
rg "UserProv" providers/ -t ts -t tsxRepository: Recoupable-com/Recoup-Chat Length of output: 97 🏁 Script executed: # Let's read the ComposioLoginResult.tsx file to verify other issues
cat -n components/VercelChat/tools/composio/ComposioLoginResult.tsxRepository: Recoupable-com/Recoup-Chat Length of output: 2411 🏁 Script executed: # Also verify the content of the provider file
cat -n providers/UserProvder.tsxRepository: Recoupable-com/Recoup-Chat Length of output: 950 Remove hardcoded "Google" text and use The notice at line 60-62 assumes all toolkits redirect to Google, which breaks reusability for non-Google Composio integrations. Make the redirect service dynamic via props or pass it through context. Additionally, line 46 uses string concatenation for className instead of the Note: The import path 🤖 Prompt for AI Agents |
||
| import AccountIdDisplay from "@/components/ArtistSetting/AccountIdDisplay"; | ||
|
|
||
| interface ComposioLoginResultProps { | ||
| toolkitName: string; | ||
| description: string; | ||
| icon: LucideIcon; | ||
| buttonColor: string; | ||
| isLoading: boolean; | ||
| onLogin: () => void; | ||
| } | ||
|
|
||
| export function ComposioLoginResult({ | ||
| toolkitName, | ||
| description, | ||
| icon: Icon, | ||
| buttonColor, | ||
| isLoading, | ||
| onLogin, | ||
| }: ComposioLoginResultProps) { | ||
| const { userData } = useUserProvider(); | ||
| const accountId = userData?.account_id; | ||
|
|
||
| return ( | ||
| <div className="flex flex-col space-y-3 p-4 rounded-lg bg-muted border border-border my-2 max-w-md"> | ||
| <div className="flex items-center space-x-2"> | ||
| <Icon className="h-5 w-5 text-muted-foreground" /> | ||
| <span className="font-medium text-foreground"> | ||
| {toolkitName} Access Required | ||
| </span> | ||
| </div> | ||
|
|
||
| <p className="text-sm text-muted-foreground"> | ||
| Connect your {toolkitName} account to enable {description}. | ||
| </p> | ||
|
|
||
| {accountId && <AccountIdDisplay accountId={accountId} label="Account" />} | ||
|
|
||
| <Button | ||
| onClick={onLogin} | ||
| className={`w-full ${buttonColor} text-white`} | ||
| size="sm" | ||
| disabled={isLoading || !accountId} | ||
| > | ||
| {isLoading ? ( | ||
| <span>Connecting...</span> | ||
| ) : ( | ||
| <> | ||
| <Icon className="h-4 w-4 mr-2" /> | ||
| Connect {toolkitName} | ||
| </> | ||
| )} | ||
| </Button> | ||
|
|
||
| <p className="text-xs text-muted-foreground text-center"> | ||
| You'll be redirected to Google to authorize access to your{" "} | ||
| {toolkitName} for this account. | ||
| </p> | ||
| </div> | ||
| ); | ||
| } | ||
|
|
||
| export default ComposioLoginResult; | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,7 @@ | ||
| import { ComposioLoginLoading } from "../composio/ComposioLoginLoading"; | ||
|
|
||
| export function GoogleDriveLoginLoading() { | ||
| return <ComposioLoginLoading toolkitName="Google Drive" />; | ||
| } | ||
|
|
||
| export default GoogleDriveLoginLoading; |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,22 @@ | ||
| "use client"; | ||
|
|
||
| import { HardDrive } from "lucide-react"; | ||
| import { useGoogleDriveLogin } from "@/hooks/useGoogleDriveLogin"; | ||
| import { ComposioLoginResult } from "../composio/ComposioLoginResult"; | ||
|
|
||
| export function GoogleDriveLoginResult() { | ||
| const { isLoading, handleLogin } = useGoogleDriveLogin(); | ||
|
|
||
| return ( | ||
| <ComposioLoginResult | ||
| toolkitName="Google Drive" | ||
| description="uploading, downloading, and managing files" | ||
| icon={HardDrive} | ||
| buttonColor="bg-blue-600 hover:bg-blue-700" | ||
| isLoading={isLoading} | ||
| onLogin={handleLogin} | ||
| /> | ||
| ); | ||
| } | ||
|
|
||
| export default GoogleDriveLoginResult; |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,12 +1,7 @@ | ||
| import { Loader } from "lucide-react"; | ||
| import { ComposioLoginLoading } from "../composio/ComposioLoginLoading"; | ||
|
|
||
| export function GoogleSheetsLoginLoading() { | ||
| return ( | ||
| <div className="flex items-center gap-1 py-1 px-2 bg-muted/50 rounded-sm border border-border w-fit text-xs text-muted-foreground"> | ||
| <Loader className="h-3 w-3 animate-spin text-foreground" /> | ||
| <span>Initializing Google Sheets login...</span> | ||
| </div> | ||
| ); | ||
| return <ComposioLoginLoading toolkitName="Google Sheets" />; | ||
| } | ||
|
|
||
| export default GoogleSheetsLoginLoading; |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,59 @@ | ||
| import { useState } from "react"; | ||
| import { useUserProvider } from "@/providers/UserProvder"; | ||
| import { useVercelChatContext } from "@/providers/VercelChatProvider"; | ||
| import getLatestUserMessageText from "@/lib/messages/getLatestUserMessageText"; | ||
| import { fetchConnectedAccountsRefresh } from "@/lib/composio/fetchConnectedAccountsRefresh"; | ||
| import { | ||
| ComposioToolkitKey, | ||
| getToolkitConfig, | ||
| } from "@/lib/composio/toolkits"; | ||
| import { toast } from "sonner"; | ||
|
|
||
| /** | ||
| * Hook for handling Composio toolkit login. | ||
| * | ||
| * @param toolkitKey - The toolkit to login to (e.g., "GOOGLE_SHEETS", "GOOGLE_DRIVE") | ||
| */ | ||
| export function useComposioLogin(toolkitKey: ComposioToolkitKey) { | ||
| const [isLoading, setIsLoading] = useState(false); | ||
| const { userData } = useUserProvider(); | ||
| const { messages } = useVercelChatContext(); | ||
| const accountId = userData?.account_id as string; | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Add null safety for accountId. The 🔒 Proposed fix with early validation const { userData } = useUserProvider();
const { messages } = useVercelChatContext();
- const accountId = userData?.account_id as string;
+ const accountId = userData?.account_id;
const config = getToolkitConfig(toolkitKey);
const handleLogin = async () => {
+ if (!accountId) {
+ toast.error("Account information is missing. Please refresh the page.");
+ return;
+ }
+
const latestUserMessageText = getLatestUserMessageText(messages);Additionally, consider reflecting this state in the consuming components by including 🤖 Prompt for AI Agents |
||
| const config = getToolkitConfig(toolkitKey); | ||
|
|
||
| const handleLogin = async () => { | ||
| const latestUserMessageText = getLatestUserMessageText(messages); | ||
| // Use current origin so it works in both local dev and production | ||
| const baseUrl = typeof window !== "undefined" ? window.location.origin : "https://chat.recoupable.com"; | ||
|
Comment on lines
+26
to
+27
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. KISS principle - why not just always use |
||
| const redirectUrl = `${baseUrl}?q=${encodeURIComponent( | ||
| latestUserMessageText | ||
| )}`; | ||
|
|
||
| setIsLoading(true); | ||
|
|
||
| try { | ||
| const data = await fetchConnectedAccountsRefresh(toolkitKey, { | ||
| accountId, | ||
| redirectUrl, | ||
| }); | ||
|
|
||
| if (data.redirect_url) { | ||
| // Use location.href instead of window.open to avoid popup blocker | ||
| // since this is called after an async operation | ||
| window.location.href = data.redirect_url; | ||
| } | ||
| } catch (error) { | ||
| toast.error( | ||
| `Failed to initiate ${config.name} login. Please try again.` | ||
| ); | ||
| } finally { | ||
| setIsLoading(false); | ||
| } | ||
| }; | ||
|
|
||
| return { | ||
| isLoading, | ||
| handleLogin, | ||
| toolkitName: config.name, | ||
| }; | ||
| } | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. KISS principle - is this hook really necessary?
|
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,8 @@ | ||
| import { useComposioLogin } from "./useComposioLogin"; | ||
|
|
||
| /** | ||
| * Hook for Google Drive login. | ||
| */ | ||
| export function useGoogleDriveLogin() { | ||
| return useComposioLogin("GOOGLE_DRIVE"); | ||
| } |
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. KISS principle - is this hook really necessary?
|
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,43 +1,8 @@ | ||
| import { useState } from "react"; | ||
| import { useUserProvider } from "@/providers/UserProvder"; | ||
| import { useVercelChatContext } from "@/providers/VercelChatProvider"; | ||
| import getLatestUserMessageText from "@/lib/messages/getLatestUserMessageText"; | ||
| import { fetchConnectedAccountsRefresh } from "@/lib/composio/googleSheets/fetchConnectedAccountsRefresh"; | ||
| import { toast } from "sonner"; | ||
| import { useComposioLogin } from "./useComposioLogin"; | ||
|
|
||
| /** | ||
| * Hook for Google Sheets login. | ||
| */ | ||
| export function useGoogleSheetsLogin() { | ||
| const [isLoading, setIsLoading] = useState(false); | ||
| const { userData } = useUserProvider(); | ||
| const { messages } = useVercelChatContext(); | ||
| const accountId = userData?.account_id as string; | ||
|
|
||
| const handleLogin = async () => { | ||
| const latestUserMessageText = getLatestUserMessageText(messages); | ||
| const redirectUrl = `https://chat.recoupable.com?q=${encodeURIComponent( | ||
| latestUserMessageText | ||
| )}`; | ||
|
|
||
| setIsLoading(true); | ||
|
|
||
| try { | ||
| const data = await fetchConnectedAccountsRefresh({ | ||
| accountId, | ||
| redirectUrl, | ||
| }); | ||
|
|
||
| if (data.redirect_url) { | ||
| window.open(data.redirect_url, "_blank", "noopener,noreferrer"); | ||
| } | ||
| } catch (error) { | ||
| console.error("Error initiating Google Sheets login:", error); | ||
| toast.error("Failed to initiate Google Sheets login. Please try again."); | ||
| } finally { | ||
| setIsLoading(false); | ||
| } | ||
| }; | ||
|
|
||
| return { | ||
| isLoading, | ||
| handleLogin, | ||
| }; | ||
| return useComposioLogin("GOOGLE_SHEETS"); | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
DRY principle - This component should already exist from google sheets login.