@applicaster/quick-brick-core 16.0.0-rc.99 → 17.0.0-rc.1
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.
- package/App/ActionSetters/index.ts +2 -2
- package/App/ActionsProvider/ActionsProvider.tsx +1 -1
- package/App/ActionsProvider/LegacyActionsRegistryAdapter.tsx +17 -1
- package/App/ActionsProvider/__tests__/LegacyActionsRegistryAdapter.test.tsx +64 -0
- package/App/AssetCache/logger.ts +1 -1
- package/App/DeepLinking/URLSchemeHandler/SchemeHandlerHooks/usePresentSchemeHandler.ts +3 -0
- package/App/DeepLinking/URLSchemeListener/URLSchemeContextProvider.tsx +4 -1
- package/App/DeepLinking/logger.ts +1 -1
- package/App/ErrorBoundary/index.tsx +5 -2
- package/App/NavigationProvider/NavigationProvider.tsx +17 -11
- package/App/NavigationProvider/__tests__/navToolbox.test.ts +227 -0
- package/App/NavigationProvider/__tests__/navigationProvider.test.tsx +56 -0
- package/App/NavigationProvider/navToolbox.ts +224 -0
- package/App/NavigationProvider/utils.ts +23 -2
- package/App/NetworkStatusProvider/NetworkStatusProvider.tsx +40 -4
- package/App/NetworkStatusProvider/ResumeVerificationController.ts +282 -0
- package/App/NetworkStatusProvider/__tests__/NetworkStatusProvider.test.tsx +167 -1
- package/App/NetworkStatusProvider/__tests__/ResumeVerificationController.test.ts +486 -0
- package/App/NetworkStatusProvider/__tests__/resumeVerification.test.ts +126 -0
- package/App/NetworkStatusProvider/__tests__/useResumeVerifiedNetInfo.test.ts +204 -0
- package/App/NetworkStatusProvider/resumeVerification.ts +54 -0
- package/App/NetworkStatusProvider/useResumeVerifiedNetInfo.ts +33 -0
- package/App/ThemeManager/index.tsx +6 -1
- package/App/appLifeCycleManager/index.tsx +2 -2
- package/App/appRemoteDataLoader/index.tsx +2 -2
- package/App/appRemoteDataLoader/remoteDataLoader.ts +2 -2
- package/App/index.tsx +8 -2
- package/App/layoutPreview/LayoutPreviewReminder.tsx +136 -0
- package/App/layoutPreview/__tests__/persistence.test.ts +63 -0
- package/App/layoutPreview/persistence.ts +228 -0
- package/App/logger.js +2 -2
- package/App/remoteContextReloader/__tests__/getNativeRemoteContextData.test.ts +148 -0
- package/App/remoteContextReloader/__tests__/getPromiseForType.test.js +31 -0
- package/App/remoteContextReloader/getRemoteContextData/getNativeRemoteContextData.ts +107 -29
- package/App/remoteContextReloader/helpers.ts +2 -1
- package/App/uiActions/index.ts +13 -0
- package/App/uiActions/muteUnmute/__tests__/assetResolver.test.ts +226 -0
- package/App/uiActions/muteUnmute/__tests__/binding.test.ts +51 -0
- package/App/uiActions/muteUnmute/__tests__/defaultAssets.test.ts +47 -0
- package/App/uiActions/muteUnmute/__tests__/entryState.test.ts +57 -0
- package/App/uiActions/muteUnmute/__tests__/labelResolver.test.ts +35 -0
- package/App/uiActions/muteUnmute/assetResolver.ts +87 -0
- package/App/uiActions/muteUnmute/defaultAssets.ts +44 -0
- package/App/uiActions/muteUnmute/entryState.ts +28 -0
- package/App/uiActions/muteUnmute/index.ts +79 -0
- package/App/uiActions/muteUnmute/labelResolver.ts +15 -0
- package/App/uiActions/muteUnmute/types.ts +18 -0
- package/package.json +8 -8
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
import { Platform } from "react-native";
|
|
2
|
+
import {
|
|
3
|
+
RESUME_VERIFICATION_DELAY_MS,
|
|
4
|
+
describeNetInfo,
|
|
5
|
+
getResumeDecision,
|
|
6
|
+
isBackgroundAppState,
|
|
7
|
+
isResumeVerificationEnabled,
|
|
8
|
+
} from "../resumeVerification";
|
|
9
|
+
|
|
10
|
+
describe("resumeVerification", () => {
|
|
11
|
+
it("uses a 5s verification delay", () => {
|
|
12
|
+
expect(RESUME_VERIFICATION_DELAY_MS).toBe(5000);
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
describe("isBackgroundAppState", () => {
|
|
16
|
+
it.each([
|
|
17
|
+
["inactive", false],
|
|
18
|
+
["background", true],
|
|
19
|
+
["active", false],
|
|
20
|
+
["unknown", false],
|
|
21
|
+
["extension", false],
|
|
22
|
+
[null, false],
|
|
23
|
+
[undefined, false],
|
|
24
|
+
])("%s -> %s", (state, expected) => {
|
|
25
|
+
expect(isBackgroundAppState(state)).toBe(expected);
|
|
26
|
+
});
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
describe("getResumeDecision", () => {
|
|
30
|
+
it.each([
|
|
31
|
+
[true, true, "still_online"],
|
|
32
|
+
[true, false, "went_offline"],
|
|
33
|
+
[false, true, "back_online"],
|
|
34
|
+
[false, false, "still_offline"],
|
|
35
|
+
])("wasOnline=%s isOnlineNow=%s -> %s", (was, now, expected) => {
|
|
36
|
+
expect(getResumeDecision(was, now)).toBe(expected);
|
|
37
|
+
});
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
describe("isResumeVerificationEnabled", () => {
|
|
41
|
+
const originalOS = Platform.OS;
|
|
42
|
+
|
|
43
|
+
afterEach(() => {
|
|
44
|
+
Platform.OS = originalOS;
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
it.each([
|
|
48
|
+
["ios", true],
|
|
49
|
+
["android", true],
|
|
50
|
+
["web", false],
|
|
51
|
+
["samsung_tv", false],
|
|
52
|
+
["lg_tv", false],
|
|
53
|
+
])("%s -> %s", (os, expected) => {
|
|
54
|
+
Platform.OS = os as any;
|
|
55
|
+
expect(isResumeVerificationEnabled()).toBe(expected);
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
it.each(["ios", "android"])(
|
|
59
|
+
"is disabled on TV platforms (%s isTV)",
|
|
60
|
+
(os) => {
|
|
61
|
+
const isTVDescriptor = Object.getOwnPropertyDescriptor(
|
|
62
|
+
Platform,
|
|
63
|
+
"isTV"
|
|
64
|
+
);
|
|
65
|
+
|
|
66
|
+
Platform.OS = os as any;
|
|
67
|
+
|
|
68
|
+
Object.defineProperty(Platform, "isTV", {
|
|
69
|
+
value: true,
|
|
70
|
+
configurable: true,
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
try {
|
|
74
|
+
expect(isResumeVerificationEnabled()).toBe(false);
|
|
75
|
+
} finally {
|
|
76
|
+
if (isTVDescriptor) {
|
|
77
|
+
Object.defineProperty(Platform, "isTV", isTVDescriptor);
|
|
78
|
+
} else {
|
|
79
|
+
delete (Platform as any).isTV;
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
);
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
describe("describeNetInfo", () => {
|
|
87
|
+
it("summarises the status without leaking details (ip, subnet, carrier)", () => {
|
|
88
|
+
const summary = describeNetInfo({
|
|
89
|
+
type: "wifi",
|
|
90
|
+
isConnected: false,
|
|
91
|
+
isInternetReachable: false,
|
|
92
|
+
details: { ipAddress: "10.0.0.2", subnet: "255.255.255.0" },
|
|
93
|
+
deviceStatus: null,
|
|
94
|
+
} as any);
|
|
95
|
+
|
|
96
|
+
expect(summary).toEqual({
|
|
97
|
+
online: false,
|
|
98
|
+
isReady: true,
|
|
99
|
+
type: "wifi",
|
|
100
|
+
isConnected: false,
|
|
101
|
+
isInternetReachable: false,
|
|
102
|
+
deviceOnline: undefined,
|
|
103
|
+
at: expect.any(String),
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
expect(JSON.stringify(summary)).not.toContain("10.0.0.2");
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
it("reports not-ready info as online (mirrors isOnline)", () => {
|
|
110
|
+
expect(
|
|
111
|
+
describeNetInfo({
|
|
112
|
+
type: "unknown",
|
|
113
|
+
isConnected: null,
|
|
114
|
+
isInternetReachable: null,
|
|
115
|
+
details: null,
|
|
116
|
+
} as any)
|
|
117
|
+
).toEqual(expect.objectContaining({ online: true, isReady: false }));
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
it("handles null input", () => {
|
|
121
|
+
expect(describeNetInfo(null)).toEqual(
|
|
122
|
+
expect.objectContaining({ online: true, isReady: false })
|
|
123
|
+
);
|
|
124
|
+
});
|
|
125
|
+
});
|
|
126
|
+
});
|
|
@@ -0,0 +1,204 @@
|
|
|
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
|
+
// subsystemLogger runs at module load, before mockLogger is initialised: delegate lazily
|
|
18
|
+
jest.mock("@applicaster/zapp-react-native-utils/logger", () => {
|
|
19
|
+
const actual = jest.requireActual(
|
|
20
|
+
"@applicaster/zapp-react-native-utils/logger"
|
|
21
|
+
);
|
|
22
|
+
|
|
23
|
+
return {
|
|
24
|
+
...actual,
|
|
25
|
+
// Only the logger of this subsystem is replaced; every other module keeps its real one.
|
|
26
|
+
subsystemLogger: (subsystem: string) => {
|
|
27
|
+
if (subsystem !== "network/reachability") {
|
|
28
|
+
return actual.subsystemLogger(subsystem);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
const logger = {
|
|
32
|
+
warning: (event) => mockLogger.warning(event),
|
|
33
|
+
debug: (event) => mockLogger.debug(event),
|
|
34
|
+
info: (event) => mockLogger.info(event),
|
|
35
|
+
log: (event) => mockLogger.log(event),
|
|
36
|
+
error: (event) => mockLogger.error(event),
|
|
37
|
+
addSubsystem: () => logger,
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
return logger;
|
|
41
|
+
},
|
|
42
|
+
};
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
jest.mock("react-native/Libraries/AppState/AppState", () => {
|
|
46
|
+
const { EventEmitter } = require("events");
|
|
47
|
+
const emitter = new EventEmitter();
|
|
48
|
+
|
|
49
|
+
return {
|
|
50
|
+
emitter,
|
|
51
|
+
currentState: "active",
|
|
52
|
+
addEventListener: (type, handler) => {
|
|
53
|
+
emitter.addListener(type, handler);
|
|
54
|
+
|
|
55
|
+
return { remove: () => emitter.removeListener(type, handler) };
|
|
56
|
+
},
|
|
57
|
+
};
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
const ONLINE = {
|
|
61
|
+
type: "wifi",
|
|
62
|
+
details: { isConnectionExpensive: false },
|
|
63
|
+
isConnected: true,
|
|
64
|
+
isInternetReachable: true,
|
|
65
|
+
deviceStatus: null,
|
|
66
|
+
} as any;
|
|
67
|
+
|
|
68
|
+
const OFFLINE = {
|
|
69
|
+
type: "wifi",
|
|
70
|
+
details: { isConnectionExpensive: false },
|
|
71
|
+
isConnected: false,
|
|
72
|
+
isInternetReachable: false,
|
|
73
|
+
deviceStatus: null,
|
|
74
|
+
} as any;
|
|
75
|
+
|
|
76
|
+
const mockAppState = AppState as any;
|
|
77
|
+
|
|
78
|
+
const changeAppState = (state: string) =>
|
|
79
|
+
act(() => {
|
|
80
|
+
mockAppState.currentState = state;
|
|
81
|
+
mockAppState.emitter.emit("change", state);
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
const advance = (ms: number) =>
|
|
85
|
+
act(() => {
|
|
86
|
+
jest.advanceTimersByTime(ms);
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
const setup = (initial = ONLINE, options = {}) => {
|
|
90
|
+
const hook = renderHook(({ info }) => useResumeVerifiedNetInfo(info), {
|
|
91
|
+
initialProps: { info: initial },
|
|
92
|
+
...options,
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
const setLive = (info) => hook.rerender({ info });
|
|
96
|
+
const publishedOnline = () => isOnline(hook.result.current.netInfo) === true;
|
|
97
|
+
|
|
98
|
+
return { ...hook, setLive, publishedOnline };
|
|
99
|
+
};
|
|
100
|
+
|
|
101
|
+
const infoMessages = () =>
|
|
102
|
+
mockLogger.info.mock.calls.map(([event]) => event.message);
|
|
103
|
+
|
|
104
|
+
describe("useResumeVerifiedNetInfo", () => {
|
|
105
|
+
const originalOS = Platform.OS;
|
|
106
|
+
|
|
107
|
+
beforeEach(() => {
|
|
108
|
+
jest.useFakeTimers();
|
|
109
|
+
Platform.OS = "ios";
|
|
110
|
+
mockAppState.currentState = "active";
|
|
111
|
+
mockAppState.emitter.removeAllListeners();
|
|
112
|
+
jest.clearAllMocks();
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
afterEach(() => {
|
|
116
|
+
jest.useRealTimers();
|
|
117
|
+
Platform.OS = originalOS;
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
it("publishes live changes immediately while active", () => {
|
|
121
|
+
const { setLive, result } = setup(ONLINE);
|
|
122
|
+
|
|
123
|
+
setLive(OFFLINE);
|
|
124
|
+
|
|
125
|
+
expect(result.current.netInfo).toBe(OFFLINE);
|
|
126
|
+
expect(result.current.stillOfflineAfterResumeCount).toBe(0);
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
it("holds the state in background and publishes the verified state 5s after resume", () => {
|
|
130
|
+
const { setLive, publishedOnline } = setup(ONLINE);
|
|
131
|
+
|
|
132
|
+
changeAppState("background");
|
|
133
|
+
setLive(OFFLINE);
|
|
134
|
+
changeAppState("active");
|
|
135
|
+
|
|
136
|
+
advance(RESUME_VERIFICATION_DELAY_MS - 1);
|
|
137
|
+
expect(publishedOnline()).toBe(true);
|
|
138
|
+
|
|
139
|
+
advance(1);
|
|
140
|
+
expect(publishedOnline()).toBe(false);
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
it("logs the resume verification at info level (delegates to the controller)", () => {
|
|
144
|
+
setup(ONLINE);
|
|
145
|
+
|
|
146
|
+
changeAppState("background");
|
|
147
|
+
changeAppState("active");
|
|
148
|
+
advance(RESUME_VERIFICATION_DELAY_MS);
|
|
149
|
+
|
|
150
|
+
expect(infoMessages()).toEqual(
|
|
151
|
+
expect.arrayContaining([
|
|
152
|
+
"NetworkStatus: resume verification started",
|
|
153
|
+
"NetworkStatus: resume verification finished",
|
|
154
|
+
])
|
|
155
|
+
);
|
|
156
|
+
|
|
157
|
+
expect(mockLogger.warning).not.toHaveBeenCalled();
|
|
158
|
+
});
|
|
159
|
+
|
|
160
|
+
it("exposes stillOfflineAfterResumeCount after an offline resume", () => {
|
|
161
|
+
const { result } = setup(OFFLINE);
|
|
162
|
+
|
|
163
|
+
changeAppState("background");
|
|
164
|
+
changeAppState("active");
|
|
165
|
+
advance(RESUME_VERIFICATION_DELAY_MS);
|
|
166
|
+
|
|
167
|
+
expect(result.current.stillOfflineAfterResumeCount).toBe(1);
|
|
168
|
+
});
|
|
169
|
+
|
|
170
|
+
it("stops the controller on unmount (no AppState listener, pending window never finishes)", () => {
|
|
171
|
+
const { setLive, unmount } = setup(ONLINE);
|
|
172
|
+
|
|
173
|
+
changeAppState("background");
|
|
174
|
+
changeAppState("active");
|
|
175
|
+
setLive(OFFLINE);
|
|
176
|
+
|
|
177
|
+
unmount();
|
|
178
|
+
advance(RESUME_VERIFICATION_DELAY_MS);
|
|
179
|
+
|
|
180
|
+
expect(mockAppState.emitter.listenerCount("change")).toBe(0);
|
|
181
|
+
|
|
182
|
+
expect(infoMessages()).not.toContain(
|
|
183
|
+
"NetworkStatus: resume verification finished"
|
|
184
|
+
);
|
|
185
|
+
});
|
|
186
|
+
|
|
187
|
+
it("works under React.StrictMode (double-invoked effects)", () => {
|
|
188
|
+
const { setLive, publishedOnline } = setup(ONLINE, {
|
|
189
|
+
wrapper: React.StrictMode,
|
|
190
|
+
});
|
|
191
|
+
|
|
192
|
+
expect(mockAppState.emitter.listenerCount("change")).toBe(1);
|
|
193
|
+
|
|
194
|
+
changeAppState("background");
|
|
195
|
+
changeAppState("active");
|
|
196
|
+
setLive(OFFLINE);
|
|
197
|
+
|
|
198
|
+
advance(RESUME_VERIFICATION_DELAY_MS - 1);
|
|
199
|
+
expect(publishedOnline()).toBe(true);
|
|
200
|
+
|
|
201
|
+
advance(1);
|
|
202
|
+
expect(publishedOnline()).toBe(false);
|
|
203
|
+
});
|
|
204
|
+
});
|
|
@@ -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
|
+
}
|
|
@@ -100,7 +100,12 @@ const MemoizedThemeContextProvider = React.memo(
|
|
|
100
100
|
function ThemeManagerComponent({ children, plugins }: Props) {
|
|
101
101
|
const isTablet = useIsTablet();
|
|
102
102
|
|
|
103
|
-
|
|
103
|
+
// Stable reference keeps MemoizedThemeContextProvider's byReference check
|
|
104
|
+
// effective, so theme consumers don't re-render on every parent render
|
|
105
|
+
const themes = React.useMemo(
|
|
106
|
+
() => selectThemes(isTablet, plugins),
|
|
107
|
+
[isTablet, plugins]
|
|
108
|
+
);
|
|
104
109
|
|
|
105
110
|
const [selectedThemeId, setSelectedThemeId] =
|
|
106
111
|
React.useState(DEFAULT_THEME_ID);
|
|
@@ -12,13 +12,13 @@ import { isAndroidPlatform } from "@applicaster/zapp-react-native-utils/reactUti
|
|
|
12
12
|
import { networkStatusReady } from "../NetworkStatusProvider/networkStatusReady";
|
|
13
13
|
import { updateSessionNumber } from "../SessionCounter/sessionCounter";
|
|
14
14
|
|
|
15
|
-
import { coreAppLogger } from "../logger";
|
|
16
15
|
import { sessionStorage } from "@applicaster/zapp-react-native-bridge/ZappStorage/SessionStorage";
|
|
17
16
|
import { isEmptyOrNil } from "@applicaster/zapp-react-native-utils/cellUtils";
|
|
18
17
|
import { requiredSessionKeys } from "../../const";
|
|
19
18
|
import { initializeActionSetters, useActionSetters } from "../ActionSetters";
|
|
19
|
+
import { subsystemLogger } from "@applicaster/zapp-react-native-utils/logger";
|
|
20
20
|
|
|
21
|
-
const logger =
|
|
21
|
+
const logger = subsystemLogger("app/lifecycle", "AppLifeCycleManager");
|
|
22
22
|
|
|
23
23
|
const isAndroid = isAndroidPlatform();
|
|
24
24
|
|
|
@@ -2,11 +2,11 @@ import * as React from "react";
|
|
|
2
2
|
|
|
3
3
|
import { getRemoteContextData } from "../remoteContextReloader/getRemoteContextData";
|
|
4
4
|
|
|
5
|
-
import { coreAppLogger } from "../logger";
|
|
6
5
|
import { UnknownAction, Dispatch } from "@reduxjs/toolkit";
|
|
7
6
|
import { isAndroidPlatform } from "@applicaster/zapp-react-native-utils/reactUtils";
|
|
7
|
+
import { subsystemLogger } from "@applicaster/zapp-react-native-utils/logger";
|
|
8
8
|
|
|
9
|
-
const logger =
|
|
9
|
+
const logger = subsystemLogger("app/loading", "AppRemoteDataLoader");
|
|
10
10
|
|
|
11
11
|
/**
|
|
12
12
|
* adds a call to the QuickBrickManager to tell the app is ready, reloads remote
|
|
@@ -5,8 +5,8 @@ import { createLogger } from "@applicaster/zapp-react-native-utils/logger";
|
|
|
5
5
|
import { coreAppLogger } from "../logger";
|
|
6
6
|
|
|
7
7
|
const logger = createLogger({
|
|
8
|
-
category: "
|
|
9
|
-
subsystem: "
|
|
8
|
+
category: "RemoteDataLoader",
|
|
9
|
+
subsystem: "app/loading",
|
|
10
10
|
parent: coreAppLogger,
|
|
11
11
|
});
|
|
12
12
|
|
package/App/index.tsx
CHANGED
|
@@ -32,7 +32,9 @@ import { withScreenDataContextProvider } from "@applicaster/zapp-react-native-ui
|
|
|
32
32
|
import { LogicAggregatorContainer } from "./LogicAggregatorContainer";
|
|
33
33
|
import { LoadingOverlay } from "./LoadingOverlay";
|
|
34
34
|
import { NotificationToastRenderer } from "./NotificationToastRenderer";
|
|
35
|
+
import { LayoutPreviewReminder } from "./layoutPreview/LayoutPreviewReminder";
|
|
35
36
|
import { ZappAppWrapper } from "./components/ZappAppWrapper";
|
|
37
|
+
import { registerCoreUIActions } from "./uiActions";
|
|
36
38
|
|
|
37
39
|
interface AppOptions {
|
|
38
40
|
Layout: React.ComponentType;
|
|
@@ -52,8 +54,10 @@ const App = ({
|
|
|
52
54
|
OfflineHandler,
|
|
53
55
|
SplashLoader,
|
|
54
56
|
InteractionManager,
|
|
55
|
-
}: AppOptions): ZappApp =>
|
|
56
|
-
|
|
57
|
+
}: AppOptions): ZappApp => {
|
|
58
|
+
registerCoreUIActions();
|
|
59
|
+
|
|
60
|
+
return React.memo(function ZappApp(props: AppComponentProps) {
|
|
57
61
|
const {
|
|
58
62
|
styles,
|
|
59
63
|
components: { AppContainer },
|
|
@@ -79,6 +83,7 @@ const App = ({
|
|
|
79
83
|
<ModalProvider />
|
|
80
84
|
</URLSchemeListener>
|
|
81
85
|
<LoadingOverlay />
|
|
86
|
+
<LayoutPreviewReminder />
|
|
82
87
|
</ErrorBoundary>
|
|
83
88
|
<LogicAggregatorContainer
|
|
84
89
|
components={React.useMemo(
|
|
@@ -92,6 +97,7 @@ const App = ({
|
|
|
92
97
|
</ZappAppWrapper>
|
|
93
98
|
);
|
|
94
99
|
});
|
|
100
|
+
};
|
|
95
101
|
|
|
96
102
|
export function createQuickBrickApp(options): ZappApp {
|
|
97
103
|
const {
|
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
import * as React from "react";
|
|
2
|
+
import { AppState, Platform } from "react-native";
|
|
3
|
+
|
|
4
|
+
import { useAppSelector } from "@applicaster/zapp-react-native-redux/hooks";
|
|
5
|
+
import { selectAppReady } from "@applicaster/zapp-react-native-redux";
|
|
6
|
+
import {
|
|
7
|
+
showAlertDialog,
|
|
8
|
+
showConfirmationDialog,
|
|
9
|
+
} from "@applicaster/zapp-react-native-utils/alertUtils";
|
|
10
|
+
import { confirmationDialogStore } from "@applicaster/zapp-react-native-ui-components/Contexts/ConfirmDialogState";
|
|
11
|
+
import { isWeb } from "@applicaster/zapp-react-native-utils/reactUtils";
|
|
12
|
+
import * as QuickBrickManager from "@applicaster/zapp-react-native-bridge/QuickBrick";
|
|
13
|
+
import { QUICK_BRICK_EVENTS } from "@applicaster/zapp-react-native-bridge/QuickBrick";
|
|
14
|
+
import { notificationToastManager } from "../NotificationToastRenderer/NotificationToastManager";
|
|
15
|
+
import {
|
|
16
|
+
PREVIEW_BACK,
|
|
17
|
+
PREVIEW_CONTINUE,
|
|
18
|
+
PREVIEW_MESSAGE,
|
|
19
|
+
PREVIEW_TITLE,
|
|
20
|
+
UNAVAILABLE_MESSAGE,
|
|
21
|
+
UNAVAILABLE_TITLE,
|
|
22
|
+
acknowledgeLayoutPreview,
|
|
23
|
+
consumePreviewUnavailable,
|
|
24
|
+
leavePreviewForDefault,
|
|
25
|
+
readReminderState,
|
|
26
|
+
shouldShowReminder,
|
|
27
|
+
} from "./persistence";
|
|
28
|
+
|
|
29
|
+
let promptOpen = false;
|
|
30
|
+
let checkingPrompt = false;
|
|
31
|
+
|
|
32
|
+
function showUnavailableNotice(): void {
|
|
33
|
+
if (Platform.isTV && !isWeb()) {
|
|
34
|
+
showAlertDialog({
|
|
35
|
+
title: UNAVAILABLE_TITLE,
|
|
36
|
+
message: UNAVAILABLE_MESSAGE,
|
|
37
|
+
okButtonText: "OK",
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
return;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
notificationToastManager.showToast({
|
|
44
|
+
message: UNAVAILABLE_TITLE,
|
|
45
|
+
extraMessage: UNAVAILABLE_MESSAGE,
|
|
46
|
+
});
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function showPreviewDialog(): void {
|
|
50
|
+
const onContinue = () => {
|
|
51
|
+
promptOpen = false;
|
|
52
|
+
void acknowledgeLayoutPreview();
|
|
53
|
+
};
|
|
54
|
+
|
|
55
|
+
const onBack = () => {
|
|
56
|
+
promptOpen = false;
|
|
57
|
+
|
|
58
|
+
void leavePreviewForDefault().finally(() => {
|
|
59
|
+
QuickBrickManager.sendQuickBrickEvent(
|
|
60
|
+
QUICK_BRICK_EVENTS.FORCE_APP_RELOAD
|
|
61
|
+
);
|
|
62
|
+
});
|
|
63
|
+
};
|
|
64
|
+
|
|
65
|
+
showConfirmationDialog({
|
|
66
|
+
title: PREVIEW_TITLE,
|
|
67
|
+
message: PREVIEW_MESSAGE,
|
|
68
|
+
cancelButtonText: PREVIEW_CONTINUE,
|
|
69
|
+
okButtonText: PREVIEW_BACK,
|
|
70
|
+
cancelCompletion: onContinue,
|
|
71
|
+
confirmCompletion: onBack,
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
if (!isWeb()) return;
|
|
75
|
+
|
|
76
|
+
// Remote Back hides the web dialog without calling either completion.
|
|
77
|
+
const unsubscribe = confirmationDialogStore.subscribe((state) => {
|
|
78
|
+
if (state.isDialogVisible) return;
|
|
79
|
+
|
|
80
|
+
promptOpen = false;
|
|
81
|
+
unsubscribe();
|
|
82
|
+
});
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
async function presentLayoutPreviewPrompt(): Promise<void> {
|
|
86
|
+
if (promptOpen || checkingPrompt) return;
|
|
87
|
+
|
|
88
|
+
checkingPrompt = true;
|
|
89
|
+
|
|
90
|
+
try {
|
|
91
|
+
if (consumePreviewUnavailable()) {
|
|
92
|
+
showUnavailableNotice();
|
|
93
|
+
|
|
94
|
+
return;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
const { hasOverride, ackAt } = await readReminderState();
|
|
98
|
+
|
|
99
|
+
if (!shouldShowReminder(hasOverride, ackAt, Date.now())) return;
|
|
100
|
+
|
|
101
|
+
promptOpen = true;
|
|
102
|
+
showPreviewDialog();
|
|
103
|
+
} finally {
|
|
104
|
+
checkingPrompt = false;
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
export function LayoutPreviewReminder(): null {
|
|
109
|
+
const appReady = useAppSelector(selectAppReady);
|
|
110
|
+
const checkedForeground = React.useRef(false);
|
|
111
|
+
|
|
112
|
+
React.useEffect(() => {
|
|
113
|
+
if (!appReady) return;
|
|
114
|
+
|
|
115
|
+
const showIfNeeded = () => {
|
|
116
|
+
if (checkedForeground.current) return;
|
|
117
|
+
|
|
118
|
+
checkedForeground.current = true;
|
|
119
|
+
void presentLayoutPreviewPrompt();
|
|
120
|
+
};
|
|
121
|
+
|
|
122
|
+
showIfNeeded();
|
|
123
|
+
|
|
124
|
+
const subscription = AppState.addEventListener("change", (state) => {
|
|
125
|
+
if (state === "background") {
|
|
126
|
+
checkedForeground.current = false;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
if (state === "active") showIfNeeded();
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
return () => subscription.remove();
|
|
133
|
+
}, [appReady]);
|
|
134
|
+
|
|
135
|
+
return null;
|
|
136
|
+
}
|