@applicaster/quick-brick-core 13.0.0-alpha.5855333506 → 13.0.0-alpha.5944303214
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/DeepLinking/URLSchemeListener/__tests__/URLSchemeListener.test.js +40 -0
- package/App/DeepLinking/URLSchemeListener/index.tsx +1 -1
- package/App/ErrorBoundary/__tests__/errorBoundary.test.jsx +232 -0
- package/App/ErrorBoundary/__tests__/store.test.js +143 -0
- package/App/ErrorBoundary/index.tsx +45 -21
- package/App/ErrorBoundary/store.ts +26 -0
- package/App/LoadingOverlay/LoadingOverlayManager.tsx +27 -0
- package/App/LoadingOverlay/index.tsx +49 -0
- package/App/LoadingOverlay/useLoadingOverlayVisibility.tsx +18 -0
- package/App/ModalProvider/ModalBottomSheet/index.tsx +4 -2
- package/App/ModalProvider/index.tsx +3 -4
- package/App/NavigationProvider/NavigationProvider.tsx +27 -0
- package/App/NetworkStatusProvider/utils.ts +0 -1
- package/App/StoreConnector/__tests__/storeConnector.test.js +17 -11
- package/App/appLifeCycleManager/__tests__/appLifeCycleManager.test.tsx +146 -0
- package/App/appLifeCycleManager/index.tsx +6 -7
- package/App/index.tsx +2 -0
- package/App/remoteContextReloader/__tests__/remoteContextReloader.test.js +11 -21
- package/App/remoteContextReloader/getRemoteContextData/getNativeRemoteContextData.ts +14 -0
- package/createZappApp/__tests__/__snapshots__/createZappApp.test.js.snap +5 -128
- package/createZappApp/__tests__/createZappApp.test.js +7 -20
- package/defaults/__tests__/defaults.test.js +3 -4
- package/index.d.ts +1 -1
- package/package.json +10 -14
- package/App/DeepLinking/URLSchemeListener/__tests__/URLSchemeListenerTest.js +0 -177
- package/App/ErrorBoundary/__tests__/__snapshots__/errorBoundary.test.js.snap +0 -9
- package/App/ErrorBoundary/__tests__/errorBoundary.test.js +0 -72
- package/App/StoreConnector/__tests__/__snapshots__/storeConnector.test.js.snap +0 -35
- package/App/appLifeCycleManager/__tests__/__snapshots__/appLifeCycleManager.test.js.snap +0 -30
- package/App/appLifeCycleManager/__tests__/appLifeCycleManager.test.js +0 -134
- package/App/remoteContextReloader/__tests__/__snapshots__/remoteContextReloader.test.js.snap +0 -43
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import * as React from "react";
|
|
2
|
+
import { View, Text } from "react-native";
|
|
3
|
+
import { renderWithProviders } from "@applicaster/zapp-react-native-utils/testUtils";
|
|
4
|
+
|
|
5
|
+
import { URLSchemeListener } from "../index";
|
|
6
|
+
|
|
7
|
+
const TestComponent = (props) => (
|
|
8
|
+
<View testID="test-comp" {...props}>
|
|
9
|
+
<Text>I'm a test component</Text>
|
|
10
|
+
</View>
|
|
11
|
+
);
|
|
12
|
+
|
|
13
|
+
const navigator = {
|
|
14
|
+
push: jest.fn(),
|
|
15
|
+
replace: jest.fn(),
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
jest.spyOn(console, "warn").mockImplementation(() => jest.fn());
|
|
19
|
+
|
|
20
|
+
function clearMocks() {
|
|
21
|
+
navigator.push.mockClear();
|
|
22
|
+
navigator.replace.mockClear();
|
|
23
|
+
console.warn.mockClear();
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
describe("URLSchemeHandler", () => {
|
|
27
|
+
const renderComponent = (props = {}) =>
|
|
28
|
+
renderWithProviders(
|
|
29
|
+
<URLSchemeListener {...props}>
|
|
30
|
+
<TestComponent />
|
|
31
|
+
</URLSchemeListener>
|
|
32
|
+
);
|
|
33
|
+
|
|
34
|
+
beforeEach(clearMocks);
|
|
35
|
+
|
|
36
|
+
it("renders correctly (default case)", () => {
|
|
37
|
+
const { getByTestId } = renderComponent();
|
|
38
|
+
expect(getByTestId("test-comp")).toBeDefined();
|
|
39
|
+
});
|
|
40
|
+
});
|
|
@@ -5,7 +5,7 @@ import { URLSchemeContext } from "./URLSchemeContextProvider";
|
|
|
5
5
|
import { useNavigation } from "@applicaster/zapp-react-native-utils/reactHooks/navigation";
|
|
6
6
|
import { usePickFromState } from "@applicaster/zapp-react-native-redux/hooks";
|
|
7
7
|
|
|
8
|
-
export function URLSchemeListener({ children }) {
|
|
8
|
+
export function URLSchemeListener({ children }: { children: React.ReactNode }) {
|
|
9
9
|
const { url, done } = useContext(URLSchemeContext);
|
|
10
10
|
|
|
11
11
|
const navigator = useNavigation();
|
|
@@ -0,0 +1,232 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
import { View, Text, TouchableOpacity } from "react-native";
|
|
3
|
+
import { render, fireEvent, act } from "@testing-library/react-native";
|
|
4
|
+
import { useErrorStore } from "../store";
|
|
5
|
+
import { ErrorBoundary } from "../index";
|
|
6
|
+
|
|
7
|
+
jest.mock(
|
|
8
|
+
"@applicaster/zapp-react-native-ui-components/Components/ErrorScreen",
|
|
9
|
+
() => ({
|
|
10
|
+
// eslint-disable-next-line react/prop-types
|
|
11
|
+
ErrorScreen: ({ dismissError, message, buttons }) => (
|
|
12
|
+
<View testID="error-screen">
|
|
13
|
+
<Text testID="error-message">{message || "Default error message"}</Text>
|
|
14
|
+
{/* eslint-disable-next-line react/prop-types */}
|
|
15
|
+
{buttons?.map((button, index) => (
|
|
16
|
+
<TouchableOpacity
|
|
17
|
+
key={index}
|
|
18
|
+
onPress={button.onPress}
|
|
19
|
+
testID={`button-${index}`}
|
|
20
|
+
>
|
|
21
|
+
<Text>{button.text}</Text>
|
|
22
|
+
</TouchableOpacity>
|
|
23
|
+
))}
|
|
24
|
+
|
|
25
|
+
{dismissError ? (
|
|
26
|
+
<TouchableOpacity onPress={dismissError} testID="dismiss-button">
|
|
27
|
+
<Text>Dismiss Error</Text>
|
|
28
|
+
</TouchableOpacity>
|
|
29
|
+
) : null}
|
|
30
|
+
</View>
|
|
31
|
+
),
|
|
32
|
+
})
|
|
33
|
+
);
|
|
34
|
+
|
|
35
|
+
jest.mock("@applicaster/zapp-react-native-bridge/QuickBrick", () => ({
|
|
36
|
+
sendQuickBrickEvent: jest.fn(),
|
|
37
|
+
QUICK_BRICK_EVENTS: {
|
|
38
|
+
FORCE_APP_RELOAD: "force_app_reload",
|
|
39
|
+
},
|
|
40
|
+
}));
|
|
41
|
+
|
|
42
|
+
jest.mock("@applicaster/zapp-react-native-utils/logger", () => ({
|
|
43
|
+
coreLogger: {
|
|
44
|
+
addSubsystem: () => ({
|
|
45
|
+
error: jest.fn(),
|
|
46
|
+
}),
|
|
47
|
+
},
|
|
48
|
+
}));
|
|
49
|
+
|
|
50
|
+
// eslint-disable-next-line react/prop-types
|
|
51
|
+
const TestComponent = ({ error, errorMessage = "error !!" }) => {
|
|
52
|
+
if (error) {
|
|
53
|
+
throw new Error(errorMessage);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
return (
|
|
57
|
+
<View>
|
|
58
|
+
<Text>all is good</Text>
|
|
59
|
+
</View>
|
|
60
|
+
);
|
|
61
|
+
};
|
|
62
|
+
|
|
63
|
+
describe("<ErrorBoundary />", () => {
|
|
64
|
+
// Clear all error states between tests
|
|
65
|
+
beforeEach(() => {
|
|
66
|
+
useErrorStore.getState().dismissError();
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
describe("when there is no error", () => {
|
|
70
|
+
it("renders children correctly", () => {
|
|
71
|
+
const { getByText } = render(
|
|
72
|
+
<ErrorBoundary>
|
|
73
|
+
<TestComponent />
|
|
74
|
+
</ErrorBoundary>
|
|
75
|
+
);
|
|
76
|
+
|
|
77
|
+
expect(getByText("all is good")).toBeTruthy();
|
|
78
|
+
});
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
describe("when an error is caught", () => {
|
|
82
|
+
const _consoleLogs = {
|
|
83
|
+
error: console.error,
|
|
84
|
+
log: console.log,
|
|
85
|
+
};
|
|
86
|
+
|
|
87
|
+
beforeAll(() => {
|
|
88
|
+
console.error = jest.fn();
|
|
89
|
+
console.log = jest.fn();
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
afterAll(() => {
|
|
93
|
+
console.error = _consoleLogs.error;
|
|
94
|
+
console.log = _consoleLogs.log;
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
it("renders error state with default values (recoverable error)", () => {
|
|
98
|
+
const { getByTestId } = render(
|
|
99
|
+
<ErrorBoundary>
|
|
100
|
+
<TestComponent error />
|
|
101
|
+
</ErrorBoundary>
|
|
102
|
+
);
|
|
103
|
+
|
|
104
|
+
expect(getByTestId("error-screen")).toBeTruthy();
|
|
105
|
+
expect(getByTestId("dismiss-button")).toBeTruthy(); // Should be recoverable by default
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
it("handles non-recoverable errors (no dismiss button)", () => {
|
|
109
|
+
const { getByTestId, queryByTestId } = render(
|
|
110
|
+
<ErrorBoundary recoverable={false}>
|
|
111
|
+
<TestComponent error />
|
|
112
|
+
</ErrorBoundary>
|
|
113
|
+
);
|
|
114
|
+
|
|
115
|
+
expect(getByTestId("error-screen")).toBeTruthy();
|
|
116
|
+
expect(queryByTestId("dismiss-button")).toBeFalsy(); // Should not be recoverable
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
it("shows custom error message from caught error", () => {
|
|
120
|
+
const customErrorMessage = "Custom error message for testing";
|
|
121
|
+
|
|
122
|
+
const { getByTestId } = render(
|
|
123
|
+
<ErrorBoundary>
|
|
124
|
+
<TestComponent error errorMessage={customErrorMessage} />
|
|
125
|
+
</ErrorBoundary>
|
|
126
|
+
);
|
|
127
|
+
|
|
128
|
+
// The error info should be passed to the ErrorScreen
|
|
129
|
+
expect(getByTestId("error-message").props.children).toBeTruthy();
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
it("can dismiss the error and restore the UI", () => {
|
|
133
|
+
// First render with error
|
|
134
|
+
const { getByTestId, queryByText, rerender } = render(
|
|
135
|
+
<ErrorBoundary>
|
|
136
|
+
<TestComponent error />
|
|
137
|
+
</ErrorBoundary>
|
|
138
|
+
);
|
|
139
|
+
|
|
140
|
+
// Verify error screen is shown
|
|
141
|
+
expect(getByTestId("error-screen")).toBeTruthy();
|
|
142
|
+
|
|
143
|
+
// Dismiss the error
|
|
144
|
+
fireEvent.press(getByTestId("dismiss-button"));
|
|
145
|
+
|
|
146
|
+
// Force rerender to see the change
|
|
147
|
+
rerender(
|
|
148
|
+
<ErrorBoundary>
|
|
149
|
+
<TestComponent />
|
|
150
|
+
</ErrorBoundary>
|
|
151
|
+
);
|
|
152
|
+
|
|
153
|
+
// Now the regular content should be visible
|
|
154
|
+
expect(queryByText("all is good")).toBeTruthy();
|
|
155
|
+
});
|
|
156
|
+
});
|
|
157
|
+
|
|
158
|
+
describe("error store functionality", () => {
|
|
159
|
+
it("properly sets error state", () => {
|
|
160
|
+
const error = new Error("Test error");
|
|
161
|
+
const errorInfo = "Test error info";
|
|
162
|
+
|
|
163
|
+
act(() => {
|
|
164
|
+
useErrorStore.getState().setError(error, errorInfo, true);
|
|
165
|
+
});
|
|
166
|
+
|
|
167
|
+
const state = useErrorStore.getState();
|
|
168
|
+
expect(state.hasError).toBe(true);
|
|
169
|
+
expect(state.error).toBe(error);
|
|
170
|
+
expect(state.info).toBe(errorInfo);
|
|
171
|
+
expect(state.recoverable).toBe(true);
|
|
172
|
+
});
|
|
173
|
+
|
|
174
|
+
it("properly resets error state", () => {
|
|
175
|
+
// First set an error
|
|
176
|
+
act(() => {
|
|
177
|
+
useErrorStore
|
|
178
|
+
.getState()
|
|
179
|
+
.setError(new Error("Test error"), "Info", true);
|
|
180
|
+
});
|
|
181
|
+
|
|
182
|
+
// Then dismiss it
|
|
183
|
+
act(() => {
|
|
184
|
+
useErrorStore.getState().dismissError();
|
|
185
|
+
});
|
|
186
|
+
|
|
187
|
+
const state = useErrorStore.getState();
|
|
188
|
+
expect(state.hasError).toBe(false);
|
|
189
|
+
expect(state.error).toBeUndefined();
|
|
190
|
+
expect(state.info).toBeUndefined();
|
|
191
|
+
expect(state.recoverable).toBe(true); // Default value when reset
|
|
192
|
+
});
|
|
193
|
+
|
|
194
|
+
it("remembers recoverable state", () => {
|
|
195
|
+
act(() => {
|
|
196
|
+
useErrorStore
|
|
197
|
+
.getState()
|
|
198
|
+
.setError(new Error("Test error"), "Info", false);
|
|
199
|
+
});
|
|
200
|
+
|
|
201
|
+
const state = useErrorStore.getState();
|
|
202
|
+
expect(state.recoverable).toBe(false);
|
|
203
|
+
});
|
|
204
|
+
});
|
|
205
|
+
|
|
206
|
+
describe("integration with ErrorScreen", () => {
|
|
207
|
+
it("renders restart app button for non-recoverable errors", () => {
|
|
208
|
+
const { getAllByTestId } = render(
|
|
209
|
+
<ErrorBoundary recoverable={false}>
|
|
210
|
+
<TestComponent error />
|
|
211
|
+
</ErrorBoundary>
|
|
212
|
+
);
|
|
213
|
+
|
|
214
|
+
// Should have the restart app button
|
|
215
|
+
const buttons = getAllByTestId(/button-/);
|
|
216
|
+
expect(buttons.length).toBeGreaterThan(0);
|
|
217
|
+
});
|
|
218
|
+
|
|
219
|
+
it("passes error info to the ErrorScreen component", () => {
|
|
220
|
+
const customErrorMessage = "Very specific error message";
|
|
221
|
+
|
|
222
|
+
const { getByTestId } = render(
|
|
223
|
+
<ErrorBoundary>
|
|
224
|
+
<TestComponent error errorMessage={customErrorMessage} />
|
|
225
|
+
</ErrorBoundary>
|
|
226
|
+
);
|
|
227
|
+
|
|
228
|
+
// The error info should be visible in the UI
|
|
229
|
+
expect(getByTestId("error-message")).toBeTruthy();
|
|
230
|
+
});
|
|
231
|
+
});
|
|
232
|
+
});
|
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
import { act, renderHook } from "@testing-library/react-hooks";
|
|
2
|
+
import { useErrorStore } from "../store";
|
|
3
|
+
|
|
4
|
+
describe("Error Store", () => {
|
|
5
|
+
beforeEach(() => {
|
|
6
|
+
act(() => {
|
|
7
|
+
useErrorStore.getState().dismissError();
|
|
8
|
+
});
|
|
9
|
+
});
|
|
10
|
+
|
|
11
|
+
describe("Initial state", () => {
|
|
12
|
+
it("should have correct default values", () => {
|
|
13
|
+
const { result } = renderHook(() => useErrorStore());
|
|
14
|
+
|
|
15
|
+
expect(result.current).toMatchObject({
|
|
16
|
+
hasError: false,
|
|
17
|
+
error: undefined,
|
|
18
|
+
info: undefined,
|
|
19
|
+
recoverable: true,
|
|
20
|
+
setError: expect.any(Function),
|
|
21
|
+
dismissError: expect.any(Function),
|
|
22
|
+
});
|
|
23
|
+
});
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
describe("setError action", () => {
|
|
27
|
+
it("should set error state with provided values", () => {
|
|
28
|
+
const testError = new Error("Test error");
|
|
29
|
+
const testInfo = "Test error info";
|
|
30
|
+
const isRecoverable = true;
|
|
31
|
+
|
|
32
|
+
const { result } = renderHook(() => useErrorStore());
|
|
33
|
+
|
|
34
|
+
act(() => {
|
|
35
|
+
result.current.setError(testError, testInfo, isRecoverable);
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
expect(result.current).toMatchObject({
|
|
39
|
+
hasError: true,
|
|
40
|
+
error: testError,
|
|
41
|
+
info: testInfo,
|
|
42
|
+
recoverable: isRecoverable,
|
|
43
|
+
setError: expect.any(Function),
|
|
44
|
+
dismissError: expect.any(Function),
|
|
45
|
+
});
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
it("should set non-recoverable error when specified", () => {
|
|
49
|
+
const testError = new Error("Test error");
|
|
50
|
+
const testInfo = "Test error info";
|
|
51
|
+
const isRecoverable = false;
|
|
52
|
+
|
|
53
|
+
const { result } = renderHook(() => useErrorStore());
|
|
54
|
+
|
|
55
|
+
act(() => {
|
|
56
|
+
result.current.setError(testError, testInfo, isRecoverable);
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
expect(result.current).toMatchObject({
|
|
60
|
+
recoverable: false,
|
|
61
|
+
});
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
it("should default to recoverable error when not specified", () => {
|
|
65
|
+
const testError = new Error("Test error");
|
|
66
|
+
const testInfo = "Test error info";
|
|
67
|
+
|
|
68
|
+
const { result } = renderHook(() => useErrorStore());
|
|
69
|
+
|
|
70
|
+
act(() => {
|
|
71
|
+
result.current.setError(testError, testInfo);
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
expect(result.current).toMatchObject({
|
|
75
|
+
recoverable: true,
|
|
76
|
+
});
|
|
77
|
+
});
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
describe("dismissError action", () => {
|
|
81
|
+
it("should reset error state to defaults", () => {
|
|
82
|
+
const testError = new Error("Test error");
|
|
83
|
+
const { result } = renderHook(() => useErrorStore());
|
|
84
|
+
|
|
85
|
+
act(() => {
|
|
86
|
+
result.current.setError(testError, "info", false);
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
expect(result.current).toMatchObject({
|
|
90
|
+
hasError: true,
|
|
91
|
+
recoverable: false,
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
act(() => {
|
|
95
|
+
result.current.dismissError();
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
expect(result.current).toMatchObject({
|
|
99
|
+
hasError: false,
|
|
100
|
+
error: undefined,
|
|
101
|
+
info: undefined,
|
|
102
|
+
recoverable: true,
|
|
103
|
+
setError: expect.any(Function),
|
|
104
|
+
dismissError: expect.any(Function),
|
|
105
|
+
});
|
|
106
|
+
});
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
describe("Direct store access via API", () => {
|
|
110
|
+
it("should allow direct access to store outside React components", () => {
|
|
111
|
+
act(() => {
|
|
112
|
+
useErrorStore
|
|
113
|
+
.getState()
|
|
114
|
+
.setError(new Error("API error"), "API info", false);
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
const { result } = renderHook(() => useErrorStore());
|
|
118
|
+
|
|
119
|
+
expect(result.current).toMatchObject({
|
|
120
|
+
hasError: true,
|
|
121
|
+
error: expect.objectContaining({ message: "API error" }),
|
|
122
|
+
info: "API info",
|
|
123
|
+
recoverable: false,
|
|
124
|
+
});
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
it("should allow clearing errors via API", () => {
|
|
128
|
+
act(() => {
|
|
129
|
+
useErrorStore.getState().setError(new Error("API error"), "API info");
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
act(() => {
|
|
133
|
+
useErrorStore.getState().dismissError();
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
const { result } = renderHook(() => useErrorStore());
|
|
137
|
+
|
|
138
|
+
expect(result.current).toMatchObject({
|
|
139
|
+
hasError: false,
|
|
140
|
+
});
|
|
141
|
+
});
|
|
142
|
+
});
|
|
143
|
+
});
|
|
@@ -1,42 +1,66 @@
|
|
|
1
1
|
import * as React from "react";
|
|
2
|
+
import pick from "lodash/pick";
|
|
2
3
|
|
|
3
4
|
import { coreLogger } from "@applicaster/zapp-react-native-utils/logger";
|
|
4
5
|
import { ErrorScreen } from "@applicaster/zapp-react-native-ui-components/Components/ErrorScreen";
|
|
5
6
|
|
|
7
|
+
import { useErrorStore } from "./store";
|
|
8
|
+
import * as QuickBrickManager from "@applicaster/zapp-react-native-bridge/QuickBrick";
|
|
9
|
+
|
|
6
10
|
const errorBoundaryLogger = coreLogger.addSubsystem("react_error-boundary");
|
|
7
11
|
|
|
8
12
|
type Props = {
|
|
9
13
|
children: React.ReactNode;
|
|
14
|
+
recoverable?: boolean;
|
|
10
15
|
};
|
|
11
16
|
|
|
12
|
-
type
|
|
13
|
-
|
|
14
|
-
error?: Error;
|
|
15
|
-
info?: string;
|
|
17
|
+
type ErrorBoundaryContentProps = {
|
|
18
|
+
children: React.ReactNode;
|
|
16
19
|
};
|
|
17
20
|
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
21
|
+
const restartApp = () => {
|
|
22
|
+
QuickBrickManager.sendQuickBrickEvent(
|
|
23
|
+
QuickBrickManager.QUICK_BRICK_EVENTS.FORCE_APP_RELOAD
|
|
24
|
+
);
|
|
25
|
+
};
|
|
23
26
|
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
+
const restartButton = {
|
|
28
|
+
text: "Restart the app",
|
|
29
|
+
onPress: restartApp,
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
const buttons = [restartButton];
|
|
33
|
+
|
|
34
|
+
const ErrorBoundaryContent: React.FC<ErrorBoundaryContentProps> = ({
|
|
35
|
+
children,
|
|
36
|
+
}) => {
|
|
37
|
+
const { hasError, dismissError, info } = pick(
|
|
38
|
+
useErrorStore((state) => state),
|
|
39
|
+
["hasError", "dismissError", "info"]
|
|
40
|
+
);
|
|
41
|
+
|
|
42
|
+
if (hasError) {
|
|
43
|
+
return (
|
|
44
|
+
<ErrorScreen
|
|
45
|
+
dismissError={dismissError}
|
|
46
|
+
message={info}
|
|
47
|
+
buttons={buttons}
|
|
48
|
+
/>
|
|
49
|
+
);
|
|
27
50
|
}
|
|
28
51
|
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
};
|
|
52
|
+
return <>{children}</>;
|
|
53
|
+
};
|
|
32
54
|
|
|
33
|
-
|
|
34
|
-
|
|
55
|
+
export class ErrorBoundary extends React.Component<Props> {
|
|
56
|
+
componentDidCatch(error, info) {
|
|
57
|
+
errorBoundaryLogger.error({ error, data: info, message: error.message });
|
|
58
|
+
const { recoverable = true } = this.props;
|
|
35
59
|
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
}
|
|
60
|
+
useErrorStore.getState().setError(error, info, recoverable);
|
|
61
|
+
}
|
|
39
62
|
|
|
40
|
-
|
|
63
|
+
render() {
|
|
64
|
+
return <ErrorBoundaryContent>{this.props.children}</ErrorBoundaryContent>;
|
|
41
65
|
}
|
|
42
66
|
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { create } from "zustand";
|
|
2
|
+
|
|
3
|
+
type ErrorState = {
|
|
4
|
+
hasError: boolean;
|
|
5
|
+
error?: Error;
|
|
6
|
+
info?: string;
|
|
7
|
+
recoverable: boolean;
|
|
8
|
+
setError: (error: Error, info: string, recoverable?: boolean) => void;
|
|
9
|
+
dismissError: () => void;
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
export const useErrorStore = create<ErrorState>((set) => ({
|
|
13
|
+
hasError: false,
|
|
14
|
+
error: undefined,
|
|
15
|
+
info: undefined,
|
|
16
|
+
recoverable: true,
|
|
17
|
+
setError: (error: Error, info: string, recoverable = true) =>
|
|
18
|
+
set({ hasError: true, error, info, recoverable }),
|
|
19
|
+
dismissError: () =>
|
|
20
|
+
set({
|
|
21
|
+
hasError: false,
|
|
22
|
+
error: undefined,
|
|
23
|
+
info: undefined,
|
|
24
|
+
recoverable: true,
|
|
25
|
+
}),
|
|
26
|
+
}));
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { BehaviorSubject } from "rxjs";
|
|
2
|
+
|
|
3
|
+
class LoadingOverlayManager {
|
|
4
|
+
visibilitySubject: BehaviorSubject<boolean>;
|
|
5
|
+
|
|
6
|
+
constructor() {
|
|
7
|
+
this.visibilitySubject = new BehaviorSubject(false);
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
show() {
|
|
11
|
+
this.visibilitySubject.next(true);
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
hide() {
|
|
15
|
+
this.visibilitySubject.next(false);
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
onVisibilityStateChange(cb: (value: boolean) => void) {
|
|
19
|
+
return this.visibilitySubject.subscribe(cb);
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
get visibility(): boolean {
|
|
23
|
+
return this.visibilitySubject.value;
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export const loadingOverlayManager = new LoadingOverlayManager();
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import * as React from "react";
|
|
2
|
+
import { View, StyleSheet } from "react-native";
|
|
3
|
+
import { focusManager } from "@applicaster/zapp-react-native-utils/appUtils/focusManager";
|
|
4
|
+
import { isVizioPlatform } from "@applicaster/zapp-react-native-utils/reactUtils";
|
|
5
|
+
import { Spinner } from "@applicaster/zapp-react-native-ui-components/Components/Spinner";
|
|
6
|
+
import { useTheme } from "@applicaster/zapp-react-native-utils/theme";
|
|
7
|
+
|
|
8
|
+
import { useLoadingOverlayVisibility } from "./useLoadingOverlayVisibility";
|
|
9
|
+
|
|
10
|
+
export { loadingOverlayManager } from "./LoadingOverlayManager";
|
|
11
|
+
|
|
12
|
+
const styles = StyleSheet.create({
|
|
13
|
+
container: {
|
|
14
|
+
opacity: 0.5,
|
|
15
|
+
alignItems: "center",
|
|
16
|
+
justifyContent: "center",
|
|
17
|
+
zIndex: 100,
|
|
18
|
+
flex: 1,
|
|
19
|
+
},
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
const LoadingOverlayComponent = () => {
|
|
23
|
+
const visible = useLoadingOverlayVisibility();
|
|
24
|
+
const theme = useTheme();
|
|
25
|
+
|
|
26
|
+
const backgroundColor = React.useMemo(() => {
|
|
27
|
+
return theme?.app_background_color || "black";
|
|
28
|
+
}, [theme?.app_background_color]);
|
|
29
|
+
|
|
30
|
+
React.useEffect(() => {
|
|
31
|
+
if (visible) {
|
|
32
|
+
focusManager.disableFocus();
|
|
33
|
+
} else {
|
|
34
|
+
focusManager.enableFocus();
|
|
35
|
+
}
|
|
36
|
+
}, [visible]);
|
|
37
|
+
|
|
38
|
+
return visible ? (
|
|
39
|
+
<View
|
|
40
|
+
style={[StyleSheet.absoluteFill, styles.container, { backgroundColor }]}
|
|
41
|
+
>
|
|
42
|
+
<Spinner />
|
|
43
|
+
</View>
|
|
44
|
+
) : null;
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
export const LoadingOverlay = isVizioPlatform()
|
|
48
|
+
? LoadingOverlayComponent
|
|
49
|
+
: () => null;
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import * as React from "react";
|
|
2
|
+
import { loadingOverlayManager } from "./LoadingOverlayManager";
|
|
3
|
+
|
|
4
|
+
export const useLoadingOverlayVisibility = () => {
|
|
5
|
+
const [visible, setVisible] = React.useState(
|
|
6
|
+
loadingOverlayManager.visibility
|
|
7
|
+
);
|
|
8
|
+
|
|
9
|
+
React.useEffect(() => {
|
|
10
|
+
const listener = loadingOverlayManager.onVisibilityStateChange(setVisible);
|
|
11
|
+
|
|
12
|
+
return () => {
|
|
13
|
+
listener.unsubscribe();
|
|
14
|
+
};
|
|
15
|
+
}, []);
|
|
16
|
+
|
|
17
|
+
return visible;
|
|
18
|
+
};
|
|
@@ -4,7 +4,7 @@ import { platformSelect } from "@applicaster/zapp-react-native-utils/reactUtils"
|
|
|
4
4
|
import { useSafeAreaInsets } from "react-native-safe-area-context";
|
|
5
5
|
|
|
6
6
|
import React, { useCallback, useEffect, useMemo, useState } from "react";
|
|
7
|
-
import {
|
|
7
|
+
import { Keyboard, StyleSheet, View } from "react-native";
|
|
8
8
|
|
|
9
9
|
import { Overlay } from "./Overlay";
|
|
10
10
|
import { ModalBottomSheetFrame } from "./ModalBottomSheetFrame";
|
|
@@ -83,6 +83,8 @@ export function ModalBottomSheet(props: Props) {
|
|
|
83
83
|
[isTablet]
|
|
84
84
|
);
|
|
85
85
|
|
|
86
|
+
const modalVisible = visible && sheetHeight > 0;
|
|
87
|
+
|
|
86
88
|
return (
|
|
87
89
|
<View style={styles.container}>
|
|
88
90
|
<Overlay
|
|
@@ -98,7 +100,7 @@ export function ModalBottomSheet(props: Props) {
|
|
|
98
100
|
paddingBottom={bottomOffset}
|
|
99
101
|
maxWidth={maxWidth}
|
|
100
102
|
dimensions={dimensions}
|
|
101
|
-
visible={
|
|
103
|
+
visible={modalVisible}
|
|
102
104
|
closeToast={dismiss}
|
|
103
105
|
handleProps={{
|
|
104
106
|
enabled: sheetHandleEnabled,
|
|
@@ -35,13 +35,12 @@ export function ModalProvider() {
|
|
|
35
35
|
|
|
36
36
|
const pathname = `${ROUTE_TYPES.MODAL}/${modalScreenProps?.screenId}`;
|
|
37
37
|
|
|
38
|
+
const shouldShowModal = modalVisible && modalState?.visible;
|
|
39
|
+
|
|
38
40
|
return (
|
|
39
41
|
<PathnameContext.Provider value={pathname}>
|
|
40
42
|
<ScreenContextProvider pathname={pathname}>
|
|
41
|
-
<ModalPresenter
|
|
42
|
-
visible={modalVisible && modalState?.visible}
|
|
43
|
-
{...modalState?.options}
|
|
44
|
-
>
|
|
43
|
+
<ModalPresenter visible={shouldShowModal} {...modalState?.options}>
|
|
45
44
|
<ModalChildrenWrapper>
|
|
46
45
|
<ModalContent modalScreenProps={modalScreenProps} />
|
|
47
46
|
</ModalChildrenWrapper>
|