feat: implement payment flow for event joining including success/failure pages and navigation

This commit is contained in:
louiscklaw
2025-06-19 16:21:11 +08:00
parent 7b230d4f8b
commit c4f8a6902c
9 changed files with 414 additions and 4 deletions

View File

@@ -68,6 +68,8 @@ import MemberProfile from './pages/MemberProfile';
import OrderDetail from './pages/OrderDetail';
import DummyPayPage from './pages/DummyEventPayPage';
import DummyEventPayPage from './pages/DummyEventPayPage';
import PaymentSuccess from './pages/PaymentSuccess';
import PaymentFailed from './pages/PaymentFailed';
setupIonicReact();
@@ -133,9 +135,12 @@ const IonicApp: React.FC<IonicAppProps> = ({ darkMode, schedule, setIsLoggedIn,
<Route exact={true} path={PATHS.PRIVACY_AGREEMENT} component={PrivacyAgreement} />
{/* Event and profile detail pages */}
<Route exact={true} path="/dummy_pay_page" component={DummyPayPage} />
<Route exact={true} path="/event_detail/:id" component={EventDetail} />
<Route exact={true} path="/dummy_pay_page" component={DummyPayPage} />
<Route exact={true} path="/payment_success" component={PaymentSuccess} />
<Route exact={true} path="/payment_failed" component={PaymentFailed} />
<Route exact={true} path="/profile/:id" component={MemberProfile} />
{/* component make the ":id" available in the "OrderDetail" */}

View File

@@ -15,6 +15,9 @@ const PATHS = {
PRIVACY_AGREEMENT: '/privacy_agreement',
SIGN_IN: '/mylogin',
// event detail
getEventDetailPath: (eventId: string) => `/event_detail/${eventId}`,
// Order-related routes
ORDER_DETAIL: '/order_detail/:id',
getOrderDetail: (id: string) => `/order_detail/${id}`,
@@ -32,6 +35,8 @@ const PATHS = {
PARTY_USER_SIGN_UP: '/partyUserSignUp',
DUMMY_EVENT_PAY_PAGE: '/DummyEventPayPage',
PAYMENT_SUCCESS: '/payment_success',
PAYMENT_FAILED: '/payment_failed',
//
TABS_DEBUG: '/tabs/debug',

View File

@@ -56,7 +56,7 @@ const DummyPayPage: React.FC<PageProps> = ({
},
});
router.goBack();
router.push(PATHS.PAYMENT_SUCCESS, 'back', 'replace');
setShowJoinOKToast(true);
} catch (error) {

View File

@@ -124,7 +124,7 @@ const EventDetail: React.FC<PageProps> = ({ event_detail, setEventIdToJoin }) =>
function handleJoinClick() {
if (event_detail && event_detail?.id) {
setEventIdToJoin(event_detail.id);
router.push(PATHS.DUMMY_EVENT_PAY_PAGE);
router.push(PATHS.DUMMY_EVENT_PAY_PAGE, 'forward', 'push');
}
}

View File

@@ -0,0 +1,77 @@
// src/pages/PaymentFailed/index.tsx
//
// PURPOSE:
// - Provides functionality view user profile
//
// RULES:
// - T.B.A.
//
// Requirements:
// - REQ0189
//
import { IonPage, IonHeader, IonToolbar, IonTitle, IonContent, useIonRouter } from '@ionic/react';
import React, { useEffect } from 'react';
import './style.scss';
import PATHS from '../../PATHS';
import { UserState } from '../../data/user/user.state';
import { connect } from '../../data/connect';
import * as selectors from '../../data/selectors';
import NotLoggedIn from '../MyProfile/NotLoggedIn';
interface OwnProps {}
interface StateProps {
isLoggedin: boolean;
//
partyUserState: UserState;
//
joinEventId: string;
}
interface DispatchProps {}
interface PageProps extends OwnProps, StateProps, DispatchProps {}
const PaymentFailedPage: React.FC<PageProps> = ({
isLoggedin,
partyUserState,
//
joinEventId,
}) => {
const router = useIonRouter();
if (!isLoggedin) return <NotLoggedIn />;
useEffect(() => {
setTimeout(() => {
router.push(PATHS.getEventDetailPath(joinEventId), 'forward', 'replace');
}, 3000);
}, []);
return (
<IonPage id="speaker-list">
<IonHeader translucent={true} className="ion-no-border">
<IonToolbar>
<IonTitle>payment failed</IonTitle>
</IonToolbar>
</IonHeader>
<IonContent fullscreen={true}>
<div>Sorry, payment failed</div>
<div>Will soon redirect back to event detail page</div>
</IonContent>
</IonPage>
);
};
export default connect<OwnProps, StateProps, DispatchProps>({
mapStateToProps: (state) => ({
isLoggedin: state.user.isLoggedin,
//
joinEventId: selectors.getEventIdToJoin(state),
//
partyUserState: selectors.getPartyUserState(state),
}),
component: PaymentFailedPage,
});

View File

@@ -0,0 +1,103 @@
#about-page {
ion-toolbar {
position: absolute;
top: 0;
left: 0;
right: 0;
--background: transparent;
--color: white;
}
ion-toolbar ion-back-button,
ion-toolbar ion-button,
ion-toolbar ion-menu-button {
--color: white;
}
.about-header {
position: relative;
width: 100%;
height: 30%;
}
.about-header .about-image {
position: absolute;
top: 0;
left: 0;
bottom: 0;
right: 0;
background-position: center;
background-size: cover;
background-repeat: no-repeat;
opacity: 0;
transition: opacity 500ms ease-in-out;
}
.about-header .madison {
background-image: url('/assets/img/about/madison.jpg');
}
.about-header .austin {
background-image: url('/assets/img/about/austin.jpg');
}
.about-header .chicago {
background-image: url('/assets/img/about/chicago.jpg');
}
.about-header .seattle {
background-image: url('/assets/img/about/seattle.jpg');
}
.about-info {
position: relative;
margin-top: -10px;
border-radius: 10px;
background: var(--ion-background-color, #fff);
z-index: 2; // display rounded border above header image
}
.about-info h3 {
margin-top: 0;
}
.about-info ion-list {
padding-top: 0;
}
.about-info p {
line-height: 130%;
color: var(--ion-color-dark);
}
.about-info ion-icon {
margin-inline-end: 32px;
}
/*
* iOS Only
*/
.ios .about-info {
--ion-padding: 19px;
}
.ios .about-info h3 {
font-weight: 700;
}
}
#date-input-popover {
--offset-y: -var(--ion-safe-area-bottom);
--max-width: 90%;
--width: 336px;
}

View File

@@ -0,0 +1,88 @@
// src/pages/PaymentSuccess/index.tsx
//
// PURPOSE:
// - Provides functionality view user profile
//
// RULES:
// - T.B.A.
//
// Requirements:
// - REQ0189
//
import { IonPage, IonHeader, IonToolbar, IonTitle, IonContent, useIonRouter } from '@ionic/react';
import React, { useEffect, useState } from 'react';
import './style.scss';
import PATHS from '../../PATHS';
import { UserState } from '../../data/user/user.state';
import { connect } from '../../data/connect';
import * as selectors from '../../data/selectors';
import NotLoggedIn from '../MyProfile/NotLoggedIn';
interface OwnProps {}
interface StateProps {
isLoggedin: boolean;
//
partyUserState: UserState;
//
joinEventId: string;
}
interface DispatchProps {}
interface PageProps extends OwnProps, StateProps, DispatchProps {}
const PaymentSuccessPage: React.FC<PageProps> = ({
isLoggedin,
partyUserState,
//
joinEventId,
}) => {
const router = useIonRouter();
const [countDown, setCountDown] = useState(3);
if (!isLoggedin) return <NotLoggedIn />;
useEffect(() => {
setTimeout(() => {
if (joinEventId === '') {
// router.push(PATHS.EVENT_LIST, 'root', 'replace');
} else {
router.push(PATHS.getEventDetailPath(joinEventId), 'back', 'replace');
window.location.reload();
}
}, 3000);
setTimeout(() => {
setCountDown(countDown - 1);
}, 1000);
}, []);
return (
<IonPage id="speaker-list">
<IonHeader translucent={true} className="ion-no-border">
<IonToolbar>
<IonTitle>Payment Success</IonTitle>
</IonToolbar>
</IonHeader>
<IonContent fullscreen={true}>
<div>Congrats, payment done !</div>
<div>Will soon redirect back to event detail page</div>
<div>{countDown}</div>
</IonContent>
</IonPage>
);
};
export default connect<OwnProps, StateProps, DispatchProps>({
mapStateToProps: (state) => ({
isLoggedin: state.user.isLoggedin,
//
joinEventId: selectors.getEventIdToJoin(state),
//
partyUserState: selectors.getPartyUserState(state),
}),
component: PaymentSuccessPage,
});

View File

@@ -0,0 +1,103 @@
#about-page {
ion-toolbar {
position: absolute;
top: 0;
left: 0;
right: 0;
--background: transparent;
--color: white;
}
ion-toolbar ion-back-button,
ion-toolbar ion-button,
ion-toolbar ion-menu-button {
--color: white;
}
.about-header {
position: relative;
width: 100%;
height: 30%;
}
.about-header .about-image {
position: absolute;
top: 0;
left: 0;
bottom: 0;
right: 0;
background-position: center;
background-size: cover;
background-repeat: no-repeat;
opacity: 0;
transition: opacity 500ms ease-in-out;
}
.about-header .madison {
background-image: url('/assets/img/about/madison.jpg');
}
.about-header .austin {
background-image: url('/assets/img/about/austin.jpg');
}
.about-header .chicago {
background-image: url('/assets/img/about/chicago.jpg');
}
.about-header .seattle {
background-image: url('/assets/img/about/seattle.jpg');
}
.about-info {
position: relative;
margin-top: -10px;
border-radius: 10px;
background: var(--ion-background-color, #fff);
z-index: 2; // display rounded border above header image
}
.about-info h3 {
margin-top: 0;
}
.about-info ion-list {
padding-top: 0;
}
.about-info p {
line-height: 130%;
color: var(--ion-color-dark);
}
.about-info ion-icon {
margin-inline-end: 32px;
}
/*
* iOS Only
*/
.ios .about-info {
--ion-padding: 19px;
}
.ios .about-info h3 {
font-weight: 700;
}
}
#date-input-popover {
--offset-y: -var(--ion-safe-area-bottom);
--max-width: 90%;
--width: 336px;
}