Skip to content
Draft
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
92 changes: 66 additions & 26 deletions apps/desktop/src/calendar/components/oauth/provider-content.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -17,12 +17,14 @@ import { useAuth } from "~/auth";
import { useBillingAccess } from "~/auth/billing";
import { useConnections } from "~/auth/useConnections";
import type { CalendarProvider } from "~/calendar/components/shared";
import { openIntegrationUrl } from "~/shared/integration";
import { useOAuthFlow } from "~/shared/hooks/useOAuthFlow";
import { buildIntegrationUrl } from "~/shared/integration";

export function OAuthProviderContent({ config }: { config: CalendarProvider }) {
const auth = useAuth();
const { isPro, upgradeToPro } = useBillingAccess();
const { data: connections, isError } = useConnections(isPro);
const { start: startOAuthFlow } = useOAuthFlow();
const providerConnections = useMemo(
() =>
connections?.filter(
Expand All @@ -31,16 +33,20 @@ export function OAuthProviderContent({ config }: { config: CalendarProvider }) {
[connections, config.nangoIntegrationId],
);

const handleAddAccount = useCallback(
() =>
openIntegrationUrl(
config.nangoIntegrationId,
undefined,
"connect",
"calendar",
),
[config.nangoIntegrationId],
);
const handleAddAccount = useCallback(async () => {
const url = await buildIntegrationUrl(
config.nangoIntegrationId,
undefined,
"connect",
"calendar",
);
if (!url) return;
await startOAuthFlow({
url,
title: `Connect ${config.displayName} Calendar`,
description: `Complete the connection in your browser, then return to Char.`,
});
}, [config.nangoIntegrationId, config.displayName, startOAuthFlow]);

if (!auth.session) {
return (
Expand Down Expand Up @@ -86,22 +92,34 @@ export function OAuthProviderContent({ config }: { config: CalendarProvider }) {
<ReconnectRequiredContent
key={connection.connection_id}
config={config}
onReconnect={() =>
openIntegrationUrl(
onReconnect={async () => {
const url = await buildIntegrationUrl(
config.nangoIntegrationId,
connection.connection_id,
"reconnect",
"calendar",
)
}
onDisconnect={() =>
openIntegrationUrl(
);
if (!url) return;
await startOAuthFlow({
url,
title: `Reconnect ${config.displayName} Calendar`,
description: `Complete the reconnection in your browser, then return to Char.`,
});
}}
onDisconnect={async () => {
const url = await buildIntegrationUrl(
config.nangoIntegrationId,
connection.connection_id,
"disconnect",
"calendar",
)
}
);
if (!url) return;
await startOAuthFlow({
url,
title: `Disconnect ${config.displayName} Calendar`,
description: `Complete the disconnection in your browser, then return to Char.`,
});
}}
errorDescription={connection.last_error_description ?? null}
/>
))}
Expand Down Expand Up @@ -183,6 +201,7 @@ function ConnectedContent({
}) {
const { groups, connectionSourceMap, handleToggle, isLoading } =
useOAuthCalendarSelection(config);
const { start: startOAuthFlow } = useOAuthFlow();

const groupsWithMenus = useMemo(
() =>
Expand All @@ -201,29 +220,50 @@ function ConnectedContent({
{
id: `reconnect-${connection.connection_id}`,
text: "Reconnect",
action: () =>
void openIntegrationUrl(
action: async () => {
const url = await buildIntegrationUrl(
config.nangoIntegrationId,
connection.connection_id,
"reconnect",
"calendar",
),
);
if (!url) return;
await startOAuthFlow({
url,
title: `Reconnect ${config.displayName} Calendar`,
description: `Complete the reconnection in your browser, then return to Char.`,
});
},
},
{
id: `disconnect-${connection.connection_id}`,
text: "Disconnect",
action: () =>
void openIntegrationUrl(
action: async () => {
const url = await buildIntegrationUrl(
config.nangoIntegrationId,
connection.connection_id,
"disconnect",
"calendar",
),
);
if (!url) return;
await startOAuthFlow({
url,
title: `Disconnect ${config.displayName} Calendar`,
description: `Complete the disconnection in your browser, then return to Char.`,
});
},
},
],
};
}),
[config.nangoIntegrationId, connectionSourceMap, connections, groups],
[
config.nangoIntegrationId,
config.displayName,
connectionSourceMap,
connections,
groups,
startOAuthFlow,
],
);

return (
Expand Down
27 changes: 26 additions & 1 deletion apps/desktop/src/routeTree.gen.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import { Route as rootRouteImport } from './routes/__root'
import { Route as AppRouteRouteImport } from './routes/app/route'
import { Route as AuthCallbackRouteImport } from './routes/auth/callback'
import { Route as AppControlRouteImport } from './routes/app/control'
import { Route as AppBrowserPendingRouteImport } from './routes/app/browser-pending'
import { Route as AppMainLayoutRouteImport } from './routes/app/main/_layout'
import { Route as AppMainLayoutIndexRouteImport } from './routes/app/main/_layout.index'

Expand All @@ -30,6 +31,11 @@ const AppControlRoute = AppControlRouteImport.update({
path: '/control',
getParentRoute: () => AppRouteRoute,
} as any)
const AppBrowserPendingRoute = AppBrowserPendingRouteImport.update({
id: '/browser-pending',
path: '/browser-pending',
getParentRoute: () => AppRouteRoute,
} as any)
const AppMainLayoutRoute = AppMainLayoutRouteImport.update({
id: '/main/_layout',
path: '/main',
Expand All @@ -43,20 +49,23 @@ const AppMainLayoutIndexRoute = AppMainLayoutIndexRouteImport.update({

export interface FileRoutesByFullPath {
'/app': typeof AppRouteRouteWithChildren
'/app/browser-pending': typeof AppBrowserPendingRoute
'/app/control': typeof AppControlRoute
'/auth/callback': typeof AuthCallbackRoute
'/app/main': typeof AppMainLayoutRouteWithChildren
'/app/main/': typeof AppMainLayoutIndexRoute
}
export interface FileRoutesByTo {
'/app': typeof AppRouteRouteWithChildren
'/app/browser-pending': typeof AppBrowserPendingRoute
'/app/control': typeof AppControlRoute
'/auth/callback': typeof AuthCallbackRoute
'/app/main': typeof AppMainLayoutIndexRoute
}
export interface FileRoutesById {
__root__: typeof rootRouteImport
'/app': typeof AppRouteRouteWithChildren
'/app/browser-pending': typeof AppBrowserPendingRoute
'/app/control': typeof AppControlRoute
'/auth/callback': typeof AuthCallbackRoute
'/app/main/_layout': typeof AppMainLayoutRouteWithChildren
Expand All @@ -66,15 +75,22 @@ export interface FileRouteTypes {
fileRoutesByFullPath: FileRoutesByFullPath
fullPaths:
| '/app'
| '/app/browser-pending'
| '/app/control'
| '/auth/callback'
| '/app/main'
| '/app/main/'
fileRoutesByTo: FileRoutesByTo
to: '/app' | '/app/control' | '/auth/callback' | '/app/main'
to:
| '/app'
| '/app/browser-pending'
| '/app/control'
| '/auth/callback'
| '/app/main'
id:
| '__root__'
| '/app'
| '/app/browser-pending'
| '/app/control'
| '/auth/callback'
| '/app/main/_layout'
Expand Down Expand Up @@ -109,6 +125,13 @@ declare module '@tanstack/react-router' {
preLoaderRoute: typeof AppControlRouteImport
parentRoute: typeof AppRouteRoute
}
'/app/browser-pending': {
id: '/app/browser-pending'
path: '/browser-pending'
fullPath: '/app/browser-pending'
preLoaderRoute: typeof AppBrowserPendingRouteImport
parentRoute: typeof AppRouteRoute
}
'/app/main/_layout': {
id: '/app/main/_layout'
path: '/main'
Expand Down Expand Up @@ -139,11 +162,13 @@ const AppMainLayoutRouteWithChildren = AppMainLayoutRoute._addFileChildren(
)

interface AppRouteRouteChildren {
AppBrowserPendingRoute: typeof AppBrowserPendingRoute
AppControlRoute: typeof AppControlRoute
AppMainLayoutRoute: typeof AppMainLayoutRouteWithChildren
}

const AppRouteRouteChildren: AppRouteRouteChildren = {
AppBrowserPendingRoute: AppBrowserPendingRoute,
AppControlRoute: AppControlRoute,
AppMainLayoutRoute: AppMainLayoutRouteWithChildren,
}
Expand Down
54 changes: 54 additions & 0 deletions apps/desktop/src/routes/app/browser-pending.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
import { createFileRoute, useNavigate } from "@tanstack/react-router";
import { useCallback } from "react";

import { commands as windowsCommands } from "@hypr/plugin-windows";

export const Route = createFileRoute("/app/browser-pending")({
validateSearch: (search): { title: string; description: string } => ({
title: String((search as { title?: string }).title ?? ""),
description: String((search as { description?: string }).description ?? ""),
}),
component: BrowserPendingRoute,
});

function BrowserPendingRoute() {
const { title, description } = Route.useSearch();
const navigate = useNavigate();

const handleCancel = useCallback(async () => {
await windowsCommands.windowRestoreFrameAnimated({ type: "main" });
await navigate({ to: "/app/main" });
}, [navigate]);

return (
<div
data-tauri-drag-region
className="flex h-full flex-col items-center justify-center gap-6 p-8 select-none"
>
<img
src="/assets/char-logo-icon-black.svg"
alt=""
className="h-10 w-10"
/>

<div className="flex flex-col items-center gap-2 text-center">
<h2 className="font-serif text-lg font-semibold">{title}</h2>
<p className="text-sm text-neutral-500">{description}</p>
</div>

<div className="flex items-center gap-2">
<div className="h-1.5 w-1.5 animate-bounce rounded-full bg-neutral-400 [animation-delay:-0.3s]" />
<div className="h-1.5 w-1.5 animate-bounce rounded-full bg-neutral-400 [animation-delay:-0.15s]" />
<div className="h-1.5 w-1.5 animate-bounce rounded-full bg-neutral-400" />
</div>

<button
type="button"
onClick={() => void handleCancel()}
className="text-xs text-neutral-400 underline-offset-2 transition-colors hover:text-neutral-600 hover:underline"
>
Cancel
</button>
</div>
);
}
Loading
Loading