Add SaaS frontend code (#5879)

# Description of Changes
Adds the code for the SaaS frontend as proprietary code to the OSS repo.
This version of the code is adapted from 22/1/2026, which was the last
SaaS version based on the 'V2' design. This will move us closer to being
able to have the OSS products understand whether the user has a SaaS
account, and provide the correct UI in those cases.
This commit is contained in:
James Brunton
2026-03-11 11:53:54 +00:00
committed by GitHub
parent 8bc37bf5ae
commit fa8c52b2be
114 changed files with 12408 additions and 99 deletions
@@ -0,0 +1,88 @@
import React, { useEffect, useState } from 'react'
import { useTranslation } from 'react-i18next'
import CloseIcon from '@mui/icons-material/Close'
import PersonAddIcon from '@mui/icons-material/PersonAdd'
import { useAuth } from '@app/auth/UseSession'
import { isUserAnonymous } from '@app/auth/supabase'
import { withBasePath } from '@app/constants/app'
import '@app/components/auth/GuestUserBanner.css'
interface GuestUserBannerProps {
className?: string
}
// Ensure the toast only appears once per full page load, not on re-hydration
let hasShownThisLoad = false
/**
* Guest user toast encouraging account creation.
* Appears 2s after load, top-right of the viewport, offset by right rail.
*/
export function GuestUserBanner({ className = '' }: GuestUserBannerProps) {
const { t } = useTranslation()
const { session } = useAuth()
const [isDismissed, setIsDismissed] = useState(false)
const [visible, setVisible] = useState(false)
const isAnon = Boolean(session?.user && isUserAnonymous(session.user))
useEffect(() => {
if (!isAnon || hasShownThisLoad) return
const timer = setTimeout(() => {
setVisible(true)
hasShownThisLoad = true
}, 2000)
return () => clearTimeout(timer)
}, [isAnon])
if (!isAnon || isDismissed || !visible) {
return null
}
const handleSignUp = () => {
window.location.href = withBasePath('/signup')
}
const handleDismiss = () => {
setIsDismissed(true)
}
return (
<div
className={`guest-banner ${className || ''}`}
role="status"
aria-live="polite"
>
<div className="guest-banner-content">
<div className="guest-banner-text">
<div className="guest-banner-title">
{t('guestBanner.title', "You're using Stirling PDF as a guest!")}
</div>
<div className="guest-banner-message">
{t('guestBanner.message', 'Create a free account to save your work, access more features, and support the project.')}
</div>
</div>
<div className="guest-banner-actions">
<button
onClick={handleDismiss}
aria-label={t('guestBanner.dismiss', 'Dismiss banner')}
className="guest-banner-dismiss"
>
<CloseIcon className="guest-banner-icon" />
</button>
<button
onClick={handleSignUp}
className="guest-banner-signup"
>
<PersonAddIcon className="guest-banner-signup-icon" />
{t('guestBanner.signUp', 'Sign Up Free')}
</button>
</div>
</div>
</div>
)
}
export default GuestUserBanner