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
4 changes: 2 additions & 2 deletions apps/desktop/src/auth/useConnections.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import { useAuth } from "./context";

import { env } from "~/env";

export function useConnections() {
export function useConnections({ enabled = true }: { enabled?: boolean } = {}) {
const auth = useAuth();
const userId = auth?.session?.user.id;

Expand All @@ -25,6 +25,6 @@ export function useConnections() {
}
return data?.connections ?? [];
},
enabled: !!userId,
enabled: !!userId && enabled,
});
}
38 changes: 17 additions & 21 deletions apps/desktop/src/calendar/components/oauth/provider-content.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,11 +13,15 @@ import { useBillingAccess } from "~/auth/billing";
import { useConnections } from "~/auth/useConnections";
import type { CalendarProvider } from "~/calendar/components/shared";
import { buildWebAppUrl } from "~/shared/utils";
import { useTabs } from "~/store/zustand/tabs";

export function OAuthProviderContent({ config }: { config: CalendarProvider }) {
const auth = useAuth();
const billing = useBillingAccess();
const { data: connections, isError } = useConnections();
const openNew = useTabs((state) => state.openNew);
const { data: connections, isError } = useConnections({
enabled: billing.isPro,
});
const connection = connections?.find(
(c) => c.integration_id === config.nangoIntegrationId,
);
Expand Down Expand Up @@ -118,16 +122,6 @@ export function OAuthProviderContent({ config }: { config: CalendarProvider }) {
);
}

if (isError) {
return (
<div className="px-1 pt-1 pb-2">
<span className="text-xs text-red-600">
Failed to load integration status
</span>
</div>
);
}

