@applicaster/quick-brick-core 16.0.0-rc.104 → 16.0.0-rc.106

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,189 @@
1
+ import React from "react";
2
+ import { renderHook, act } from "@testing-library/react-native";
3
+ import { AppState, Platform } from "react-native";
4
+
5
+ import { useResumeVerifiedNetInfo } from "../useResumeVerifiedNetInfo";
6
+ import { RESUME_VERIFICATION_DELAY_MS } from "../resumeVerification";
7
+ import { isOnline } from "../utils";
8
+
9
+ const mockLogger = {
10
+ warning: jest.fn(),
11
+ debug: jest.fn(),
12
+ info: jest.fn(),
13
+ log: jest.fn(),
14
+ error: jest.fn(),
15
+ };
16
+
17
+ // addSubsystem runs at module load, before mockLogger is initialised: delegate lazily
18
+ jest.mock("../../logger", () => ({
19
+ coreAppLogger: {
20
+ addSubsystem: () => ({
21
+ warning: (event) => mockLogger.warning(event),
22
+ debug: (event) => mockLogger.debug(event),
23
+ info: (event) => mockLogger.info(event),
24
+ log: (event) => mockLogger.log(event),
25
+ error: (event) => mockLogger.error(event),
26
+ }),
27
+ },
28
+ }));
29
+
30
+ jest.mock("react-native/Libraries/AppState/AppState", () => {
31
+ const { EventEmitter } = require("events");
32
+ const emitter = new EventEmitter();
33
+
34
+ return {
35
+ emitter,
36
+ currentState: "active",
37
+ addEventListener: (type, handler) => {
38
+ emitter.addListener(type, handler);
39
+
40
+ return { remove: () => emitter.removeListener(type, handler) };
41
+ },
42
+ };
43
+ });
44
+
45
+ const ONLINE = {
46
+ type: "wifi",
47
+ details: { isConnectionExpensive: false },
48
+ isConnected: true,
49
+ isInternetReachable: true,
50
+ deviceStatus: null,
51
+ } as any;
52
+
53
+ const OFFLINE = {
54
+ type: "wifi",
55
+ details: { isConnectionExpensive: false },
56
+ isConnected: false,
57
+ isInternetReachable: false,
58
+ deviceStatus: null,
59
+ } as any;
60
+
61
+ const mockAppState = AppState as any;
62
+
63
+ const changeAppState = (state: string) =>
64
+ act(() => {
65
+ mockAppState.currentState = state;
66
+ mockAppState.emitter.emit("change", state);
67
+ });
68
+
69
+ const advance = (ms: number) =>
70
+ act(() => {
71
+ jest.advanceTimersByTime(ms);
72
+ });
73
+
74
+ const setup = (initial = ONLINE, options = {}) => {
75
+ const hook = renderHook(({ info }) => useResumeVerifiedNetInfo(info), {
76
+ initialProps: { info: initial },
77
+ ...options,
78
+ });
79
+
80
+ const setLive = (info) => hook.rerender({ info });
81
+ const publishedOnline = () => isOnline(hook.result.current.netInfo) === true;
82
+
83
+ return { ...hook, setLive, publishedOnline };
84
+ };
85
+
86
+ const infoMessages = () =>
87
+ mockLogger.info.mock.calls.map(([event]) => event.message);
88
+
89
+ describe("useResumeVerifiedNetInfo", () => {
90
+ const originalOS = Platform.OS;
91
+
92
+ beforeEach(() => {
93
+ jest.useFakeTimers();
94
+ Platform.OS = "ios";
95
+ mockAppState.currentState = "active";
96
+ mockAppState.emitter.removeAllListeners();
97
+ jest.clearAllMocks();
98
+ });
99
+
100
+ afterEach(() => {
101
+ jest.useRealTimers();
102
+ Platform.OS = originalOS;
103
+ });
104
+
105
+ it("publishes live changes immediately while active", () => {
106
+ const { setLive, result } = setup(ONLINE);
107
+
108
+ setLive(OFFLINE);
109
+
110
+ expect(result.current.netInfo).toBe(OFFLINE);
111
+ expect(result.current.stillOfflineAfterResumeCount).toBe(0);
112
+ });
113
+
114
+ it("holds the state in background and publishes the verified state 5s after resume", () => {
115
+ const { setLive, publishedOnline } = setup(ONLINE);
116
+
117
+ changeAppState("background");
118
+ setLive(OFFLINE);
119
+ changeAppState("active");
120
+
121
+ advance(RESUME_VERIFICATION_DELAY_MS - 1);
122
+ expect(publishedOnline()).toBe(true);
123
+
124
+ advance(1);
125
+ expect(publishedOnline()).toBe(false);
126
+ });
127
+
128
+ it("logs the resume verification at info level (delegates to the controller)", () => {
129
+ setup(ONLINE);
130
+
131
+ changeAppState("background");
132
+ changeAppState("active");
133
+ advance(RESUME_VERIFICATION_DELAY_MS);
134
+
135
+ expect(infoMessages()).toEqual(
136
+ expect.arrayContaining([
137
+ "NetworkStatus: resume verification started",
138
+ "NetworkStatus: resume verification finished",
139
+ ])
140
+ );
141
+
142
+ expect(mockLogger.warning).not.toHaveBeenCalled();
143
+ });
144
+
145
+ it("exposes stillOfflineAfterResumeCount after an offline resume", () => {
146
+ const { result } = setup(OFFLINE);
147
+
148
+ changeAppState("background");
149
+ changeAppState("active");
150
+ advance(RESUME_VERIFICATION_DELAY_MS);
151
+
152
+ expect(result.current.stillOfflineAfterResumeCount).toBe(1);
153
+ });
154
+
155
+ it("stops the controller on unmount (no AppState listener, pending window never finishes)", () => {
156
+ const { setLive, unmount } = setup(ONLINE);
157
+
158
+ changeAppState("background");
159
+ changeAppState("active");
160
+ setLive(OFFLINE);
161
+
162
+ unmount();
163
+ advance(RESUME_VERIFICATION_DELAY_MS);
164
+
165
+ expect(mockAppState.emitter.listenerCount("change")).toBe(0);
166
+
167
+ expect(infoMessages()).not.toContain(
168
+ "NetworkStatus: resume verification finished"
169
+ );
170
+ });
171
+
172
+ it("works under React.StrictMode (double-invoked effects)", () => {
173
+ const { setLive, publishedOnline } = setup(ONLINE, {
174
+ wrapper: React.StrictMode,
175
+ });
176
+
177
+ expect(mockAppState.emitter.listenerCount("change")).toBe(1);
178
+
179
+ changeAppState("background");
180
+ changeAppState("active");
181
+ setLive(OFFLINE);
182
+
183
+ advance(RESUME_VERIFICATION_DELAY_MS - 1);
184
+ expect(publishedOnline()).toBe(true);
185
+
186
+ advance(1);
187
+ expect(publishedOnline()).toBe(false);
188
+ });
189
+ });
@@ -0,0 +1,54 @@
1
+ import { Platform } from "react-native";
2
+ import { NetInfoState } from "@react-native-community/netinfo";
3
+ import { isConnectionInfoReady, isOnline } from "./utils";
4
+
5
+ type ConnectionInfo = NetInfoState & { deviceStatus?: NetStatus };
6
+
7
+ export type ResumeDecision =
8
+ | "still_online"
9
+ | "went_offline"
10
+ | "back_online"
11
+ | "still_offline";
12
+
13
+ /**
14
+ * Time to wait after the app returns to foreground before trusting NetInfo.
15
+ * On iOS the JS reachability check (NetInfo internetReachability) is suspended
16
+ * with the app and reports a stale failure right after resume/unlock.
17
+ */
18
+ export const RESUME_VERIFICATION_DELAY_MS = 5000;
19
+
20
+ // Only "background" suspends JS (and NetInfo's reachability check). "inactive"
21
+ // (Control Center, Notification Center, system alerts) keeps JS running, and is
22
+ // where users toggle airplane mode / Wi-Fi, so changes there must pass through.
23
+ export const isBackgroundAppState = (state?: string | null): boolean =>
24
+ state === "background";
25
+
26
+ // Mobile only: tvOS / Android TV report Platform.OS "ios" / "android" too
27
+ export const isResumeVerificationEnabled = (): boolean =>
28
+ (Platform.OS === "ios" || Platform.OS === "android") && !Platform.isTV;
29
+
30
+ export const getResumeDecision = (
31
+ wasOnline: boolean,
32
+ isOnlineNow: boolean
33
+ ): ResumeDecision => {
34
+ if (wasOnline) {
35
+ return isOnlineNow ? "still_online" : "went_offline";
36
+ }
37
+
38
+ return isOnlineNow ? "back_online" : "still_offline";
39
+ };
40
+
41
+ // Log-safe summary: details holds ipAddress/subnet/carrier, never log it as is
42
+ export const describeNetInfo = (info: ConnectionInfo | null) => {
43
+ const safeInfo = (info ?? {}) as ConnectionInfo;
44
+
45
+ return {
46
+ online: isOnline(safeInfo) === true,
47
+ isReady: !!isConnectionInfoReady(safeInfo),
48
+ type: safeInfo.type,
49
+ isConnected: safeInfo.isConnected,
50
+ isInternetReachable: safeInfo.isInternetReachable,
51
+ deviceOnline: safeInfo.deviceStatus?.online,
52
+ at: new Date().toISOString(),
53
+ };
54
+ };
@@ -0,0 +1,33 @@
1
+ import * as React from "react";
2
+
3
+ import {
4
+ ConnectionInfo,
5
+ ResumeVerificationController,
6
+ ResumeVerifiedState,
7
+ } from "./ResumeVerificationController";
8
+
9
+ /**
10
+ * Network status verified after the app returns from background,
11
+ * see ResumeVerificationController.
12
+ * liveNetInfo must keep its reference until it really changes (memoize it),
13
+ * every new reference is treated as a network update.
14
+ */
15
+ export function useResumeVerifiedNetInfo<T extends ConnectionInfo>(
16
+ liveNetInfo: T
17
+ ): ResumeVerifiedState<T> {
18
+ const [controller] = React.useState(
19
+ () => new ResumeVerificationController(liveNetInfo)
20
+ );
21
+
22
+ React.useLayoutEffect(() => {
23
+ controller.start();
24
+
25
+ return () => controller.stop();
26
+ }, [controller]);
27
+
28
+ React.useLayoutEffect(() => {
29
+ controller.setNetInfo(liveNetInfo);
30
+ }, [controller, liveNetInfo]);
31
+
32
+ return React.useSyncExternalStore(controller.subscribe, controller.getState);
33
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@applicaster/quick-brick-core",
3
- "version": "16.0.0-rc.104",
3
+ "version": "16.0.0-rc.106",
4
4
  "description": "Core package for Applicaster's Quick Brick App",
5
5
  "main": "index.js",
6
6
  "types": "index.d.ts",
@@ -28,13 +28,13 @@
28
28
  },
