diff --git a/app/client/src/ce/constants/messages.ts b/app/client/src/ce/constants/messages.ts index e45425ae9c..1830b12cdd 100644 --- a/app/client/src/ce/constants/messages.ts +++ b/app/client/src/ce/constants/messages.ts @@ -87,6 +87,8 @@ export const ALREADY_HAVE_AN_ACCOUNT = () => `Already have an account?`; export const LOOKING_TO_SELF_HOST = () => "Looking to self-host Appsmith?"; export const VISIT_OUR_DOCS = () => "Visit our docs"; export const ALREADY_USING_APPSMITH = () => `Already using Appsmith?`; +export const USING_APPSMITH = () => `Using Appsmith?`; +export const YOU_VE_ALREADY_SIGNED_INTO = () => `You've already signed into`; export const SIGN_IN_TO_AN_EXISTING_ORGANISATION = () => `Sign in to an existing organisation`; diff --git a/app/client/src/ce/pages/Applications/index.tsx b/app/client/src/ce/pages/Applications/index.tsx index da9669310c..e25b2a0301 100644 --- a/app/client/src/ce/pages/Applications/index.tsx +++ b/app/client/src/ce/pages/Applications/index.tsx @@ -140,6 +140,7 @@ import { GitImportOverrideModal, } from "git"; import OldRepoLimitExceededErrorModal from "pages/Editor/gitSync/RepoLimitExceededErrorModal"; +import { trackCurrentDomain } from "utils/multiOrgDomains"; function GitModals() { const isGitModEnabled = useGitModEnabled(); @@ -1149,6 +1150,9 @@ export class Applications< // Whenever we go back to home page from application page, // we should reset current workspace, as this workspace is not in context anymore this.props.resetCurrentWorkspace(); + + // Track the current domain for multi-org functionality + trackCurrentDomain(); } componentWillUnmount() { diff --git a/app/client/src/pages/UserAuth/SignUp.tsx b/app/client/src/pages/UserAuth/SignUp.tsx index 17cd3f7844..c6ff0937c8 100644 --- a/app/client/src/pages/UserAuth/SignUp.tsx +++ b/app/client/src/pages/UserAuth/SignUp.tsx @@ -26,14 +26,14 @@ import { GOOGLE_RECAPTCHA_KEY_ERROR, LOOKING_TO_SELF_HOST, VISIT_OUR_DOCS, - ALREADY_USING_APPSMITH, SIGN_IN_TO_AN_EXISTING_ORGANISATION, - LOGIN_PAGE_TITLE, + USING_APPSMITH, + YOU_VE_ALREADY_SIGNED_INTO, } from "ee/constants/messages"; import FormTextField from "components/utils/ReduxFormTextField"; import ThirdPartyAuth from "pages/UserAuth/ThirdPartyAuth"; import { FormGroup } from "@appsmith/ads-old"; -import { Button, Link, Callout } from "@appsmith/ads"; +import { Button, Link, Callout, Text } from "@appsmith/ads"; import { isEmail, isStrongPassword, isEmptyString } from "utils/formhelpers"; import type { SignupFormValues } from "pages/UserAuth/helpers"; @@ -66,6 +66,7 @@ import { isLoginHostname } from "utils/cloudBillingUtils"; import { appsmithTelemetry } from "instrumentation"; import { getIsAiAgentInstanceEnabled } from "ee/selectors/aiAgentSelectors"; import { getSafeErrorMessage } from "ee/constants/approvedErrorMessages"; +import { getRecentDomains, isValidAppsmithDomain } from "utils/multiOrgDomains"; declare global { interface Window { @@ -75,6 +76,7 @@ declare global { } } const { cloudHosting, googleRecaptchaSiteKey } = getAppsmithConfigs(); +const recentDomains = getRecentDomains(); const validate = (values: SignupFormValues) => { const errors: SignupFormValues = {}; @@ -94,6 +96,59 @@ const validate = (values: SignupFormValues) => { return errors; }; +const recentDomainsSection = recentDomains.length > 0 && ( +
+
+ {createMessage(YOU_VE_ALREADY_SIGNED_INTO)} +
+ +
+ {recentDomains.map((domain, index) => { + const orgName = domain + .split(".")[0] + .split("-") + .map((word) => word.charAt(0).toUpperCase() + word.slice(1)) + .join(" "); + + const avatarLetter = String.fromCharCode(65 + (index % 26)); + + return ( +
+
+
+ {avatarLetter} +
+
+ + {orgName} + + + {domain} + +
+
+ +
+ ); + })} +
+
+); + type SignUpFormProps = InjectedFormProps< SignupFormValues, { emailValue: string } @@ -200,33 +255,34 @@ export function SignUp(props: SignUpFormProps) { } }; + const cloudBillingSignIn = ( +
+ {createMessage(USING_APPSMITH)} + + {createMessage(SIGN_IN_TO_AN_EXISTING_ORGANISATION)} + +
+ ); + const footerSection = ( <> - {isCloudBillingEnabled && isHostnameEqualtoLogin ? ( -
- {createMessage(ALREADY_USING_APPSMITH)} - - {createMessage(SIGN_IN_TO_AN_EXISTING_ORGANISATION)} - -
- ) : ( -
- {createMessage(ALREADY_HAVE_AN_ACCOUNT)}  - - {createMessage(SIGNUP_PAGE_LOGIN_LINK_TEXT)} - -
- )} +
+ {createMessage(ALREADY_HAVE_AN_ACCOUNT)}  + + {createMessage(SIGNUP_PAGE_LOGIN_LINK_TEXT)} + +
+ {cloudHosting && !isAiAgentInstanceEnabled && ( <> or @@ -249,10 +305,8 @@ export function SignUp(props: SignUpFormProps) { return ( {htmlPageTitle} @@ -313,6 +367,8 @@ export function SignUp(props: SignUpFormProps) { )} + {isCloudBillingEnabled && isHostnameEqualtoLogin && cloudBillingSignIn} + {isCloudBillingEnabled && isHostnameEqualtoLogin && recentDomainsSection} ); } diff --git a/app/client/src/utils/multiOrgDomains.ts b/app/client/src/utils/multiOrgDomains.ts new file mode 100644 index 0000000000..aaf70057c9 --- /dev/null +++ b/app/client/src/utils/multiOrgDomains.ts @@ -0,0 +1,105 @@ +const COOKIE_NAME = "appsmith_recent_domains"; +const EXPIRY_DAYS = 30; +const MAX_DOMAINS = 10; + +interface DomainEntry { + domain: string; + timestamp: number; +} + +function getCurrentDomain(): string { + return window.location.hostname; +} + +function isValidAppsmithDomain(domain: string): boolean { + return ( + domain.endsWith(".appsmith.com") && + !domain.startsWith("login.") && + !domain.startsWith("release.") && + !domain.startsWith("dev.") && + /^[a-z0-9-]+\.appsmith\.com$/i.test(domain) + ); +} + +function isMultiOrgDomain(): boolean { + const hostname = getCurrentDomain(); + + return isValidAppsmithDomain(hostname); +} + +function getStoredDomains(): DomainEntry[] { + const cookieValue = getCookie(COOKIE_NAME); + + if (!cookieValue) return []; + + try { + return JSON.parse(decodeURIComponent(cookieValue)); + } catch (e) { + return []; + } +} + +function storeDomains(domains: DomainEntry[]): void { + const expires = new Date(); + + expires.setDate(expires.getDate() + EXPIRY_DAYS); + + const cookieValue = encodeURIComponent(JSON.stringify(domains)); + + document.cookie = `${COOKIE_NAME}=${cookieValue}; expires=${expires.toUTCString()}; domain=.appsmith.com; path=/; SameSite=Lax`; +} + +function getCookie(name: string): string | null { + const value = `; ${document.cookie}`; + const parts = value.split(`; ${name}=`); + + if (parts.length === 2) { + return parts.pop()?.split(";").shift() || null; + } + + return null; +} + +export function trackCurrentDomain(): void { + if (!isMultiOrgDomain()) { + return; + } + + const currentDomain = getCurrentDomain(); + const currentTime = Date.now(); + + let domains = getStoredDomains(); + + domains = domains.filter((entry) => entry.domain !== currentDomain); + + domains.unshift({ + domain: currentDomain, + timestamp: currentTime, + }); + + domains = domains.slice(0, MAX_DOMAINS); + + const thirtyDaysAgo = currentTime - 30 * 24 * 60 * 60 * 1000; + + domains = domains.filter((entry) => entry.timestamp > thirtyDaysAgo); + + storeDomains(domains); +} + +export function getRecentDomains(): string[] { + const domains = getStoredDomains(); + + const thirtyDaysAgo = Date.now() - 30 * 24 * 60 * 60 * 1000; + + return domains + .filter((entry) => entry.timestamp > thirtyDaysAgo) + .map((entry) => entry.domain) + .filter((domain) => domain !== getCurrentDomain()) + .filter((domain) => isValidAppsmithDomain(domain)); +} + +export function clearRecentDomains(): void { + document.cookie = `${COOKIE_NAME}=; expires=Thu, 01 Jan 1970 00:00:00 UTC; domain=.appsmith.com; path=/;`; +} + +export { isValidAppsmithDomain };