-
Notifications
You must be signed in to change notification settings - Fork 1
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Push notifications (receiver side, initial work)
- Loading branch information
Showing
7 changed files
with
405 additions
and
96 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,97 @@ | ||
export default { | ||
expo: { | ||
name: "health-app", | ||
slug: "health-app", | ||
version: "1.0.0", | ||
orientation: "portrait", | ||
icon: "./assets/images/icon.png", | ||
scheme: "myapp", | ||
userInterfaceStyle: "automatic", | ||
newArchEnabled: true, | ||
jsEngine: "hermes", | ||
ios: { | ||
supportsTablet: true, | ||
config: { | ||
usesNonExemptEncryption: false, | ||
}, | ||
bundleIdentifier: "com.vinta.healthapp", | ||
infoPlist: { | ||
UIBackgroundModes: ["remote-notification"], | ||
}, | ||
}, | ||
android: { | ||
adaptiveIcon: { | ||
foregroundImage: "./assets/images/adaptive-icon.png", | ||
backgroundColor: "#ffffff", | ||
}, | ||
permissions: [ | ||
"android.permission.RECORD_AUDIO", | ||
"android.permission.RECEIVE_BOOT_COMPLETED", | ||
"android.permission.SCHEDULE_EXACT_ALARM", | ||
"android.permission.POST_NOTIFICATIONS", | ||
"android.permission.USE_FULL_SCREEN_INTENT", | ||
], | ||
package: "com.vinta.healthapp", | ||
googleServicesFile: process.env.GOOGLE_SERVICES_JSON || "./_dev/google-services.json", | ||
}, | ||
web: { | ||
bundler: "metro", | ||
output: "static", | ||
favicon: "./assets/images/favicon.png", | ||
}, | ||
plugins: [ | ||
"expo-router", | ||
[ | ||
"expo-splash-screen", | ||
{ | ||
image: "./assets/images/splash-icon.png", | ||
imageWidth: 200, | ||
resizeMode: "contain", | ||
backgroundColor: "#ffffff", | ||
}, | ||
], | ||
[ | ||
"expo-secure-store", | ||
{ | ||
requireAuthentication: false, | ||
}, | ||
], | ||
[ | ||
"expo-image-picker", | ||
{ | ||
photosPermission: | ||
"The app needs media access when you want to attach media to your messages.", | ||
cameraPermission: | ||
"The app needs camera access when you want to attach media to your messages.", | ||
microphonePermission: | ||
"The app needs microphone access when you want to attach media to your messages.", | ||
}, | ||
], | ||
[ | ||
"expo-video", | ||
{ | ||
supportsBackgroundPlayback: true, | ||
supportsPictureInPicture: true, | ||
}, | ||
], | ||
[ | ||
"expo-notifications", | ||
{ | ||
icon: "./assets/images/adaptive-icon.png", | ||
color: "#ffffff", | ||
}, | ||
], | ||
], | ||
experiments: { | ||
typedRoutes: true, | ||
}, | ||
extra: { | ||
router: { | ||
origin: false, | ||
}, | ||
eas: { | ||
projectId: "b90347a9-ca6d-4949-9545-82fcce6ed6aa", | ||
}, | ||
}, | ||
}, | ||
}; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,161 @@ | ||
import { MedplumClient } from "@medplum/core"; | ||
import { useMedplum } from "@medplum/react-hooks"; | ||
import Constants from "expo-constants"; | ||
import * as Device from "expo-device"; | ||
import * as Notifications from "expo-notifications"; | ||
import { router } from "expo-router"; | ||
import { createContext, useCallback, useContext, useEffect, useRef, useState } from "react"; | ||
import { Platform } from "react-native"; | ||
|
||
Notifications.setNotificationHandler({ | ||
handleNotification: async () => ({ | ||
shouldShowAlert: true, | ||
shouldPlaySound: true, | ||
shouldSetBadge: true, | ||
}), | ||
}); | ||
|
||
interface NotificationsContextType { | ||
isNotificationsEnabled: boolean; | ||
setUpPushNotifications: () => Promise<boolean>; | ||
} | ||
|
||
const NotificationsContext = createContext<NotificationsContextType>({ | ||
isNotificationsEnabled: false, | ||
setUpPushNotifications: async () => false, | ||
}); | ||
|
||
async function registerForPushNotificationsAsync() { | ||
let token; | ||
|
||
if (!Device.isDevice) { | ||
return; | ||
} | ||
|
||
if (Platform.OS === "android") { | ||
await Notifications.setNotificationChannelAsync("default", { | ||
name: "default", | ||
importance: Notifications.AndroidImportance.MAX, | ||
vibrationPattern: [0, 250, 250, 250], | ||
lightColor: "#FF231F7C", | ||
}); | ||
} | ||
|
||
try { | ||
const projectId = Constants.expoConfig?.extra?.eas?.projectId; | ||
if (!projectId) { | ||
throw new Error("Missing EAS project ID"); | ||
} | ||
|
||
token = await Notifications.getExpoPushTokenAsync({ | ||
projectId, | ||
}); | ||
return token?.data; | ||
} catch (error) { | ||
console.error("Error getting push token:", error); | ||
} | ||
} | ||
|
||
function updateMedplumPushToken(medplum: MedplumClient, token: string): Promise<void> { | ||
return (async () => { | ||
try { | ||
const profile = medplum.getProfile(); | ||
if (!profile) return; | ||
|
||
// Check if token already exists and matches | ||
const existingToken = profile.extension?.find( | ||
(e) => e.url === "https://medplum.com/push-token", | ||
)?.valueString; | ||
|
||
if (existingToken === token) { | ||
return; // Token hasn't changed, no need to update | ||
} | ||
|
||
// Update the token | ||
const extensions = | ||
profile.extension?.filter((e) => e.url !== "https://medplum.com/push-token") || []; | ||
extensions.push({ | ||
url: "https://medplum.com/push-token", | ||
valueString: token, | ||
}); | ||
await medplum.updateResource({ | ||
...profile, | ||
extension: extensions, | ||
}); | ||
} catch (error) { | ||
console.error("Error updating push token in Medplum:", error); | ||
} | ||
})(); | ||
} | ||
|
||
function handleMessageNotificationInteraction(response: Notifications.NotificationResponse) { | ||
const data = response.notification.request.content.data; | ||
if (data.threadId) { | ||
router.push(`/thread/${data.threadId}`); | ||
} | ||
} | ||
|
||
export function NotificationsProvider({ children }: { children: React.ReactNode }) { | ||
const [isNotificationsEnabled, setIsNotificationsEnabled] = useState(false); | ||
const responseListener = useRef<Notifications.EventSubscription>(); | ||
const medplum = useMedplum(); | ||
|
||
const setUpPushNotifications = useCallback(async () => { | ||
// Check and request notification permissions | ||
const { status: existingStatus } = await Notifications.getPermissionsAsync(); | ||
let finalStatus = existingStatus; | ||
if (existingStatus !== "granted") { | ||
({ status: finalStatus } = await Notifications.requestPermissionsAsync()); | ||
} | ||
|
||
// Set up push notifications if we have permission | ||
const isEnabled = finalStatus === "granted"; | ||
setIsNotificationsEnabled(isEnabled); | ||
if (isEnabled) { | ||
const token = await registerForPushNotificationsAsync(); | ||
if (token) { | ||
await updateMedplumPushToken(medplum, token); | ||
} | ||
|
||
// Set up notification listeners | ||
responseListener.current = Notifications.addNotificationResponseReceivedListener( | ||
handleMessageNotificationInteraction, | ||
); | ||
} | ||
return isEnabled; | ||
}, [medplum]); | ||
|
||
// Effect to handle notifications when app is terminated | ||
useEffect(() => { | ||
const getInitialNotification = async () => { | ||
const lastNotificationResponse = await Notifications.getLastNotificationResponseAsync(); | ||
if (lastNotificationResponse) { | ||
handleMessageNotificationInteraction(lastNotificationResponse); | ||
} | ||
}; | ||
|
||
getInitialNotification(); | ||
}, []); | ||
|
||
// Cleanup | ||
useEffect(() => { | ||
return () => { | ||
responseListener.current?.remove(); | ||
}; | ||
}, [responseListener]); | ||
|
||
return ( | ||
<NotificationsContext.Provider | ||
value={{ | ||
isNotificationsEnabled, | ||
setUpPushNotifications, | ||
}} | ||
> | ||
{children} | ||
</NotificationsContext.Provider> | ||
); | ||
} | ||
|
||
export function useNotifications() { | ||
return useContext(NotificationsContext); | ||
} |
Oops, something went wrong.