Skip to content

Commit ba34174

Browse files
committed
#69 add onboarding flow
1 parent a36efb9 commit ba34174

File tree

3 files changed

+125
-19
lines changed

3 files changed

+125
-19
lines changed

frontend/src/components/app/App.tsx

Lines changed: 62 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,11 @@ import Clients from '../../pages/Clients';
1616
import Settings from '../../pages/Settings';
1717
import Help from '../../pages/Help';
1818

19+
import React, { useContext, useEffect } from 'react';
20+
import { AppContext } from './AppContext';
21+
import { useZipCaseApi } from '../../hooks';
22+
import { useNavigate, useLocation } from 'react-router-dom';
23+
1924
// Create theme for Amplify UI components
2025
const amplifyTheme = {
2126
tokens: {
@@ -130,30 +135,72 @@ const components = {
130135
},
131136
};
132137

138+
// Onboarding logic: check for portal credentials on login, set firstTimeUser, and redirect if needed
133139
const App: React.FC = () => {
134140
return (
135141
<ThemeProvider theme={{ ...defaultTheme, ...amplifyTheme }}>
136142
<Authenticator hideSignUp={true} components={components}>
137143
<QueryClientProvider client={queryClient}>
138-
<AppContextProvider>
139-
<BrowserRouter>
140-
<Routes>
141-
<Route path="/" element={<Navigate to="/search/case" />} />
142-
<Route element={<Shell />}>
143-
<Route path="/search" element={<Navigate to="/search/case" />} />
144-
<Route path="/search/case" element={<Search type="case" />} />
145-
<Route path="/search/name" element={<Search type="name" />} />
146-
<Route path="/clients" element={<Clients />} />
147-
<Route path="/settings" element={<Settings />} />
148-
<Route path="/help" element={<Help />} />
149-
</Route>
150-
</Routes>
151-
</BrowserRouter>
152-
</AppContextProvider>
144+
<BrowserRouter>
145+
<AppContextProvider>
146+
<OnboardingRouter />
147+
</AppContextProvider>
148+
</BrowserRouter>
153149
</QueryClientProvider>
154150
</Authenticator>
155151
</ThemeProvider>
156152
);
157153
};
158154

155+
const OnboardingRouter: React.FC = () => {
156+
const { dispatch } = useContext(AppContext);
157+
const getPortalCredentials = useZipCaseApi(client => client.credentials.get()).callApi;
158+
const [checked, setChecked] = React.useState(false);
159+
const navigate = useNavigate();
160+
const location = useLocation();
161+
162+
useEffect(() => {
163+
// Check onboarding on first mount and on login (when location or credentials change)
164+
let cancelled = false;
165+
(async () => {
166+
try {
167+
const resp = await getPortalCredentials();
168+
const isFirstTime = !resp.success || !resp.data || !resp.data.username;
169+
dispatch({ type: 'SET_FIRST_TIME_USER', payload: isFirstTime });
170+
if (isFirstTime && location.pathname.startsWith('/search')) {
171+
navigate('/settings', { replace: true });
172+
}
173+
} catch {
174+
dispatch({ type: 'SET_FIRST_TIME_USER', payload: true });
175+
if (location.pathname.startsWith('/search')) {
176+
navigate('/settings', { replace: true });
177+
}
178+
} finally {
179+
if (!cancelled) setChecked(true);
180+
}
181+
})();
182+
return () => {
183+
cancelled = true;
184+
};
185+
// Run on mount and when location changes (to catch initial login)
186+
}, [location.pathname, dispatch, getPortalCredentials, navigate]);
187+
188+
// Wait until onboarding check is done before rendering routes
189+
if (!checked) return null;
190+
191+
return (
192+
<Routes>
193+
<Route path="/" element={<Navigate to="/search/case" />} />
194+
<Route element={<Shell />}>
195+
<Route path="/search" element={<Navigate to="/search/case" />} />
196+
<Route path="/search/case" element={<Search type="case" />} />
197+
<Route path="/search/name" element={<Search type="name" />} />
198+
<Route path="/clients" element={<Clients />} />
199+
<Route path="/settings" element={<Settings />} />
200+
<Route path="/help" element={<Help />} />
201+
</Route>
202+
</Routes>
203+
);
204+
};
205+
159206
export default App;

frontend/src/components/app/AppContext.tsx

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,23 +2,29 @@ import { createContext, Dispatch } from 'react';
22

33
export interface AppContextType {
44
token: string;
5+
firstTimeUser: boolean;
56
dispatch: Dispatch<AppContextAction>;
67
}
78

89
export const defaultContext = {
910
token: '',
11+
firstTimeUser: false,
1012
dispatch: () => {},
1113
};
1214

1315
export const AppContext = createContext<AppContextType>(defaultContext);
1416

15-
export type AppContextAction = { type: 'SET_TOKEN'; payload: string };
17+
export type AppContextAction =
18+
| { type: 'SET_TOKEN'; payload: string }
19+
| { type: 'SET_FIRST_TIME_USER'; payload: boolean };
1620

1721
export const appReducer = (state: AppContextType, action: AppContextAction): AppContextType => {
1822
switch (action.type) {
1923
case 'SET_TOKEN':
2024
return { ...state, token: action.payload };
25+
case 'SET_FIRST_TIME_USER':
26+
return { ...state, firstTimeUser: action.payload };
2127
default:
2228
return state;
2329
}
24-
};
30+
};

