mirror of
https://github.com/arsvendg/Stirling-PDF.git
synced 2026-07-16 11:23:10 +02:00
Add Sign Up functionality to desktop app (#5244)
# Description of Changes Adds Sign Up with email to desktop app. SSO sign up will come in a future PR.
This commit is contained in:
@@ -14,6 +14,7 @@ interface SaaSLoginScreenProps {
|
||||
onLogin: (username: string, password: string) => Promise<void>;
|
||||
onOAuthSuccess: (userInfo: UserInfo) => Promise<void>;
|
||||
onSelfHostedClick: () => void;
|
||||
onSwitchToSignup: () => void;
|
||||
loading: boolean;
|
||||
error: string | null;
|
||||
}
|
||||
@@ -23,6 +24,7 @@ export const SaaSLoginScreen: React.FC<SaaSLoginScreenProps> = ({
|
||||
onLogin,
|
||||
onOAuthSuccess,
|
||||
onSelfHostedClick,
|
||||
onSwitchToSignup,
|
||||
loading,
|
||||
error,
|
||||
}) => {
|
||||
@@ -89,6 +91,20 @@ export const SaaSLoginScreen: React.FC<SaaSLoginScreenProps> = ({
|
||||
submitButtonText={t('setup.login.submit', 'Login')}
|
||||
/>
|
||||
|
||||
<div className="navigation-link-container" style={{ marginTop: '0.5rem', textAlign: 'right' }}>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => {
|
||||
setValidationError(null);
|
||||
onSwitchToSignup();
|
||||
}}
|
||||
className="navigation-link-button"
|
||||
disabled={loading}
|
||||
>
|
||||
{t('signup.signUp', 'Sign Up')}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<SelfHostedLink onClick={onSelfHostedClick} disabled={loading} />
|
||||
</>
|
||||
);
|
||||
|
||||
@@ -0,0 +1,104 @@
|
||||
import React, { useState } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import LoginHeader from '@app/routes/login/LoginHeader';
|
||||
import ErrorMessage from '@app/routes/login/ErrorMessage';
|
||||
import SignupForm from '@app/routes/signup/SignupForm';
|
||||
import { useSignupFormValidation, SignupFieldErrors } from '@app/routes/signup/SignupFormValidation';
|
||||
import { authService } from '@app/services/authService';
|
||||
import '@app/routes/authShared/auth.css';
|
||||
|
||||
interface SaaSSignupScreenProps {
|
||||
loading: boolean;
|
||||
error: string | null;
|
||||
onLogin: (username: string, password: string) => Promise<void>;
|
||||
onSwitchToLogin: () => void;
|
||||
}
|
||||
|
||||
export const SaaSSignupScreen: React.FC<SaaSSignupScreenProps> = ({
|
||||
loading,
|
||||
error,
|
||||
onLogin: _onLogin,
|
||||
onSwitchToLogin: _onSwitchToLogin,
|
||||
}) => {
|
||||
const { t } = useTranslation();
|
||||
const [email, setEmail] = useState('');
|
||||
const [password, setPassword] = useState('');
|
||||
const [confirmPassword, setConfirmPassword] = useState('');
|
||||
const [validationError, setValidationError] = useState<string | null>(null);
|
||||
const [signupFieldErrors, setSignupFieldErrors] = useState<SignupFieldErrors>({});
|
||||
const [signupSuccessMessage, setSignupSuccessMessage] = useState<string | null>(null);
|
||||
const [isSignupSubmitting, setIsSignupSubmitting] = useState(false);
|
||||
const { validateSignupForm } = useSignupFormValidation();
|
||||
|
||||
const displayError = error || validationError;
|
||||
|
||||
const handleSignupSubmit = async () => {
|
||||
setValidationError(null);
|
||||
setSignupSuccessMessage(null);
|
||||
setSignupFieldErrors({});
|
||||
|
||||
const validation = validateSignupForm(email, password, confirmPassword);
|
||||
if (!validation.isValid) {
|
||||
setValidationError(validation.error);
|
||||
setSignupFieldErrors(validation.fieldErrors || {});
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
setIsSignupSubmitting(true);
|
||||
await authService.signUpSaas(email.trim(), password);
|
||||
setSignupSuccessMessage(t('signup.checkEmailConfirmation', 'Check your email for a confirmation link to complete your registration.'));
|
||||
setSignupFieldErrors({});
|
||||
setValidationError(null);
|
||||
} catch (err) {
|
||||
setSignupSuccessMessage(null);
|
||||
const message = err instanceof Error ? err.message : t('signup.unexpectedError', { message: 'Unknown error' });
|
||||
setValidationError(message);
|
||||
} finally {
|
||||
setIsSignupSubmitting(false);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<LoginHeader
|
||||
title={t('signup.title', 'Create an account')}
|
||||
subtitle={t('signup.subtitle', 'Join Stirling PDF')}
|
||||
/>
|
||||
|
||||
<ErrorMessage error={displayError} />
|
||||
{signupSuccessMessage && (
|
||||
<div className="success-message">
|
||||
<p className="success-message-text">{signupSuccessMessage}</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<SignupForm
|
||||
email={email}
|
||||
password={password}
|
||||
confirmPassword={confirmPassword}
|
||||
setEmail={(value) => {
|
||||
setEmail(value);
|
||||
setValidationError(null);
|
||||
setSignupFieldErrors({});
|
||||
}}
|
||||
setPassword={(value) => {
|
||||
setPassword(value);
|
||||
setValidationError(null);
|
||||
setSignupFieldErrors({});
|
||||
}}
|
||||
setConfirmPassword={(value) => {
|
||||
setConfirmPassword(value);
|
||||
setValidationError(null);
|
||||
setSignupFieldErrors({});
|
||||
}}
|
||||
onSubmit={handleSignupSubmit}
|
||||
isSubmitting={loading || isSignupSubmitting}
|
||||
fieldErrors={signupFieldErrors}
|
||||
showName={false}
|
||||
showTerms={false}
|
||||
/>
|
||||
|
||||
</>
|
||||
);
|
||||
};
|
||||
@@ -2,16 +2,20 @@ import React, { useState } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { DesktopAuthLayout } from '@app/components/SetupWizard/DesktopAuthLayout';
|
||||
import { SaaSLoginScreen } from '@app/components/SetupWizard/SaaSLoginScreen';
|
||||
import { SaaSSignupScreen } from '@app/components/SetupWizard/SaaSSignupScreen';
|
||||
import { ServerSelectionScreen } from '@app/components/SetupWizard/ServerSelectionScreen';
|
||||
import { SelfHostedLoginScreen } from '@app/components/SetupWizard/SelfHostedLoginScreen';
|
||||
import { ServerConfig, connectionModeService } from '@app/services/connectionModeService';
|
||||
import { authService, UserInfo } from '@app/services/authService';
|
||||
import { tauriBackendService } from '@app/services/tauriBackendService';
|
||||
import { STIRLING_SAAS_URL } from '@desktop/constants/connection';
|
||||
import { listen } from '@tauri-apps/api/event';
|
||||
import { useEffect } from 'react';
|
||||
import '@app/routes/authShared/auth.css';
|
||||
|
||||
enum SetupStep {
|
||||
SaaSLogin,
|
||||
SaaSSignup,
|
||||
ServerSelection,
|
||||
SelfHostedLogin,
|
||||
}
|
||||
@@ -80,6 +84,16 @@ export const SetupWizard: React.FC<SetupWizardProps> = ({ onComplete }) => {
|
||||
setActiveStep(SetupStep.ServerSelection);
|
||||
};
|
||||
|
||||
const handleSwitchToSignup = () => {
|
||||
setError(null);
|
||||
setActiveStep(SetupStep.SaaSSignup);
|
||||
};
|
||||
|
||||
const handleSwitchToLogin = () => {
|
||||
setError(null);
|
||||
setActiveStep(SetupStep.SaaSLogin);
|
||||
};
|
||||
|
||||
const handleServerSelection = (config: ServerConfig) => {
|
||||
setServerConfig(config);
|
||||
setError(null);
|
||||
@@ -128,6 +142,48 @@ export const SetupWizard: React.FC<SetupWizardProps> = ({ onComplete }) => {
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
const unsubscribePromise = listen<string>('deep-link', async (event) => {
|
||||
const url = event.payload;
|
||||
if (!url) return;
|
||||
|
||||
try {
|
||||
const parsed = new URL(url);
|
||||
|
||||
// Supabase sends tokens in the URL hash
|
||||
const hash = parsed.hash.replace(/^#/, '');
|
||||
const params = new URLSearchParams(hash);
|
||||
const accessToken = params.get('access_token');
|
||||
const type = params.get('type') || parsed.searchParams.get('type');
|
||||
|
||||
if (!type || (type !== 'signup' && type !== 'recovery' && type !== 'magiclink')) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!accessToken) {
|
||||
console.error('[SetupWizard] Deep link missing access_token');
|
||||
return;
|
||||
}
|
||||
|
||||
setLoading(true);
|
||||
setError(null);
|
||||
|
||||
await authService.completeSupabaseSession(accessToken, serverConfig?.url || STIRLING_SAAS_URL);
|
||||
await connectionModeService.switchToSaaS(serverConfig?.url || STIRLING_SAAS_URL);
|
||||
tauriBackendService.startBackend().catch(console.error);
|
||||
onComplete();
|
||||
} catch (err) {
|
||||
console.error('[SetupWizard] Failed to handle deep link', err);
|
||||
setError(err instanceof Error ? err.message : 'Failed to complete signup');
|
||||
setLoading(false);
|
||||
}
|
||||
});
|
||||
|
||||
return () => {
|
||||
void unsubscribePromise.then((unsub) => unsub());
|
||||
};
|
||||
}, [onComplete, serverConfig?.url]);
|
||||
|
||||
const handleBack = () => {
|
||||
setError(null);
|
||||
if (activeStep === SetupStep.SelfHostedLogin) {
|
||||
@@ -135,6 +191,8 @@ export const SetupWizard: React.FC<SetupWizardProps> = ({ onComplete }) => {
|
||||
} else if (activeStep === SetupStep.ServerSelection) {
|
||||
setActiveStep(SetupStep.SaaSLogin);
|
||||
setServerConfig({ url: STIRLING_SAAS_URL });
|
||||
} else if (activeStep === SetupStep.SaaSSignup) {
|
||||
setActiveStep(SetupStep.SaaSLogin);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -147,11 +205,21 @@ export const SetupWizard: React.FC<SetupWizardProps> = ({ onComplete }) => {
|
||||
onLogin={handleSaaSLogin}
|
||||
onOAuthSuccess={handleSaaSLoginOAuth}
|
||||
onSelfHostedClick={handleSelfHostedClick}
|
||||
onSwitchToSignup={handleSwitchToSignup}
|
||||
loading={loading}
|
||||
error={error}
|
||||
/>
|
||||
)}
|
||||
|
||||
{activeStep === SetupStep.SaaSSignup && (
|
||||
<SaaSSignupScreen
|
||||
loading={loading}
|
||||
error={error}
|
||||
onLogin={handleSaaSLogin}
|
||||
onSwitchToLogin={handleSwitchToLogin}
|
||||
/>
|
||||
)}
|
||||
|
||||
{activeStep === SetupStep.ServerSelection && (
|
||||
<ServerSelectionScreen
|
||||
onSelect={handleServerSelection}
|
||||
|
||||
Reference in New Issue
Block a user