Skip to content

Commit cfacc29

Browse files
committed
backend of notifications works
1 parent e8d7430 commit cfacc29

13 files changed

Lines changed: 562 additions & 23 deletions

File tree

KonditionExpo/app.json

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,15 @@
1111
"ios": {
1212
"supportsTablet": true
1313
},
14+
"plugins": [
15+
[
16+
"expo-notifications",
17+
{
18+
"icon": "./assets/images/bell.png",
19+
"color": "#ffffff"
20+
}
21+
]
22+
],
1423
"android": {
1524
"adaptiveIcon": {
1625
"foregroundImage": "./assets/images/adaptive-icon.png",

KonditionExpo/app/_layout.tsx

Lines changed: 119 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,21 +1,127 @@
1-
import { DarkTheme, DefaultTheme, ThemeProvider } from '@react-navigation/native';
2-
import { useFonts } from 'expo-font';
3-
import { Stack } from 'expo-router';
4-
import { StatusBar } from 'expo-status-bar';
5-
import { AuthProvider } from '@/contexts/AuthContext';
6-
import { ProtectedRoute } from '@/components/ProtectedRoute';
7-
import 'react-native-reanimated';
8-
9-
import { useColorScheme } from '@/hooks/useColorScheme';
10-
import { UserProvider } from '@/contexts/UserContext';
11-
import { WorkoutProvider } from '@/contexts/WorkoutContext';
1+
// app/_layout.tsx
2+
3+
import React, { useEffect, useRef, useState } from "react";
4+
import { Platform, Alert } from "react-native";
5+
6+
import { DarkTheme, DefaultTheme, ThemeProvider } from "@react-navigation/native";
7+
import { useFonts } from "expo-font";
8+
import { Stack } from "expo-router";
9+
import { StatusBar } from "expo-status-bar";
10+
import { AuthProvider } from "@/contexts/AuthContext";
11+
import { ProtectedRoute } from "@/components/ProtectedRoute";
12+
import "react-native-reanimated";
13+
14+
import { useColorScheme } from "@/hooks/useColorScheme";
15+
import { UserProvider } from "@/contexts/UserContext";
16+
import { WorkoutProvider } from "@/contexts/WorkoutContext";
17+
18+
// ─── IMPORTS FOR PUSH NOTIFICATIONS ─────────────────────────────────────────────
19+
import * as Device from "expo-device";
20+
import * as Notifications from "expo-notifications";
21+
// ────────────────────────────────────────────────────────────────────────────────
1222

1323
export default function RootLayout() {
1424
const colorScheme = useColorScheme();
1525
const [loaded] = useFonts({
16-
SpaceMono: require('../assets/fonts/SpaceMono-Regular.ttf'),
26+
SpaceMono: require("../assets/fonts/SpaceMono-Regular.ttf"),
1727
});
1828

29+
// ─── STATE / REFS FOR PUSH REGISTRATION ────────────────────────────────────────
30+
const [expoPushToken, setExpoPushToken] = useState<string>("");
31+
const notificationListener = useRef<any>();
32+
const responseListener = useRef<any>();
33+
34+
// Replace this with your actual user‐ID retrieval logic (e.g. from AuthContext)
35+
const fakeUserId = "user123";
36+
// ───────────────────────────────────────────────────────────────────────────────
37+
38+
useEffect(() => {
39+
// 1) Configure how notifications are handled when the app is foregrounded:
40+
Notifications.setNotificationHandler({
41+
handleNotification: async (): Promise<Notifications.NotificationBehavior> => ({
42+
shouldShowAlert: true, // (still allowed, but deprecated)
43+
shouldShowBanner: true, // required on iOS 14+ to actually display in‐app banners
44+
shouldShowList: true, // required on iOS 14+ to show in Notification Center
45+
shouldPlaySound: true,
46+
shouldSetBadge: false,
47+
}),
48+
});
49+
50+
// 2) Register for push notifications & get Expo Push Token
51+
(async () => {
52+
if (!Device.isDevice) {
53+
Alert.alert("Push notifications require a physical device.");
54+
return;
55+
}
56+
57+
// 2a) Check existing permissions
58+
const { status: existingStatus } = await Notifications.getPermissionsAsync();
59+
let finalStatus = existingStatus;
60+
if (existingStatus !== "granted") {
61+
const { status } = await Notifications.requestPermissionsAsync();
62+
finalStatus = status;
63+
}
64+
if (finalStatus !== "granted") {
65+
Alert.alert("Failed to get push token for push notifications!");
66+
return;
67+
}
68+
69+
// 2b) Get the Expo Push Token
70+
const tokenData = await Notifications.getExpoPushTokenAsync();
71+
const token = tokenData.data;
72+
console.log("Obtained Expo Push Token:", token);
73+
setExpoPushToken(token);
74+
75+
// 2c) Send that token to your FastAPI backend
76+
try {
77+
await fetch("http://localhost:8000/api/v1/notifications/register_push_token", {
78+
method: "POST",
79+
headers: { "Content-Type": "application/json" },
80+
body: JSON.stringify({
81+
user_id: fakeUserId,
82+
expo_token: token,
83+
}),
84+
});
85+
} catch (err) {
86+
console.error("Error sending push token to backend:", err);
87+
}
88+
89+
// 2d) (Android only) Create a notification channel
90+
if (Platform.OS === "android") {
91+
await Notifications.setNotificationChannelAsync("default", {
92+
name: "default",
93+
importance: Notifications.AndroidImportance.MAX,
94+
vibrationPattern: [0, 250, 250, 250],
95+
lightColor: "#FF231F7C",
96+
});
97+
}
98+
})();
99+
100+
// 3) Foreground‐notification listener
101+
notificationListener.current = Notifications.addNotificationReceivedListener(
102+
(notification) => {
103+
console.log("Notification Received (foreground):", notification);
104+
}
105+
);
106+
107+
// 4) User‐tap listener (background or foreground)
108+
responseListener.current = Notifications.addNotificationResponseReceivedListener(
109+
(response) => {
110+
console.log("User tapped on notification:", response);
111+
// You can navigate or handle notification.data here
112+
}
113+
);
114+
115+
return () => {
116+
if (notificationListener.current) {
117+
Notifications.removeNotificationSubscription(notificationListener.current);
118+
}
119+
if (responseListener.current) {
120+
Notifications.removeNotificationSubscription(responseListener.current);
121+
}
122+
};
123+
}, []);
124+
19125
if (!loaded) {
20126
return null;
21127
}
@@ -24,7 +130,7 @@ export default function RootLayout() {
24130
<AuthProvider>
25131
<UserProvider>
26132
<WorkoutProvider>
27-
<ThemeProvider value={colorScheme === 'dark' ? DarkTheme : DefaultTheme}>
133+
<ThemeProvider value={colorScheme === "dark" ? DarkTheme : DefaultTheme}>
28134
<ProtectedRoute>
29135
<Stack>
30136
<Stack.Screen name="index" options={{ headerShown: false }} />
@@ -45,4 +151,3 @@ export default function RootLayout() {
45151
</AuthProvider>
46152
);
47153
}
48-

KonditionExpo/package.json

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313
"dependencies": {
1414
"@expo/ngrok": "^4.1.3",
1515
"@expo/vector-icons": "^14.1.0",
16+
"@react-native-async-storage/async-storage": "2.1.2",
1617
"@react-native-picker/picker": "^2.11.0",
1718
"@react-navigation/bottom-tabs": "^7.3.10",
1819
"@react-navigation/elements": "^2.3.8",
@@ -21,10 +22,13 @@
2122
"expo": "^53.0.9",
2223
"expo-blur": "~14.1.4",
2324
"expo-constants": "~17.1.6",
25+
"expo-device": "~7.1.4",
2426
"expo-font": "~13.3.1",
2527
"expo-haptics": "~14.1.4",
2628
"expo-image": "~2.1.7",
2729
"expo-linking": "~7.1.5",
30+
"expo-notifications": "~0.31.2",
31+
"expo-permissions": "^14.4.0",
2832
"expo-router": "~5.0.7",
2933
"expo-splash-screen": "~0.30.8",
3034
"expo-status-bar": "~2.2.3",
@@ -42,8 +46,7 @@
4246
"react-native-screens": "~4.10.0",
4347
"react-native-svg": "15.11.2",
4448
"react-native-web": "~0.20.0",
45-
"react-native-webview": "13.13.5",
46-
"@react-native-async-storage/async-storage": "2.1.2"
49+
"react-native-webview": "13.13.5"
4750
},
4851
"devDependencies": {
4952
"@babel/core": "^7.25.2",

backend/app/api/main.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
from fastapi import APIRouter
22

3-
from app.api.routes import items, login, private, social, users, utils, workouts
3+
from app.api.routes import items, login, private, social, users, utils, workouts, notifications
44
from app.core.config import settings
55

66
api_router = APIRouter()
@@ -10,7 +10,7 @@
1010
api_router.include_router(items.router)
1111
api_router.include_router(social.router)
1212
api_router.include_router(workouts.router)
13-
13+
api_router.include_router(notifications.router, prefix="/notifications")
1414

1515
if settings.ENVIRONMENT == "local":
1616
api_router.include_router(private.router)
Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
from datetime import datetime
2+
import uuid
3+
4+
from fastapi import APIRouter, Depends, HTTPException
5+
from pydantic import BaseModel
6+
from sqlmodel import Session, select
7+
8+
from app.crudFuncs import (
9+
create_or_update_push_token,
10+
schedule_custom_reminder
11+
)
12+
from app.core.db import get_session # adjust if your DB‐session dependency lives elsewhere
13+
from app.models import User
14+
15+
router = APIRouter(tags=["notifications"])
16+
17+
18+
# Pydantic schema that the client will POST when registering a token
19+
class PushTokenPayload(BaseModel):
20+
user_id: uuid.UUID
21+
expo_token: str
22+
23+
24+
@router.post("/register_push_token", status_code=201)
25+
def register_push_token(
26+
payload: PushTokenPayload,
27+
session: Session = Depends(get_session),
28+
):
29+
"""
30+
Client calls this with { user_id, expo_token } to store/update the Expo token.
31+
"""
32+
try:
33+
token_obj = create_or_update_push_token(
34+
session=session,
35+
user_id=payload.user_id,
36+
expo_token=payload.expo_token,
37+
)
38+
return {"status": "ok", "push_token_id": token_obj.id}
39+
except ValueError as e:
40+
# If user_id doesn’t exist in Users table
41+
raise HTTPException(status_code=404, detail=str(e))
42+
43+
44+
# Pydantic schema that the client will POST when scheduling a reminder
45+
class CustomReminderPayload(BaseModel):
46+
user_id: uuid.UUID
47+
expo_token: str
48+
remind_time: datetime # e.g. "2025-06-05T12:00:00Z"
49+
message: str
50+
51+
52+
@router.post("/schedule_reminder", status_code=201)
53+
def schedule_reminder_endpoint(
54+
payload: CustomReminderPayload,
55+
session: Session = Depends(get_session),
56+
):
57+
"""
58+
Client calls this with { user_id, expo_token, remind_time, message }
59+
to schedule a one‐off reminder.
60+
"""
61+
try:
62+
rem_obj = schedule_custom_reminder(
63+
session=session,
64+
user_id=payload.user_id,
65+
expo_token=payload.expo_token,
66+
remind_time=payload.remind_time,
67+
message=payload.message,
68+
)
69+
return {"status": "scheduled", "reminder_id": rem_obj.id}
70+
except ValueError as e:
71+
raise HTTPException(status_code=404, detail=str(e))

backend/app/core/db.py

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,3 +31,10 @@ def init_db(session: Session) -> None:
3131
is_superuser=True,
3232
)
3333
user = crud.create_user(session=session, user_create=user_in)
34+
35+
def get_session():
36+
"""
37+
Provide a transactional SQLModel Session for dependency injection.
38+
"""
39+
with Session(engine) as session:
40+
yield session

0 commit comments

Comments
 (0)