Compare commits

...
Author SHA1 Message Date
BFlat 95a6e9f19d Merge branch 'main' into bflat/settings-pane-update 2025-05-08 17:40:18 +07:00
bflatastic 7433096a53 login step 2025-05-08 17:31:47 +07:00
bflatastic e76aea2c12 login logic and settings disconnect/connect sora account 2025-05-08 14:14:54 +07:00
bflatastic 0e111c4233 settings modal refactor 2025-05-08 12:33:08 +07:00
bflatastic 1b71199bf2 remove console logs 2025-05-08 12:33:08 +07:00
bflatastic bc2e44efdf settings pane update with switch 2025-05-08 12:33:08 +07:00
5 changed files with 323 additions and 39 deletions
@@ -1,8 +1,6 @@
import { useSignals } from "@preact/signals-react/runtime";
import { AUTH_STATUS } from "~/enums";
import { authentication } from "~/signals";
import { ButtonLink } from "~/components";
import ProfileDropdown from "./ProfileDropdown";
@@ -25,6 +25,13 @@ import { OnboardingHelper } from "./comps/OnboardingHelper";
import { FocalLengthDisplay } from "./comps/FocalLengthDisplay/FocalLengthDisplay";
import { appTabId, setAppTabId } from "~/signals/appTab";
import { LoginModal } from "@storyteller/ui-login-modal";
import {
disableHotkeyInput,
enableHotkeyInput,
DomLevels,
} from "./signals/hotkeys";
import { AUTH_STATUS } from "../../enums/authentication";
import { authentication } from "../../signals/authentication/authentication";
export const PageEditor = () => {
useSignals();
@@ -156,26 +163,22 @@ export const PageEditor = () => {
<Timeline />
</div> */}
{/* <DemoModal
title="Welcome to ArtCraft 3D"
subTitle="Your 3D editor for digital art and design"
description="Set up your scene by adding objects and start bringing your ideas to life!"
videoSrc="/resources/videos/artcraft-3d-demo.mp4"
buttonText="Sign in to OpenAI to get started"
buttonOnClick={async () => {
if (IsDesktopApp()) {
await invoke("open_sora_login_command");
} else {
console.error("This is not the Desktop app.");
}
}}
/> */}
<LoginModal
onClose={() => {}}
videoSrc2D="/resources/videos/artcraft-canvas-demo.mp4"
videoSrc3D="/resources/videos/artcraft-3d-demo.mp4"
openAiLogo="/resources/images/openai-logo.png"
onOpenChange={(isOpen: boolean) => {
if (isOpen) {
disableHotkeyInput(DomLevels.DIALOGUE);
} else {
enableHotkeyInput(DomLevels.DIALOGUE);
}
}}
onArtCraftAuthSuccess={(userInfo: any) => {
authentication.status.value = AUTH_STATUS.LOGGED_IN;
authentication.userInfo.value = userInfo;
}}
/>
</div>
);
@@ -116,8 +116,10 @@ export const GalleryModal = React.memo(
setUsername(session.data.user.username);
}
};
getUsername();
}, [usersApi]);
if (isOpen) {
getUsername();
}
}, [usersApi, isOpen]);
useEffect(() => {
const loadItems = async () => {
@@ -0,0 +1,148 @@
import React, { useState, useEffect } from "react";
import { Input } from "@storyteller/ui-input";
import { Button } from "@storyteller/ui-button";
import { faUser, faKey, faEnvelope } from "@fortawesome/pro-solid-svg-icons";
interface ArtCraftSignUpProps {
onSubmit: (
username: string,
email: string,
password: string,
passwordConfirmation: string
) => void;
isSignUp: boolean;
onToggleMode: () => void;
formRef?: React.RefObject<HTMLFormElement | null>;
errorMessage?: string;
}
export const ArtCraftSignUp = ({
onSubmit,
isSignUp,
onToggleMode,
formRef,
errorMessage,
}: ArtCraftSignUpProps) => {
const [localError, setLocalError] = useState<string | undefined>(undefined);
useEffect(() => {
setLocalError(errorMessage);
}, [errorMessage]);
const handleSubmit = (e: React.FormEvent<HTMLFormElement>) => {
e.preventDefault();
const form = e.currentTarget;
if (isSignUp) {
const username = (form.elements.namedItem("username") as HTMLInputElement)
.value;
const email = (form.elements.namedItem("email") as HTMLInputElement)
.value;
const password = (form.elements.namedItem("password") as HTMLInputElement)
.value;
const confirmPassword = (
form.elements.namedItem("confirmPassword") as HTMLInputElement
).value;
if (password !== confirmPassword) {
setLocalError("Passwords do not match.");
return;
}
onSubmit(username, email, password, confirmPassword);
} else {
const usernameOrEmail = (
form.elements.namedItem("usernameOrEmail") as HTMLInputElement
).value;
const password = (form.elements.namedItem("password") as HTMLInputElement)
.value;
onSubmit(usernameOrEmail, "", password, "");
}
};
return (
<div className="flex flex-col items-center justify-center h-full">
<h2 className="text-3xl font-bold mb-3 text-center">
{isSignUp ? "Sign up for ArtCraft" : "Log in to ArtCraft"}
</h2>
<form
className="flex flex-col gap-3 w-full max-w-md"
onSubmit={handleSubmit}
ref={formRef}
>
{isSignUp ? (
<>
<Input
label="Username"
icon={faUser}
name="username"
placeholder="Username"
required
autoComplete="off"
/>
<Input
label="Email"
icon={faEnvelope}
name="email"
type="email"
placeholder="Email"
required={isSignUp}
style={isSignUp ? {} : { display: "none" }}
autoComplete="off"
/>
</>
) : (
<Input
label="Email or Username"
icon={faUser}
name="usernameOrEmail"
placeholder="Email or Username"
required
autoComplete="off"
/>
)}
<Input
label="Password"
icon={faKey}
type="password"
name="password"
placeholder="Password"
required
autoComplete="off"
/>
{isSignUp && (
<Input
label="Confirm Password"
icon={faKey}
type="password"
name="confirmPassword"
placeholder="Confirm Password"
required
autoComplete="off"
/>
)}
<button type="submit" className="hidden" />
<div className="relative mt-3">
<div className="flex gap-2 items-center justify-center p-2 bg-white/5 rounded-lg">
<p className="text-sm opacity-80">
{isSignUp ? "Already have an account?" : "Don't have an account?"}{" "}
</p>
<Button
variant="secondary"
className="p-0 bg-transparent hover:bg-transparent"
type="button"
onClick={onToggleMode}
>
<span className="text-sm text-primary-400 underline">
{isSignUp ? "Log in" : "Sign up"}
</span>
</Button>
</div>
{localError && (
<div className="absolute -bottom-10 left-0 right-0 text-red text-sm text-center mb-2 font-semibold">
{localError}
</div>
)}
</div>
</form>
</div>
);
};
@@ -1,7 +1,7 @@
import { Button } from "@storyteller/ui-button";
import { Transition, TransitionChild } from "@headlessui/react";
import { createPortal } from "react-dom";
import { useState, useEffect } from "react";
import { useState, useEffect, useRef } from "react";
import {
faArrowRight,
faRightToBracket,
@@ -14,12 +14,15 @@ import {
useSoraLoginListener,
} from "@storyteller/tauri-api";
import { invoke } from "@tauri-apps/api/core";
import { ArtCraftSignUp } from "./ArtCraftSignUp";
import { UsersApi } from "@storyteller/api";
interface LoginModalProps {
onClose: () => void;
videoSrc2D?: string;
videoSrc3D?: string;
openAiLogo?: string;
onOpenChange?: (isOpen: boolean) => void;
onArtCraftAuthSuccess?: (userInfo: any) => void;
}
export function LoginModal({
@@ -27,31 +30,140 @@ export function LoginModal({
videoSrc2D,
videoSrc3D,
openAiLogo,
onOpenChange,
onArtCraftAuthSuccess,
}: LoginModalProps) {
const [step, setStep] = useState(1);
const [isLoading, setIsLoading] = useState(false);
const [isOpen, setIsOpen] = useState(false);
const [isLoggedInArtCraft, setIsLoggedInArtCraft] = useState(false);
const [isSignUp, setIsSignUp] = useState(true);
const [errorMessage, setErrorMessage] = useState("");
const totalSteps = isLoggedInArtCraft ? 3 : 4;
const artCraftFormRef = useRef<HTMLFormElement>(null);
const totalSteps = 3;
const initSession = async () => {
const result = await CheckSoraSession();
const sessionExists = result.state === SoraSessionState.Valid;
return sessionExists;
};
const checkArtCraftLogin = async () => {
const usersApi = new UsersApi();
const session = await usersApi.GetSession();
const loggedIn = session.data?.loggedIn;
return loggedIn;
};
useSoraLoginListener((payload) => {
console.log("Login success!", payload);
setStep(3);
if (isLoggedInArtCraft) {
setStep(4);
} else {
setStep(3);
}
});
// Check session on component mount
useEffect(() => {
const initSession = async () => {
const result = await CheckSoraSession();
const sessionExists = result.state === SoraSessionState.Valid;
if (!sessionExists) {
setIsOpen(true);
setStep(1);
// Run both checks in parallel
Promise.all([initSession(), checkArtCraftLogin()]).then(
([soraSessionExists, loggedIn]) => {
if (soraSessionExists && !loggedIn) {
// Sora session exists, but not logged in to ArtCraft
setIsOpen(true);
setStep(3);
} else if (!soraSessionExists) {
// No Sora session, start at step 1
setIsOpen(true);
setStep(1);
} else if (loggedIn) {
// Already logged in, no need to show modal
setIsLoggedInArtCraft(true);
// setIsOpen(false);
}
}
};
initSession();
);
}, []);
useEffect(() => {
if (onOpenChange) onOpenChange(isOpen);
}, [isOpen, onOpenChange]);
const handleArtCraftAuth = async (
username: string,
email: string,
password: string,
passwordConfirmation: string
) => {
setIsLoading(true);
const usersApi = new UsersApi();
try {
let signupResponse, loginResponse;
if (isSignUp) {
// Sign up
signupResponse = await usersApi.Signup({
username,
email,
password,
passwordConfirmation,
});
if (!signupResponse.success) {
// TODO: Add error handling UI
console.error(signupResponse.errorMessage || "Signup failed");
setErrorMessage(
signupResponse.errorMessage || "Signup failed, please try again."
);
return;
}
// Automatically login after successful signup
loginResponse = await usersApi.Login({
usernameOrEmail: username || email,
password,
});
if (!loginResponse.success) {
// TODO: Add error handling UI
console.error(
loginResponse.errorMessage || "Login after signup failed"
);
setErrorMessage(
loginResponse.errorMessage ||
"Login after signup failed, please try again. "
);
return;
}
} else {
// Login
loginResponse = await usersApi.Login({
usernameOrEmail: username || email,
password,
});
if (!loginResponse.success) {
// TODO: Add error handling UI
console.error(loginResponse.errorMessage || "Login failed");
setErrorMessage(
loginResponse.errorMessage || "Login failed, please try again."
);
return;
}
}
setStep(4);
const session = await usersApi.GetSession();
const userInfo = session.data?.user;
if (onArtCraftAuthSuccess && userInfo) {
onArtCraftAuthSuccess(userInfo);
}
} catch (e) {
// handle error (show error message, etc.)
// TODO: Add error handling UI
console.error(e);
} finally {
setIsLoading(false);
}
};
const handleClose = () => {
setIsOpen(false);
onClose();
@@ -63,8 +175,6 @@ export function LoginModal({
try {
if (IsDesktopApp()) {
await invoke("open_sora_login_command");
// Add 3 second delay before setting session to true
await new Promise((resolve) => setTimeout(resolve, 3000));
const result = await CheckSoraSession();
const sessionExists = result.state === SoraSessionState.Valid;
@@ -73,10 +183,8 @@ export function LoginModal({
}
} else {
alert("Please open the desktop app to login");
// Add 3 second delay before setting session to true
await new Promise((resolve) => setTimeout(resolve, 3000));
const sessionExists = true;
if (sessionExists) {
setStep(step + 1);
}
@@ -86,7 +194,12 @@ export function LoginModal({
} finally {
setIsLoading(false);
}
} else if (step < totalSteps) {
} else if (step === 3) {
// Trigger the form submit in ArtCraftSignUp
if (artCraftFormRef.current) {
artCraftFormRef.current.requestSubmit();
}
} else {
setStep(step + 1);
}
};
@@ -111,6 +224,10 @@ export function LoginModal({
// Step content rendering
const renderStepContent = () => {
if (step === 3 && isLoggedInArtCraft) {
setStep(4);
return null;
}
switch (step) {
case 1:
return (
@@ -177,6 +294,16 @@ export function LoginModal({
</div>
);
case 3:
return (
<ArtCraftSignUp
onSubmit={handleArtCraftAuth}
isSignUp={isSignUp}
onToggleMode={() => setIsSignUp((prev) => !prev)}
formRef={artCraftFormRef}
errorMessage={errorMessage}
/>
);
case 4:
return (
<div className="flex flex-col items-center justify-center h-full">
<h2 className="text-3xl font-bold mb-2 text-center">
@@ -237,7 +364,7 @@ export function LoginModal({
{renderProgress()}
{renderStepContent()}
<div className="flex items-end justify-center gap-2.5 mt-10 grow">
{step > 1 && step !== 3 ? (
{step === 2 ? (
<Button
variant="secondary"
onClick={handleBack}
@@ -255,7 +382,13 @@ export function LoginModal({
loading={isLoading}
disabled={isLoading}
>
{step === 2 ? "Login with OpenAI" : "Continue"}
{step === 2
? "Login with OpenAI"
: step === 3
? isSignUp
? "Sign up"
: "Login"
: "Continue"}
</Button>
</>
) : (
@@ -278,4 +411,4 @@ export function LoginModal({
);
}
export default LoginModal;
export default LoginModal;