diff --git a/.changeset/afraid-cobras-call.md b/.changeset/afraid-cobras-call.md new file mode 100644 index 00000000..0fe42601 --- /dev/null +++ b/.changeset/afraid-cobras-call.md @@ -0,0 +1,6 @@ +--- +"@knocklabs/react-native": minor +"@knocklabs/expo": minor +--- + +Add KnockPushNotificationProvider diff --git a/packages/expo/src/modules/push/KnockExpoPushNotificationProvider.tsx b/packages/expo/src/modules/push/KnockExpoPushNotificationProvider.tsx index 9b4c1502..680e04bb 100644 --- a/packages/expo/src/modules/push/KnockExpoPushNotificationProvider.tsx +++ b/packages/expo/src/modules/push/KnockExpoPushNotificationProvider.tsx @@ -1,9 +1,10 @@ -import { - ChannelData, - Message, - MessageEngagementStatus, -} from "@knocklabs/client"; +import { Message, MessageEngagementStatus } from "@knocklabs/client"; import { useKnockClient } from "@knocklabs/react-core"; +import { + type KnockPushNotificationContextType, + KnockPushNotificationProvider, + usePushNotifications, +} from "@knocklabs/react-native"; import Constants from "expo-constants"; import * as Device from "expo-device"; import * as Notifications from "expo-notifications"; @@ -15,14 +16,10 @@ import React, { useState, } from "react"; -export interface KnockExpoPushNotificationContextType { +export interface KnockExpoPushNotificationContextType + extends KnockPushNotificationContextType { expoPushToken: string | null; registerForPushNotifications: () => Promise; - registerPushTokenToChannel(token: string, channelId: string): Promise; - unregisterPushTokenFromChannel( - token: string, - channelId: string, - ): Promise; onNotificationReceived: ( handler: (notification: Notifications.Notification) => void, ) => void; @@ -114,7 +111,7 @@ async function requestPermissionAndGetPushToken(): Promise = ({ knockExpoChannelId, @@ -122,6 +119,8 @@ export const KnockExpoPushNotificationProvider: React.FC< children, autoRegister = true, }) => { + const { registerPushTokenToChannel, unregisterPushTokenFromChannel } = + usePushNotifications(); const [expoPushToken, setExpoPushToken] = useState(null); const knockClient = useKnockClient(); @@ -173,58 +172,6 @@ export const KnockExpoPushNotificationProvider: React.FC< [knockClient], ); - const registerNewTokenDataOnServer = useCallback( - async (tokens: string[], channelId: string): Promise => { - return knockClient.user.setChannelData({ - channelId: channelId, - channelData: { tokens: tokens }, - }); - }, - [knockClient], - ); - - const registerPushTokenToChannel = useCallback( - async (token: string, channelId: string): Promise => { - knockClient.user - .getChannelData({ channelId: channelId }) - .then((result: ChannelData) => { - const tokens: string[] = result.data["tokens"]; - if (!tokens.includes(token)) { - tokens.push(token); - return registerNewTokenDataOnServer(tokens, channelId); - } - knockClient.log("[Knock] registerPushTokenToChannel success"); - }) - .catch((_) => { - // No data registered on that channel for that user, we'll create a new record - return registerNewTokenDataOnServer([token], channelId); - }); - }, - [knockClient, registerNewTokenDataOnServer], - ); - - const unregisterPushTokenFromChannel = useCallback( - async (token: string, channelId: string): Promise => { - knockClient.user - .getChannelData({ channelId: channelId }) - .then((result: ChannelData) => { - const tokens: string[] = result.data["tokens"]; - const updatedTokens = tokens.filter( - (channelToken) => channelToken !== token, - ); - knockClient.log("unregisterPushTokenFromChannel success"); - return registerNewTokenDataOnServer(updatedTokens, channelId); - }) - .catch((error) => { - console.error( - `[Knock] Error unregistering push token from channel:`, - error, - ); - }); - }, - [knockClient, registerNewTokenDataOnServer], - ); - useEffect(() => { Notifications.setNotificationHandler({ handleNotification: @@ -311,12 +258,22 @@ export const KnockExpoPushNotificationProvider: React.FC< ); }; +export const KnockExpoPushNotificationProvider: React.FC< + KnockExpoPushNotificationProviderProps +> = (props) => { + return ( + + + + ); +}; + export const useExpoPushNotifications = (): KnockExpoPushNotificationContextType => { const context = useContext(KnockExpoPushNotificationContext); if (context === undefined) { throw new Error( - "[Knock] useExpoPushNotifications must be used within a PushNotificationProvider", + "[Knock] useExpoPushNotifications must be used within a KnockExpoPushNotificationProvider", ); } return context; diff --git a/packages/react-native/src/index.ts b/packages/react-native/src/index.ts index 5ea4e960..bbce3d31 100644 --- a/packages/react-native/src/index.ts +++ b/packages/react-native/src/index.ts @@ -1,3 +1,4 @@ export * from "./modules/feed"; +export * from "./modules/push"; export * from "@knocklabs/react-core"; export * from "./assets"; diff --git a/packages/react-native/src/modules/push/KnockPushNotificationProvider.tsx b/packages/react-native/src/modules/push/KnockPushNotificationProvider.tsx new file mode 100644 index 00000000..c0e2b6ad --- /dev/null +++ b/packages/react-native/src/modules/push/KnockPushNotificationProvider.tsx @@ -0,0 +1,95 @@ +import { ChannelData } from "@knocklabs/client"; +import { useKnockClient } from "@knocklabs/react-core"; +import React, { createContext, useCallback, useContext } from "react"; + +export interface KnockPushNotificationContextType { + registerPushTokenToChannel(token: string, channelId: string): Promise; + unregisterPushTokenFromChannel( + token: string, + channelId: string, + ): Promise; +} + +const KnockPushNotificationContext = createContext< + KnockPushNotificationContextType | undefined +>(undefined); + +export interface KnockPushNotificationProviderProps { + children?: React.ReactElement; +} + +export const KnockPushNotificationProvider: React.FC< + KnockPushNotificationProviderProps +> = ({ children }) => { + const knockClient = useKnockClient(); + + const registerNewTokenDataOnServer = useCallback( + async (tokens: string[], channelId: string): Promise => { + return knockClient.user.setChannelData({ + channelId: channelId, + channelData: { tokens: tokens }, + }); + }, + [knockClient], + ); + + const registerPushTokenToChannel = useCallback( + async (token: string, channelId: string): Promise => { + knockClient.user + .getChannelData({ channelId: channelId }) + .then((result: ChannelData) => { + const tokens: string[] = result.data["tokens"]; + if (!tokens.includes(token)) { + tokens.push(token); + return registerNewTokenDataOnServer(tokens, channelId); + } + knockClient.log("[Knock] registerPushTokenToChannel success"); + }) + .catch((_) => { + // No data registered on that channel for that user, we'll create a new record + return registerNewTokenDataOnServer([token], channelId); + }); + }, + [knockClient, registerNewTokenDataOnServer], + ); + + const unregisterPushTokenFromChannel = useCallback( + async (token: string, channelId: string): Promise => { + knockClient.user + .getChannelData({ channelId: channelId }) + .then((result: ChannelData) => { + const tokens: string[] = result.data["tokens"]; + const updatedTokens = tokens.filter( + (channelToken) => channelToken !== token, + ); + knockClient.log("unregisterPushTokenFromChannel success"); + return registerNewTokenDataOnServer(updatedTokens, channelId); + }) + .catch((error) => { + console.error( + `[Knock] Error unregistering push token from channel:`, + error, + ); + }); + }, + [knockClient, registerNewTokenDataOnServer], + ); + + return ( + + {children} + + ); +}; + +export const usePushNotifications = (): KnockPushNotificationContextType => { + const context = useContext(KnockPushNotificationContext); + if (context === undefined) { + throw new Error( + "[Knock] usePushNotifications must be used within a KnockPushNotificationProvider", + ); + } + return context; +}; diff --git a/packages/react-native/src/modules/push/index.ts b/packages/react-native/src/modules/push/index.ts new file mode 100644 index 00000000..4ab8fefe --- /dev/null +++ b/packages/react-native/src/modules/push/index.ts @@ -0,0 +1 @@ +export * from "./KnockPushNotificationProvider";