diff --git a/packages/expo/package.json b/packages/expo/package.json index b5348bcf8..6a6a71d12 100644 --- a/packages/expo/package.json +++ b/packages/expo/package.json @@ -1,6 +1,6 @@ { "name": "@knocklabs/expo", - "version": "0.4.0", + "version": "0.5.0", "author": "@knocklabs", "license": "MIT", "main": "dist/cjs/index.js", @@ -73,6 +73,7 @@ "expo": "~53.0.22", "expo-constants": "~17.1.7", "expo-device": "^7.1.4", + "expo-network": "^8.0.0", "expo-notifications": "^0.31.4", "jsdom": "^27.1.0", "react": "^19.0.0", diff --git a/packages/expo/src/modules/push/KnockExpoPushNotificationProvider.tsx b/packages/expo/src/modules/push/KnockExpoPushNotificationProvider.tsx index d1c5ff58a..0532e78b1 100644 --- a/packages/expo/src/modules/push/KnockExpoPushNotificationProvider.tsx +++ b/packages/expo/src/modules/push/KnockExpoPushNotificationProvider.tsx @@ -7,6 +7,7 @@ import { } from "@knocklabs/react-native"; import Constants from "expo-constants"; import * as Device from "expo-device"; +import * as Network from "expo-network"; import * as Notifications from "expo-notifications"; import React, { createContext, @@ -127,6 +128,7 @@ const InternalKnockExpoPushNotificationProvider: React.FC< usePushNotifications(); const [expoPushToken, setExpoPushToken] = useState(null); const knockClient = useKnockClient(); + const { isInternetReachable } = Network.useNetworkState(); const [notificationReceivedHandler, setNotificationReceivedHandler] = useState<(notification: Notifications.Notification) => void>( @@ -188,12 +190,56 @@ const InternalKnockExpoPushNotificationProvider: React.FC< [knockClient], ); + useEffect(() => { + if (expoPushToken) { + return; + } + + const fetchExpoTokenIfNeeded = async () => { + const { status: existingStatus } = + await Notifications.getPermissionsAsync(); + if ( + existingStatus === "granted" && + Device.isDevice && + isInternetReachable + ) { + try { + const token = await getExpoPushToken(); + setExpoPushToken(token ? token.data : null); + } catch (error) { + console.error(`[Knock] Error getting expo push token:`, error); + } + } + }; + + fetchExpoTokenIfNeeded(); + }, [isInternetReachable, expoPushToken]); + useEffect(() => { Notifications.setNotificationHandler({ handleNotification: customNotificationHandler ?? defaultNotificationHandler, }); + const response = Notifications.getLastNotificationResponse(); + if (response?.notification) { + knockClient.log( + "[Knock] Expo Push Notification was interacted with (initial)", + ); + updateKnockMessageStatusFromNotification( + response.notification, + "interacted", + ); + notificationTappedHandler(response); + } + }, [ + customNotificationHandler, + knockClient, + notificationTappedHandler, + updateKnockMessageStatusFromNotification, + ]); + + useEffect(() => { if (autoRegister) { registerForPushNotifications() .then(() => { @@ -241,18 +287,16 @@ const InternalKnockExpoPushNotificationProvider: React.FC< notificationReceivedSubscription.remove(); notificationResponseSubscription.remove(); }; - - // TODO: Remove when possible and ensure dependency array is correct - // eslint-disable-next-line react-hooks/exhaustive-deps }, [ registerForPushNotifications, notificationReceivedHandler, notificationTappedHandler, - customNotificationHandler, autoRegister, expoPushToken, knockExpoChannelId, knockClient, + registerPushTokenToChannel, + updateKnockMessageStatusFromNotification, ]); return ( diff --git a/packages/react-native/src/modules/push/KnockPushNotificationProvider.tsx b/packages/react-native/src/modules/push/KnockPushNotificationProvider.tsx index 625fd9ffb..787119c2d 100644 --- a/packages/react-native/src/modules/push/KnockPushNotificationProvider.tsx +++ b/packages/react-native/src/modules/push/KnockPushNotificationProvider.tsx @@ -12,6 +12,7 @@ export interface KnockPushNotificationContextType { registerPushTokenToChannel( token: string, channelId: string, + locale?: string, ): Promise; unregisterPushTokenFromChannel( token: string, @@ -44,10 +45,14 @@ export const KnockPushNotificationProvider: React.FC< // Acts like an upsert. Inserts or updates const registerPushTokenToChannel = useCallback( - async (token: string, channelId: string): Promise => { + async ( + token: string, + channelId: string, + locale: string = Intl.DateTimeFormat().resolvedOptions().locale, + ): Promise => { const newDevice: Device = { token, - locale: Intl.DateTimeFormat().resolvedOptions().locale, + locale, timezone: Intl.DateTimeFormat().resolvedOptions().timeZone, }; diff --git a/yarn.lock b/yarn.lock index 093f85b0b..ac0041d67 100644 --- a/yarn.lock +++ b/yarn.lock @@ -4071,6 +4071,7 @@ __metadata: expo: "npm:~53.0.22" expo-constants: "npm:~17.1.7" expo-device: "npm:^7.1.4" + expo-network: "npm:^8.0.0" expo-notifications: "npm:^0.31.4" jsdom: "npm:^27.1.0" react: "npm:^19.0.0" @@ -12061,6 +12062,16 @@ __metadata: languageName: node linkType: hard +"expo-network@npm:^8.0.0": + version: 8.0.8 + resolution: "expo-network@npm:8.0.8" + peerDependencies: + expo: "*" + react: "*" + checksum: 10c0/e12e4d136dae02e22ceaf0344ed7ee053e5aed126a71e69718c91bf7161c8bc97f745eec6e794f66e60cfba44d341b4462ce2987c958671a80fc5bea85dd124d + languageName: node + linkType: hard + "expo-notifications@npm:^0.31.4": version: 0.31.4 resolution: "expo-notifications@npm:0.31.4"