Logged-out home stays the product page #120
No files matched your search
+14
-5
@@ -11,6 +11,7 @@ import {
|
||||
isTokenNotValidError,
|
||||
} from "./llm-fe/auth/jwtHelpers";
|
||||
import { redirectToAppPath } from "./llm-fe/platform/nativePlatform";
|
||||
import { shouldRedirectToSignIn } from "./llm-fe/auth/authRedirect";
|
||||
|
||||
const baseURL = process.env.REACT_APP_BACKEND_REST_API_BASE_URL;
|
||||
|
||||
@@ -97,8 +98,10 @@ axiosInstance.interceptors.response.use(
|
||||
if (error.response.status === 401 && isRefreshCall) {
|
||||
await clearTokens();
|
||||
applyAccessToken(null);
|
||||
// Hash-safe on Capacitor (#24); plain path on web BrowserRouter.
|
||||
redirectToAppPath("/signin/");
|
||||
// Stay on the product page. Only bounce protected app routes.
|
||||
if (shouldRedirectToSignIn(requestUrl, window.location.pathname)) {
|
||||
redirectToAppPath("/signin/");
|
||||
}
|
||||
return Promise.reject(error);
|
||||
}
|
||||
|
||||
@@ -109,7 +112,9 @@ axiosInstance.interceptors.response.use(
|
||||
if (!refresh_token) {
|
||||
await clearTokens();
|
||||
applyAccessToken(null);
|
||||
redirectToAppPath("/signin/");
|
||||
if (shouldRedirectToSignIn(requestUrl, window.location.pathname)) {
|
||||
redirectToAppPath("/signin/");
|
||||
}
|
||||
return Promise.reject(error);
|
||||
}
|
||||
|
||||
@@ -127,14 +132,18 @@ axiosInstance.interceptors.response.use(
|
||||
} catch (err) {
|
||||
await clearTokens();
|
||||
applyAccessToken(null);
|
||||
redirectToAppPath("/signin/");
|
||||
if (shouldRedirectToSignIn(requestUrl, window.location.pathname)) {
|
||||
redirectToAppPath("/signin/");
|
||||
}
|
||||
return Promise.reject(err);
|
||||
}
|
||||
}
|
||||
|
||||
await clearTokens();
|
||||
applyAccessToken(null);
|
||||
redirectToAppPath("/signin/");
|
||||
if (shouldRedirectToSignIn(requestUrl, window.location.pathname)) {
|
||||
redirectToAppPath("/signin/");
|
||||
}
|
||||
}
|
||||
|
||||
return Promise.reject(error);
|
||||
|
||||
@@ -0,0 +1,15 @@
|
||||
import { shouldRedirectToSignIn } from './authRedirect';
|
||||
|
||||
describe('shouldRedirectToSignIn', () => {
|
||||
it('keeps a logged-out visitor on the product page', () => {
|
||||
expect(shouldRedirectToSignIn('/public/usage/', '/')).toBe(false);
|
||||
expect(shouldRedirectToSignIn('/public/settings/', '/pricing/')).toBe(false);
|
||||
expect(shouldRedirectToSignIn('/user/get/', '/')).toBe(false);
|
||||
expect(shouldRedirectToSignIn('/user/get/', '/changelog/')).toBe(false);
|
||||
});
|
||||
|
||||
it('still sends an expired session on the app to sign in', () => {
|
||||
expect(shouldRedirectToSignIn('/user/get/', '/account/')).toBe(true);
|
||||
expect(shouldRedirectToSignIn('/token/refresh/', '/document_storage')).toBe(true);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,38 @@
|
||||
/** Public marketing routes. An auth failure here must not replace the page with Sign In. */
|
||||
|
||||
const PUBLIC_PATHS = new Set([
|
||||
"/",
|
||||
"/pricing",
|
||||
"/privacy",
|
||||
"/changelog",
|
||||
"/terms_of_service",
|
||||
"/legal/terms-of-service",
|
||||
"/account-deletion",
|
||||
"/signin",
|
||||
"/signup",
|
||||
"/password_reset",
|
||||
"/password_reset_confirmation",
|
||||
"/set_password",
|
||||
]);
|
||||
|
||||
export function normalizeAppPath(pathname: string | null | undefined): string {
|
||||
if (!pathname || pathname === "/") return "/";
|
||||
return pathname.endsWith("/") ? pathname.slice(0, -1) : pathname;
|
||||
}
|
||||
|
||||
export function isPublicApiRequest(requestUrl: string | null | undefined): boolean {
|
||||
return String(requestUrl || "").includes("/public/");
|
||||
}
|
||||
|
||||
export function isPublicAppPath(pathname: string | null | undefined): boolean {
|
||||
return PUBLIC_PATHS.has(normalizeAppPath(pathname));
|
||||
}
|
||||
|
||||
export function shouldRedirectToSignIn(
|
||||
requestUrl: string | null | undefined,
|
||||
pathname: string | null | undefined,
|
||||
): boolean {
|
||||
if (isPublicApiRequest(requestUrl)) return false;
|
||||
if (isPublicAppPath(pathname)) return false;
|
||||
return true;
|
||||
}
|
||||
@@ -3,6 +3,7 @@ import { Link } from 'react-router-dom';
|
||||
import styled from 'styled-components';
|
||||
import { axiosInstance } from '../../../axiosApi';
|
||||
import { AnalyticsEvents, trackEvent } from '../../utils/analytics';
|
||||
import { publicAccent, publicCard, publicLine, publicText } from './publicColors';
|
||||
|
||||
const Stack = styled.div`
|
||||
display: flex;
|
||||
@@ -34,12 +35,13 @@ const Primary = styled(Link)`
|
||||
`;
|
||||
|
||||
const Secondary = styled(Link)`
|
||||
color: #d6dcff;
|
||||
color: ${publicAccent};
|
||||
font-weight: 600;
|
||||
`;
|
||||
|
||||
const Quiet = styled(Link)`
|
||||
color: rgba(255, 255, 255, 0.72);
|
||||
color: ${publicText};
|
||||
opacity: 0.75;
|
||||
font-size: 0.95rem;
|
||||
`;
|
||||
|
||||
@@ -63,11 +65,11 @@ const Label = styled.label`
|
||||
|
||||
const Input = styled.input`
|
||||
flex: 1 1 14rem;
|
||||
background: rgba(255, 255, 255, 0.06);
|
||||
border: 1px solid rgba(255, 255, 255, 0.16);
|
||||
background: ${publicCard};
|
||||
border: 1px solid ${publicLine};
|
||||
border-radius: 0.5rem;
|
||||
padding: 0.9rem 1rem;
|
||||
color: #fff;
|
||||
color: ${publicText};
|
||||
font-size: 1rem;
|
||||
`;
|
||||
|
||||
@@ -89,7 +91,7 @@ const Submit = styled.button`
|
||||
|
||||
const Note = styled.p`
|
||||
margin: 0;
|
||||
color: rgba(255, 255, 255, 0.8);
|
||||
color: ${publicText};
|
||||
font-size: 0.95rem;
|
||||
`;
|
||||
|
||||
|
||||
@@ -1,12 +1,21 @@
|
||||
import React from 'react';
|
||||
import { Link } from 'react-router-dom';
|
||||
import styled from 'styled-components';
|
||||
import ParticleBackground from '../ParticleBackground/ParticleBackground';
|
||||
import { PUBLIC_FOOTER_LINKS, PUBLIC_HEADER_LINKS } from './publicNav';
|
||||
|
||||
const text = ({ theme }: { theme?: { colors?: { text?: string } } }) =>
|
||||
theme?.colors?.text ?? '#344767';
|
||||
|
||||
const line = ({ theme }: { theme?: { colors?: { cardBorder?: string } } }) =>
|
||||
theme?.colors?.cardBorder ?? 'rgba(0, 0, 0, 0.08)';
|
||||
|
||||
const Page = styled.div`
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
min-height: 100vh;
|
||||
background: #07080d;
|
||||
color: #fff;
|
||||
background: transparent;
|
||||
color: ${text};
|
||||
font-family: Inter, sans-serif;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
@@ -19,11 +28,11 @@ const Header = styled.header`
|
||||
justify-content: space-between;
|
||||
gap: 1rem;
|
||||
padding: 1.25rem 1.5rem;
|
||||
border-bottom: 1px solid rgba(255, 255, 255, 0.08);
|
||||
border-bottom: 1px solid ${line};
|
||||
`;
|
||||
|
||||
const Brand = styled(Link)`
|
||||
color: #fff;
|
||||
color: ${text};
|
||||
font-weight: 700;
|
||||
font-size: 1.15rem;
|
||||
text-decoration: none;
|
||||
@@ -38,19 +47,20 @@ const Nav = styled.nav`
|
||||
`;
|
||||
|
||||
const NavLink = styled(Link)`
|
||||
color: rgba(255, 255, 255, 0.82);
|
||||
color: ${text};
|
||||
text-decoration: none;
|
||||
font-size: 0.95rem;
|
||||
opacity: 0.85;
|
||||
|
||||
&:hover {
|
||||
color: #fff;
|
||||
opacity: 1;
|
||||
}
|
||||
`;
|
||||
|
||||
const SignInLink = styled(Link)`
|
||||
color: #fff;
|
||||
color: ${text};
|
||||
text-decoration: none;
|
||||
border: 1px solid rgba(255, 255, 255, 0.28);
|
||||
border: 1px solid ${line};
|
||||
border-radius: 0.5rem;
|
||||
padding: 0.45rem 0.8rem;
|
||||
font-size: 0.95rem;
|
||||
@@ -64,10 +74,11 @@ const Main = styled.main`
|
||||
`;
|
||||
|
||||
const FooterBar = styled.footer`
|
||||
border-top: 1px solid rgba(255, 255, 255, 0.08);
|
||||
border-top: 1px solid ${line};
|
||||
padding: 1.5rem;
|
||||
color: rgba(255, 255, 255, 0.7);
|
||||
color: ${text};
|
||||
font-size: 0.9rem;
|
||||
opacity: 0.85;
|
||||
`;
|
||||
|
||||
const FooterNav = styled.nav`
|
||||
@@ -78,7 +89,7 @@ const FooterNav = styled.nav`
|
||||
`;
|
||||
|
||||
const FooterLink = styled(Link)`
|
||||
color: rgba(255, 255, 255, 0.82);
|
||||
color: ${text};
|
||||
`;
|
||||
|
||||
type PublicSiteLayoutProps = {
|
||||
@@ -88,6 +99,7 @@ type PublicSiteLayoutProps = {
|
||||
const PublicSiteLayout = ({ children }: PublicSiteLayoutProps): JSX.Element => {
|
||||
return (
|
||||
<Page>
|
||||
<ParticleBackground />
|
||||
<Header>
|
||||
<Brand to="/">Hesychia</Brand>
|
||||
<Nav aria-label="Primary">
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import styled from 'styled-components';
|
||||
import { axiosInstance } from '../../../axiosApi';
|
||||
import { publicText } from './publicColors';
|
||||
|
||||
const Section = styled.section`
|
||||
margin-top: 1.5rem;
|
||||
color: rgba(255, 255, 255, 0.86);
|
||||
color: ${publicText};
|
||||
font-size: 0.95rem;
|
||||
`;
|
||||
|
||||
@@ -15,7 +16,8 @@ const Figures = styled.p`
|
||||
|
||||
const AsOf = styled.p`
|
||||
margin: 0;
|
||||
color: rgba(255, 255, 255, 0.62);
|
||||
color: ${publicText};
|
||||
opacity: 0.7;
|
||||
`;
|
||||
|
||||
type PublicUsage = {
|
||||
|
||||
@@ -0,0 +1,17 @@
|
||||
type PublicTheme = {
|
||||
colors?: {
|
||||
text?: string;
|
||||
cardBackground?: string;
|
||||
cardBorder?: string;
|
||||
};
|
||||
};
|
||||
|
||||
export const publicText = ({ theme }: { theme?: PublicTheme }) => theme?.colors?.text ?? '#344767';
|
||||
|
||||
export const publicCard = ({ theme }: { theme?: PublicTheme }) =>
|
||||
theme?.colors?.cardBackground ?? 'rgba(255, 255, 255, 0.72)';
|
||||
|
||||
export const publicLine = ({ theme }: { theme?: PublicTheme }) =>
|
||||
theme?.colors?.cardBorder ?? 'rgba(0, 0, 0, 0.08)';
|
||||
|
||||
export const publicAccent = '#667eea';
|
||||
@@ -4,6 +4,8 @@ import { MemoryRouter } from 'react-router-dom';
|
||||
import ChangelogPage from './ChangelogPage';
|
||||
import { CHANGELOG_ENTRIES } from './changelogEntries';
|
||||
|
||||
jest.mock('../../components/ParticleBackground/ParticleBackground', () => () => null);
|
||||
|
||||
describe('ChangelogPage', () => {
|
||||
it('lists dated shipped notes and no founder', () => {
|
||||
render(
|
||||
|
||||
@@ -2,6 +2,7 @@ import React from 'react';
|
||||
import styled from 'styled-components';
|
||||
import PublicSiteLayout from '../../components/PublicSite/PublicSiteLayout';
|
||||
import { CHANGELOG_ENTRIES } from './changelogEntries';
|
||||
import { publicLine, publicText } from '../../components/PublicSite/publicColors';
|
||||
|
||||
const Title = styled.h1`
|
||||
font-size: clamp(2rem, 4vw, 2.75rem);
|
||||
@@ -17,13 +18,14 @@ const List = styled.ol`
|
||||
`;
|
||||
|
||||
const Item = styled.li`
|
||||
border-top: 1px solid rgba(255, 255, 255, 0.1);
|
||||
border-top: 1px solid ${publicLine};
|
||||
padding-top: 1rem;
|
||||
`;
|
||||
|
||||
const When = styled.time`
|
||||
display: block;
|
||||
color: rgba(255, 255, 255, 0.6);
|
||||
color: ${publicText};
|
||||
opacity: 0.65;
|
||||
font-size: 0.9rem;
|
||||
margin-bottom: 0.35rem;
|
||||
`;
|
||||
@@ -35,7 +37,7 @@ const Heading = styled.h2`
|
||||
|
||||
const Note = styled.p`
|
||||
margin: 0;
|
||||
color: rgba(255, 255, 255, 0.82);
|
||||
color: ${publicText};
|
||||
line-height: 1.5;
|
||||
max-width: 40rem;
|
||||
`;
|
||||
|
||||
@@ -4,6 +4,8 @@ import userEvent from '@testing-library/user-event';
|
||||
import { MemoryRouter } from 'react-router-dom';
|
||||
import LandingPage from './LandingPage';
|
||||
|
||||
jest.mock('../../components/ParticleBackground/ParticleBackground', () => () => null);
|
||||
|
||||
const mockGet = jest.fn();
|
||||
const mockPost = jest.fn();
|
||||
|
||||
|
||||
@@ -5,6 +5,7 @@ import hesychiaMark from '../../assets/brand/hesychia-mark.png';
|
||||
import GetStarted from '../../components/PublicSite/GetStarted';
|
||||
import PublicSiteLayout from '../../components/PublicSite/PublicSiteLayout';
|
||||
import UsageCounts from '../../components/PublicSite/UsageCounts';
|
||||
import { publicAccent, publicCard, publicLine, publicText } from '../../components/PublicSite/publicColors';
|
||||
|
||||
const Hero = styled.header`
|
||||
max-width: 40rem;
|
||||
@@ -27,14 +28,15 @@ const Lede = styled.p`
|
||||
font-size: 1.25rem;
|
||||
line-height: 1.45;
|
||||
margin: 0 0 0.75rem;
|
||||
color: rgba(255, 255, 255, 0.92);
|
||||
color: ${publicText};
|
||||
`;
|
||||
|
||||
const Memory = styled.p`
|
||||
font-size: 1.05rem;
|
||||
line-height: 1.5;
|
||||
margin: 0 0 1.5rem;
|
||||
color: rgba(255, 255, 255, 0.82);
|
||||
color: ${publicText};
|
||||
opacity: 0.85;
|
||||
`;
|
||||
|
||||
const Privacy = styled.section`
|
||||
@@ -45,12 +47,13 @@ const Privacy = styled.section`
|
||||
const PrivacyList = styled.ul`
|
||||
margin: 0.5rem 0 0.75rem;
|
||||
padding-left: 1.2rem;
|
||||
color: rgba(255, 255, 255, 0.82);
|
||||
color: ${publicText};
|
||||
opacity: 0.85;
|
||||
line-height: 1.5;
|
||||
`;
|
||||
|
||||
const TextLink = styled(Link)`
|
||||
color: #c9d4ff;
|
||||
color: ${publicAccent};
|
||||
`;
|
||||
|
||||
const Features = styled.section`
|
||||
@@ -60,10 +63,10 @@ const Features = styled.section`
|
||||
`;
|
||||
|
||||
const Feature = styled.article`
|
||||
border: 1px solid rgba(255, 255, 255, 0.1);
|
||||
border: 1px solid ${publicLine};
|
||||
border-radius: 0.75rem;
|
||||
padding: 1.25rem 1.35rem;
|
||||
background: rgba(255, 255, 255, 0.03);
|
||||
background: ${publicCard};
|
||||
`;
|
||||
|
||||
const FeatureTitle = styled.h2`
|
||||
@@ -73,7 +76,8 @@ const FeatureTitle = styled.h2`
|
||||
|
||||
const FeatureBody = styled.p`
|
||||
margin: 0;
|
||||
color: rgba(255, 255, 255, 0.8);
|
||||
color: ${publicText};
|
||||
opacity: 0.85;
|
||||
line-height: 1.5;
|
||||
`;
|
||||
|
||||
|
||||
@@ -3,6 +3,8 @@ import { render, screen } from '@testing-library/react';
|
||||
import { MemoryRouter } from 'react-router-dom';
|
||||
import PricingPage from './PricingPage';
|
||||
|
||||
jest.mock('../../components/ParticleBackground/ParticleBackground', () => () => null);
|
||||
|
||||
jest.mock('../../../axiosApi', () => ({
|
||||
axiosInstance: {
|
||||
get: () => Promise.resolve({ data: { enable_account_registration: false } }),
|
||||
|
||||
@@ -3,6 +3,7 @@ import { Link } from 'react-router-dom';
|
||||
import styled from 'styled-components';
|
||||
import GetStarted from '../../components/PublicSite/GetStarted';
|
||||
import PublicSiteLayout from '../../components/PublicSite/PublicSiteLayout';
|
||||
import { publicAccent, publicText } from '../../components/PublicSite/publicColors';
|
||||
|
||||
const Title = styled.h1`
|
||||
font-size: clamp(2rem, 4vw, 2.75rem);
|
||||
@@ -15,13 +16,13 @@ const Price = styled.p`
|
||||
`;
|
||||
|
||||
const Body = styled.p`
|
||||
color: rgba(255, 255, 255, 0.82);
|
||||
color: ${publicText};
|
||||
line-height: 1.5;
|
||||
max-width: 36rem;
|
||||
`;
|
||||
|
||||
const Back = styled(Link)`
|
||||
color: #c9d4ff;
|
||||
color: ${publicAccent};
|
||||
`;
|
||||
|
||||
const PricingPage = (): JSX.Element => {
|
||||
|
||||
@@ -3,6 +3,8 @@ import { render, screen } from '@testing-library/react';
|
||||
import { MemoryRouter } from 'react-router-dom';
|
||||
import PrivacyPage from './PrivacyPage';
|
||||
|
||||
jest.mock('../../components/ParticleBackground/ParticleBackground', () => () => null);
|
||||
|
||||
describe('PrivacyPage', () => {
|
||||
it('states storage, no ads, and that cancel does not delete documents', () => {
|
||||
render(
|
||||
|
||||
@@ -2,6 +2,7 @@ import React from 'react';
|
||||
import { Link } from 'react-router-dom';
|
||||
import styled from 'styled-components';
|
||||
import PublicSiteLayout from '../../components/PublicSite/PublicSiteLayout';
|
||||
import { publicAccent, publicText } from '../../components/PublicSite/publicColors';
|
||||
|
||||
const Title = styled.h1`
|
||||
font-size: clamp(2rem, 4vw, 2.75rem);
|
||||
@@ -20,12 +21,12 @@ const Heading = styled.h2`
|
||||
|
||||
const Body = styled.p`
|
||||
margin: 0;
|
||||
color: rgba(255, 255, 255, 0.82);
|
||||
color: ${publicText};
|
||||
line-height: 1.55;
|
||||
`;
|
||||
|
||||
const TextLink = styled(Link)`
|
||||
color: #c9d4ff;
|
||||
color: ${publicAccent};
|
||||
`;
|
||||
|
||||
const PrivacyPage = (): JSX.Element => {
|
||||
|
||||
Reference in new issue
Block a user