Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -107,7 +107,9 @@ const BasicInfoForm: React.FC<BasicInfoFormProps> = ({
async (values: BasicInfoFormSchemaType, slugIndex: number) => {
try {
const slugEnd = slugIndex ? `-${slugIndex + 1}` : '';
const slug = `${slugify(values['schema:name'])}${slugEnd}`;
const slug = values['schema:name']
? `${slugify(values['schema:name'])}${slugEnd}`
: undefined;
if (!isEdit && projectId === DRAFT_ID) {
const shouldNavigateNow =
shouldNavigateRef?.current && !isOrganizationAccount;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import { useQuery } from '@tanstack/react-query';
import { isMobile as checkIsMobile } from '@walletconnect/browser-utils';
import { REGEN_DENOM } from 'config/allowedBaseDenoms';
import { DRAFT_ID } from 'legacy-pages/Dashboard/MyProjects/MyProjects.constants';
import { useRouter } from 'next/navigation';
import { usePathname, useRouter } from 'next/navigation';

import { Body } from 'web-components/src/components/typography';

Expand All @@ -23,6 +23,7 @@ const ListProject = () => {
const { activeAccountId, activeAccount } = useAuth();
const router = useRouter();
const isConnectingRef = useRef(false);
const pathname = usePathname();

const {
isModalOpen,
Expand Down Expand Up @@ -51,7 +52,12 @@ const ListProject = () => {
}}
onClick={
activeAccountId
? () => router.push(`/project-pages/${DRAFT_ID}/account`)
? () =>
router.push(
`/project-pages/${DRAFT_ID}/account?from=${encodeURIComponent(
pathname,
)}`,
)
: onButtonClick
}
>
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import React, { useState } from 'react';
import { useNavigate } from 'react-router-dom';
import { useLocation } from 'react-router-dom';
import { useLingui } from '@lingui/react';
import { useMediaQuery, useTheme } from '@mui/material';
import { useAtom } from 'jotai';
Expand Down Expand Up @@ -60,7 +60,7 @@ const ProjectDashboardBanner = ({
const isMobile = useMediaQuery(theme.breakpoints.down('sm'));
const [projectsCurrentStep] = useAtom(projectsCurrentStepAtom);
const router = useRouter();
const navigate = useNavigate();
const location = useLocation();

const truncatedPlace = truncateEnd(
project.place ?? '',
Expand Down Expand Up @@ -217,9 +217,17 @@ const ProjectDashboardBanner = ({
if (isDraft) {
const currentStep =
projectsCurrentStep[id] || 'basic-info';
navigate(`/project-pages/${id}/${currentStep}`);
router.push(
`/project-pages/${id}/${currentStep}?from=${encodeURIComponent(
location.pathname,
)}`,
);
} else {
navigate(`/project-pages/${id}/edit/basic-info`);
router.push(
`/project-pages/${id}/edit/basic-info?from=${encodeURIComponent(
location.pathname,
)}`,
);
}
}}
>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -118,6 +118,7 @@ export const SellOrdersActionsBar = ({
`/project-pages/${
onChainProjectId ?? offChainProjectId
}/edit/basic-info`,
{ state: { from: location.pathname } },
)
}
>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -128,7 +128,7 @@ export const SellOrdersActionsBar = ({
router.push(
`/project-pages/${
onChainProjectId ?? offChainProjectId
}/edit/basic-info`,
}/edit/basic-info?from=${encodeURIComponent(pathname)}`,
)
}
>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import { Grid } from '@mui/material';
import { useQuery } from '@tanstack/react-query';
import { useSetAtom } from 'jotai';
import { projectsDraftState } from 'legacy-pages/ProjectCreate/ProjectCreate.store';
import { useRouter } from 'next/navigation';

import { CreateProjectCard } from 'web-components/src/components/cards/CreateCards/CreateProjectCard';
import ProjectCard from 'web-components/src/components/cards/ProjectCard';
Expand Down Expand Up @@ -37,6 +38,7 @@ import {

const MyProjects = (): JSX.Element => {
const { _ } = useLingui();
const router = useRouter();
const navigate = useNavigate();
const location = useLocation();
const {
Expand Down Expand Up @@ -94,12 +96,13 @@ const MyProjects = (): JSX.Element => {
emptyTitle={MY_PROJECTS_EMPTY_TITLE}
isFirstProject={isFirstProject}
onClick={() => {
navigate(`/project-pages/${DRAFT_ID}/account`, {
state: {
fromDashboard: true,
isOrganization: isOrganizationDashboard,
},
const params = new URLSearchParams({
from: location.pathname,
...(isOrganizationDashboard && { isOrganization: 'true' }),
});
router.push(
`/project-pages/${DRAFT_ID}/account?${params.toString()}`,
);
}}
sx={{ height: { xs: '100%' } }}
/>
Expand Down
38 changes: 22 additions & 16 deletions web-marketplace/src/legacy-pages/ProjectAccount/ProjectAccount.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { useEffect, useMemo, useRef, useState } from 'react';
import { useLocation, useParams } from 'react-router-dom';
import { useParams, useSearchParams } from 'react-router-dom';
import { msg } from '@lingui/core/macro';
import { useLingui } from '@lingui/react';
import { getDefaultAvatar } from 'legacy-pages/Dashboard/Dashboard.utils';
Expand Down Expand Up @@ -28,10 +28,10 @@ import { useCreateProjectContext } from '../ProjectCreate/ProjectCreate';
export const ProjectAccount = (): JSX.Element | null => {
const { _ } = useLingui();
const { projectId } = useParams();
const location = useLocation();
const state = location.state as
| { fromDashboard?: boolean; isOrganization?: boolean }
| undefined;
const [searchParams] = useSearchParams();
const fromPath = searchParams.get('from');
const hasOriginPath = !!fromPath;
const isOrganizationParam = searchParams.get('isOrganization') === 'true';
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this param only added from MyProjects? If a user bookmarks or shares a URL with isOrganization=true, could it cause unexpected behavior?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yes
if the user bookmarks the URL then it means it would try to create the project from the org account, which makes sense, I don't see how this is unexpected behavior

const { activeAccount } = useAuth();
const dao = useDaoOrganization();
const {
Expand Down Expand Up @@ -79,7 +79,7 @@ export const ProjectAccount = (): JSX.Element | null => {
const shouldSkip =
!dao || !activeAccount || !personalAccount || !organizationAccount;

// Track if we've initialized from dashboard state
// Track if we've initialized from origin path state
const initializedRef = useRef(false);
const [isStateReady, setIsStateReady] = useState(false);

Expand All @@ -88,9 +88,9 @@ export const ProjectAccount = (): JSX.Element | null => {
// Don't run if we've already initialized or accounts aren't ready
if (initializedRef.current) return;

if (state?.fromDashboard) {
// Coming from dashboard - use the passed state
const isOrg = !!state.isOrganization;
if (hasOriginPath) {
// Coming from an origin path - use the passed params
const isOrg = isOrganizationParam;
const address = isOrg
? organizationAccount?.address
: personalAccount?.address;
Expand All @@ -116,7 +116,8 @@ export const ProjectAccount = (): JSX.Element | null => {
setIsStateReady(true);
}
}, [
state,
hasOriginPath,
isOrganizationParam,
organizationAccount?.address,
personalAccount?.address,
projectCreatorAddress,
Expand All @@ -141,22 +142,27 @@ export const ProjectAccount = (): JSX.Element | null => {

// Navigate away if user should skip this step, but only after:
// 1. Issuer check completes
// 2. State is properly initialized (for dashboard redirects)
// 2. State is properly initialized (for origin path redirects)
useEffect(() => {
const canNavigate =
!isLoadingIsIssuer &&
(shouldSkip || (state?.fromDashboard && isStateReady));
!isLoadingIsIssuer && (shouldSkip || (hasOriginPath && isStateReady));
if (canNavigate) {
navigateNext();
}
}, [shouldSkip, isLoadingIsIssuer, navigateNext, state, isStateReady]);
}, [
shouldSkip,
isLoadingIsIssuer,
navigateNext,
hasOriginPath,
isStateReady,
]);

// Show loading while:
// 1. Checking issuer status
// 2. Processing dashboard redirect (waiting for state to be ready)
// 2. Processing origin path redirect (waiting for state to be ready)
// 3. User should skip this page
const showLoading =
isLoadingIsIssuer || (state?.fromDashboard && !isStateReady) || shouldSkip;
isLoadingIsIssuer || (hasOriginPath && !isStateReady) || shouldSkip;

if (showLoading) {
return (
Expand Down
76 changes: 69 additions & 7 deletions web-marketplace/src/legacy-pages/ProjectCreate/ProjectCreate.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,13 @@
import { MutableRefObject, useCallback, useRef, useState } from 'react';
import {
MutableRefObject,
useCallback,
useEffect,
useRef,
useState,
} from 'react';
import {
Outlet,
useNavigate,
useLocation,
useOutletContext,
useParams,
} from 'react-router-dom';
Expand All @@ -12,13 +18,15 @@ import { useLingui } from '@lingui/react';
import { useQuery } from '@tanstack/react-query';
import { useAtom } from 'jotai';
import { DRAFT_ID } from 'legacy-pages/Dashboard/MyProjects/MyProjects.constants';
import { useRouter } from 'next/navigation';

import CloseIcon from 'web-components/src/components/icons/CloseIcon';

import { selectedLanguageAtom } from 'lib/atoms/languageSwitcher.atoms';
import { getProjectByIdQuery } from 'lib/queries/react-query/registry-server/graphql/getProjectByIdQuery/getProjectByIdQuery';

import { FormRef } from 'components/molecules/Form/Form';
import { useDaoOrganization } from 'hooks/useDaoOrganization';

import { projectsDraftState, ProjectsDraftStatus } from './ProjectCreate.store';

Expand Down Expand Up @@ -58,9 +66,21 @@ const defaultProjectCreateContext: ContextType = {
setIsOrganizationAccount: () => void 0,
};

/** Returns true only for same-origin relative paths, blocking open-redirect attacks. */
const isSafeRelativePath = (path: string): boolean => {
try {
const resolved = new URL(path, window.location.origin);
return resolved.origin === window.location.origin;
} catch {
return false;
}
};

export const ProjectCreate = (): JSX.Element => {
const { _ } = useLingui();
const navigate = useNavigate();
const router = useRouter();
const location = useLocation();
const dao = useDaoOrganization();

// TODO: possibly replace these with `useMsgClient` and pass downstream
const [deliverTxResponse, setDeliverTxResponse] =
Expand All @@ -82,6 +102,23 @@ export const ProjectCreate = (): JSX.Element => {
const formRef = useRef();
const shouldNavigateRef = useRef(true);
const isDraftRef = useRef(false);
const originPathRef = useRef<string | null>(null);

// Capture the entry path only once, when the layout first mounts.
// Child subroute navigations will change `location` but we only want the original.
// We check both React Router state (set by navigate() callers) and a `?from` query
// param (set by Next.js router.push() callers that can't pass router state).
useEffect(() => {
if (originPathRef.current === null) {
const fromState =
(location.state as { from?: string } | null)?.from ?? null;
const fromParam = new URLSearchParams(location.search).get('from');
const safeFromParam =
fromParam && isSafeRelativePath(fromParam) ? fromParam : null;
originPathRef.current = fromState ?? safeFromParam;
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);

const graphqlClient = useApolloClient();
const [selectedLanguage] = useAtom(selectedLanguageAtom);
Expand All @@ -96,10 +133,35 @@ export const ProjectCreate = (): JSX.Element => {
const offChainProject = projectByOffChainIdRes?.data?.projectById;

const handleRequestClose = useCallback(() => {
if (isOrganizationAccount || offChainProject?.adminDaoAddress)
navigate('/dashboard/organization', { replace: true });
else navigate('/dashboard', { replace: true });
}, [navigate, isOrganizationAccount, offChainProject]);
// If we know where the user came from, send them back there.
// We use Next.js router.replace() (not React Router navigate) because the origin
// may be a Next.js App Router page (e.g. /project/slug) that lives outside
// React Router's route tree.
if (originPathRef.current) {
router.replace(originPathRef.current);
return;
}
// Fallback: new draft projects have no meaningful manage page yet → homepage.
if (projectId === DRAFT_ID) {
router.replace('/');
return;
}
// Fallback for existing projects: dashboard manage page.
const projectPath = `projects/${projectId}/manage`;
if (
isOrganizationAccount ||
(offChainProject?.adminDaoAddress &&
offChainProject?.adminDaoAddress === dao?.address)
)
router.replace(`/dashboard/organization/${projectPath}`);
else router.replace(`/dashboard/${projectPath}`);
}, [
router,
projectId,
isOrganizationAccount,
offChainProject?.adminDaoAddress,
dao?.address,
]);

return (
<>
Expand Down
Original file line number Diff line number Diff line change
@@ -1,20 +1,19 @@
import { useNavigate } from 'react-router-dom';
import { useRouter } from 'next/navigation';

import { useCreateProjectContext } from '../ProjectCreate';

export const useProjectSaveAndExit = () => {
const { formRef, shouldNavigateRef, isOrganizationAccount } =
useCreateProjectContext();
const navigate = useNavigate();
const router = useRouter();

const saveAndExit = async (): Promise<void> => {
if (shouldNavigateRef) {
shouldNavigateRef.current = false;
await formRef?.current?.submitForm(true);
shouldNavigateRef.current = true;
}
if (isOrganizationAccount) navigate('/dashboard/organization/projects');
else navigate('/dashboard/projects');
if (isOrganizationAccount) router.push('/dashboard/organization/projects');
else router.push('/dashboard/projects');
};

return saveAndExit;
Expand Down
6 changes: 4 additions & 2 deletions web-marketplace/src/legacy-pages/ProjectEdit/ProjectEdit.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ import { useCanAccessManageProjectWithRole } from 'legacy-pages/Dashboard/MyProj
import { useFeeGranter } from 'legacy-pages/Dashboard/MyProjects/hooks/useFeeGranter';
import NotFoundPage from 'legacy-pages/NotFound';
import { startCase } from 'lodash';
import { useRouter } from 'next/navigation';

import Banner from 'web-components/src/components/banner';
import ArrowDownIcon from 'web-components/src/components/icons/ArrowDownIcon';
Expand Down Expand Up @@ -101,6 +102,7 @@ function ProjectEdit(): JSX.Element {
const navigate = useNavigate();
const graphqlClient = useApolloClient();
const { queryClient } = useLedger();
const router = useRouter();

const setProcessingModalAtom = useSetAtom(processingModalAtom);
const setErrorCodeAtom = useSetAtom(errorCodeAtom);
Expand Down Expand Up @@ -240,10 +242,10 @@ function ProjectEdit(): JSX.Element {
if (isFormDirty) {
setIsWarningModalOpen(path);
} else {
navigate(path);
router.push(path);
}
} else {
navigate('/dashboard/projects');
router.push('/dashboard/projects');
}
};

Expand Down
Loading