"feat: enhance user authentication flow with loading state, improved reducer handling, and login/logout actions"

This commit is contained in:
louiscklaw
2025-06-04 11:31:36 +08:00
parent df9992454b
commit b78709db9b
9 changed files with 130 additions and 35 deletions

View File

@@ -15,13 +15,18 @@ import { endpoints } from '../../pages/MyLogin/endpoints';
export const loadUserData = () => async (dispatch: React.Dispatch<any>) => { export const loadUserData = () => async (dispatch: React.Dispatch<any>) => {
dispatch(setLoading(true)); dispatch(setLoading(true));
const data = await getUserData(); const data = await getUserData();
dispatch(setData(data)); dispatch(setData(data));
dispatch(setLoading(false)); dispatch(setLoading(false));
}; };
export const setLoading = (isLoading: boolean) => export const setLoading = (isLoading: boolean) =>
({ type: 'set-user-loading', isLoading } as const); ({
type: 'set-user-loading',
isLoading,
} as const);
export const setData = (data: Partial<UserState>) => export const setData = (data: Partial<UserState>) =>
({ ({
@@ -30,6 +35,7 @@ export const setData = (data: Partial<UserState>) =>
} as const); } as const);
export const logoutUser = () => async (dispatch: React.Dispatch<any>) => { export const logoutUser = () => async (dispatch: React.Dispatch<any>) => {
//
await setIsLoggedInData(false); await setIsLoggedInData(false);
dispatch(setUsername()); dispatch(setUsername());
}; };
@@ -44,6 +50,8 @@ export const setIsLoggedIn = (loggedIn: boolean) => async (dispatch: React.Dispa
export const setUsername = (username?: string) => async (dispatch: React.Dispatch<any>) => { export const setUsername = (username?: string) => async (dispatch: React.Dispatch<any>) => {
await setUsernameData(username); await setUsernameData(username);
console.log('setUsername triggered');
return { return {
type: 'set-username', type: 'set-username',
username, username,
@@ -52,6 +60,7 @@ export const setUsername = (username?: string) => async (dispatch: React.Dispatc
export const setAccessToken = (token?: string) => async (dispatch: React.Dispatch<any>) => { export const setAccessToken = (token?: string) => async (dispatch: React.Dispatch<any>) => {
await setAccessTokenData(token); await setAccessTokenData(token);
return { return {
type: 'set-access-token', type: 'set-access-token',
token, token,
@@ -99,6 +108,7 @@ export const checkUserSession = () => async (dispatch: React.Dispatch<any>) => {
export const setHasSeenTutorial = export const setHasSeenTutorial =
(hasSeenTutorial: boolean) => async (dispatch: React.Dispatch<any>) => { (hasSeenTutorial: boolean) => async (dispatch: React.Dispatch<any>) => {
debugger;
await setHasSeenTutorialData(hasSeenTutorial); await setHasSeenTutorialData(hasSeenTutorial);
return { return {
type: 'set-has-seen-tutorial', type: 'set-has-seen-tutorial',
@@ -120,5 +130,4 @@ export type UserActions =
| ActionType<typeof setHasSeenTutorial> | ActionType<typeof setHasSeenTutorial>
| ActionType<typeof setDarkMode> | ActionType<typeof setDarkMode>
| ActionType<typeof setAccessToken> | ActionType<typeof setAccessToken>
// | ActionType<typeof setSession>
| ActionType<typeof checkUserSession>; | ActionType<typeof checkUserSession>;

View File

@@ -15,11 +15,9 @@ export function userReducer(state: UserState, action: UserActions): UserState {
return { ...state, darkMode: action.darkMode }; return { ...state, darkMode: action.darkMode };
case 'set-is-loggedin': case 'set-is-loggedin':
return { ...state, isLoggedin: action.loggedIn }; return { ...state, isLoggedin: action.loggedIn };
case 'set-access-token':
return { ...state, token: action.token };
case 'check-user-session': case 'check-user-session':
return { ...state, isSessionValid: action.sessionValid }; return { ...state, isSessionValid: action.sessionValid };
// case 'set-active-session':
// return { ...state, session: action.session };
// case 'set-access-token':
// return { ...state, token: action.token };
} }
} }

View File

@@ -1,9 +1,9 @@
export interface UserState { export interface UserState {
isLoggedin: boolean;
username?: string;
darkMode: boolean;
hasSeenTutorial: boolean;
loading: boolean; loading: boolean;
username?: string;
hasSeenTutorial: boolean;
darkMode: boolean;
isLoggedin: boolean;
isSessionValid: boolean; isSessionValid: boolean;
session?: any; session?: any;
token?: string; token?: string;

View File

@@ -74,6 +74,7 @@ const Login: React.FC<LoginProps> = (props) => {
setUsername: setUsernameAction, setUsername: setUsernameAction,
checkUserSession, checkUserSession,
isSessionValid, isSessionValid,
isLoggedin,
} = props; } = props;
const history = useHistory(); const history = useHistory();
@@ -103,9 +104,11 @@ const Login: React.FC<LoginProps> = (props) => {
// ---------- // ----------
// email:
// password:
const defaultValues: SignInSchemaType = { const defaultValues: SignInSchemaType = {
email: '', email: 'demo@minimals.c',
password: '', password: '@2Minimal',
}; };
const methods = useForm<SignInSchemaType>({ const methods = useForm<SignInSchemaType>({
@@ -137,10 +140,11 @@ const Login: React.FC<LoginProps> = (props) => {
}, [isSessionValid]); }, [isSessionValid]);
const onSubmit = handleSubmit(async (data) => { const onSubmit = handleSubmit(async (data) => {
console.log({ data }); // console.log({ data });
try { try {
let token = await signInWithPassword({ email: values.email, password: values.password }); let token = await signInWithPassword({ email: values.email, password: values.password });
console.log({ token }); // console.log({ token });
if (token) setAccessToken(token); if (token) setAccessToken(token);
await checkUserSession(); await checkUserSession();

Binary file not shown.

After

Width:  |  Height:  |  Size: 161 KiB

View File

@@ -56,6 +56,7 @@ import AboutPopover from '../../../components/AboutPopover';
import paths from '../../../paths'; import paths from '../../../paths';
import { getProfileById } from '../../../api/getProfileById'; import { getProfileById } from '../../../api/getProfileById';
import { defaultMember, Member } from '../../MemberProfile/type'; import { defaultMember, Member } from '../../MemberProfile/type';
import SignUpPng from './SignUp.png';
interface OwnProps {} interface OwnProps {}
@@ -144,29 +145,53 @@ const MyProfile: React.FC<SpeakerListProps> = ({ speakers, speakerSessions }) =>
<div <div
style={{ style={{
height: '50vh', height: '80vh',
//
display: 'flex', display: 'flex',
flexDirection: 'column', flexDirection: 'column',
alignItems: 'center', alignItems: 'center',
justifyContent: 'center', justifyContent: 'center',
//
gap: '2rem',
}} }}
> >
<div>
not login yet, <br />
please login or sign up
</div>
<div <div
style={{ style={{
height: '50vh', width: '95vw',
height: '95vw',
backgroundImage: `url(${SignUpPng})`,
backgroundSize: 'contain',
backgroundPosition: 'center',
backgroundRepeat: 'no-repeat',
}}
></div>
<div
style={{
display: 'flex', display: 'flex',
flexDirection: 'column', flexDirection: 'column',
alignItems: 'center', alignItems: 'center',
justifyContent: 'center', justifyContent: 'center',
//
gap: '2rem',
}} }}
> >
<IonButton disabled={disableForwardLoginButton} onClick={handleForwardLoginPage}> <div>
Login not login yet, <br />
</IonButton> please login or sign up
</div>
<div
style={{
display: 'flex',
flexDirection: 'column',
alignItems: 'center',
justifyContent: 'center',
}}
>
<IonButton disabled={disableForwardLoginButton} onClick={handleForwardLoginPage}>
Login
</IonButton>
</div>
</div> </div>
</div> </div>
</IonContent> </IonContent>

Binary file not shown.

After

Width:  |  Height:  |  Size: 161 KiB

View File

@@ -44,7 +44,14 @@ import '../SpeakerList.scss';
import { getEvents } from '../../api/getEvents'; import { getEvents } from '../../api/getEvents';
import { format } from 'date-fns'; import { format } from 'date-fns';
import { Event } from './types'; import { Event } from './types';
import { alertOutline, chevronDownCircleOutline, createOutline, heart, menuOutline, settingsOutline } from 'ionicons/icons'; import {
alertOutline,
chevronDownCircleOutline,
createOutline,
heart,
menuOutline,
settingsOutline,
} from 'ionicons/icons';
import AboutPopover from '../../components/AboutPopover'; import AboutPopover from '../../components/AboutPopover';
import paths from '../../paths'; import paths from '../../paths';
import { getProfileById } from '../../api/getProfileById'; import { getProfileById } from '../../api/getProfileById';
@@ -54,6 +61,8 @@ import NotLoggedIn from './NotLoggedIn';
interface OwnProps {} interface OwnProps {}
interface StateProps { interface StateProps {
isLoggedin: boolean;
//
speakers: Speaker[]; speakers: Speaker[];
speakerSessions: { [key: string]: Session[] }; speakerSessions: { [key: string]: Session[] };
} }
@@ -62,7 +71,9 @@ interface DispatchProps {}
interface SpeakerListProps extends OwnProps, StateProps, DispatchProps {} interface SpeakerListProps extends OwnProps, StateProps, DispatchProps {}
const MyProfile: React.FC<SpeakerListProps> = ({ speakers, speakerSessions }) => { const MyProfilePage: React.FC<SpeakerListProps> = ({ speakers, speakerSessions, isLoggedin }) => {
if (!isLoggedin) return <NotLoggedIn />;
const [profile, setProfile] = useState<Member>(defaultMember); const [profile, setProfile] = useState<Member>(defaultMember);
const [showPopover, setShowPopover] = useState(false); const [showPopover, setShowPopover] = useState(false);
@@ -94,7 +105,6 @@ const MyProfile: React.FC<SpeakerListProps> = ({ speakers, speakerSessions }) =>
}, []); }, []);
if (!profile) return <>loading</>; if (!profile) return <>loading</>;
if (profile.id == -1) return <NotLoggedIn />;
return ( return (
<IonPage id="speaker-list"> <IonPage id="speaker-list">
@@ -112,7 +122,12 @@ const MyProfile: React.FC<SpeakerListProps> = ({ speakers, speakerSessions }) =>
<IonContent fullscreen={true}> <IonContent fullscreen={true}>
<IonRefresher slot="fixed" onIonRefresh={handleRefresh}> <IonRefresher slot="fixed" onIonRefresh={handleRefresh}>
<IonRefresherContent pullingIcon={chevronDownCircleOutline} pullingText="Pull to refresh" refreshingSpinner="circles" refreshingText="Refreshing..."></IonRefresherContent> <IonRefresherContent
pullingIcon={chevronDownCircleOutline}
pullingText="Pull to refresh"
refreshingSpinner="circles"
refreshingText="Refreshing..."
></IonRefresherContent>
</IonRefresher> </IonRefresher>
<IonHeader collapse="condense" className="ion-no-border"> <IonHeader collapse="condense" className="ion-no-border">
@@ -133,7 +148,10 @@ const MyProfile: React.FC<SpeakerListProps> = ({ speakers, speakerSessions }) =>
> >
<div style={{ padding: '1rem', display: 'flex', gap: '1rem' }}> <div style={{ padding: '1rem', display: 'flex', gap: '1rem' }}>
<IonAvatar> <IonAvatar>
<img alt="Silhouette of a person's head" src="https://plus.unsplash.com/premium_photo-1683121126477-17ef068309bc" /> <img
alt="Silhouette of a person's head"
src="https://plus.unsplash.com/premium_photo-1683121126477-17ef068309bc"
/>
</IonAvatar> </IonAvatar>
<div style={{ flexGrow: 1 }}> <div style={{ flexGrow: 1 }}>
<div <div
@@ -150,7 +168,12 @@ const MyProfile: React.FC<SpeakerListProps> = ({ speakers, speakerSessions }) =>
</div> </div>
</div> </div>
<div> <div>
<IonButton shape="round" fill="clear" size="large" onClick={handleNotImplementedClick}> <IonButton
shape="round"
fill="clear"
size="large"
onClick={handleNotImplementedClick}
>
<IonIcon slot="icon-only" icon={createOutline}></IonIcon> <IonIcon slot="icon-only" icon={createOutline}></IonIcon>
</IonButton> </IonButton>
</div> </div>
@@ -179,7 +202,12 @@ const MyProfile: React.FC<SpeakerListProps> = ({ speakers, speakerSessions }) =>
<div style={{ fontWeight: 'bold', fontSize: '1.1rem' }}>Membership</div> <div style={{ fontWeight: 'bold', fontSize: '1.1rem' }}>Membership</div>
<div>7 of the exclusive privileges</div> <div>7 of the exclusive privileges</div>
<div> <div>
<IonButton expand="full" shape="round" size="large" onClick={handleNotImplementedClick}> <IonButton
expand="full"
shape="round"
size="large"
onClick={handleNotImplementedClick}
>
Unlock Unlock
</IonButton> </IonButton>
</div> </div>
@@ -237,7 +265,12 @@ const MyProfile: React.FC<SpeakerListProps> = ({ speakers, speakerSessions }) =>
</IonContent> </IonContent>
{/* REQ0079/event-filter */} {/* REQ0079/event-filter */}
<IonModal ref={modal} trigger="my-profile-open-modal" initialBreakpoint={0.5} breakpoints={[0, 0.25, 0.5, 0.75]}> <IonModal
ref={modal}
trigger="my-profile-open-modal"
initialBreakpoint={0.5}
breakpoints={[0, 0.25, 0.5, 0.75]}
>
<IonContent className="ion-padding"> <IonContent className="ion-padding">
<div <div
style={{ style={{
@@ -303,6 +336,7 @@ export default connect<OwnProps, StateProps, DispatchProps>({
mapStateToProps: (state) => ({ mapStateToProps: (state) => ({
speakers: selectors.getSpeakers(state), speakers: selectors.getSpeakers(state),
speakerSessions: selectors.getSpeakerSessions(state), speakerSessions: selectors.getSpeakerSessions(state),
isLoggedin: state.user.isLoggedin,
}), }),
component: React.memo(MyProfile), component: React.memo(MyProfilePage),
}); });

View File

@@ -63,6 +63,7 @@ import {
import AboutPopover from '../../components/AboutPopover'; import AboutPopover from '../../components/AboutPopover';
import { OverlayEventDetail } from '@ionic/react/dist/types/components/react-component-lib/interfaces'; import { OverlayEventDetail } from '@ionic/react/dist/types/components/react-component-lib/interfaces';
import paths from '../../paths'; import paths from '../../paths';
import { logoutUser, setAccessToken, setIsLoggedIn } from '../../data/user/user.actions';
interface OwnProps {} interface OwnProps {}
@@ -71,11 +72,21 @@ interface StateProps {
speakerSessions: { [key: string]: Session[] }; speakerSessions: { [key: string]: Session[] };
} }
interface DispatchProps {} interface DispatchProps {
logoutUser: typeof logoutUser;
setAccessToken: typeof setAccessToken;
setIsLoggedIn: typeof setIsLoggedIn;
}
interface SpeakerListProps extends OwnProps, StateProps, DispatchProps {} interface SpeakerListProps extends OwnProps, StateProps, DispatchProps {}
const EventList: React.FC<SpeakerListProps> = ({ speakers, speakerSessions }) => { const EventList: React.FC<SpeakerListProps> = ({
speakers,
speakerSessions,
logoutUser,
setAccessToken,
setIsLoggedIn,
}) => {
const [events, setEvents] = useState<Event[] | []>([]); const [events, setEvents] = useState<Event[] | []>([]);
const [showPopover, setShowPopover] = useState(false); const [showPopover, setShowPopover] = useState(false);
const [popoverEvent, setPopoverEvent] = useState<MouseEvent>(); const [popoverEvent, setPopoverEvent] = useState<MouseEvent>();
@@ -116,6 +127,11 @@ const EventList: React.FC<SpeakerListProps> = ({ speakers, speakerSessions }) =>
} }
function handleLogoutClick() { function handleLogoutClick() {
setAccessToken();
setIsLoggedIn(false);
router.push('/tabs', 'forward', 'replace');
setShowLogoutConfirmModal(false); setShowLogoutConfirmModal(false);
} }
function handleLogoutCancel() { function handleLogoutCancel() {
@@ -190,7 +206,11 @@ const EventList: React.FC<SpeakerListProps> = ({ speakers, speakerSessions }) =>
</IonContent> </IonContent>
{/* REQ0058/logout */} {/* REQ0058/logout */}
<IonModal isOpen={showLogoutConfirmModal} initialBreakpoint={0.5} breakpoints={[0, 0.25, 0.5, 0.75]}> <IonModal
isOpen={showLogoutConfirmModal}
initialBreakpoint={0.5}
breakpoints={[0, 0.25, 0.5, 0.75]}
>
<IonContent <IonContent
className="ion-padding" className="ion-padding"
style={{ style={{
@@ -251,5 +271,10 @@ export default connect<OwnProps, StateProps, DispatchProps>({
speakers: selectors.getSpeakers(state), speakers: selectors.getSpeakers(state),
speakerSessions: selectors.getSpeakerSessions(state), speakerSessions: selectors.getSpeakerSessions(state),
}), }),
mapDispatchToProps: {
logoutUser,
setAccessToken,
setIsLoggedIn,
},
component: React.memo(EventList), component: React.memo(EventList),
}); });