Skip to content
Merged
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
114 changes: 65 additions & 49 deletions apps/desktop/src/auth.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import {
useCallback,
useContext,
useEffect,
useMemo,
useState,
} from "react";

Expand Down Expand Up @@ -104,41 +105,44 @@ export function AuthProvider({ children }: { children: React.ReactNode }) {
const [session, setSession] = useState<Session | null>(null);
const [serverReachable, setServerReachable] = useState(true);

const setSessionFromTokens = async (
accessToken: string,
refreshToken: string,
) => {
if (!supabase) {
console.error("Supabase client not found");
return;
}
const setSessionFromTokens = useCallback(
async (accessToken: string, refreshToken: string) => {
if (!supabase) {
console.error("Supabase client not found");
return;
}

const res = await supabase.auth.setSession({
access_token: accessToken,
refresh_token: refreshToken,
});
const res = await supabase.auth.setSession({
access_token: accessToken,
refresh_token: refreshToken,
});

if (res.error) {
console.error(res.error);
} else {
setSession(res.data.session);
setServerReachable(true);
void supabase.auth.startAutoRefresh();
}
};

const handleAuthCallback = async (url: string) => {
const parsed = new URL(url);
const accessToken = parsed.searchParams.get("access_token");
const refreshToken = parsed.searchParams.get("refresh_token");

if (!accessToken || !refreshToken) {
console.error("invalid_callback_url");
return;
}
if (res.error) {
console.error(res.error);
} else {
setSession(res.data.session);
setServerReachable(true);
void supabase.auth.startAutoRefresh();
}
},
[],
);

const handleAuthCallback = useCallback(
async (url: string) => {
const parsed = new URL(url);
const accessToken = parsed.searchParams.get("access_token");
const refreshToken = parsed.searchParams.get("refresh_token");

if (!accessToken || !refreshToken) {
console.error("invalid_callback_url");
return;
}

await setSessionFromTokens(accessToken, refreshToken);
};
await setSessionFromTokens(accessToken, refreshToken);
},
[setSessionFromTokens],
);

useEffect(() => {
if (!supabase) {
Expand Down Expand Up @@ -242,13 +246,13 @@ export function AuthProvider({ children }: { children: React.ReactNode }) {
};
}, []);

const signIn = async () => {
const signIn = useCallback(async () => {
const base = env.VITE_APP_URL ?? "http://localhost:3000";
const scheme = await getScheme();
await openUrl(`${base}/auth?flow=desktop&scheme=${scheme}`);
};
}, []);

const signOut = async () => {
const signOut = useCallback(async () => {
if (!supabase) {
return;
}
Expand All @@ -275,9 +279,9 @@ export function AuthProvider({ children }: { children: React.ReactNode }) {
setSession(null);
}
}
};
}, []);

const refreshSession = async (): Promise<Session | null> => {
const refreshSession = useCallback(async (): Promise<Session | null> => {
if (!supabase) {
return null;
}
Expand All @@ -291,7 +295,7 @@ export function AuthProvider({ children }: { children: React.ReactNode }) {
return data.session;
}
return null;
};
}, []);

const getHeaders = useCallback(() => {
if (!session) {
Expand Down Expand Up @@ -320,17 +324,29 @@ export function AuthProvider({ children }: { children: React.ReactNode }) {
return `https://gravatar.com/avatar/${hash}`;
}, [session]);

const value = {
session,
supabase,
signIn,
signOut,
refreshSession,
handleAuthCallback,
setSessionFromTokens,
getHeaders,
getAvatarUrl,
};
const value = useMemo(
() => ({
session,
supabase,
signIn,
signOut,
refreshSession,
handleAuthCallback,
setSessionFromTokens,
getHeaders,
getAvatarUrl,
}),
[
session,
signIn,
signOut,
refreshSession,
handleAuthCallback,
setSessionFromTokens,
getHeaders,
getAvatarUrl,
],
);

return <AuthContext.Provider value={value}>{children}</AuthContext.Provider>;
}
Expand Down
8 changes: 4 additions & 4 deletions apps/desktop/src/components/onboarding/calendar.tsx
Original file line number Diff line number Diff line change
@@ -1,13 +1,13 @@
import { Icon } from "@iconify-icon/react";
import { platform } from "@tauri-apps/plugin-os";

import { useAuth } from "../../auth";
import { getNextAfterConfigureNotice, type StepProps } from "./config";
import { Route } from "../../routes/app/onboarding";
import { getNext, type StepProps } from "./config";
import { Divider, IntegrationRow, OnboardingContainer } from "./shared";

export function Calendars({ onNavigate }: StepProps) {
const search = Route.useSearch();
const auth = useAuth();
const currentPlatform = platform();
const isLoggedIn = !!auth?.session;

return (
Expand Down Expand Up @@ -51,7 +51,7 @@ export function Calendars({ onNavigate }: StepProps) {
</div>

<button
onClick={() => onNavigate(getNextAfterConfigureNotice(currentPlatform))}
onClick={() => onNavigate({ ...search, step: getNext(search) })}
className="mt-4 text-sm text-neutral-400 transition-colors hover:text-neutral-600"
>
skip
Expand Down
71 changes: 45 additions & 26 deletions apps/desktop/src/components/onboarding/config.tsx
Original file line number Diff line number Diff line change
@@ -1,44 +1,63 @@
import type { ComponentType } from "react";

import { ConfigureNotice } from "./configure-notice";
import { Login } from "./login";
import { Permissions } from "./permissions";
import { Welcome } from "./welcome";
import type { Search } from "../../routes/app/onboarding";
import { ConfigureNotice, STEP_ID_CONFIGURE_NOTICE } from "./configure-notice";
import { Login, STEP_ID_LOGIN } from "./login";
import { Permissions, STEP_ID_PERMISSIONS } from "./permissions";
import { STEP_ID_WELCOME, Welcome } from "./welcome";

export type OnboardingStepId =
| "welcome"
| "login"
| "configure-notice"
| "permissions";
export type NavigateTarget = Omit<Search, "step"> & {
step: Search["step"] | "done";
};

export type StepProps = {
onNavigate: (step: OnboardingStepId | "done") => void;
onNavigate: (ctx: NavigateTarget) => void;
};

export function getNextAfterLogin(
platform: string,
isPro: boolean,
): OnboardingStepId | "done" {
if (!isPro) {
return "configure-notice";
export function getNext(ctx: Search): Search["step"] | "done" {
switch (ctx.step) {
case STEP_ID_WELCOME:
if (ctx.local) return STEP_ID_CONFIGURE_NOTICE;
if (ctx.platform === "macos") return STEP_ID_PERMISSIONS;
return STEP_ID_LOGIN;
case STEP_ID_PERMISSIONS:
return ctx.local ? STEP_ID_CONFIGURE_NOTICE : STEP_ID_LOGIN;
case STEP_ID_LOGIN:
return ctx.pro ? "done" : STEP_ID_CONFIGURE_NOTICE;
case STEP_ID_CONFIGURE_NOTICE:
return "done";
}
return platform === "macos" ? "permissions" : "done";
}

export function getNextAfterConfigureNotice(
platform: string,
): OnboardingStepId | "done" {
return platform === "macos" ? "permissions" : "done";
export function getBack(ctx: Search): Search["step"] | null {
switch (ctx.step) {
case STEP_ID_WELCOME:
return null;
case STEP_ID_PERMISSIONS:
return STEP_ID_WELCOME;
case STEP_ID_LOGIN:
return ctx.platform === "macos" ? STEP_ID_PERMISSIONS : STEP_ID_WELCOME;
case STEP_ID_CONFIGURE_NOTICE:
if (ctx.local) return STEP_ID_WELCOME;
return STEP_ID_LOGIN;
}
}

type StepConfig = {
id: OnboardingStepId;
id: Search["step"];
component: ComponentType<StepProps>;
};

export const STEP_IDS = [
STEP_ID_WELCOME,
STEP_ID_LOGIN,
STEP_ID_CONFIGURE_NOTICE,
STEP_ID_PERMISSIONS,
] as const;

export const STEP_CONFIGS: StepConfig[] = [
{ id: "welcome", component: Welcome },
{ id: "login", component: Login },
{ id: "configure-notice", component: ConfigureNotice },
{ id: "permissions", component: Permissions },
{ id: STEP_ID_WELCOME, component: Welcome },
{ id: STEP_ID_LOGIN, component: Login },
{ id: STEP_ID_CONFIGURE_NOTICE, component: ConfigureNotice },
{ id: STEP_ID_PERMISSIONS, component: Permissions },
];
18 changes: 10 additions & 8 deletions apps/desktop/src/components/onboarding/configure-notice.tsx
Original file line number Diff line number Diff line change
@@ -1,15 +1,19 @@
import { platform } from "@tauri-apps/plugin-os";

import { getNextAfterConfigureNotice, type StepProps } from "./config";
import { Route } from "../../routes/app/onboarding";
import { getBack, getNext, type StepProps } from "./config";
import { OnboardingContainer } from "./shared";

export const STEP_ID_CONFIGURE_NOTICE = "configure-notice" as const;

export function ConfigureNotice({ onNavigate }: StepProps) {
const currentPlatform = platform();
const search = Route.useSearch();
const backStep = getBack(search);

return (
<OnboardingContainer
title="AI models are needed for best experience"
onBack={() => onNavigate("welcome")}
onBack={
backStep ? () => onNavigate({ ...search, step: backStep }) : undefined
}
>
<div className="flex flex-col gap-4">
<Requirement
Expand All @@ -25,9 +29,7 @@ export function ConfigureNotice({ onNavigate }: StepProps) {

<div className="flex flex-col gap-3 mt-4">
<button
onClick={() =>
onNavigate(getNextAfterConfigureNotice(currentPlatform))
}
onClick={() => onNavigate({ ...search, step: getNext(search) })}
className="w-full py-3 rounded-full bg-gradient-to-t from-stone-600 to-stone-500 text-white text-sm font-medium duration-150 hover:scale-[1.01] active:scale-[0.99]"
>
I will configure it later
Expand Down
22 changes: 15 additions & 7 deletions apps/desktop/src/components/onboarding/login.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
import { useMutation } from "@tanstack/react-query";
import { platform } from "@tauri-apps/plugin-os";
import { useCallback, useEffect, useState } from "react";

import { getRpcCanStartTrial, postBillingStartTrial } from "@hypr/api-client";
Expand All @@ -8,13 +7,17 @@ import { createClient, createConfig } from "@hypr/api-client/client";
import { useAuth } from "../../auth";
import { getEntitlementsFromToken } from "../../billing";
import { env } from "../../env";
import { Route } from "../../routes/app/onboarding";
import * as settings from "../../store/tinybase/settings";
import { getNextAfterLogin, type StepProps } from "./config";
import { getBack, getNext, type StepProps } from "./config";
import { STEP_ID_CONFIGURE_NOTICE } from "./configure-notice";
import { Divider, OnboardingContainer } from "./shared";

export const STEP_ID_LOGIN = "login" as const;

export function Login({ onNavigate }: StepProps) {
const search = Route.useSearch();
const auth = useAuth();
const currentPlatform = platform();
const [callbackUrl, setCallbackUrl] = useState("");

const setLlmProvider = settings.UI.useSetValueCallback(
Expand Down Expand Up @@ -76,11 +79,12 @@ export function Login({ onNavigate }: StepProps) {
if (isPro) {
setTrialDefaults();
}
onNavigate(getNextAfterLogin(currentPlatform, isPro));
const nextSearch = { ...search, pro: isPro };
onNavigate({ ...nextSearch, step: getNext(nextSearch) });
},
onError: (e) => {
console.error(e);
onNavigate("configure-notice");
onNavigate({ ...search, step: STEP_ID_CONFIGURE_NOTICE });
},
});

Expand All @@ -96,13 +100,17 @@ export function Login({ onNavigate }: StepProps) {
if (isIdle && !auth?.session) {
void auth?.signIn();
}
}, [auth, isIdle]);
}, [auth?.session, auth?.signIn, isIdle]);

const backStep = getBack(search);

return (
<OnboardingContainer
title="Waiting for sign in..."
description="Complete the process in your browser"
onBack={() => onNavigate("welcome")}
onBack={
backStep ? () => onNavigate({ ...search, step: backStep }) : undefined
}
>
<button
onClick={() => auth?.signIn()}
Expand Down
Loading
Loading