29
29
  "homepage": "https://github.com/applicaster/quickbrick#readme",
30
30
  "dependencies": {
31
- "@applicaster/applicaster-types": "16.0.0-rc.104",
32
- "@applicaster/quick-brick-core-plugins": "16.0.0-rc.104",
33
- "@applicaster/zapp-pipes-v2-client": "16.0.0-rc.104",
34
- "@applicaster/zapp-react-native-bridge": "16.0.0-rc.104",
35
- "@applicaster/zapp-react-native-redux": "16.0.0-rc.104",
36
- "@applicaster/zapp-react-native-ui-components": "16.0.0-rc.104",
37
- "@applicaster/zapp-react-native-utils": "16.0.0-rc.104",
31
+ "@applicaster/applicaster-types": "16.0.0-rc.106",
32
+ "@applicaster/quick-brick-core-plugins": "16.0.0-rc.106",
33
+ "@applicaster/zapp-pipes-v2-client": "16.0.0-rc.106",
34
+ "@applicaster/zapp-react-native-bridge": "16.0.0-rc.106",
35
+ "@applicaster/zapp-react-native-redux": "16.0.0-rc.106",
36
+ "@applicaster/zapp-react-native-ui-components": "16.0.0-rc.106",
37
+ "@applicaster/zapp-react-native-utils": "16.0.0-rc.106",
38
38
  "atob": "^2.1.2",
39
39
  "axios": "^0.28.0",
40
40
  "btoa": "^1.2.1",