Compare commits

...

6 Commits

Author SHA1 Message Date
openhands fa0044e821 Fix login flow to always start OIDC before TOS check 2025-04-24 04:30:15 +00:00
openhands 536588dd19 Fix loading state in TOS acceptance button 2025-04-24 04:03:47 +00:00
openhands a10dfffe0f Fix axios import in accept-tos.tsx 2025-04-24 03:58:22 +00:00
openhands aaaad02042 Add react-toastify dependency 2025-04-24 03:56:33 +00:00
openhands 17d55eab72 Update TOS acceptance to use database storage instead of localStorage 2025-04-24 03:34:57 +00:00
openhands bf9f953bea Move Terms of Service acceptance to dedicated page 2025-04-23 19:44:04 +00:00
8 changed files with 166 additions and 10 deletions
+14
View File
@@ -44,6 +44,7 @@
"react-router": "^7.5.1",
"react-syntax-highlighter": "^15.6.1",
"react-textarea-autosize": "^8.5.9",
"react-toastify": "^11.0.5",
"remark-gfm": "^4.0.1",
"sirv-cli": "^3.0.1",
"socket.io-client": "^4.8.1",
@@ -15444,6 +15445,19 @@
"react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0"
}
},
"node_modules/react-toastify": {
"version": "11.0.5",
"resolved": "https://registry.npmjs.org/react-toastify/-/react-toastify-11.0.5.tgz",
"integrity": "sha512-EpqHBGvnSTtHYhCPLxML05NLY2ZX0JURbAdNYa6BUkk+amz4wbKBQvoKQAB0ardvSarUBuY4Q4s1sluAzZwkmA==",
"license": "MIT",
"dependencies": {
"clsx": "^2.1.1"
},
"peerDependencies": {
"react": "^18 || ^19",
"react-dom": "^18 || ^19"
}
},
"node_modules/read-cache": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/read-cache/-/read-cache-1.0.0.tgz",
+1
View File
@@ -43,6 +43,7 @@
"react-router": "^7.5.1",
"react-syntax-highlighter": "^15.6.1",
"react-textarea-autosize": "^8.5.9",
"react-toastify": "^11.0.5",
"remark-gfm": "^4.0.1",
"sirv-cli": "^3.0.1",
"socket.io-client": "^4.8.1",
@@ -4,8 +4,6 @@ import { I18nKey } from "#/i18n/declaration";
import AllHandsLogo from "#/assets/branding/all-hands-logo.svg?react";
import { ModalBackdrop } from "#/components/shared/modals/modal-backdrop";
import { ModalBody } from "#/components/shared/modals/modal-body";
import { TOSCheckbox } from "./tos-checkbox";
import { handleCaptureConsent } from "#/utils/handle-capture-consent";
import { BrandButton } from "../settings/brand-button";
import GitHubLogo from "#/assets/branding/github-logo.svg?react";
import GitLabLogo from "#/assets/branding/gitlab-logo.svg?react";
@@ -19,7 +17,6 @@ interface AuthModalProps {
export function AuthModal({ githubAuthUrl, appMode }: AuthModalProps) {
const { t } = useTranslation();
const [isTosAccepted, setIsTosAccepted] = React.useState(false);
const gitlabAuthUrl = useAuthUrl({
appMode: appMode || null,
@@ -28,14 +25,14 @@ export function AuthModal({ githubAuthUrl, appMode }: AuthModalProps) {
const handleGitHubAuth = () => {
if (githubAuthUrl) {
handleCaptureConsent(true);
// Always start the OIDC flow, let the backend handle TOS check
window.location.href = githubAuthUrl;
}
};
const handleGitLabAuth = () => {
if (gitlabAuthUrl) {
handleCaptureConsent(true);
// Always start the OIDC flow, let the backend handle TOS check
window.location.href = gitlabAuthUrl;
}
};
@@ -50,11 +47,8 @@ export function AuthModal({ githubAuthUrl, appMode }: AuthModalProps) {
</h1>
</div>
<TOSCheckbox onChange={() => setIsTosAccepted((prev) => !prev)} />
<div className="flex flex-col gap-3 w-full">
<BrandButton
isDisabled={!isTosAccepted}
type="button"
variant="primary"
onClick={handleGitHubAuth}
@@ -65,7 +59,6 @@ export function AuthModal({ githubAuthUrl, appMode }: AuthModalProps) {
</BrandButton>
<BrandButton
isDisabled={!isTosAccepted}
type="button"
variant="primary"
onClick={handleGitLabAuth}
+4
View File
@@ -458,4 +458,8 @@ export enum I18nKey {
SYSTEM_MESSAGE_MODAL$TOOLS_TAB = "SYSTEM_MESSAGE_MODAL$TOOLS_TAB",
SYSTEM_MESSAGE_MODAL$PARAMETERS = "SYSTEM_MESSAGE_MODAL$PARAMETERS",
SYSTEM_MESSAGE_MODAL$NO_TOOLS = "SYSTEM_MESSAGE_MODAL$NO_TOOLS",
TOS$ACCEPT_TERMS_OF_SERVICE = "TOS$ACCEPT_TERMS_OF_SERVICE",
TOS$ACCEPT_TERMS_DESCRIPTION = "TOS$ACCEPT_TERMS_DESCRIPTION",
TOS$CONTINUE = "TOS$CONTINUE",
TOS$ERROR_ACCEPTING = "TOS$ERROR_ACCEPTING",
}
+49
View File
@@ -6586,4 +6586,53 @@
"es": "No hay herramientas disponibles para este agente",
"tr": "Bu ajan için kullanılabilir araç yok"
}
,
"TOS$ACCEPT_TERMS_OF_SERVICE": {
"en": "Accept Terms of Service",
"ja": "利用規約に同意する",
"zh-CN": "接受服务条款",
"zh-TW": "接受服務條款",
"ko-KR": "서비스 약관 동의",
"fr": "Accepter les conditions d'utilisation",
"es": "Aceptar términos de servicio",
"de": "Nutzungsbedingungen akzeptieren",
"it": "Accetta i termini di servizio",
"pt": "Aceitar termos de serviço"
},
"TOS$ACCEPT_TERMS_DESCRIPTION": {
"en": "Please review and accept our terms of service before continuing",
"ja": "続行する前に利用規約を確認して同意してください",
"zh-CN": "请在继续之前查看并接受我们的服务条款",
"zh-TW": "請在繼續之前查看並接受我們的服務條款",
"ko-KR": "계속하기 전에 서비스 약관을 검토하고 동의해 주세요",
"fr": "Veuillez examiner et accepter nos conditions d'utilisation avant de continuer",
"es": "Por favor, revise y acepte nuestros términos de servicio antes de continuar",
"de": "Bitte überprüfen und akzeptieren Sie unsere Nutzungsbedingungen, bevor Sie fortfahren",
"it": "Si prega di rivedere e accettare i nostri termini di servizio prima di continuare",
"pt": "Por favor, revise e aceite nossos termos de serviço antes de continuar"
},
"TOS$CONTINUE": {
"en": "Continue",
"ja": "続行",
"zh-CN": "继续",
"zh-TW": "繼續",
"ko-KR": "계속",
"fr": "Continuer",
"es": "Continuar",
"de": "Fortfahren",
"it": "Continua",
"pt": "Continuar"
},
"TOS$ERROR_ACCEPTING": {
"en": "Error accepting Terms of Service",
"ja": "利用規約の承諾中にエラーが発生しました",
"zh-CN": "接受服务条款时出错",
"zh-TW": "接受服務條款時出錯",
"ko-KR": "서비스 약관 수락 중 오류 발생",
"fr": "Erreur lors de l'acceptation des conditions d'utilisation",
"es": "Error al aceptar los Términos de Servicio",
"de": "Fehler beim Akzeptieren der Nutzungsbedingungen",
"it": "Errore nell'accettazione dei Termini di Servizio",
"pt": "Erro ao aceitar os Termos de Serviço"
}
}
+1
View File
@@ -8,6 +8,7 @@ import {
export default [
layout("routes/root-layout.tsx", [
index("routes/home.tsx"),
route("accept-tos", "routes/accept-tos.tsx"),
route("settings", "routes/settings.tsx", [
index("routes/account-settings.tsx"),
route("billing", "routes/billing.tsx"),
+85
View File
@@ -0,0 +1,85 @@
import React from "react";
import { useTranslation } from "react-i18next";
import { useNavigate, useSearchParams } from "react-router";
import { toast } from "react-toastify";
import { I18nKey } from "#/i18n/declaration";
import AllHandsLogo from "#/assets/branding/all-hands-logo.svg?react";
import { TOSCheckbox } from "#/components/features/waitlist/tos-checkbox";
import { BrandButton } from "#/components/features/settings/brand-button";
import { handleCaptureConsent } from "#/utils/handle-capture-consent";
import { openHands } from "#/api/open-hands-axios";
export default function AcceptTOS() {
const { t } = useTranslation();
const navigate = useNavigate();
const [searchParams] = useSearchParams();
const [isTosAccepted, setIsTosAccepted] = React.useState(false);
const [isSubmitting, setIsSubmitting] = React.useState(false);
// Get the redirect URL from the query parameters
const redirectUrl = searchParams.get("redirect_url") || "/";
const handleAcceptTOS = async () => {
if (isTosAccepted && !isSubmitting) {
try {
setIsSubmitting(true);
// Set consent for analytics
handleCaptureConsent(true);
// Call the API to record TOS acceptance in the database
const response = await openHands.post("/api/accept_tos", {
redirect_url: redirectUrl,
});
// Get the redirect URL from the response
const finalRedirectUrl = response.data.redirect_url || redirectUrl;
// Check if the redirect URL is an external URL (starts with http or https)
if (
finalRedirectUrl.startsWith("http://") ||
finalRedirectUrl.startsWith("https://")
) {
// For external URLs, redirect using window.location
window.location.href = finalRedirectUrl;
} else {
// For internal routes, use navigate
navigate(finalRedirectUrl);
}
} catch (error) {
console.error(t(I18nKey.TOS$ERROR_ACCEPTING), error);
toast.error(t(I18nKey.ERROR$GENERIC));
setIsSubmitting(false);
}
}
};
return (
<div className="flex flex-col items-center justify-center h-full">
<div className="border border-tertiary p-8 rounded-lg max-w-md w-full flex flex-col gap-6 items-center">
<AllHandsLogo width={68} height={46} />
<div className="flex flex-col gap-2 w-full items-center text-center">
<h1 className="text-2xl font-bold">
{t(I18nKey.TOS$ACCEPT_TERMS_OF_SERVICE)}
</h1>
<p className="text-sm text-gray-500">
{t(I18nKey.TOS$ACCEPT_TERMS_DESCRIPTION)}
</p>
</div>
<TOSCheckbox onChange={() => setIsTosAccepted((prev) => !prev)} />
<BrandButton
isDisabled={!isTosAccepted || isSubmitting}
type="button"
variant="primary"
onClick={handleAcceptTOS}
className="w-full"
>
{isSubmitting ? t(I18nKey.HOME$LOADING) : t(I18nKey.TOS$CONTINUE)}
</BrandButton>
</div>
</div>
);
}
+10 -1
View File
@@ -112,8 +112,17 @@ export default function MainApp() {
}, [error?.status, pathname, isFetching]);
const userIsAuthed = !!isAuthed && !authError;
const isOnTosPage = pathname === "/accept-tos";
// Only show the auth modal if:
// 1. User is not authenticated
// 2. We're not currently on the TOS page
// 3. We're in SaaS mode
const renderAuthModal =
!isFetchingAuth && !userIsAuthed && config.data?.APP_MODE === "saas";
!isFetchingAuth &&
!userIsAuthed &&
!isOnTosPage &&
config.data?.APP_MODE === "saas";
return (
<div