Skip to content

Commit ecbfed1

Browse files
authored
chore: upgrade expo and react native packages to their latest versions (#926)
1 parent d55ffdc commit ecbfed1

File tree

14 files changed

+2219
-1205
lines changed

14 files changed

+2219
-1205
lines changed

.changeset/old-cats-drop.md

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
---
2+
"@knocklabs/expo-example": minor
3+
"@knocklabs/react-native": minor
4+
"@knocklabs/expo": minor
5+
---
6+
7+
chore: upgrade expo and react native packages to their latest version, ensure expo go builds on android get the proper warning about deprecated notification support from expo, ensure expo-example app works as expected.

examples/expo-example/app.json

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,6 @@
77
"icon": "./assets/images/icon.png",
88
"scheme": "expoexample",
99
"userInterfaceStyle": "automatic",
10-
"newArchEnabled": true,
1110
"ios": {
1211
"supportsTablet": true,
1312
"bundleIdentifier": "com.knocklabs.expoexample"
@@ -17,7 +16,6 @@
1716
"foregroundImage": "./assets/images/adaptive-icon.png",
1817
"backgroundColor": "#ffffff"
1918
},
20-
"edgeToEdgeEnabled": true,
2119
"package": "com.knocklabs.expoexample"
2220
},
2321
"web": {
@@ -38,8 +36,7 @@
3836
]
3937
],
4038
"experiments": {
41-
"typedRoutes": true,
42-
"reactCanary": true
39+
"typedRoutes": true
4340
},
4441
"extra": {
4542
"router": {},

examples/expo-example/package.json

Lines changed: 35 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -10,41 +10,43 @@
1010
"lint": "expo lint"
1111
},
1212
"dependencies": {
13+
"@expo/log-box": "~55.0.8",
1314
"@expo/vector-icons": "^15.0.2",
1415
"@knocklabs/expo": "workspace:^",
15-
"@react-navigation/bottom-tabs": "^7.3.10",
16-
"@react-navigation/elements": "^2.6.3",
17-
"@react-navigation/native": "^7.1.6",
18-
"expo": "~53.0.22",
19-
"expo-blur": "~14.1.5",
20-
"expo-constants": "~17.1.7",
21-
"expo-device": "^7.1.4",
22-
"expo-font": "~13.3.2",
23-
"expo-haptics": "~14.1.4",
24-
"expo-image": "~2.4.0",
25-
"expo-linking": "~7.1.7",
26-
"expo-notifications": "^0.31.4",
27-
"expo-router": "~5.1.6",
28-
"expo-splash-screen": "~0.30.10",
29-
"expo-status-bar": "~2.2.3",
30-
"expo-symbols": "~0.4.5",
31-
"expo-system-ui": "~5.0.11",
32-
"expo-web-browser": "~14.2.0",
16+
"@react-navigation/bottom-tabs": "^7.15.5",
17+
"@react-navigation/elements": "^2.9.14",
18+
"@react-navigation/native": "^7.1.33",
19+
"expo": "~55.0.9",
20+
"expo-blur": "~55.0.10",
21+
"expo-constants": "~55.0.9",
22+
"expo-device": "~55.0.10",
23+
"expo-font": "~55.0.4",
24+
"expo-haptics": "~55.0.9",
25+
"expo-image": "~55.0.6",
26+
"expo-linking": "~55.0.9",
27+
"expo-notifications": "~55.0.14",
28+
"expo-router": "~55.0.8",
29+
"expo-splash-screen": "~55.0.13",
30+
"expo-status-bar": "~55.0.4",
31+
"expo-symbols": "~55.0.5",
32+
"expo-system-ui": "~55.0.11",
33+
"expo-web-browser": "~55.0.10",
3334
"react": "^19.0.0",
3435
"react-dom": "^19.0.0",
35-
"react-native": "^0.79.2",
36-
"react-native-gesture-handler": "^2.27.1",
37-
"react-native-reanimated": "~3.17.4",
38-
"react-native-safe-area-context": "5.4.0",
39-
"react-native-screens": "~4.11.1",
40-
"react-native-web": "~0.21.2",
41-
"react-native-webview": "13.13.5"
36+
"react-native": "^0.83.4",
37+
"react-native-gesture-handler": "~2.30.0",
38+
"react-native-reanimated": "~4.2.1",
39+
"react-native-safe-area-context": "~5.6.2",
40+
"react-native-screens": "~4.23.0",
41+
"react-native-web": "~0.21.0",
42+
"react-native-webview": "13.16.0",
43+
"react-native-worklets": "~0.7.2"
4244
},
4345
"devDependencies": {
4446
"@babel/core": "^7.28.0",
4547
"@types/react": "^19.1.8",
4648
"eslint": "^8.56.0",
47-
"eslint-config-expo": "~9.2.0",
49+
"eslint-config-expo": "~55.0.0",
4850
"eslint-import-resolver-typescript": "^4.4.4",
4951
"eslint-plugin-import": "^2.32.0",
5052
"eslint-plugin-prettier": "^5.5.1",
@@ -53,5 +55,12 @@
5355
"private": true,
5456
"engines": {
5557
"node": "22.17.0"
58+
},
59+
"expo": {
60+
"install": {
61+
"exclude": [
62+
"typescript"
63+
]
64+
}
5665
}
5766
}

