Skip to content

Commit 933c7f6

Browse files
authored
fix: APP-868 close project edit/create (#2801)
1 parent b382b6a commit 933c7f6

File tree

10 files changed

+132
-43
lines changed

10 files changed

+132
-43
lines changed

web-marketplace/src/components/organisms/BasicInfoForm/BasicInfoForm.tsx

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -107,7 +107,9 @@ const BasicInfoForm: React.FC<BasicInfoFormProps> = ({
107107
async (values: BasicInfoFormSchemaType, slugIndex: number) => {
108108
try {
109109
const slugEnd = slugIndex ? `-${slugIndex + 1}` : '';
110-
const slug = `${slugify(values['schema:name'])}${slugEnd}`;
110+
const slug = values['schema:name']
111+
? `${slugify(values['schema:name'])}${slugEnd}`
112+
: undefined;
111113
if (!isEdit && projectId === DRAFT_ID) {
112114
const shouldNavigateNow =
113115
shouldNavigateRef?.current && !isOrganizationAccount;

web-marketplace/src/components/organisms/ListProject/ListProject.tsx

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ import { useQuery } from '@tanstack/react-query';
44
import { isMobile as checkIsMobile } from '@walletconnect/browser-utils';
55
import { REGEN_DENOM } from 'config/allowedBaseDenoms';
66
import { DRAFT_ID } from 'legacy-pages/Dashboard/MyProjects/MyProjects.constants';
7-
import { useRouter } from 'next/navigation';
7+
import { usePathname, useRouter } from 'next/navigation';
88

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

@@ -23,6 +23,7 @@ const ListProject = () => {
2323
const { activeAccountId, activeAccount } = useAuth();
2424
const router = useRouter();
2525
const isConnectingRef = useRef(false);
26+
const pathname = usePathname();
2627

2728
const {
2829
isModalOpen,
@@ -51,7 +52,12 @@ const ListProject = () => {
5152
}}
5253
onClick={
5354
activeAccountId
54-
? () => router.push(`/project-pages/${DRAFT_ID}/account`)
55+
? () =>
56+
router.push(
57+
`/project-pages/${DRAFT_ID}/account?from=${encodeURIComponent(
58+
pathname,
59+
)}`,
60+
)
5561
: onButtonClick
5662
}
5763
>

web-marketplace/src/components/organisms/ProjectDashboardBanner/ProjectDashboardBanner.tsx

Lines changed: 12 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import React, { useState } from 'react';
2-
import { useNavigate } from 'react-router-dom';
2+
import { useLocation } from 'react-router-dom';
33
import { useLingui } from '@lingui/react';
44
import { useMediaQuery, useTheme } from '@mui/material';
55
import { useAtom } from 'jotai';
@@ -60,7 +60,7 @@ const ProjectDashboardBanner = ({
6060
const isMobile = useMediaQuery(theme.breakpoints.down('sm'));
6161
const [projectsCurrentStep] = useAtom(projectsCurrentStepAtom);
6262
const router = useRouter();
63-
const navigate = useNavigate();
63+
const location = useLocation();
6464

6565
const truncatedPlace = truncateEnd(
6666
project.place ?? '',
@@ -217,9 +217,17 @@ const ProjectDashboardBanner = ({
217217
if (isDraft) {
218218
const currentStep =
219219
projectsCurrentStep[id] || 'basic-info';
220-
navigate(`/project-pages/${id}/${currentStep}`);
220+
router.push(
221+
`/project-pages/${id}/${currentStep}?from=${encodeURIComponent(
222+
location.pathname,
223+
)}`,
224+
);
221225
} else {
222-
navigate(`/project-pages/${id}/edit/basic-info`);
226+
router.push(
227+
`/project-pages/${id}/edit/basic-info?from=${encodeURIComponent(
228+
location.pathname,
229+
)}`,
230+
);
223231
}
224232
}}
225233
>

web-marketplace/src/components/organisms/SellOrdersActionsBar/SellOrdersActionsBar.legacy.tsx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -118,6 +118,7 @@ export const SellOrdersActionsBar = ({
118118
`/project-pages/${
119119
onChainProjectId ?? offChainProjectId
120120
}/edit/basic-info`,
121+
{ state: { from: location.pathname } },
121122
)
122123
}
123124
>

web-marketplace/src/components/organisms/SellOrdersActionsBar/SellOrdersActionsBar.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -128,7 +128,7 @@ export const SellOrdersActionsBar = ({
128128
router.push(
129129
`/project-pages/${
130130
onChainProjectId ?? offChainProjectId
131-
}/edit/basic-info`,
131+
}/edit/basic-info?from=${encodeURIComponent(pathname)}`,
132132
)
133133
}
134134
>

web-marketplace/src/legacy-pages/Dashboard/MyProjects/MyProjects.tsx

Lines changed: 8 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import { Grid } from '@mui/material';
1010
import { useQuery } from '@tanstack/react-query';
1111
import { useSetAtom } from 'jotai';
1212
import { projectsDraftState } from 'legacy-pages/ProjectCreate/ProjectCreate.store';
13+
import { useRouter } from 'next/navigation';
1314

1415
import { CreateProjectCard } from 'web-components/src/components/cards/CreateCards/CreateProjectCard';
1516
import ProjectCard from 'web-components/src/components/cards/ProjectCard';
@@ -37,6 +38,7 @@ import {
3738

3839
const MyProjects = (): JSX.Element => {
3940
const { _ } = useLingui();
41+
const router = useRouter();
4042
const navigate = useNavigate();
4143
const location = useLocation();
4244
const {
@@ -94,12 +96,13 @@ const MyProjects = (): JSX.Element => {
9496
emptyTitle={MY_PROJECTS_EMPTY_TITLE}
9597
isFirstProject={isFirstProject}
9698
onClick={() => {
97-
navigate(`/project-pages/${DRAFT_ID}/account`, {
98-
state: {
99-
fromDashboard: true,
100-
isOrganization: isOrganizationDashboard,
101-
},
99+
const params = new URLSearchParams({
100+
from: location.pathname,
101+
...(isOrganizationDashboard && { isOrganization: 'true' }),
102102
});
103+
router.push(
104+
`/project-pages/${DRAFT_ID}/account?${params.toString()}`,
105+
);
103106
}}
104107
sx={{ height: { xs: '100%' } }}
105108
/>

web-marketplace/src/legacy-pages/ProjectAccount/ProjectAccount.tsx

Lines changed: 22 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { useEffect, useMemo, useRef, useState } from 'react';
2-
import { useLocation, useParams } from 'react-router-dom';
2+
import { useParams, useSearchParams } from 'react-router-dom';
33
import { msg } from '@lingui/core/macro';
44
import { useLingui } from '@lingui/react';
55
import { getDefaultAvatar } from 'legacy-pages/Dashboard/Dashboard.utils';
@@ -28,10 +28,10 @@ import { useCreateProjectContext } from '../ProjectCreate/ProjectCreate';
2828
export const ProjectAccount = (): JSX.Element | null => {
2929
const { _ } = useLingui();
3030
const { projectId } = useParams();
31-
const location = useLocation();
32-
const state = location.state as
33-
| { fromDashboard?: boolean; isOrganization?: boolean }
34-
| undefined;
31+
const [searchParams] = useSearchParams();
32+
const fromPath = searchParams.get('from');
33+
const hasOriginPath = !!fromPath;
34+
const isOrganizationParam = searchParams.get('isOrganization') === 'true';
3535
const { activeAccount } = useAuth();
3636
const dao = useDaoOrganization();
3737
const {
@@ -79,7 +79,7 @@ export const ProjectAccount = (): JSX.Element | null => {
7979
const shouldSkip =
8080
!dao || !activeAccount || !personalAccount || !organizationAccount;
8181

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

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

91-
if (state?.fromDashboard) {
92-
// Coming from dashboard - use the passed state
93-
const isOrg = !!state.isOrganization;
91+
if (hasOriginPath) {
92+
// Coming from an origin path - use the passed params
93+
const isOrg = isOrganizationParam;
9494
const address = isOrg
9595
? organizationAccount?.address
9696
: personalAccount?.address;
@@ -116,7 +116,8 @@ export const ProjectAccount = (): JSX.Element | null => {
116116
setIsStateReady(true);
117117
}
118118
}, [
119-
state,
119+
hasOriginPath,
120+
isOrganizationParam,
120121
organizationAccount?.address,
121122
personalAccount?.address,
122123
projectCreatorAddress,
@@ -141,22 +142,27 @@ export const ProjectAccount = (): JSX.Element | null => {
141142

142143
// Navigate away if user should skip this step, but only after:
143144
// 1. Issuer check completes
144-
// 2. State is properly initialized (for dashboard redirects)
145+
// 2. State is properly initialized (for origin path redirects)
145146
useEffect(() => {
146147
const canNavigate =
147-
!isLoadingIsIssuer &&
148-
(shouldSkip || (state?.fromDashboard && isStateReady));
148+
!isLoadingIsIssuer && (shouldSkip || (hasOriginPath && isStateReady));
149149
if (canNavigate) {
150150
navigateNext();
151151
}
152-
}, [shouldSkip, isLoadingIsIssuer, navigateNext, state, isStateReady]);
152+
}, [
153+
shouldSkip,
154+
isLoadingIsIssuer,
155+
navigateNext,
156+
hasOriginPath,
157+
isStateReady,
158+
]);
153159

154160
// Show loading while:
155161
// 1. Checking issuer status
156-
// 2. Processing dashboard redirect (waiting for state to be ready)
162+
// 2. Processing origin path redirect (waiting for state to be ready)
157163
// 3. User should skip this page
158164
const showLoading =
159-
isLoadingIsIssuer || (state?.fromDashboard && !isStateReady) || shouldSkip;
165+
isLoadingIsIssuer || (hasOriginPath && !isStateReady) || shouldSkip;
160166

161167
if (showLoading) {
162168
return (

web-marketplace/src/legacy-pages/ProjectCreate/ProjectCreate.tsx

Lines changed: 69 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,13 @@
1-
import { MutableRefObject, useCallback, useRef, useState } from 'react';
1+
import {
2+
MutableRefObject,
3+
useCallback,
4+
useEffect,
5+
useRef,
6+
useState,
7+
} from 'react';
28
import {
39
Outlet,
4-
useNavigate,
10+
useLocation,
511
useOutletContext,
612
useParams,
713
} from 'react-router-dom';
@@ -12,13 +18,15 @@ import { useLingui } from '@lingui/react';
1218
import { useQuery } from '@tanstack/react-query';
1319
import { useAtom } from 'jotai';
1420
import { DRAFT_ID } from 'legacy-pages/Dashboard/MyProjects/MyProjects.constants';
21+
import { useRouter } from 'next/navigation';
1522

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

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

2128
import { FormRef } from 'components/molecules/Form/Form';
29+
import { useDaoOrganization } from 'hooks/useDaoOrganization';
2230

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

@@ -58,9 +66,21 @@ const defaultProjectCreateContext: ContextType = {
5866
setIsOrganizationAccount: () => void 0,
5967
};
6068

69+
/** Returns true only for same-origin relative paths, blocking open-redirect attacks. */
70+
const isSafeRelativePath = (path: string): boolean => {
71+
try {
72+
const resolved = new URL(path, window.location.origin);
73+
return resolved.origin === window.location.origin;
74+
} catch {
75+
return false;
76+
}
77+
};
78+
6179
export const ProjectCreate = (): JSX.Element => {
6280
const { _ } = useLingui();
63-
const navigate = useNavigate();
81+
const router = useRouter();
82+
const location = useLocation();
83+
const dao = useDaoOrganization();
6484

6585
// TODO: possibly replace these with `useMsgClient` and pass downstream
6686
const [deliverTxResponse, setDeliverTxResponse] =
@@ -82,6 +102,23 @@ export const ProjectCreate = (): JSX.Element => {
82102
const formRef = useRef();
83103
const shouldNavigateRef = useRef(true);
84104
const isDraftRef = useRef(false);
105+
const originPathRef = useRef<string | null>(null);
106+
107+
// Capture the entry path only once, when the layout first mounts.
108+
// Child subroute navigations will change `location` but we only want the original.
109+
// We check both React Router state (set by navigate() callers) and a `?from` query
110+
// param (set by Next.js router.push() callers that can't pass router state).
111+
useEffect(() => {
112+
if (originPathRef.current === null) {
113+
const fromState =
114+
(location.state as { from?: string } | null)?.from ?? null;
115+
const fromParam = new URLSearchParams(location.search).get('from');
116+
const safeFromParam =
117+
fromParam && isSafeRelativePath(fromParam) ? fromParam : null;
118+
originPathRef.current = fromState ?? safeFromParam;
119+
}
120+
// eslint-disable-next-line react-hooks/exhaustive-deps
121+
}, []);
85122

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

98135
const handleRequestClose = useCallback(() => {
99-
if (isOrganizationAccount || offChainProject?.adminDaoAddress)
100-
navigate('/dashboard/organization', { replace: true });
101-
else navigate('/dashboard', { replace: true });
102-
}, [navigate, isOrganizationAccount, offChainProject]);
136+
// If we know where the user came from, send them back there.
137+
// We use Next.js router.replace() (not React Router navigate) because the origin
138+
// may be a Next.js App Router page (e.g. /project/slug) that lives outside
139+
// React Router's route tree.
140+
if (originPathRef.current) {
141+
router.replace(originPathRef.current);
142+
return;
143+
}
144+
// Fallback: new draft projects have no meaningful manage page yet → homepage.
145+
if (projectId === DRAFT_ID) {
146+
router.replace('/');
147+
return;
148+
}
149+
// Fallback for existing projects: dashboard manage page.
150+
const projectPath = `projects/${projectId}/manage`;
151+
if (
152+
isOrganizationAccount ||
153+
(offChainProject?.adminDaoAddress &&
154+
offChainProject?.adminDaoAddress === dao?.address)
155+
)
156+
router.replace(`/dashboard/organization/${projectPath}`);
157+
else router.replace(`/dashboard/${projectPath}`);
158+
}, [
159+
router,
160+
projectId,
161+
isOrganizationAccount,
162+
offChainProject?.adminDaoAddress,
163+
dao?.address,
164+
]);
103165

104166
return (
105167
<>

web-marketplace/src/legacy-pages/ProjectCreate/hooks/useProjectSaveAndExit.ts

Lines changed: 4 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,20 +1,19 @@
1-
import { useNavigate } from 'react-router-dom';
1+
import { useRouter } from 'next/navigation';
22

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

55
export const useProjectSaveAndExit = () => {
66
const { formRef, shouldNavigateRef, isOrganizationAccount } =
77
useCreateProjectContext();
8-
const navigate = useNavigate();
8+
const router = useRouter();
99

1010
const saveAndExit = async (): Promise<void> => {
1111
if (shouldNavigateRef) {
1212
shouldNavigateRef.current = false;
1313
await formRef?.current?.submitForm(true);
14-
shouldNavigateRef.current = true;
1514
}
16-
if (isOrganizationAccount) navigate('/dashboard/organization/projects');
17-
else navigate('/dashboard/projects');
15+
if (isOrganizationAccount) router.push('/dashboard/organization/projects');
16+
else router.push('/dashboard/projects');
1817
};
1918

2019
return saveAndExit;

web-marketplace/src/legacy-pages/ProjectEdit/ProjectEdit.tsx

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ import { useCanAccessManageProjectWithRole } from 'legacy-pages/Dashboard/MyProj
2121
import { useFeeGranter } from 'legacy-pages/Dashboard/MyProjects/hooks/useFeeGranter';
2222
import NotFoundPage from 'legacy-pages/NotFound';
2323
import { startCase } from 'lodash';
24+
import { useRouter } from 'next/navigation';
2425

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

105107
const setProcessingModalAtom = useSetAtom(processingModalAtom);
106108
const setErrorCodeAtom = useSetAtom(errorCodeAtom);
@@ -240,10 +242,10 @@ function ProjectEdit(): JSX.Element {
240242
if (isFormDirty) {
241243
setIsWarningModalOpen(path);
242244
} else {
243-
navigate(path);
245+
router.push(path);
244246
}
245247
} else {
246-
navigate('/dashboard/projects');
248+
router.push('/dashboard/projects');
247249
}
248250
};
249251

0 commit comments

Comments
 (0)