if (!auth.session) {
const connectButton = (
<button
Expand Down Expand Up @@ -158,27 +152,29 @@ export function OAuthProviderContent({ config }: { config: CalendarProvider }) {
if (!billing.isPro) {
return (
<div className="flex flex-col gap-1.5 px-1 pt-1 pb-2">
<div className="flex items-center gap-1.5">
<span className="rounded border border-amber-200 bg-amber-50 px-1.5 py-0.5 text-[10px] font-medium text-amber-700">
Pro
</span>
<span className="text-xs text-neutral-500">
Required to connect {config.displayName} Calendar
</span>
</div>
<button
onClick={() => billing.upgradeToPro()}
onClick={() => openNew({ type: "settings" })}
className={cn([
"flex h-9 w-full cursor-pointer items-center justify-center rounded-lg text-sm font-medium transition-all",
"bg-neutral-900 text-white hover:bg-neutral-800 active:scale-[98%]",
])}
>
Upgrade to Pro
Pro Plan Required
</button>
</div>
);
}

if (isError) {
return (
<div className="px-1 pt-1 pb-2">
<span className="text-xs text-red-600">
Failed to load integration status
</span>
</div>
);
}

return (
<div className="px-1 pt-1 pb-2">
<button
Expand Down
21 changes: 21 additions & 0 deletions apps/web/src/hooks/use-api-client.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import { useCallback } from "react";

import { createClient } from "@hypr/api-client/client";

import { env } from "@/env";
import { getAccessToken } from "@/functions/access-token";

export type ApiClient = ReturnType<typeof createClient>;

export function useApiClient() {
const getClient = useCallback(async (): Promise<ApiClient> => {
const token = await getAccessToken();

return createClient({
baseUrl: env.VITE_API_URL,
headers: { Authorization: `Bearer ${token}` },
});
}, []);

return { getClient };
}
12 changes: 6 additions & 6 deletions apps/web/src/hooks/use-billing.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,21 +35,18 @@ function deriveFromStripe(
const DEFAULT_BILLING = deriveBillingInfo(null);

export function useBilling() {
const queryClient = useQueryClient();

const jwtQuery = useQuery({
queryKey: ["billing", "jwt"],
queryFn: async () => {
const token = await getAccessToken();
console.log("decodeJwtPayload", decodeJwtPayload(token));
return deriveBillingInfo(decodeJwtPayload(token));
},
retry: false,
});

const stripeQuery = useQuery({
queryKey: ["billing", "stripe"],
queryFn: async () => deriveFromStripe(await syncAfterSuccess()),
retry: false,
});

const billing: BillingInfo =
Expand All @@ -60,8 +57,11 @@ export function useBilling() {
const refreshBilling = useCallback(async () => {
const supabase = getSupabaseBrowserClient();
await supabase.auth.refreshSession();
await queryClient.invalidateQueries({ queryKey: ["billing"] });
}, [queryClient]);
console.log("refreshBilling");
await jwtQuery.refetch();
await stripeQuery.refetch();
console.log("refreshBilling done");
}, [jwtQuery, stripeQuery]);

return {
...billing,
Expand Down
14 changes: 6 additions & 8 deletions apps/web/src/hooks/use-connections.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,9 @@
import { useQuery } from "@tanstack/react-query";

import { listConnections } from "@hypr/api-client";
import { createClient } from "@hypr/api-client/client";

import { env } from "@/env";
import { getAccessToken } from "@/functions/access-token";
import { useApiClient } from "@/hooks/use-api-client";

function decodeUserId(token: string): string {
const payload = JSON.parse(atob(token.split(".")[1])) as { sub?: string };
Expand All @@ -14,7 +13,9 @@ function decodeUserId(token: string): string {
return payload.sub;
}

export function useConnections() {
export function useConnections({ enabled = true }: { enabled?: boolean } = {}) {
const { getClient } = useApiClient();

const authQuery = useQuery({
queryKey: ["integration-status", "auth"],
queryFn: async () => {
Expand All @@ -29,12 +30,9 @@ export function useConnections() {

return useQuery({
queryKey: ["integration-status", authQuery.data?.userId],
enabled: !!authQuery.data?.userId,
enabled: !!authQuery.data?.userId && enabled,
queryFn: async () => {
const client = createClient({
baseUrl: env.VITE_API_URL,
headers: { Authorization: `Bearer ${authQuery.data?.token}` },
});
const client = await getClient();
const { data, error } = await listConnections({ client });
if (error) {
throw new Error("Failed to load integrations");
Expand Down
31 changes: 19 additions & 12 deletions apps/web/src/routes/_view/app/-account-integrations.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,13 @@ const INTEGRATIONS = [
export function IntegrationsSettingsCard() {
const navigate = useNavigate();
const billing = useBilling();
const { data: connections, isLoading, isError } = useConnections();
const {
data: connections,
isLoading,
isError,
} = useConnections({
enabled: billing.isPro,
});

const getConnectionStatus = (integrationId: string) => {
return connections?.find((c) => c.integration_id === integrationId);
Expand Down Expand Up @@ -43,17 +49,18 @@ export function IntegrationsSettingsCard() {
Pro
</span>
)}
{isLoading ? (
<span className="text-xs text-neutral-400">Checking...</span>
) : isError ? (
<span className="rounded-full bg-red-50 px-2 py-0.5 text-xs text-red-600">
Check failed
</span>
) : isConnected ? (
<span className="rounded-full bg-green-50 px-2 py-0.5 text-xs text-green-600">
Connected
</span>
) : null}
{billing.isPro &&
(isLoading ? (
<span className="text-xs text-neutral-400">Checking...</span>
) : isError ? (
<span className="rounded-full bg-red-50 px-2 py-0.5 text-xs text-red-600">
Check failed
</span>
) : isConnected ? (
<span className="rounded-full bg-green-50 px-2 py-0.5 text-xs text-green-600">
Connected
</span>
) : null)}
</div>
{!billing.isPro && !isConnected ? (
<Link
Expand Down
3 changes: 2 additions & 1 deletion apps/web/src/routes/_view/app/-account-settings.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -98,7 +98,8 @@ export function AccountSettingsCard() {

<div className="flex items-center justify-between border-t border-neutral-100 p-4">
<div className="text-sm">
Current plan: <span className="font-medium">{planDisplay}</span>
Current plan: <span className="font-medium">{planDisplay}</span>{" "}
<button onClick={() => billing.refreshBilling()}>Refresh</button>
</div>
{renderPlanButton()}
</div>
Expand Down
56 changes: 56 additions & 0 deletions apps/web/src/routes/_view/app/-integration-ui.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
import type { ButtonHTMLAttributes, ReactNode } from "react";

import { cn } from "@hypr/utils";

const BUTTON_BASE =
"flex h-12 w-full items-center justify-center gap-2 rounded-full text-base font-medium transition-all";

const BUTTON_VARIANTS = {
primary: "bg-linear-to-t from-stone-600 to-stone-500 text-white shadow-md",
danger: "bg-linear-to-t from-red-600 to-red-500 text-white shadow-md",
secondary:
"border border-neutral-300 bg-linear-to-b from-white to-stone-50 text-neutral-700 shadow-xs",
} as const;

const BUTTON_INTERACTIVE =
"cursor-pointer hover:scale-[102%] hover:shadow-lg active:scale-[98%]";

const BUTTON_DISABLED =
"disabled:cursor-not-allowed disabled:opacity-70 disabled:pointer-events-none";

export function integrationButtonClassName(
variant: keyof typeof BUTTON_VARIANTS,
) {
return cn([
BUTTON_BASE,
BUTTON_VARIANTS[variant],
BUTTON_INTERACTIVE,
BUTTON_DISABLED,
]);
}

export function IntegrationButton({
variant = "primary",
className,
...props
}: {
variant?: keyof typeof BUTTON_VARIANTS;
className?: string;
} & ButtonHTMLAttributes<HTMLButtonElement>) {
return (
<button
className={cn([integrationButtonClassName(variant), className])}
{...props}
/>
);
}

export function IntegrationPageLayout({ children }: { children: ReactNode }) {
return (
<div className="flex min-h-screen items-center justify-center bg-linear-to-b from-white via-stone-50/20 to-white p-6">
<div className="flex w-full max-w-md flex-col gap-8 text-center">
{children}
</div>
</div>
);
}
Loading
Loading