frontend/src/pages/Settings.tsx

Lines changed: 55 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,13 @@
1-
import React from 'react';
1+
import React, { useContext, useState } from 'react';
22
import { useQuery } from '@tanstack/react-query';
3-
import { SettingsPortalCredentials, SettingsApiKeys } from '../components';
3+
import { SettingsPortalCredentials, SettingsApiKeys, TextLink } from '../components';
4+
import {
5+
Dialog,
6+
DialogTitle,
7+
DialogDescription,
8+
DialogBody,
9+
DialogActions,
10+
} from '../components/tailwind/dialog';
411
import { useZipCaseApi } from '../hooks';
512
import { AppContext } from '../components/app/AppContext';
613

@@ -29,10 +36,56 @@ const Settings: React.FC = () => {
2936
}),
3037
});
3138

39+
// Onboarding modal logic
40+
const { firstTimeUser, dispatch } = useContext(AppContext);
41+
const [showModal, setShowModal] = useState(firstTimeUser);
42+
const [hasVisited, setHasVisited] = useState(false);
43+
44+
React.useEffect(() => {
45+
if (firstTimeUser && !hasVisited) {
46+
setShowModal(true);
47+
setHasVisited(true);
48+
}
49+
}, [firstTimeUser, hasVisited]);
50+
51+
const handleClose = () => {
52+
setShowModal(false);
53+
dispatch({ type: 'SET_FIRST_TIME_USER', payload: false });
54+
};
55+
3256
return (
3357
<>
3458
<SettingsPortalCredentials portalCredentials={credentialsQuery} />
3559
<SettingsApiKeys apiSettings={apiSettingsQuery} />
60+
<Dialog open={showModal} onClose={handleClose} size="md">
61+
<DialogTitle>Welcome to ZipCase!</DialogTitle>
62+
<DialogDescription>
63+
<span role="img" aria-label="wave">
64+
👋
65+
</span>{' '}
66+
Before you can start looking up cases, you'll need to add your court portal
67+
credentials here.
68+
</DialogDescription>
69+
<DialogBody>
70+
<p className="mb-2">
71+
If you have questions about why we need your credentials or how your data is
72+
protected, check out our{' '}
73+
<TextLink href="/help" onClick={handleClose}>
74+
FAQ
75+
</TextLink>
76+
.
77+
</p>
78+
</DialogBody>
79+
<DialogActions>
80+
<button
81+
type="button"
82+
onClick={handleClose}
83+
className="bg-primary hover:bg-primary-light active:bg-primary-dark rounded-md px-3 py-2 text-sm font-semibold text-white shadow-xs focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-primary"
84+
>
85+
Got it
86+
</button>
87+
</DialogActions>
88+
</Dialog>
3689
</>
3790
);
3891
};

0 commit comments

Comments
 (0)