@applicaster/quick-brick-core 16.0.0-rc.98 → 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.
Files changed (48) hide show
  1. package/App/ActionSetters/index.ts +2 -2
  2. package/App/ActionsProvider/ActionsProvider.tsx +1 -1
  3. package/App/ActionsProvider/LegacyActionsRegistryAdapter.tsx +17 -1
  4. package/App/ActionsProvider/__tests__/LegacyActionsRegistryAdapter.test.tsx +64 -0
  5. package/App/AssetCache/logger.ts +1 -1
  6. package/App/DeepLinking/URLSchemeHandler/SchemeHandlerHooks/usePresentSchemeHandler.ts +3 -0
  7. package/App/DeepLinking/URLSchemeListener/URLSchemeContextProvider.tsx +4 -1
  8. package/App/DeepLinking/logger.ts +1 -1
  9. package/App/ErrorBoundary/index.tsx +5 -2
  10. package/App/NavigationProvider/NavigationProvider.tsx +17 -11
  11. package/App/NavigationProvider/__tests__/navToolbox.test.ts +227 -0
  12. package/App/NavigationProvider/__tests__/navigationProvider.test.tsx +56 -0
  13. package/App/NavigationProvider/navToolbox.ts +224 -0
  14. package/App/NavigationProvider/utils.ts +23 -2
  15. package/App/NetworkStatusProvider/NetworkStatusProvider.tsx +40 -4
  16. package/App/NetworkStatusProvider/ResumeVerificationController.ts +282 -0
  17. package/App/NetworkStatusProvider/__tests__/NetworkStatusProvider.test.tsx +167 -1
  18. package/App/NetworkStatusProvider/__tests__/ResumeVerificationController.test.ts +486 -0
  19. package/App/NetworkStatusProvider/__tests__/resumeVerification.test.ts +126 -0
  20. package/App/NetworkStatusProvider/__tests__/useResumeVerifiedNetInfo.test.ts +204 -0
  21. package/App/NetworkStatusProvider/resumeVerification.ts +54 -0
  22. package/App/NetworkStatusProvider/useResumeVerifiedNetInfo.ts +33 -0
  23. package/App/ThemeManager/index.tsx +6 -1
  24. package/App/appLifeCycleManager/index.tsx +2 -2
  25. package/App/appRemoteDataLoader/index.tsx +2 -2
  26. package/App/appRemoteDataLoader/remoteDataLoader.ts +2 -2
  27. package/App/index.tsx +8 -2
  28. package/App/layoutPreview/LayoutPreviewReminder.tsx +136 -0
  29. package/App/layoutPreview/__tests__/persistence.test.ts +63 -0
  30. package/App/layoutPreview/persistence.ts +228 -0
  31. package/App/logger.js +2 -2
  32. package/App/remoteContextReloader/__tests__/getNativeRemoteContextData.test.ts +148 -0
  33. package/App/remoteContextReloader/__tests__/getPromiseForType.test.js +31 -0
  34. package/App/remoteContextReloader/getRemoteContextData/getNativeRemoteContextData.ts +107 -29
  35. package/App/remoteContextReloader/helpers.ts +2 -1
  36. package/App/uiActions/index.ts +13 -0
  37. package/App/uiActions/muteUnmute/__tests__/assetResolver.test.ts +226 -0
  38. package/App/uiActions/muteUnmute/__tests__/binding.test.ts +51 -0
  39. package/App/uiActions/muteUnmute/__tests__/defaultAssets.test.ts +47 -0
  40. package/App/uiActions/muteUnmute/__tests__/entryState.test.ts +57 -0
  41. package/App/uiActions/muteUnmute/__tests__/labelResolver.test.ts +35 -0
  42. package/App/uiActions/muteUnmute/assetResolver.ts +87 -0
  43. package/App/uiActions/muteUnmute/defaultAssets.ts +44 -0
  44. package/App/uiActions/muteUnmute/entryState.ts +28 -0
  45. package/App/uiActions/muteUnmute/index.ts +79 -0
  46. package/App/uiActions/muteUnmute/labelResolver.ts +15 -0
  47. package/App/uiActions/muteUnmute/types.ts +18 -0
  48. 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
- const themes = selectThemes(isTablet, plugins);
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 = coreAppLogger.addSubsystem("AppLifeCycleManager");
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 = coreAppLogger.addSubsystem("AppRemoteDataLoader");
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: "PlayerController",
9
- subsystem: "RemoteDataLoader",
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
- React.memo(function ZappApp(props: AppComponentProps) {
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
+ }