Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(kno-6559): add KnockPushNotificationProvider #252

Merged
merged 32 commits into from
Oct 16, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
32 commits
Select commit Hold shift + click to select a range
ef352b7
Add new package directory for expo SDK
mattmikolay Oct 11, 2024
7f2d7f8
Add tooling config
mattmikolay Oct 11, 2024
8d205bc
Copy KnockExpoPushNotificationProvider to expo package
mattmikolay Oct 11, 2024
8c6313f
Add root index.ts to expo package
mattmikolay Oct 11, 2024
4bbf3dc
Re-export everything from react native package
mattmikolay Oct 11, 2024
4c221de
Remove push code from react native SDK
mattmikolay Oct 11, 2024
6c550a5
Remove peerDependenciesMeta from expo package.json
mattmikolay Oct 11, 2024
1b6575f
Remove expo dependencies from react native sdk
mattmikolay Oct 11, 2024
7a3947b
Remove unnecessary export
mattmikolay Oct 11, 2024
286aaa6
Update sample expo app to use expo sdk
mattmikolay Oct 11, 2024
228290a
Update more sample app imports
mattmikolay Oct 11, 2024
b891b4c
Use a type import in sample app
mattmikolay Oct 11, 2024
badb827
Add expo README
mattmikolay Oct 11, 2024
eef5435
Update rn sdk README
mattmikolay Oct 11, 2024
94952fb
Update rn sdk vite config
mattmikolay Oct 11, 2024
1eef956
Update README
mattmikolay Oct 11, 2024
ec11e9f
Merge branch 'main' into mattmik-kno-6559-react-native-sdk-ensure-exp…
mattmikolay Oct 14, 2024
02bfff7
Update @typescript-eslint/parser
mattmikolay Oct 14, 2024
6adc41d
Update version of expo package
mattmikolay Oct 14, 2024
f8fc1de
Add changeset
mattmikolay Oct 14, 2024
f13bde3
Update Expo README
mattmikolay Oct 14, 2024
69c414d
Update RN SDK README
mattmikolay Oct 14, 2024
a652c6a
Update expo SDK README
mattmikolay Oct 14, 2024
30f6c35
Set @knocklabs/expo version to 0.0.0
mattmikolay Oct 14, 2024
f6295c3
Add KnockPushNotificationProvider
mattmikolay Oct 14, 2024
936bfc1
Merge branch 'main' into mattmik-generic-rn-push-provider
mattmikolay Oct 15, 2024
ab9cc9c
Remove outdated changeset
mattmikolay Oct 15, 2024
d651973
Move KnockPushNotificationProvider to react-native package
mattmikolay Oct 15, 2024
6b9020d
Add missing export to react-native/src/index.ts
mattmikolay Oct 15, 2024
3d9ea6c
Extend type KnockPushNotificationContextType in expo package
mattmikolay Oct 15, 2024
298e47e
Use KnockPushNotificationProvider in KnockExpoPushNotificationProvider
mattmikolay Oct 15, 2024
24eccfb
Add changeset
mattmikolay Oct 15, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions .changeset/afraid-cobras-call.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
"@knocklabs/react-native": minor
"@knocklabs/expo": minor
---

Add KnockPushNotificationProvider
Original file line number Diff line number Diff line change
@@ -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";
Expand All @@ -15,14 +16,10 @@ import React, {
useState,
} from "react";

export interface KnockExpoPushNotificationContextType {
export interface KnockExpoPushNotificationContextType
extends KnockPushNotificationContextType {
expoPushToken: string | null;
registerForPushNotifications: () => Promise<void>;
registerPushTokenToChannel(token: string, channelId: string): Promise<void>;
unregisterPushTokenFromChannel(
token: string,
channelId: string,
): Promise<void>;
onNotificationReceived: (
handler: (notification: Notifications.Notification) => void,
) => void;
Expand Down Expand Up @@ -114,14 +111,16 @@ async function requestPermissionAndGetPushToken(): Promise<Notifications.ExpoPus
return getExpoPushToken();
}

export const KnockExpoPushNotificationProvider: React.FC<
const InternalKnockExpoPushNotificationProvider: React.FC<
KnockExpoPushNotificationProviderProps
> = ({
knockExpoChannelId,
customNotificationHandler,
children,
autoRegister = true,
}) => {
const { registerPushTokenToChannel, unregisterPushTokenFromChannel } =
usePushNotifications();
const [expoPushToken, setExpoPushToken] = useState<string | null>(null);
const knockClient = useKnockClient();

Expand Down Expand Up @@ -173,58 +172,6 @@ export const KnockExpoPushNotificationProvider: React.FC<
[knockClient],
);

const registerNewTokenDataOnServer = useCallback(
async (tokens: string[], channelId: string): Promise<ChannelData> => {
return knockClient.user.setChannelData({
channelId: channelId,
channelData: { tokens: tokens },
});
},
[knockClient],
);

const registerPushTokenToChannel = useCallback(
async (token: string, channelId: string): Promise<void> => {
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<void> => {
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:
Expand Down Expand Up @@ -311,12 +258,22 @@ export const KnockExpoPushNotificationProvider: React.FC<
);
};

export const KnockExpoPushNotificationProvider: React.FC<
KnockExpoPushNotificationProviderProps
> = (props) => {
return (
<KnockPushNotificationProvider>
<InternalKnockExpoPushNotificationProvider {...props} />
</KnockPushNotificationProvider>
);
};

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;
Expand Down
1 change: 1 addition & 0 deletions packages/react-native/src/index.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
export * from "./modules/feed";
export * from "./modules/push";
export * from "@knocklabs/react-core";
export * from "./assets";
Original file line number Diff line number Diff line change
@@ -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<void>;
unregisterPushTokenFromChannel(
token: string,
channelId: string,
): Promise<void>;
}

const KnockPushNotificationContext = createContext<
KnockPushNotificationContextType | undefined
>(undefined);

export interface KnockPushNotificationProviderProps {
children?: React.ReactElement;
}

export const KnockPushNotificationProvider: React.FC<
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For what it’s worth, this KnockPushNotificationProvider does not use any APIs specific to React Native. Alternatively, we could add this to @knocklabs/react-core.

@knocklabs/react-native already re-exports everything from @knocklabs/react-core, so it would still be available for use via our React Native SDK:

export * from "@knocklabs/react-core";

KnockPushNotificationProviderProps
> = ({ children }) => {
const knockClient = useKnockClient();

const registerNewTokenDataOnServer = useCallback(
async (tokens: string[], channelId: string): Promise<ChannelData> => {
return knockClient.user.setChannelData({
channelId: channelId,
channelData: { tokens: tokens },
});
},
[knockClient],
);

const registerPushTokenToChannel = useCallback(
async (token: string, channelId: string): Promise<void> => {
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<void> => {
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 (
<KnockPushNotificationContext.Provider
value={{ registerPushTokenToChannel, unregisterPushTokenFromChannel }}
>
{children}
</KnockPushNotificationContext.Provider>
);
};

export const usePushNotifications = (): KnockPushNotificationContextType => {
const context = useContext(KnockPushNotificationContext);
if (context === undefined) {
throw new Error(
"[Knock] usePushNotifications must be used within a KnockPushNotificationProvider",
);
}
return context;
};
1 change: 1 addition & 0 deletions packages/react-native/src/modules/push/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export * from "./KnockPushNotificationProvider";