318 lines
10 KiB
TypeScript
318 lines
10 KiB
TypeScript
import React, { useEffect } from 'react';
|
|
import { Redirect, Route, useHistory, useLocation } from 'react-router-dom';
|
|
import {
|
|
IonApp,
|
|
IonIcon,
|
|
IonLabel,
|
|
IonRouterOutlet,
|
|
IonTabBar,
|
|
IonTabButton,
|
|
IonTabs,
|
|
} from '@ionic/react';
|
|
import { IonReactRouter } from '@ionic/react-router';
|
|
import { Capacitor } from '@capacitor/core';
|
|
import { PushNotifications } from '@capacitor/push-notifications';
|
|
import type {
|
|
ActionPerformed,
|
|
PushNotificationSchema,
|
|
} from '@capacitor/push-notifications';
|
|
import { supabase } from './supabase';
|
|
import {
|
|
heart,
|
|
heartOutline,
|
|
listOutline,
|
|
peopleOutline,
|
|
personOutline,
|
|
} from 'ionicons/icons';
|
|
|
|
import '@ionic/react/css/core.css';
|
|
import '@ionic/react/css/normalize.css';
|
|
import '@ionic/react/css/structure.css';
|
|
import '@ionic/react/css/typography.css';
|
|
import '@ionic/react/css/padding.css';
|
|
import '@ionic/react/css/float-elements.css';
|
|
import '@ionic/react/css/text-alignment.css';
|
|
import '@ionic/react/css/text-transformation.css';
|
|
import '@ionic/react/css/flex-utils.css';
|
|
import '@ionic/react/css/display.css';
|
|
import '@ionic/react/css/palettes/dark.system.css';
|
|
import './theme/variables.css';
|
|
|
|
import { setStatusBarStyle, Style } from './utils/statusBar';
|
|
import { AuthProvider, useAuth } from './contexts/AuthContext';
|
|
import ProtectedRoute from './components/ProtectedRoute';
|
|
|
|
// Auth Pages
|
|
import AuthPage from './pages/AuthPage';
|
|
import VerifyEmailPage from './pages/VerifyEmailPage';
|
|
import ForgotPasswordPage from './pages/ForgotPasswordPage';
|
|
import VerifyResetPage from './pages/VerifyResetPage';
|
|
import SetupProfilePage from './pages/SetupProfilePage';
|
|
|
|
// Main App Pages
|
|
import HomePage from './pages/HomePage';
|
|
import RecipientsPage from './pages/RecipientsPage';
|
|
import RecipientFormPage from './pages/RecipientFormPage';
|
|
import RecipientDetailPage from './pages/RecipientDetailPage';
|
|
import SupportFlowPage from './pages/SupportFlowPage';
|
|
import OrderDetailPage from './pages/OrderDetailPage';
|
|
import VoucherDetailPage from './pages/VoucherDetailPage';
|
|
import ActivityPage from './pages/ActivityPage';
|
|
import NotificationsPage from './pages/NotificationsPage';
|
|
import ProfilePage from './pages/ProfilePage';
|
|
import EditProfilePage from './pages/EditProfilePage';
|
|
|
|
const SetupProfileRoute: React.FC = () => {
|
|
const { user, profileStatus } = useAuth();
|
|
return (
|
|
<Route
|
|
path="/setup-profile"
|
|
render={() => {
|
|
if (!user) return <Redirect to="/auth" />;
|
|
if (profileStatus === 'loading') return null;
|
|
if (profileStatus === 'loaded') return <Redirect to="/home" />;
|
|
return <SetupProfilePage />;
|
|
}}
|
|
exact
|
|
/>
|
|
);
|
|
};
|
|
|
|
const SendSupportFab: React.FC = () => {
|
|
const history = useHistory();
|
|
const location = useLocation();
|
|
const hiddenPaths = [
|
|
'/auth',
|
|
'/verify-email',
|
|
'/forgot-password',
|
|
'/verify-reset',
|
|
'/setup-profile',
|
|
'/support/new',
|
|
];
|
|
const showOnPaths = ['/home', '/recipients', '/activity', '/profile'];
|
|
const isVisibleRoute =
|
|
showOnPaths.includes(location.pathname) ||
|
|
/^\/recipients\/[0-9a-fA-F-]{36}$/.test(location.pathname);
|
|
|
|
if (!isVisibleRoute || hiddenPaths.includes(location.pathname)) return null;
|
|
|
|
return (
|
|
<button
|
|
type="button"
|
|
className="app-send-support-fab"
|
|
aria-label="Send support"
|
|
onClick={() => history.push('/support/new')}
|
|
>
|
|
<IonIcon icon={heart} />
|
|
</button>
|
|
);
|
|
};
|
|
|
|
const PushNotificationHandler: React.FC = () => {
|
|
const { user, profile } = useAuth();
|
|
const history = useHistory();
|
|
|
|
useEffect(() => {
|
|
if (!Capacitor.isNativePlatform() || !user || !profile) return;
|
|
|
|
const setupPush = async () => {
|
|
const handleNotificationTap = (notification: PushNotificationSchema) => {
|
|
const { orderId, voucherId, screen } = notification.data || {};
|
|
if (voucherId) {
|
|
history.push(`/voucher/${voucherId}`, { parentRoot: '/home' });
|
|
} else if (orderId) {
|
|
history.push(`/orders/${orderId}`, { parentRoot: '/home' });
|
|
} else if (screen) {
|
|
history.push(screen, { parentRoot: '/home' });
|
|
}
|
|
};
|
|
|
|
// Capacitor automatically triggers pushNotificationActionPerformed
|
|
// for the notification that launched the app once the listener is attached.
|
|
|
|
// Check if user has explicitly enabled push notifications
|
|
if (!profile.notification_push_enabled) return;
|
|
|
|
const status = await PushNotifications.checkPermissions();
|
|
if (status.receive === 'granted') {
|
|
await PushNotifications.register();
|
|
} else if (status.receive === 'prompt') {
|
|
const result = await PushNotifications.requestPermissions();
|
|
if (result.receive === 'granted') {
|
|
await PushNotifications.register();
|
|
}
|
|
}
|
|
|
|
PushNotifications.addListener('registration', async (token) => {
|
|
await supabase
|
|
.from('profiles')
|
|
.update({ fcm_token: token.value })
|
|
.eq('id', user.id);
|
|
});
|
|
|
|
PushNotifications.addListener('registrationError', (err) => {
|
|
console.error('Push registration error:', err);
|
|
});
|
|
|
|
PushNotifications.addListener('pushNotificationReceived', () => {
|
|
// Handled natively via presentationOptions banner
|
|
});
|
|
|
|
PushNotifications.addListener(
|
|
'pushNotificationActionPerformed',
|
|
(action: ActionPerformed) => {
|
|
handleNotificationTap(action.notification);
|
|
}
|
|
);
|
|
|
|
await PushNotifications.removeAllDeliveredNotifications();
|
|
};
|
|
|
|
setupPush();
|
|
|
|
return () => {
|
|
if (Capacitor.isNativePlatform()) {
|
|
PushNotifications.removeAllListeners();
|
|
}
|
|
};
|
|
}, [user, profile, history]);
|
|
|
|
return null;
|
|
};
|
|
|
|
const RootRedirect: React.FC = () => {
|
|
const { user, profileStatus } = useAuth();
|
|
|
|
if (!user) return <Redirect to="/auth" />;
|
|
if (profileStatus === 'loading') return null;
|
|
if (profileStatus === 'missing') {
|
|
return <Redirect to="/setup-profile" />;
|
|
}
|
|
return <Redirect to="/home" />;
|
|
};
|
|
|
|
const App: React.FC = () => {
|
|
useEffect(() => {
|
|
setStatusBarStyle(Style.Light);
|
|
}, []);
|
|
|
|
return (
|
|
<IonApp>
|
|
<AuthProvider>
|
|
<IonReactRouter>
|
|
<IonRouterOutlet>
|
|
{/* Auth Routes */}
|
|
<Route path="/auth" component={AuthPage} exact />
|
|
<Route path="/verify-email" component={VerifyEmailPage} exact />
|
|
<Route
|
|
path="/forgot-password"
|
|
component={ForgotPasswordPage}
|
|
exact
|
|
/>
|
|
<Route path="/verify-reset" component={VerifyResetPage} exact />
|
|
|
|
{/* Setup Profile Route - Requires authenticated user but not necessarily complete profile */}
|
|
<SetupProfileRoute />
|
|
|
|
{/* Top-level Protected Routes (no tab bar) */}
|
|
<ProtectedRoute
|
|
path="/support/new"
|
|
component={SupportFlowPage}
|
|
exact
|
|
/>
|
|
<ProtectedRoute
|
|
path="/orders/:id([0-9a-fA-F-]{36})"
|
|
component={OrderDetailPage}
|
|
exact
|
|
/>
|
|
<ProtectedRoute
|
|
path="/recipients/new"
|
|
component={RecipientFormPage}
|
|
exact
|
|
/>
|
|
<ProtectedRoute
|
|
path="/recipients/:id([0-9a-fA-F-]{36})"
|
|
component={RecipientDetailPage}
|
|
exact
|
|
/>
|
|
<ProtectedRoute
|
|
path="/recipients/:id([0-9a-fA-F-]{36})/edit"
|
|
component={RecipientFormPage}
|
|
exact
|
|
/>
|
|
<ProtectedRoute
|
|
path="/profile/edit"
|
|
component={EditProfilePage}
|
|
exact
|
|
/>
|
|
<ProtectedRoute
|
|
path="/notifications"
|
|
component={NotificationsPage}
|
|
exact
|
|
/>
|
|
<ProtectedRoute
|
|
path="/voucher/:id([0-9a-fA-F-]{36})"
|
|
component={VoucherDetailPage}
|
|
exact
|
|
/>
|
|
|
|
{/* Tab Routes */}
|
|
<ProtectedRoute
|
|
path={['/home', '/recipients', '/activity', '/profile']}
|
|
exact
|
|
component={() => (
|
|
<IonTabs>
|
|
<IonRouterOutlet>
|
|
<Route path="/home" component={HomePage} exact />
|
|
<Route
|
|
path="/recipients"
|
|
component={RecipientsPage}
|
|
exact
|
|
/>
|
|
<Route path="/activity" component={ActivityPage} exact />
|
|
<Route path="/profile" component={ProfilePage} exact />
|
|
</IonRouterOutlet>
|
|
|
|
<IonTabBar slot="bottom" className="app-tab-bar">
|
|
<IonTabButton tab="home" href="/home">
|
|
<IonIcon icon={heartOutline} />
|
|
<IonLabel>Care</IonLabel>
|
|
</IonTabButton>
|
|
<IonTabButton tab="recipients" href="/recipients">
|
|
<IonIcon icon={peopleOutline} />
|
|
<IonLabel>Recipients</IonLabel>
|
|
</IonTabButton>
|
|
<IonTabButton disabled tab="send-support-fab" />
|
|
<IonTabButton tab="activity" href="/activity">
|
|
<IonIcon icon={listOutline} />
|
|
<IonLabel>Activity</IonLabel>
|
|
</IonTabButton>
|
|
<IonTabButton tab="profile" href="/profile">
|
|
<IonIcon icon={personOutline} />
|
|
<IonLabel>Profile</IonLabel>
|
|
</IonTabButton>
|
|
</IonTabBar>
|
|
</IonTabs>
|
|
)}
|
|
/>
|
|
|
|
{/* Orders Redirect */}
|
|
<Route
|
|
exact
|
|
path="/orders"
|
|
render={() => <Redirect to="/activity" />}
|
|
/>
|
|
|
|
{/* Root Redirect */}
|
|
<Route exact path="/" component={RootRedirect} />
|
|
</IonRouterOutlet>
|
|
<SendSupportFab />
|
|
<PushNotificationHandler />
|
|
</IonReactRouter>
|
|
</AuthProvider>
|
|
</IonApp>
|
|
);
|
|
};
|
|
|
|
export default App;
|