packages/expo/package.json

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -54,9 +54,9 @@
5454
"@knocklabs/client": "workspace:^",
5555
"@knocklabs/react-core": "workspace:^",
5656
"@knocklabs/react-native": "workspace:^",
57-
"react-native-gesture-handler": "^2.27.1",
57+
"react-native-gesture-handler": "~2.30.0",
5858
"react-native-render-html": "^6.3.4",
59-
"react-native-svg": "^15.12.0"
59+
"react-native-svg": "~15.15.3"
6060
},
6161
"devDependencies": {
6262
"@codecov/vite-plugin": "^1.9.1",
@@ -70,13 +70,13 @@
7070
"eslint": "^8.56.0",
7171
"eslint-plugin-react-hooks": "^5.2.0",
7272
"eslint-plugin-react-refresh": "^0.5.2",
73-
"expo": "~53.0.22",
74-
"expo-constants": "~17.1.7",
75-
"expo-device": "^7.1.4",
76-
"expo-notifications": "^0.31.4",
73+
"expo": "~55.0.9",
74+
"expo-constants": "~55.0.9",
75+
"expo-device": "~55.0.10",
76+
"expo-notifications": "~55.0.14",
7777
"jsdom": "^27.1.0",
7878
"react": "^19.0.0",
79-
"react-native": "^0.79.2",
79+
"react-native": "^0.83.4",
8080
"rimraf": "^6.0.1",
8181
"typescript": "^5.8.3",
8282
"vite": "^5.4.19",

packages/expo/src/modules/push/KnockExpoPushNotificationProvider.tsx

Lines changed: 27 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,6 @@ import {
44
KnockPushNotificationProvider,
55
usePushNotifications,
66
} from "@knocklabs/react-native";
7-
import * as Notifications from "expo-notifications";
87
import React, {
98
createContext,
109
useCallback,
@@ -14,6 +13,11 @@ import React, {
1413
useState,
1514
} from "react";
1615

16+
import {
17+
type Notification,
18+
type NotificationResponse,
19+
getNotificationsModule,
20+
} from "./getNotificationsModule";
1721
import type {
1822
KnockExpoPushNotificationContextType,
1923
KnockExpoPushNotificationProviderProps,
@@ -70,18 +74,18 @@ const InternalExpoPushNotificationProvider: React.FC<
7074

7175
// Use refs for handlers to avoid re-running effects when handlers change
7276
const notificationReceivedHandlerRef = useRef<
73-
(notification: Notifications.Notification) => void
77+
(notification: Notification) => void
7478
>(() => {});
7579

7680
const notificationTappedHandlerRef = useRef<
77-
(response: Notifications.NotificationResponse) => void
81+
(response: NotificationResponse) => void
7882
>(() => {});
7983

8084
/**
8185
* Register a handler to be called when a notification is received in the foreground.
8286
*/
8387
const onNotificationReceived = useCallback(
84-
(handler: (notification: Notifications.Notification) => void) => {
88+
(handler: (notification: Notification) => void) => {
8589
notificationReceivedHandlerRef.current = handler;
8690
},
8791
[],
@@ -91,7 +95,7 @@ const InternalExpoPushNotificationProvider: React.FC<
9195
* Register a handler to be called when a notification is tapped.
9296
*/
9397
const onNotificationTapped = useCallback(
94-
(handler: (response: Notifications.NotificationResponse) => void) => {
98+
(handler: (response: NotificationResponse) => void) => {
9599
notificationTappedHandlerRef.current = handler;
96100
},
97101
[],
@@ -130,7 +134,7 @@ const InternalExpoPushNotificationProvider: React.FC<
130134
*/
131135
const updateMessageStatus = useCallback(
132136
async (
133-
notification: Notifications.Notification,
137+
notification: Notification,
134138
status: MessageEngagementStatus,
135139
): Promise<Message | void> => {
136140
const messageId = notification.request.content.data?.[
@@ -153,11 +157,14 @@ const InternalExpoPushNotificationProvider: React.FC<
153157

154158
// Set up the notification handler for foreground notifications
155159
useEffect(() => {
160+
const NotificationsModule = getNotificationsModule();
161+
if (!NotificationsModule) return;
162+
156163
const handleNotification = customNotificationHandler
157164
? customNotificationHandler
158165
: async () => DEFAULT_NOTIFICATION_BEHAVIOR;
159166

160-
Notifications.setNotificationHandler({ handleNotification });
167+
NotificationsModule.setNotificationHandler({ handleNotification });
161168
}, [customNotificationHandler]);
162169

163170
// Auto-register for push notifications on mount if enabled
@@ -196,20 +203,24 @@ const InternalExpoPushNotificationProvider: React.FC<
196203

197204
// Set up notification listeners for received and tapped notifications
198205
useEffect(() => {
199-
const receivedSubscription = Notifications.addNotificationReceivedListener(
200-
(notification) => {
206+
const NotificationsModule = getNotificationsModule();
207+
if (!NotificationsModule) return;
208+
209+
const receivedSubscription =
210+
NotificationsModule.addNotificationReceivedListener((notification) => {
201211
knockClient.log("[Knock] Notification received in foreground");
202212
updateMessageStatus(notification, "interacted");
203213
notificationReceivedHandlerRef.current(notification);
204-
},
205-
);
214+
});
206215

207216
const responseSubscription =
208-
Notifications.addNotificationResponseReceivedListener((response) => {
209-
knockClient.log("[Knock] Notification was tapped");
210-
updateMessageStatus(response.notification, "interacted");
211-
notificationTappedHandlerRef.current(response);
212-
});
217+
NotificationsModule.addNotificationResponseReceivedListener(
218+
(response) => {
219+
knockClient.log("[Knock] Notification was tapped");
220+
updateMessageStatus(response.notification, "interacted");
221+
notificationTappedHandlerRef.current(response);
222+
},
223+
);
213224

214225
return () => {
215226
receivedSubscription.remove();
Lines changed: 97 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,97 @@
1+
import Constants, { ExecutionEnvironment } from "expo-constants";
2+
import type * as Notifications from "expo-notifications";
3+
import { Platform } from "react-native";
4+
5+
/**
6+
* The type of the expo-notifications module when successfully loaded.
7+
*/
8+
export type NotificationsModule = typeof Notifications;
9+
10+
// Type aliases derived from the expo-notifications namespace so that consumers
11+
// access all expo-notifications types through this module rather than importing
12+
// from the package directly (which can trigger runtime side-effects).
13+
export type Notification = Notifications.Notification;
14+
export type NotificationResponse = Notifications.NotificationResponse;
15+
export type NotificationBehavior = Notifications.NotificationBehavior;
16+
17+
/**
18+
* Lazily load the expo-notifications module.
19+
*
20+
* In Expo SDK 55+, `import * as Notifications from "expo-notifications"` triggers
21+
* a top-level side-effect (DevicePushTokenAutoRegistration.fx.ts) that calls
22+
* `addPushTokenListener()`, which throws on Android Expo Go where push notification
23+
* functionality has been removed (since SDK 53).
24+
*
25+
* We detect Android Expo Go before attempting the require() and skip it entirely,
26+
* since the throw from expo-notifications bypasses JavaScript try/catch via
27+
* React Native's global error handler.
28+
*
29+
* On all other environments (iOS Expo Go, development builds, production),
30+
* expo-notifications loads normally.
31+
*/
32+
33+
// Cache the module after the first load to avoid repeated require() calls and
34+
// environment detection checks on every access. The three states are:
35+
// undefined = not yet loaded (initial)
36+
// null = unavailable (Android Expo Go or load failure)
37+
// module = successfully loaded
38+
let cachedModule: NotificationsModule | null | undefined = undefined;
39+
40+
function isAndroidExpoGo(): boolean {
41+
return (
42+
Platform.OS === "android" &&
43+
Constants.executionEnvironment === ExecutionEnvironment.StoreClient
44+
);
45+
}
46+
47+
// Abstracted for testability — Vitest cannot intercept require() calls
48+
// inside dynamically imported modules after vi.resetModules().
49+
/* v8 ignore next 3 -- default require is replaced in tests via _resetForTesting */
50+
let requireNotifications: () => NotificationsModule = () =>
51+
// eslint-disable-next-line @typescript-eslint/no-require-imports
52+
require("expo-notifications") as NotificationsModule;
53+
54+
export function getNotificationsModule(): NotificationsModule | null {
55+
if (cachedModule !== undefined) {
56+
return cachedModule;
57+
}
58+
59+
if (isAndroidExpoGo()) {
60+
console.warn(
61+
"[Knock] Push notifications (remote notifications) are not available in Expo Go " +
62+
"on Android. This is an Expo platform limitation — push notification support was " +
63+
"removed from Expo Go on Android in SDK 53. Push features (token registration, " +
64+
"notification listeners) will be disabled, but all other Knock features will " +
65+
"continue to work.\n\n" +
66+
"To use push notifications on Android, use a development build instead of Expo Go: " +
67+
"https://docs.expo.dev/develop/development-builds/introduction/",
68+
);
69+
cachedModule = null;
70+
return cachedModule;
71+
}
72+
73+
try {
74+
cachedModule = requireNotifications();
75+
} catch {
76+
console.warn(
77+
"[Knock] expo-notifications could not be loaded. " +
78+
"Push notification features will be disabled.",
79+
);
80+
cachedModule = null;
81+
}
82+
83+
return cachedModule;
84+
}
85+
86+
/**
87+
* @internal Test-only — reset the cached module and optionally override
88+
* the require function used to load expo-notifications.
89+
*/
90+
export function _resetForTesting(
91+
overrideRequire?: () => NotificationsModule,
92+
): void {
93+
cachedModule = undefined;
94+
if (overrideRequire) {
95+
requireNotifications = overrideRequire;
96+
}
97+
}

packages/expo/src/modules/push/types.ts

Lines changed: 10 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,12 @@
11
import type { KnockPushNotificationContextType } from "@knocklabs/react-native";
2-
import type * as Notifications from "expo-notifications";
32
import type React from "react";
43

4+
import type {
5+
Notification,
6+
NotificationBehavior,
7+
NotificationResponse,
8+
} from "./getNotificationsModule";
9+
510
/**
611
* Context type for the Expo push notification provider.
712
* Extends the base push notification context with Expo-specific functionality.
@@ -16,12 +21,12 @@ export interface KnockExpoPushNotificationContextType
1621

1722
/** Register a handler for when a notification is received in the foreground */
1823
onNotificationReceived: (
19-
handler: (notification: Notifications.Notification) => void,
24+
handler: (notification: Notification) => void,
2025
) => void;
2126

2227
/** Register a handler for when a notification is tapped */
2328
onNotificationTapped: (
24-
handler: (response: Notifications.NotificationResponse) => void,
29+
handler: (response: NotificationResponse) => void,
2530
) => void;
2631
}
2732

@@ -37,8 +42,8 @@ export interface KnockExpoPushNotificationProviderProps {
3742
* If not provided, notifications will show alerts, play sounds, and set badges.
3843
*/
3944
customNotificationHandler?: (
40-
notification: Notifications.Notification,
41-
) => Promise<Notifications.NotificationBehavior>;
45+
notification: Notification,
46+
) => Promise<NotificationBehavior>;
4247

4348
/**
4449
* Custom function to set up the Android notification channel.

0 commit comments

Comments
 (0)