"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>) => {
dispatch(setLoading(true));
const data = await getUserData();
dispatch(setData(data));
dispatch(setLoading(false));
};
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>) =>
({
@@ -30,6 +35,7 @@ export const setData = (data: Partial<UserState>) =>
} as const);
export const logoutUser = () => async (dispatch: React.Dispatch<any>) => {
//
await setIsLoggedInData(false);
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>) => {
await setUsernameData(username);
console.log('setUsername triggered');
return {
type: 'set-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>) => {
await setAccessTokenData(token);
return {
type: 'set-access-token',
token,
@@ -99,6 +108,7 @@ export const checkUserSession = () => async (dispatch: React.Dispatch<any>) => {
export const setHasSeenTutorial =
(hasSeenTutorial: boolean) => async (dispatch: React.Dispatch<any>) => {
debugger;
await setHasSeenTutorialData(hasSeenTutorial);
return {
type: 'set-has-seen-tutorial',
@@ -120,5 +130,4 @@ export type UserActions =
| ActionType<typeof setHasSeenTutorial>
| ActionType<typeof setDarkMode>
| ActionType<typeof setAccessToken>
// | ActionType<typeof setSession>
| ActionType<typeof checkUserSession>;

View File

@@ -15,11 +15,9 @@ export function userReducer(state: UserState, action: UserActions): UserState {
return { ...state, darkMode: action.darkMode };
case 'set-is-loggedin':
return { ...state, isLoggedin: action.loggedIn };
case 'set-access-token':
return { ...state, token: action.token };
case 'check-user-session':
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 {
isLoggedin: boolean;
username?: string;
darkMode: boolean;
hasSeenTutorial: boolean;
loading: boolean;
username?: string;
hasSeenTutorial: boolean;
darkMode: boolean;
isLoggedin: boolean;
isSessionValid: boolean;
session?: any;
token?: string;

View File

@@ -74,6 +74,7 @@ const Login: React.FC<LoginProps> = (props) => {
setUsername: setUsernameAction,
checkUserSession,
isSessionValid,
isLoggedin,
} = props;
const history = useHistory();
@@ -103,9 +104,11 @@ const Login: React.FC<LoginProps> = (props) => {
// ----------
// email:
// password:
const defaultValues: SignInSchemaType = {
email: '',
password: '',
email: 'demo@minimals.c',
password: '@2Minimal',
};
const methods = useForm<SignInSchemaType>({
@@ -137,10 +140,11 @@ const Login: React.FC<LoginProps> = (props) => {
}, [isSessionValid]);
const onSubmit = handleSubmit(async (data) => {
console.log({ data });
// console.log({ data });
try {
let token = await signInWithPassword({ email: values.email, password: values.password });
console.log({ token });
// console.log({ token });
if (token) setAccessToken(token);
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 { getProfileById } from '../../../api/getProfileById';
import { defaultMember, Member } from '../../MemberProfile/type';
import SignUpPng from './SignUp.png';
interface OwnProps {}
@@ -144,29 +145,53 @@ const MyProfile: React.FC<SpeakerListProps> = ({ speakers, speakerSessions }) =>
<div
style={{
height: '50vh',
height: '80vh',
//
display: 'flex',
flexDirection: 'column',
alignItems: 'center',
justifyContent: 'center',
//
gap: '2rem',
}}
>
<div>
not login yet, <br />
please login or sign up
</div>
<div
style={{
height: '50vh',
width: '95vw',
height: '95vw',
backgroundImage: `url(${SignUpPng})`,
backgroundSize: 'contain',
backgroundPosition: 'center',
backgroundRepeat: 'no-repeat',
}}
></div>
<div
style={{
display: 'flex',
flexDirection: 'column',
alignItems: 'center',
justifyContent: 'center',
//
gap: '2rem',
}}
>
<IonButton disabled={disableForwardLoginButton} onClick={handleForwardLoginPage}>
Login
</IonButton>
<div>
not login yet, <br />
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>
</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 { format } from 'date-fns';
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 paths from '../../paths';
import { getProfileById } from '../../api/getProfileById';
@@ -54,6 +61,8 @@ import NotLoggedIn from './NotLoggedIn';
interface OwnProps {}
interface StateProps {
isLoggedin: boolean;
//
speakers: Speaker[];
speakerSessions: { [key: string]: Session[] };
}
@@ -62,7 +71,9 @@ interface 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 [showPopover, setShowPopover] = useState(false);
@@ -94,7 +105,6 @@ const MyProfile: React.FC<SpeakerListProps> = ({ speakers, speakerSessions }) =>
}, []);
if (!profile) return <>loading</>;
if (profile.id == -1) return <NotLoggedIn />;
return (
<IonPage id="speaker-list">
@@ -112,7 +122,12 @@ const MyProfile: React.FC<SpeakerListProps> = ({ speakers, speakerSessions }) =>
<IonContent fullscreen={true}>
<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>
<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' }}>
<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>
<div style={{ flexGrow: 1 }}>
<div
@@ -150,7 +168,12 @@ const MyProfile: React.FC<SpeakerListProps> = ({ speakers, speakerSessions }) =>
</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>
</IonButton>
</div>
@@ -179,7 +202,12 @@ const MyProfile: React.FC<SpeakerListProps> = ({ speakers, speakerSessions }) =>
<div style={{ fontWeight: 'bold', fontSize: '1.1rem' }}>Membership</div>
<div>7 of the exclusive privileges</div>
<div>
<IonButton expand="full" shape="round" size="large" onClick={handleNotImplementedClick}>
<IonButton
expand="full"
shape="round"
size="large"
onClick={handleNotImplementedClick}
>
Unlock
</IonButton>
</div>
@@ -237,7 +265,12 @@ const MyProfile: React.FC<SpeakerListProps> = ({ speakers, speakerSessions }) =>
</IonContent>
{/* 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">
<div
style={{
@@ -303,6 +336,7 @@ export default connect<OwnProps, StateProps, DispatchProps>({
mapStateToProps: (state) => ({
speakers: selectors.getSpeakers(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 { OverlayEventDetail } from '@ionic/react/dist/types/components/react-component-lib/interfaces';
import paths from '../../paths';
import { logoutUser, setAccessToken, setIsLoggedIn } from '../../data/user/user.actions';
interface OwnProps {}
@@ -71,11 +72,21 @@ interface StateProps {
speakerSessions: { [key: string]: Session[] };
}
interface DispatchProps {}
interface DispatchProps {
logoutUser: typeof logoutUser;
setAccessToken: typeof setAccessToken;
setIsLoggedIn: typeof setIsLoggedIn;
}
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 [showPopover, setShowPopover] = useState(false);
const [popoverEvent, setPopoverEvent] = useState<MouseEvent>();
@@ -116,6 +127,11 @@ const EventList: React.FC<SpeakerListProps> = ({ speakers, speakerSessions }) =>
}
function handleLogoutClick() {
setAccessToken();
setIsLoggedIn(false);
router.push('/tabs', 'forward', 'replace');
setShowLogoutConfirmModal(false);
}
function handleLogoutCancel() {
@@ -190,7 +206,11 @@ const EventList: React.FC<SpeakerListProps> = ({ speakers, speakerSessions }) =>
</IonContent>
{/* 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
className="ion-padding"
style={{
@@ -251,5 +271,10 @@ export default connect<OwnProps, StateProps, DispatchProps>({
speakers: selectors.getSpeakers(state),
speakerSessions: selectors.getSpeakerSessions(state),
}),
mapDispatchToProps: {
logoutUser,
setAccessToken,
setIsLoggedIn,
},
component: React.memo(EventList),
});