build: 444813b5-5daf-49b2-bf1a-7e4caeab4e17
This commit is contained in:
+317
@@ -0,0 +1,317 @@
|
||||
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;
|
||||
Reference in New Issue
Block a user