diff --git a/frontend/public/locales/en-GB/translation.toml b/frontend/public/locales/en-GB/translation.toml index 487c8d4cf6..3e2fd5d6f5 100644 --- a/frontend/public/locales/en-GB/translation.toml +++ b/frontend/public/locales/en-GB/translation.toml @@ -5815,6 +5815,7 @@ subtitle = "Sign in with your Stirling account" [setup.selfhosted] title = "Sign in to Server" subtitle = "Enter your server credentials" +link = "or connect to a self hosted account" [setup.server] title = "Connect to Server" diff --git a/frontend/src/desktop/components/SetupWizard/DesktopAuthLayout.tsx b/frontend/src/desktop/components/SetupWizard/DesktopAuthLayout.tsx new file mode 100644 index 0000000000..92558857b4 --- /dev/null +++ b/frontend/src/desktop/components/SetupWizard/DesktopAuthLayout.tsx @@ -0,0 +1,72 @@ +import React, { useEffect, useMemo, useRef, useState } from 'react'; +import { useTranslation } from 'react-i18next'; +import LoginRightCarousel from '@app/components/shared/LoginRightCarousel'; +import buildLoginSlides from '@app/components/shared/loginSlides'; +import styles from '@app/routes/authShared/AuthLayout.module.css'; +import { useLogoVariant } from '@app/hooks/useLogoVariant'; + +interface DesktopAuthLayoutProps { + children: React.ReactNode; +} + +export const DesktopAuthLayout: React.FC = ({ children }) => { + const { t } = useTranslation(); + const cardRef = useRef(null); + const [hideRightPanel, setHideRightPanel] = useState(false); + const logoVariant = useLogoVariant(); + const imageSlides = useMemo(() => buildLoginSlides(logoVariant, t), [logoVariant, t]); + + // Force light mode on auth pages + useEffect(() => { + const htmlElement = document.documentElement; + const previousColorScheme = htmlElement.getAttribute('data-mantine-color-scheme'); + + // Set light mode + htmlElement.setAttribute('data-mantine-color-scheme', 'light'); + + // Cleanup: restore previous theme when leaving auth pages + return () => { + if (previousColorScheme) { + htmlElement.setAttribute('data-mantine-color-scheme', previousColorScheme); + } + }; + }, []); + + useEffect(() => { + const update = () => { + // Use viewport to avoid hysteresis when the card is already in single-column mode + const viewportWidth = window.innerWidth; + const viewportHeight = window.innerHeight; + const cardWidthIfTwoCols = Math.min(1180, viewportWidth * 0.96); // matches min(73.75rem, 96vw) + const columnWidth = cardWidthIfTwoCols / 2; + const tooNarrow = columnWidth < 470; + const tooShort = viewportHeight < 740; + setHideRightPanel(tooNarrow || tooShort); + }; + update(); + window.addEventListener('resize', update); + window.addEventListener('orientationchange', update); + return () => { + window.removeEventListener('resize', update); + window.removeEventListener('orientationchange', update); + }; + }, []); + + return ( +
+
+
+
+ {children} +
+
+ {!hideRightPanel && ( + + )} +
+
+ ); +}; diff --git a/frontend/src/desktop/components/SetupWizard/DesktopOAuthButtons.tsx b/frontend/src/desktop/components/SetupWizard/DesktopOAuthButtons.tsx new file mode 100644 index 0000000000..54a1110660 --- /dev/null +++ b/frontend/src/desktop/components/SetupWizard/DesktopOAuthButtons.tsx @@ -0,0 +1,102 @@ +import React, { useState } from 'react'; +import { useTranslation } from 'react-i18next'; +import { authService, UserInfo } from '@app/services/authService'; +import { buildOAuthCallbackHtml } from '@app/utils/oauthCallbackHtml'; +import { BASE_PATH } from '@app/constants/app'; +import '@app/routes/authShared/auth.css'; + +interface DesktopOAuthButtonsProps { + onOAuthSuccess: (userInfo: UserInfo) => Promise; + onError: (error: string) => void; + isDisabled: boolean; + serverUrl: string; + providers: Array<'google' | 'github'>; +} + +export const DesktopOAuthButtons: React.FC = ({ + onOAuthSuccess, + onError, + isDisabled, + serverUrl, + providers, +}) => { + const { t } = useTranslation(); + const [oauthLoading, setOauthLoading] = useState(false); + + const handleOAuthLogin = async (provider: 'google' | 'github') => { + // Prevent concurrent OAuth attempts + if (oauthLoading || isDisabled) { + return; + } + + try { + setOauthLoading(true); + + // Build callback page HTML with translations and dark mode support + const successHtml = buildOAuthCallbackHtml({ + title: t('oauth.success.title', 'Authentication Successful'), + message: t('oauth.success.message', 'You can close this window and return to Stirling PDF.'), + isError: false, + }); + + const errorHtml = buildOAuthCallbackHtml({ + title: t('oauth.error.title', 'Authentication Failed'), + message: t('oauth.error.message', 'Authentication was not successful. You can close this window and try again.'), + isError: true, + errorPlaceholder: true, // {error} will be replaced by Rust + }); + + const userInfo = await authService.loginWithOAuth(provider, serverUrl, successHtml, errorHtml); + + // Call the onOAuthSuccess callback to complete setup + await onOAuthSuccess(userInfo); + } catch (error) { + console.error('OAuth login failed:', error); + + const errorMessage = error instanceof Error + ? error.message + : t('setup.login.error.oauthFailed', 'OAuth login failed. Please try again.'); + + onError(errorMessage); + setOauthLoading(false); + } + }; + + const providerConfig = { + google: { label: 'Google', file: 'google.svg' }, + github: { label: 'GitHub', file: 'github.svg' }, + }; + + if (providers.length === 0) { + return null; + } + + return ( +
+ {providers.map((providerId) => { + const provider = providerConfig[providerId]; + return ( + + ); + })} + {oauthLoading && ( +

+ {t('setup.login.oauthPending', 'Opening browser for authentication...')} +

+ )} +
+ ); +}; diff --git a/frontend/src/desktop/components/SetupWizard/LoginForm.tsx b/frontend/src/desktop/components/SetupWizard/LoginForm.tsx deleted file mode 100644 index 4ad388822f..0000000000 --- a/frontend/src/desktop/components/SetupWizard/LoginForm.tsx +++ /dev/null @@ -1,225 +0,0 @@ -import React, { useState } from 'react'; -import { Stack, TextInput, PasswordInput, Button, Text, Divider, Group, Collapse, Anchor, Box } from '@mantine/core'; -import { useTranslation } from 'react-i18next'; -import { authService } from '@app/services/authService'; -import { STIRLING_SAAS_URL } from '@app/constants/connection'; -import { buildOAuthCallbackHtml } from '@app/utils/oauthCallbackHtml'; -import { BASE_PATH } from '@app/constants/app'; - -interface LoginFormProps { - serverUrl: string; - isSaaS?: boolean; - onLogin: (username: string, password: string) => Promise; - loading: boolean; -} - -export const LoginForm: React.FC = ({ serverUrl, isSaaS = false, onLogin, loading }) => { - const { t } = useTranslation(); - const [username, setUsername] = useState(''); - const [password, setPassword] = useState(''); - const [validationError, setValidationError] = useState(null); - const [oauthLoading, setOauthLoading] = useState(false); - const [showInstructions, setShowInstructions] = useState(false); - - const handleSubmit = async (e: React.FormEvent) => { - e.preventDefault(); - - // Validation - if (!username.trim()) { - setValidationError(isSaaS - ? t('setup.login.error.emptyEmail', 'Please enter your email') - : t('setup.login.error.emptyUsername', 'Please enter your username')); - return; - } - - if (!password) { - setValidationError(t('setup.login.error.emptyPassword', 'Please enter your password')); - return; - } - - setValidationError(null); - await onLogin(username.trim(), password); - }; - - const handleOAuthLogin = async (provider: 'google' | 'github') => { - // Prevent concurrent OAuth attempts - if (oauthLoading || loading) { - return; - } - - try { - setOauthLoading(true); - setValidationError(null); - - // For SaaS, use configured SaaS URL; for self-hosted, derive from serverUrl - const authServerUrl = isSaaS - ? STIRLING_SAAS_URL - : serverUrl; // Self-hosted might have its own auth - - // Build callback page HTML with translations and dark mode support - const successHtml = buildOAuthCallbackHtml({ - title: t('oauth.success.title', 'Authentication Successful'), - message: t('oauth.success.message', 'You can close this window and return to Stirling PDF.'), - isError: false, - }); - - const errorHtml = buildOAuthCallbackHtml({ - title: t('oauth.error.title', 'Authentication Failed'), - message: t('oauth.error.message', 'Authentication was not successful. You can close this window and try again.'), - isError: true, - errorPlaceholder: true, // {error} will be replaced by Rust - }); - - const userInfo = await authService.loginWithOAuth(provider, authServerUrl, successHtml, errorHtml); - - // Call the onLogin callback to complete setup (username/password not needed for OAuth) - await onLogin(userInfo.username, ''); - } catch (error) { - console.error('OAuth login failed:', error); - - const errorMessage = error instanceof Error - ? error.message - : t('setup.login.error.oauthFailed', 'OAuth login failed. Please try again.'); - - setValidationError(errorMessage); - setOauthLoading(false); - } - }; - - return ( -
- - - {t('setup.login.connectingTo', 'Connecting to:')} {isSaaS ? 'stirling.com' : serverUrl} - - - {/* Login requirement note for self-hosted servers */} - {!isSaaS && ( - - - {t('setup.login.serverRequirement', 'Note: The server must have login enabled.')}{' '} - setShowInstructions(!showInstructions)} - style={{ cursor: 'pointer' }} - > - {showInstructions - ? t('setup.login.hideInstructions', 'Hide instructions') - : t('setup.login.showInstructions', 'How to enable?')} - - - - - - - {t('setup.login.instructions', 'To enable login on your Stirling PDF server:')} - - - {t('setup.login.instructionsEnvVar', 'Set the environment variable:')} - - - SECURITY_ENABLELOGIN=true - - - {t('setup.login.instructionsOrYml', 'Or in settings.yml:')} - - - security.enableLogin: true - - - {t('setup.login.instructionsRestart', 'Then restart your server for the changes to take effect.')} - - - - - )} - - {/* OAuth Login Buttons - Only show for SaaS */} - {isSaaS && ( - <> - - - - - - - - {oauthLoading && ( - - {t('setup.login.oauthPending', 'Opening browser for authentication...')} - - )} - - - - - )} - - { - setUsername(e.target.value); - setValidationError(null); - }} - disabled={loading} - required - /> - - { - setPassword(e.target.value); - setValidationError(null); - }} - disabled={loading} - required - /> - - {validationError && ( - - {validationError} - - )} - - - -
- ); -}; diff --git a/frontend/src/desktop/components/SetupWizard/ModeSelection.tsx b/frontend/src/desktop/components/SetupWizard/ModeSelection.tsx deleted file mode 100644 index 8242fa5ed0..0000000000 --- a/frontend/src/desktop/components/SetupWizard/ModeSelection.tsx +++ /dev/null @@ -1,72 +0,0 @@ -import React from 'react'; -import { Stack, Button, Text } from '@mantine/core'; -import { useTranslation } from 'react-i18next'; -import CloudIcon from '@mui/icons-material/Cloud'; -import ComputerIcon from '@mui/icons-material/Computer'; - -interface ModeSelectionProps { - onSelect: (mode: 'saas' | 'selfhosted') => void; - loading: boolean; -} - -export const ModeSelection: React.FC = ({ onSelect, loading }) => { - const { t } = useTranslation(); - - return ( - - - - - - ); -}; diff --git a/frontend/src/desktop/components/SetupWizard/SaaSLoginScreen.tsx b/frontend/src/desktop/components/SetupWizard/SaaSLoginScreen.tsx new file mode 100644 index 0000000000..ab82bde1ed --- /dev/null +++ b/frontend/src/desktop/components/SetupWizard/SaaSLoginScreen.tsx @@ -0,0 +1,95 @@ +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 EmailPasswordForm from '@app/routes/login/EmailPasswordForm'; +import DividerWithText from '@app/components/shared/DividerWithText'; +import { DesktopOAuthButtons } from '@app/components/SetupWizard/DesktopOAuthButtons'; +import { SelfHostedLink } from '@app/components/SetupWizard/SelfHostedLink'; +import { UserInfo } from '@app/services/authService'; +import '@app/routes/authShared/auth.css'; + +interface SaaSLoginScreenProps { + serverUrl: string; + onLogin: (username: string, password: string) => Promise; + onOAuthSuccess: (userInfo: UserInfo) => Promise; + onSelfHostedClick: () => void; + loading: boolean; + error: string | null; +} + +export const SaaSLoginScreen: React.FC = ({ + serverUrl, + onLogin, + onOAuthSuccess, + onSelfHostedClick, + loading, + error, +}) => { + const { t } = useTranslation(); + const [email, setEmail] = useState(''); + const [password, setPassword] = useState(''); + const [validationError, setValidationError] = useState(null); + + const handleEmailPasswordSubmit = async () => { + // Validation + if (!email.trim()) { + setValidationError(t('setup.login.error.emptyEmail', 'Please enter your email')); + return; + } + + if (!password) { + setValidationError(t('setup.login.error.emptyPassword', 'Please enter your password')); + return; + } + + setValidationError(null); + await onLogin(email.trim(), password); + }; + + const handleOAuthError = (errorMessage: string) => { + setValidationError(errorMessage); + }; + + const displayError = error || validationError; + + return ( + <> + + + + + + + + + { + setEmail(value); + setValidationError(null); + }} + setPassword={(value) => { + setPassword(value); + setValidationError(null); + }} + onSubmit={handleEmailPasswordSubmit} + isSubmitting={loading} + submitButtonText={t('setup.login.submit', 'Login')} + /> + + + + ); +}; diff --git a/frontend/src/desktop/components/SetupWizard/SelfHostedLink.tsx b/frontend/src/desktop/components/SetupWizard/SelfHostedLink.tsx new file mode 100644 index 0000000000..6a184cc584 --- /dev/null +++ b/frontend/src/desktop/components/SetupWizard/SelfHostedLink.tsx @@ -0,0 +1,25 @@ +import React from 'react'; +import { useTranslation } from 'react-i18next'; +import '@app/routes/authShared/auth.css'; + +interface SelfHostedLinkProps { + onClick: () => void; + disabled?: boolean; +} + +export const SelfHostedLink: React.FC = ({ onClick, disabled = false }) => { + const { t } = useTranslation(); + + return ( +
+ +
+ ); +}; diff --git a/frontend/src/desktop/components/SetupWizard/SelfHostedLoginScreen.tsx b/frontend/src/desktop/components/SetupWizard/SelfHostedLoginScreen.tsx new file mode 100644 index 0000000000..7f37da67f8 --- /dev/null +++ b/frontend/src/desktop/components/SetupWizard/SelfHostedLoginScreen.tsx @@ -0,0 +1,76 @@ +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 EmailPasswordForm from '@app/routes/login/EmailPasswordForm'; +import '@app/routes/authShared/auth.css'; + +interface SelfHostedLoginScreenProps { + serverUrl: string; + onLogin: (username: string, password: string) => Promise; + loading: boolean; + error: string | null; +} + +export const SelfHostedLoginScreen: React.FC = ({ + serverUrl, + onLogin, + loading, + error, +}) => { + const { t } = useTranslation(); + const [username, setUsername] = useState(''); + const [password, setPassword] = useState(''); + const [validationError, setValidationError] = useState(null); + + const handleSubmit = async () => { + // Validation + if (!username.trim()) { + setValidationError(t('setup.login.error.emptyUsername', 'Please enter your username')); + return; + } + + if (!password) { + setValidationError(t('setup.login.error.emptyPassword', 'Please enter your password')); + return; + } + + setValidationError(null); + await onLogin(username.trim(), password); + }; + + const displayError = error || validationError; + + return ( + <> + + + + +
+

+ {t('setup.login.connectingTo', 'Connecting to:')} {serverUrl} +

+
+ + { + setUsername(value); + setValidationError(null); + }} + setPassword={(value) => { + setPassword(value); + setValidationError(null); + }} + onSubmit={handleSubmit} + isSubmitting={loading} + submitButtonText={t('setup.login.submit', 'Login')} + /> + + ); +}; diff --git a/frontend/src/desktop/components/SetupWizard/ServerSelectionScreen.tsx b/frontend/src/desktop/components/SetupWizard/ServerSelectionScreen.tsx new file mode 100644 index 0000000000..48ad5b810f --- /dev/null +++ b/frontend/src/desktop/components/SetupWizard/ServerSelectionScreen.tsx @@ -0,0 +1,34 @@ +import React from 'react'; +import { useTranslation } from 'react-i18next'; +import LoginHeader from '@app/routes/login/LoginHeader'; +import ErrorMessage from '@app/routes/login/ErrorMessage'; +import { ServerSelection } from '@app/components/SetupWizard/ServerSelection'; +import { ServerConfig } from '@app/services/connectionModeService'; +import '@app/routes/authShared/auth.css'; + +interface ServerSelectionScreenProps { + onSelect: (config: ServerConfig) => void; + loading: boolean; + error: string | null; +} + +export const ServerSelectionScreen: React.FC = ({ + onSelect, + loading, + error, +}) => { + const { t } = useTranslation(); + + return ( + <> + + + + + + + ); +}; diff --git a/frontend/src/desktop/components/SetupWizard/SetupWizard.css b/frontend/src/desktop/components/SetupWizard/SetupWizard.css deleted file mode 100644 index 0234945509..0000000000 --- a/frontend/src/desktop/components/SetupWizard/SetupWizard.css +++ /dev/null @@ -1,23 +0,0 @@ -.setup-container { - position: relative; - min-height: 100vh; - display: flex; - flex-direction: column; - align-items: center; - justify-content: center; - background: linear-gradient(135deg, - light-dark(#f5f5f5, #1a1a1a) 0%, - light-dark(#e8e8e8, #0d0d0d) 100% - ); - padding: 2rem; -} - -.setup-wrapper { - width: 100%; - max-width: 600px; -} - -.setup-card { - background-color: light-dark(var(--mantine-color-white), var(--mantine-color-dark-6)); - box-shadow: 0 20px 60px light-dark(rgba(0, 0, 0, 0.12), rgba(0, 0, 0, 0.4)); -} diff --git a/frontend/src/desktop/components/SetupWizard/index.tsx b/frontend/src/desktop/components/SetupWizard/index.tsx index c05e503de7..cf19fb671f 100644 --- a/frontend/src/desktop/components/SetupWizard/index.tsx +++ b/frontend/src/desktop/components/SetupWizard/index.tsx @@ -1,18 +1,16 @@ import React, { useState } from 'react'; -import { Container, Paper, Stack, Title, Text, Button, Image } from '@mantine/core'; import { useTranslation } from 'react-i18next'; -import { ModeSelection } from '@app/components/SetupWizard/ModeSelection'; -import { ServerSelection } from '@app/components/SetupWizard/ServerSelection'; -import { LoginForm } from '@app/components/SetupWizard/LoginForm'; -import { connectionModeService, ServerConfig } from '@app/services/connectionModeService'; -import { authService } from '@app/services/authService'; +import { DesktopAuthLayout } from '@app/components/SetupWizard/DesktopAuthLayout'; +import { SaaSLoginScreen } from '@app/components/SetupWizard/SaaSLoginScreen'; +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 { useLogoPath } from '@app/hooks/useLogoPath'; import { STIRLING_SAAS_URL } from '@desktop/constants/connection'; -import '@app/components/SetupWizard/SetupWizard.css'; +import '@app/routes/authShared/auth.css'; enum SetupStep { - ModeSelection, SaaSLogin, ServerSelection, SelfHostedLogin, @@ -24,25 +22,11 @@ interface SetupWizardProps { export const SetupWizard: React.FC = ({ onComplete }) => { const { t } = useTranslation(); - const logoPath = useLogoPath(); - const [activeStep, setActiveStep] = useState(SetupStep.ModeSelection); - const [serverConfig, setServerConfig] = useState(null); + const [activeStep, setActiveStep] = useState(SetupStep.SaaSLogin); + const [serverConfig, setServerConfig] = useState({ url: STIRLING_SAAS_URL }); const [loading, setLoading] = useState(false); const [error, setError] = useState(null); - const handleModeSelection = (mode: 'saas' | 'selfhosted') => { - setError(null); - - if (mode === 'saas') { - // For SaaS, go directly to login screen with SaaS URL - setServerConfig({ url: STIRLING_SAAS_URL }); - setActiveStep(SetupStep.SaaSLogin); - } else { - // For self-hosted, show server selection first - setActiveStep(SetupStep.ServerSelection); - } - }; - const handleSaaSLogin = async (username: string, password: string) => { if (!serverConfig) { setError('No SaaS server configured'); @@ -70,6 +54,32 @@ export const SetupWizard: React.FC = ({ onComplete }) => { } }; + const handleSaaSLoginOAuth = async (_userInfo: UserInfo) => { + if (!serverConfig) { + setError('No SaaS server configured'); + return; + } + + try { + setLoading(true); + setError(null); + + // OAuth already completed by authService.loginWithOAuth + await connectionModeService.switchToSaaS(serverConfig.url); + tauriBackendService.startBackend().catch(console.error); + onComplete(); + } catch (err) { + console.error('SaaS OAuth login completion failed:', err); + setError(err instanceof Error ? err.message : 'Failed to complete SaaS login'); + setLoading(false); + } + }; + + const handleSelfHostedClick = () => { + setError(null); + setActiveStep(SetupStep.ServerSelection); + }; + const handleServerSelection = (config: ServerConfig) => { setServerConfig(config); setError(null); @@ -99,118 +109,57 @@ export const SetupWizard: React.FC = ({ onComplete }) => { const handleBack = () => { setError(null); - if (activeStep === SetupStep.SaaSLogin) { - setActiveStep(SetupStep.ModeSelection); - setServerConfig(null); - } else if (activeStep === SetupStep.SelfHostedLogin) { + if (activeStep === SetupStep.SelfHostedLogin) { setActiveStep(SetupStep.ServerSelection); } else if (activeStep === SetupStep.ServerSelection) { - setActiveStep(SetupStep.ModeSelection); - setServerConfig(null); - } - }; - - const getStepTitle = () => { - switch (activeStep) { - case SetupStep.ModeSelection: - return t('setup.welcome', 'Welcome to Stirling PDF'); - case SetupStep.SaaSLogin: - return t('setup.saas.title', 'Sign in to Stirling Cloud'); - case SetupStep.ServerSelection: - return t('setup.server.title', 'Connect to Server'); - case SetupStep.SelfHostedLogin: - return t('setup.selfhosted.title', 'Sign in to Server'); - default: - return ''; - } - }; - - const getStepSubtitle = () => { - switch (activeStep) { - case SetupStep.ModeSelection: - return t('setup.description', 'Get started by choosing how you want to use Stirling PDF'); - case SetupStep.SaaSLogin: - return t('setup.saas.subtitle', 'Sign in with your Stirling account'); - case SetupStep.ServerSelection: - return t('setup.server.subtitle', 'Enter your self-hosted server URL'); - case SetupStep.SelfHostedLogin: - return t('setup.selfhosted.subtitle', 'Enter your server credentials'); - default: - return ''; + setActiveStep(SetupStep.SaaSLogin); + setServerConfig({ url: STIRLING_SAAS_URL }); } }; return ( -
- - - - {/* Logo Header */} - - Stirling PDF - - {getStepTitle()} - - - {getStepSubtitle()} - - + + {/* Step Content */} + {activeStep === SetupStep.SaaSLogin && ( + + )} - {/* Error Message */} - {error && ( - - - {error} - - - )} + {activeStep === SetupStep.ServerSelection && ( + + )} - {/* Step Content */} - {activeStep === SetupStep.ModeSelection && ( - - )} + {activeStep === SetupStep.SelfHostedLogin && ( + + )} - {activeStep === SetupStep.SaaSLogin && ( - - )} - - {activeStep === SetupStep.ServerSelection && ( - - )} - - {activeStep === SetupStep.SelfHostedLogin && ( - - )} - - {/* Back Button */} - {activeStep > SetupStep.ModeSelection && !loading && ( - - )} - - - -
+ {/* Back Button */} + {activeStep > SetupStep.SaaSLogin && !loading && ( +
+ +
+ )} + ); };