@bigbinary/neeto-commons-rn 2.0.3 → 2.1.0
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/align-deps-preset.js +84 -0
- package/lib/typescript/api/app_version.d.ts.map +1 -1
- package/lib/typescript/components/AppVersion.d.ts.map +1 -1
- package/lib/typescript/components/DevToolsBridge.d.ts +8 -0
- package/lib/typescript/components/DevToolsBridge.d.ts.map +1 -0
- package/lib/typescript/components/Header.d.ts.map +1 -1
- package/lib/typescript/components/ToggleSwitch.d.ts.map +1 -1
- package/lib/typescript/components/constants.d.ts +1 -0
- package/lib/typescript/components/constants.d.ts.map +1 -1
- package/lib/typescript/config/axios/constants.d.ts +1 -0
- package/lib/typescript/config/axios/constants.d.ts.map +1 -1
- package/lib/typescript/config/axios/groupingKey.d.ts +12 -0
- package/lib/typescript/config/axios/groupingKey.d.ts.map +1 -0
- package/lib/typescript/config/axios/index.d.ts.map +1 -1
- package/lib/typescript/config/bugsnag.d.ts +1 -1
- package/lib/typescript/config/bugsnag.d.ts.map +1 -1
- package/lib/typescript/constants/index.d.ts +10 -0
- package/lib/typescript/constants/index.d.ts.map +1 -1
- package/lib/typescript/helpers/notifications/firebaseMessaging.android.d.ts +10 -0
- package/lib/typescript/helpers/notifications/firebaseMessaging.android.d.ts.map +1 -0
- package/lib/typescript/helpers/notifications/firebaseMessaging.d.ts +24 -0
- package/lib/typescript/helpers/notifications/firebaseMessaging.d.ts.map +1 -0
- package/lib/typescript/helpers/notifications/getDeviceToken.d.ts.map +1 -1
- package/lib/typescript/helpers/notifications/index.d.ts +1 -0
- package/lib/typescript/helpers/notifications/index.d.ts.map +1 -1
- package/lib/typescript/helpers/scale.d.ts +10 -0
- package/lib/typescript/helpers/scale.d.ts.map +1 -1
- package/lib/typescript/helpers/utils.d.ts.map +1 -1
- package/lib/typescript/hooks/query/useAppVersionApi.d.ts.map +1 -1
- package/lib/typescript/hooks/query/useRolesApi.d.ts.map +1 -1
- package/lib/typescript/providers/NeetoCommonsRNProvider.d.ts +8 -1
- package/lib/typescript/providers/NeetoCommonsRNProvider.d.ts.map +1 -1
- package/lib/typescript/screens/authentication/LoginScreen.d.ts.map +1 -1
- package/lib/typescript/screens/authentication/OTPVerificationScreen.d.ts.map +1 -1
- package/lib/typescript/screens/authentication/Organization.d.ts.map +1 -1
- package/lib/typescript/screens/authentication/SubDomainScreen.d.ts.map +1 -1
- package/lib/typescript/screens/profile/ProfileScreen.d.ts.map +1 -1
- package/lib/typescript/screens/team_members/ProductSelect.d.ts.map +1 -1
- package/lib/typescript/screens/team_members/index.d.ts.map +1 -1
- package/lib/typescript/screens/team_members/utils.d.ts.map +1 -1
- package/lib/typescript/store/index.d.ts +3 -2
- package/lib/typescript/store/index.d.ts.map +1 -1
- package/lib/typescript/utils/apiHelpers.d.ts.map +1 -1
- package/metro-devtools.js +48 -0
- package/package.json +485 -16
- package/src/api/app_version.ts +42 -2
- package/src/components/AppVersion.ts +46 -30
- package/src/components/DevToolsBridge.ts +40 -0
- package/src/components/Header.tsx +3 -1
- package/src/components/NotificationIcon.tsx +1 -1
- package/src/components/ToggleSwitch.tsx +2 -1
- package/src/components/WorkspaceSwitcherPane.tsx +1 -1
- package/src/components/constants.ts +4 -0
- package/src/config/axios/constants.ts +3 -0
- package/src/config/axios/groupingKey.ts +25 -0
- package/src/config/axios/index.ts +7 -4
- package/src/config/bugsnag.ts +11 -1
- package/src/constants/index.ts +13 -0
- package/src/contexts/localization.tsx +1 -1
- package/src/helpers/notifications/firebaseMessaging.android.ts +21 -0
- package/src/helpers/notifications/firebaseMessaging.ts +37 -0
- package/src/helpers/notifications/getDeviceToken.ts +1 -2
- package/src/helpers/notifications/index.ts +1 -0
- package/src/helpers/notifications/registerBackgroundHandlers.ts +1 -1
- package/src/helpers/notifications/usePushNotification.ts +1 -1
- package/src/helpers/scale.ts +21 -3
- package/src/helpers/utils.ts +2 -1
- package/src/hooks/query/useAppVersionApi.ts +9 -0
- package/src/hooks/query/useRolesApi.ts +3 -1
- package/src/providers/NeetoCommonsRNProvider.tsx +23 -0
- package/src/screens/authentication/IncompleteOnboardingInfo.tsx +2 -2
- package/src/screens/authentication/LoginScreen.tsx +50 -39
- package/src/screens/authentication/OTPVerificationScreen.tsx +17 -41
- package/src/screens/authentication/Organization.tsx +2 -6
- package/src/screens/authentication/SubDomainScreen.tsx +38 -70
- package/src/screens/profile/ProfileScreen.tsx +2 -1
- package/src/screens/team_members/Card.tsx +1 -1
- package/src/screens/team_members/ProductSelect.tsx +6 -7
- package/src/screens/team_members/index.tsx +2 -1
- package/src/screens/team_members/utils.ts +3 -1
- package/src/store/index.ts +68 -1
- package/src/utils/apiHelpers.ts +4 -1
package/src/api/app_version.ts
CHANGED
|
@@ -1,6 +1,12 @@
|
|
|
1
|
+
import { type NotifiableError } from "@bugsnag/react-native";
|
|
1
2
|
import axios from "axios";
|
|
2
3
|
import Config from "react-native-config";
|
|
3
4
|
|
|
5
|
+
import { bugsnagErrorHandler } from "../config/bugsnag";
|
|
6
|
+
import {
|
|
7
|
+
APP_VERSION_ERROR_GROUPING_HASH,
|
|
8
|
+
APP_VERSION_ERROR_REPORT_INTERVAL,
|
|
9
|
+
} from "../constants";
|
|
4
10
|
import { Store } from "../store";
|
|
5
11
|
import { appVersionsUrl } from "../utils/apiHelpers";
|
|
6
12
|
|
|
@@ -10,13 +16,47 @@ export interface AppVersionInfo {
|
|
|
10
16
|
[key: string]: unknown;
|
|
11
17
|
}
|
|
12
18
|
|
|
13
|
-
|
|
19
|
+
// The version check deliberately does not use the app's axios instance: those
|
|
20
|
+
// interceptors attach auth headers, convert cases and show the user a toast,
|
|
21
|
+
// which an advisory background poll has no business doing. The timeout keeps a
|
|
22
|
+
// stalled origin from holding the request open indefinitely.
|
|
23
|
+
const client = axios.create({ timeout: 8000 });
|
|
24
|
+
|
|
25
|
+
let lastFailureReportedAt = 0;
|
|
26
|
+
|
|
27
|
+
// Reported, but rationed: the endpoint is hit on every launch by every install,
|
|
28
|
+
// so an outage would otherwise arrive as thousands of identical events. One
|
|
29
|
+
// event per hour per session, all under a single grouping hash. Offline devices
|
|
30
|
+
// are dropped by bugsnagErrorHandler's own network-error check.
|
|
31
|
+
const reportFailure = (error: unknown) => {
|
|
32
|
+
const now = Date.now();
|
|
33
|
+
|
|
34
|
+
if (now - lastFailureReportedAt < APP_VERSION_ERROR_REPORT_INTERVAL) return;
|
|
35
|
+
|
|
36
|
+
lastFailureReportedAt = now;
|
|
37
|
+
|
|
38
|
+
bugsnagErrorHandler(
|
|
39
|
+
error as NotifiableError,
|
|
40
|
+
"app_version_check_failed",
|
|
41
|
+
APP_VERSION_ERROR_GROUPING_HASH
|
|
42
|
+
);
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
const fetch = async () => {
|
|
14
46
|
// `String` keeps the pre-TypeScript behaviour for an unset appName, where
|
|
15
47
|
// template interpolation produced the literal "null" in the URL.
|
|
16
48
|
const appName = String(Store.getState().auth.appName);
|
|
17
49
|
const environment = Config.ENVIRONMENT === "staging" ? "net" : "com";
|
|
18
50
|
|
|
19
|
-
|
|
51
|
+
try {
|
|
52
|
+
return await client.get<AppVersionInfo>(
|
|
53
|
+
appVersionsUrl({ appName, environment })
|
|
54
|
+
);
|
|
55
|
+
} catch (error) {
|
|
56
|
+
reportFailure(error);
|
|
57
|
+
|
|
58
|
+
throw error;
|
|
59
|
+
}
|
|
20
60
|
};
|
|
21
61
|
|
|
22
62
|
const appVersionApi = { fetch };
|
|
@@ -2,12 +2,13 @@ import { useEffect, useState } from "react";
|
|
|
2
2
|
import { AppState, Linking, Platform } from "react-native";
|
|
3
3
|
|
|
4
4
|
import { Alert } from "@bigbinary/neetoui-rn";
|
|
5
|
-
import { compare } from "compare-versions";
|
|
5
|
+
import { compare, validate } from "compare-versions";
|
|
6
6
|
import Config from "react-native-config";
|
|
7
7
|
import { getVersion } from "react-native-device-info";
|
|
8
8
|
|
|
9
9
|
import {
|
|
10
10
|
ANDROID_APP_LINK,
|
|
11
|
+
APP_VERSION_RECHECK_INTERVAL,
|
|
11
12
|
IOS_PROD_APP_LINK,
|
|
12
13
|
IOS_STAGING_APP_LINK,
|
|
13
14
|
} from "./constants";
|
|
@@ -36,7 +37,11 @@ export const AppVersion = () => {
|
|
|
36
37
|
|
|
37
38
|
const appName = Store.getState().auth.appName;
|
|
38
39
|
|
|
39
|
-
const {
|
|
40
|
+
const {
|
|
41
|
+
data: appVersionData,
|
|
42
|
+
refetch,
|
|
43
|
+
dataUpdatedAt,
|
|
44
|
+
} = useFetchAppVersionApi();
|
|
40
45
|
const data = (appVersionData ?? {}) as AppVersionPayload;
|
|
41
46
|
|
|
42
47
|
const iOSAppLink =
|
|
@@ -59,31 +64,36 @@ export const AppVersion = () => {
|
|
|
59
64
|
}
|
|
60
65
|
};
|
|
61
66
|
|
|
67
|
+
// The endpoint is advisory. A failure or a malformed payload must leave the
|
|
68
|
+
// app fully usable and show nothing at all, so anything that is not a pair
|
|
69
|
+
// of comparable versions is dropped without a prompt — `compare` throws on
|
|
70
|
+
// invalid semver, and this runs from an effect where a throw would take the
|
|
71
|
+
// screen down with it.
|
|
62
72
|
const compareVersions = () => {
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
73
|
+
const isAndroid = Platform.OS === "android";
|
|
74
|
+
|
|
75
|
+
const minimumSupportedVersion = isAndroid
|
|
76
|
+
? data.android_minimum_supported_version
|
|
77
|
+
: data.ios_minimum_supported_version;
|
|
78
|
+
|
|
79
|
+
const currentVersion = isAndroid
|
|
80
|
+
? data.android_current_version
|
|
81
|
+
: data.ios_current_version;
|
|
82
|
+
|
|
83
|
+
const installedVersion = getVersion();
|
|
84
|
+
|
|
85
|
+
const isComparable = [
|
|
86
|
+
installedVersion,
|
|
87
|
+
minimumSupportedVersion,
|
|
88
|
+
currentVersion,
|
|
89
|
+
].every(version => !!version && validate(version));
|
|
90
|
+
|
|
91
|
+
if (!isComparable) return;
|
|
92
|
+
|
|
93
|
+
if (compare(installedVersion, minimumSupportedVersion as string, "<")) {
|
|
94
|
+
setIsMajorUpdate(true);
|
|
95
|
+
} else if (compare(installedVersion, currentVersion as string, "<")) {
|
|
96
|
+
setIsMinorUpdate(true);
|
|
87
97
|
}
|
|
88
98
|
};
|
|
89
99
|
|
|
@@ -142,13 +152,19 @@ export const AppVersion = () => {
|
|
|
142
152
|
|
|
143
153
|
useEffect(() => {
|
|
144
154
|
const subscription = AppState.addEventListener("change", state => {
|
|
145
|
-
if (state
|
|
146
|
-
|
|
147
|
-
|
|
155
|
+
if (state !== "active") return;
|
|
156
|
+
|
|
157
|
+
// `refetch` ignores staleTime, so an agent flipping between apps used to
|
|
158
|
+
// re-poll app_versions on every single foreground. `dataUpdatedAt` is 0
|
|
159
|
+
// until the first response lands, which still lets that first check run.
|
|
160
|
+
const isCheckDue =
|
|
161
|
+
Date.now() - dataUpdatedAt >= APP_VERSION_RECHECK_INTERVAL;
|
|
162
|
+
|
|
163
|
+
if (isCheckDue) refetch();
|
|
148
164
|
});
|
|
149
165
|
|
|
150
166
|
return () => subscription.remove();
|
|
151
|
-
}, []);
|
|
167
|
+
}, [dataUpdatedAt, refetch]);
|
|
152
168
|
|
|
153
169
|
return null;
|
|
154
170
|
};
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import { createElement } from "react";
|
|
2
|
+
|
|
3
|
+
import AsyncStorage from "@react-native-async-storage/async-storage";
|
|
4
|
+
import { useNetworkActivityDevTools } from "@rozenite/network-activity-plugin";
|
|
5
|
+
import {
|
|
6
|
+
createAsyncStorageAdapter,
|
|
7
|
+
useRozeniteStoragePlugin,
|
|
8
|
+
} from "@rozenite/storage-plugin";
|
|
9
|
+
import { useTanStackQueryDevTools } from "@rozenite/tanstack-query-plugin";
|
|
10
|
+
import { type QueryClient } from "@tanstack/react-query";
|
|
11
|
+
|
|
12
|
+
// The TanStack hook cannot run without a client, and hooks cannot be called
|
|
13
|
+
// conditionally — so the query panel lives in its own component that only
|
|
14
|
+
// mounts when the host passed its client.
|
|
15
|
+
const TanStackQueryDevTools = ({
|
|
16
|
+
queryClient,
|
|
17
|
+
}: {
|
|
18
|
+
queryClient: QueryClient;
|
|
19
|
+
}) => {
|
|
20
|
+
useTanStackQueryDevTools(queryClient);
|
|
21
|
+
|
|
22
|
+
return null;
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
// Null-rendering host for the Rozenite device-side hooks. This module must
|
|
26
|
+
// only ever be required from inside an `if (__DEV__)` block (see
|
|
27
|
+
// NeetoCommonsRNProvider) so metro's dead-code elimination keeps it — and
|
|
28
|
+
// the plugin packages — out of release bundles.
|
|
29
|
+
const DevToolsBridge = ({ queryClient }: { queryClient?: QueryClient }) => {
|
|
30
|
+
useNetworkActivityDevTools();
|
|
31
|
+
useRozeniteStoragePlugin({
|
|
32
|
+
storages: [createAsyncStorageAdapter({ storage: AsyncStorage })],
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
return queryClient
|
|
36
|
+
? createElement(TanStackQueryDevTools, { queryClient })
|
|
37
|
+
: null;
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
export default DevToolsBridge;
|
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import React, { useState, type ComponentType } from "react";
|
|
2
2
|
|
|
3
|
-
import
|
|
3
|
+
import HamburgerMenu from "@bigbinary/neeto-icons-rn/icons/HamburgerMenu";
|
|
4
|
+
import MenuVertical from "@bigbinary/neeto-icons-rn/icons/MenuVertical";
|
|
5
|
+
import Search from "@bigbinary/neeto-icons-rn/icons/Search";
|
|
4
6
|
import {
|
|
5
7
|
Avatar,
|
|
6
8
|
BottomSheetMenu,
|
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import React, { useEffect, useRef } from "react";
|
|
2
2
|
import { Animated, StyleSheet } from "react-native";
|
|
3
3
|
|
|
4
|
-
import
|
|
4
|
+
import Check from "@bigbinary/neeto-icons-rn/icons/Check";
|
|
5
|
+
import Close from "@bigbinary/neeto-icons-rn/icons/Close";
|
|
5
6
|
import { Container, Touchable, lightTheme } from "@bigbinary/neetoui-rn";
|
|
6
7
|
|
|
7
8
|
import { TOGGLE_ANIMATION_DURATION, TOGGLE_KNOB_TRAVEL } from "./constants";
|
|
@@ -8,6 +8,10 @@ export const IOS_PROD_APP_LINK = `https://apps.apple.com/us/app/${Config.IOS_APP
|
|
|
8
8
|
|
|
9
9
|
export const IOS_STAGING_APP_LINK = `https://testflight.apple.com/v1/app/${Config.IOS_APP_ID}`;
|
|
10
10
|
|
|
11
|
+
// Foreground re-checks of app_versions are throttled to this window. A
|
|
12
|
+
// published version moves at release cadence, not at app-switch cadence.
|
|
13
|
+
export const APP_VERSION_RECHECK_INTERVAL = 30 * 60 * 1000;
|
|
14
|
+
|
|
11
15
|
// ToggleSwitch knob travel and animation.
|
|
12
16
|
export const TOGGLE_KNOB_TRAVEL = moderateScale(24);
|
|
13
17
|
export const TOGGLE_ANIMATION_DURATION = 300;
|
|
@@ -4,6 +4,9 @@ import { type CaseConversionOptions } from "./types";
|
|
|
4
4
|
|
|
5
5
|
export const DEFAULT_CASE_CONVERSION = Config.ENABLE_CASE_CONVERSION === "true";
|
|
6
6
|
|
|
7
|
+
export const UUID_SEGMENT =
|
|
8
|
+
/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
|
|
9
|
+
|
|
7
10
|
export const shouldEnableCaseConversion = (
|
|
8
11
|
options: CaseConversionOptions = {}
|
|
9
12
|
) => {
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { UUID_SEGMENT } from "./constants";
|
|
2
|
+
|
|
3
|
+
const isIdSegment = (segment: string) =>
|
|
4
|
+
/^\d+$/.test(segment) || UUID_SEGMENT.test(segment);
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Collapses a request URL to its route shape for Bugsnag grouping: ids become
|
|
8
|
+
* `:id` and the query string is dropped.
|
|
9
|
+
*
|
|
10
|
+
* A 403 on ticket 2732 and a 403 on ticket 2698 are one bug, but grouping on
|
|
11
|
+
* the raw URL files them as separate errors — and a list endpoint whose filters
|
|
12
|
+
* ride in the query string ends up naming its group with ~900 characters of
|
|
13
|
+
* encoded params. The exact URL is still reported under
|
|
14
|
+
* `axios_error_details.requestUrl`, so nothing is lost by grouping on the shape.
|
|
15
|
+
*/
|
|
16
|
+
export const normalizeUrlForGrouping = (url?: string) => {
|
|
17
|
+
if (!url) return "";
|
|
18
|
+
|
|
19
|
+
const [path] = url.split("?");
|
|
20
|
+
|
|
21
|
+
return path
|
|
22
|
+
.split("/")
|
|
23
|
+
.map(segment => (isIdSegment(segment) ? ":id" : segment))
|
|
24
|
+
.join("/");
|
|
25
|
+
};
|
|
@@ -11,6 +11,7 @@ import Config from "react-native-config";
|
|
|
11
11
|
import DeviceInfo from "react-native-device-info";
|
|
12
12
|
|
|
13
13
|
import { convertRequestKeys, convertResponseKeys } from "./caseConversion";
|
|
14
|
+
import { normalizeUrlForGrouping } from "./groupingKey";
|
|
14
15
|
import { handleNetworkError } from "./networkErrorHandler";
|
|
15
16
|
import setParamsSerializer from "./paramsSerializer";
|
|
16
17
|
|
|
@@ -203,10 +204,12 @@ const registerResponseIntercept = () => {
|
|
|
203
204
|
responseData: filterSensitiveData(response?.data),
|
|
204
205
|
});
|
|
205
206
|
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
207
|
+
// Group by route shape, not by the resource that happened to
|
|
208
|
+
// fail — the raw URL is above in `requestUrl`.
|
|
209
|
+
const route = normalizeUrlForGrouping(config?.url);
|
|
210
|
+
|
|
211
|
+
report.context = `Axios Request Failed: ${config?.method?.toUpperCase()} ${route}`;
|
|
212
|
+
report.groupingHash = `axios-${config?.method}-${route}`;
|
|
210
213
|
});
|
|
211
214
|
} catch (bugsnagError) {
|
|
212
215
|
// eslint-disable-next-line no-console
|
package/src/config/bugsnag.ts
CHANGED
|
@@ -8,7 +8,13 @@ import {
|
|
|
8
8
|
|
|
9
9
|
import { Store } from "../store";
|
|
10
10
|
|
|
11
|
-
export const bugsnagErrorHandler = (
|
|
11
|
+
export const bugsnagErrorHandler = (
|
|
12
|
+
err: NotifiableError,
|
|
13
|
+
origin: string,
|
|
14
|
+
// Pass a hash for failures that would otherwise fragment into a group per
|
|
15
|
+
// resource or per status code.
|
|
16
|
+
groupingHash?: string
|
|
17
|
+
) => {
|
|
12
18
|
if (!__DEV__) {
|
|
13
19
|
Bugsnag.notify(err, event => {
|
|
14
20
|
if (isNetworkError(err)) {
|
|
@@ -18,6 +24,10 @@ export const bugsnagErrorHandler = (err: NotifiableError, origin: string) => {
|
|
|
18
24
|
event.severity = "warning";
|
|
19
25
|
event.context = origin;
|
|
20
26
|
|
|
27
|
+
if (groupingHash) {
|
|
28
|
+
event.groupingHash = groupingHash;
|
|
29
|
+
}
|
|
30
|
+
|
|
21
31
|
return true;
|
|
22
32
|
});
|
|
23
33
|
}
|
package/src/constants/index.ts
CHANGED
|
@@ -7,3 +7,16 @@ export const STORAGE_KEYS = {
|
|
|
7
7
|
} as const;
|
|
8
8
|
|
|
9
9
|
export const DEFAULT_PAGE_SIZE = 30;
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* A failing app_versions endpoint affects every launch of every install, so it
|
|
13
|
+
* is reported at most once per window per session — enough to see the outage
|
|
14
|
+
* and its trend, not enough to bury the rest of the project.
|
|
15
|
+
*/
|
|
16
|
+
export const APP_VERSION_ERROR_REPORT_INTERVAL = 60 * 60 * 1000;
|
|
17
|
+
|
|
18
|
+
/** One Bugsnag group for every app_versions failure, whatever the status. */
|
|
19
|
+
export const APP_VERSION_ERROR_GROUPING_HASH = "app-version-check-failed";
|
|
20
|
+
|
|
21
|
+
/** Design baseline: a ~5" phone, the width every scaled size was drawn against. */
|
|
22
|
+
export const GUIDELINE_BASE_WIDTH = 350;
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import firebaseMessaging, {
|
|
2
|
+
getInitialNotification,
|
|
3
|
+
getMessaging,
|
|
4
|
+
type FirebaseMessagingTypes,
|
|
5
|
+
} from "@react-native-firebase/messaging";
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Android build of the Firebase seam — the only module in the package that
|
|
9
|
+
* names @react-native-firebase. See firebaseMessaging.ts for why the import
|
|
10
|
+
* cannot simply sit behind a platform check.
|
|
11
|
+
*/
|
|
12
|
+
// Annotated rather than inferred: declaration emit cannot name the inferred
|
|
13
|
+
// type without reaching into the package's own dist path.
|
|
14
|
+
export const messaging: typeof firebaseMessaging = firebaseMessaging;
|
|
15
|
+
|
|
16
|
+
/** The notification that launched the app from a killed state, or null. */
|
|
17
|
+
export const getInitialFirebaseNotification =
|
|
18
|
+
(): Promise<FirebaseMessagingTypes.RemoteMessage | null> =>
|
|
19
|
+
// Modular API: the namespaced messaging().getInitialNotification() logs a
|
|
20
|
+
// deprecation warning on every cold start and goes away in the next major.
|
|
21
|
+
getInitialNotification(getMessaging());
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
// Type-only, and it must stay that way: a value import here would defeat the
|
|
2
|
+
// entire module. The statement form says so at a glance.
|
|
3
|
+
import type {
|
|
4
|
+
default as firebaseMessaging,
|
|
5
|
+
FirebaseMessagingTypes,
|
|
6
|
+
} from "@react-native-firebase/messaging";
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* iOS/default build of the Firebase seam. Firebase is linked on Android only
|
|
10
|
+
* (react-native.config.js sets `platforms.ios = null`), so on iOS there is no
|
|
11
|
+
* RNFBAppModule — and @react-native-firebase/app constructs a native event
|
|
12
|
+
* emitter over it at *module scope*:
|
|
13
|
+
*
|
|
14
|
+
* export default new RNFBNativeEventEmitter();
|
|
15
|
+
*
|
|
16
|
+
* which throws "Native module RNFBAppModule not found" the moment anything
|
|
17
|
+
* imports the package. A `Platform.OS === "android"` guard around the call
|
|
18
|
+
* does not help: the import has already run. Every Firebase import therefore
|
|
19
|
+
* goes through this module, whose .android build is the only one that
|
|
20
|
+
* mentions the package.
|
|
21
|
+
*/
|
|
22
|
+
|
|
23
|
+
/** Never reached on iOS — every caller is behind a platform check. */
|
|
24
|
+
export const messaging = (() => {
|
|
25
|
+
throw new Error(
|
|
26
|
+
"Firebase messaging is Android-only in neeto apps; iOS uses APNs directly."
|
|
27
|
+
);
|
|
28
|
+
}) as unknown as typeof firebaseMessaging;
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* The notification that launched the app from a killed state, or null. iOS
|
|
32
|
+
* has none to report: an APNs alert push is displayed by the OS and arrives
|
|
33
|
+
* through notify-kit, not Firebase.
|
|
34
|
+
*/
|
|
35
|
+
export const getInitialFirebaseNotification =
|
|
36
|
+
(): Promise<FirebaseMessagingTypes.RemoteMessage | null> =>
|
|
37
|
+
Promise.resolve(null);
|
|
@@ -1,8 +1,7 @@
|
|
|
1
1
|
import { Platform } from "react-native";
|
|
2
2
|
|
|
3
|
-
import messaging from "@react-native-firebase/messaging";
|
|
4
|
-
|
|
5
3
|
import { getAPNSToken } from "./apnsToken";
|
|
4
|
+
import { messaging } from "./firebaseMessaging";
|
|
6
5
|
|
|
7
6
|
// iOS keeps the historical APNs-direct backend contract with no Firebase at
|
|
8
7
|
// all — the raw APNs token comes from the NeetoAPNSToken native module.
|
|
@@ -1,3 +1,4 @@
|
|
|
1
1
|
export { usePushNotification } from "./usePushNotification";
|
|
2
2
|
export { registerBackgroundHandlers } from "./registerBackgroundHandlers";
|
|
3
3
|
export { NOTIFICATION_CHANNELS, NOTIFICATION_CHANNEL_IDS } from "./constants";
|
|
4
|
+
export { getInitialFirebaseNotification } from "./firebaseMessaging";
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
import { Platform } from "react-native";
|
|
2
2
|
|
|
3
|
-
import messaging from "@react-native-firebase/messaging";
|
|
4
3
|
import dayjs from "dayjs";
|
|
5
4
|
import notifee, { EventType } from "react-native-notify-kit";
|
|
6
5
|
|
|
6
|
+
import { messaging } from "./firebaseMessaging";
|
|
7
7
|
import { type NotificationPressHandler } from "./types";
|
|
8
8
|
|
|
9
9
|
import { markReadUnReadNotificationAPI } from "../../api/notifications";
|
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
import { useEffect, useCallback, useState } from "react";
|
|
2
2
|
import { Platform } from "react-native";
|
|
3
3
|
|
|
4
|
-
import messaging from "@react-native-firebase/messaging";
|
|
5
4
|
import dayjs from "dayjs";
|
|
6
5
|
import notifee, { EventType } from "react-native-notify-kit";
|
|
7
6
|
import { RESULTS, checkNotifications } from "react-native-permissions";
|
|
8
7
|
|
|
9
8
|
import { NOTIFICATION_CHANNELS } from "./constants";
|
|
9
|
+
import { messaging } from "./firebaseMessaging";
|
|
10
10
|
import { getDeviceToken } from "./getDeviceToken";
|
|
11
11
|
import {
|
|
12
12
|
type NotificationPressHandler,
|
package/src/helpers/scale.ts
CHANGED
|
@@ -1,4 +1,22 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { UnistylesRuntime } from "react-native-unistyles";
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
|
|
3
|
+
import { GUIDELINE_BASE_WIDTH } from "../constants";
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* react-native-size-matters' formula, read off the unistyles runtime instead
|
|
7
|
+
* of a Dimensions value captured at module load — that snapshot never updated
|
|
8
|
+
* for split-screen, foldables or iPad Slide Over. Reading `rt.screen` inside a
|
|
9
|
+
* unistyles StyleSheet also subscribes that sheet to window resizes.
|
|
10
|
+
*
|
|
11
|
+
* The short edge is used, so a size scales the same in either orientation, and
|
|
12
|
+
* it falls back to the baseline before unistyles has measured (its jest mock
|
|
13
|
+
* reports 0x0), leaving sizes untouched.
|
|
14
|
+
*/
|
|
15
|
+
export const moderateScale = (size: number, factor = 0.5) => {
|
|
16
|
+
const { width, height } = UnistylesRuntime.screen;
|
|
17
|
+
const shortDimension = Math.min(width, height) || GUIDELINE_BASE_WIDTH;
|
|
18
|
+
|
|
19
|
+
return Math.round(
|
|
20
|
+
size + ((shortDimension / GUIDELINE_BASE_WIDTH) * size - size) * factor
|
|
21
|
+
);
|
|
22
|
+
};
|
package/src/helpers/utils.ts
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import { Linking } from "react-native";
|
|
2
2
|
|
|
3
3
|
import { isNotPresent } from "@bigbinary/neeto-cist";
|
|
4
|
-
import
|
|
4
|
+
import equals from "ramda/src/equals";
|
|
5
|
+
import intersection from "ramda/src/intersection";
|
|
5
6
|
import Config from "react-native-config";
|
|
6
7
|
|
|
7
8
|
// eslint-disable-next-line no-console
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { useQuery } from "@tanstack/react-query";
|
|
2
|
+
import { type AxiosError } from "axios";
|
|
2
3
|
|
|
3
4
|
import { queryKeys } from "./queryKeys";
|
|
4
5
|
|
|
@@ -9,4 +10,12 @@ export const useFetchAppVersionApi = () =>
|
|
|
9
10
|
queryKey: [queryKeys.APP_VERSION],
|
|
10
11
|
queryFn: appVersionApi.fetch,
|
|
11
12
|
select: ({ data }) => data,
|
|
13
|
+
// An update banner is not worth hammering a struggling endpoint with the
|
|
14
|
+
// default three attempts: one retry, and none at all on a gateway timeout,
|
|
15
|
+
// where the upstream has already reported itself down.
|
|
16
|
+
retry: (failureCount, error) =>
|
|
17
|
+
(error as AxiosError)?.response?.status === 504
|
|
18
|
+
? false
|
|
19
|
+
: failureCount < 1,
|
|
20
|
+
retryDelay: 2000,
|
|
12
21
|
});
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
import { useQuery } from "@tanstack/react-query";
|
|
2
|
-
import
|
|
2
|
+
import groupBy from "ramda/src/groupBy";
|
|
3
|
+
import prop from "ramda/src/prop";
|
|
4
|
+
import reverse from "ramda/src/reverse";
|
|
3
5
|
|
|
4
6
|
import { queryKeys } from "./queryKeys";
|
|
5
7
|
|
|
@@ -6,6 +6,7 @@ import React, {
|
|
|
6
6
|
} from "react";
|
|
7
7
|
|
|
8
8
|
import { type NavigationContainerRef } from "@react-navigation/native";
|
|
9
|
+
import { type QueryClient } from "@tanstack/react-query";
|
|
9
10
|
import { Provider } from "react-redux";
|
|
10
11
|
import { type Reducer } from "redux";
|
|
11
12
|
import { PersistGate } from "redux-persist/integration/react";
|
|
@@ -25,6 +26,20 @@ import { updateRootReducer } from "../store/utils";
|
|
|
25
26
|
|
|
26
27
|
initializeConfigs();
|
|
27
28
|
|
|
29
|
+
// Same module-level __DEV__ gate as the store's Rozenite enhancer: metro
|
|
30
|
+
// inlines __DEV__ and strips the require — and the plugin packages behind
|
|
31
|
+
// it — from release bundles before dependency collection. The try/catch
|
|
32
|
+
// keeps hosts working if the plugins are ever absent.
|
|
33
|
+
let DevToolsBridge: React.ComponentType<{ queryClient?: QueryClient }> | null =
|
|
34
|
+
null;
|
|
35
|
+
if (__DEV__) {
|
|
36
|
+
try {
|
|
37
|
+
DevToolsBridge = require("../components/DevToolsBridge").default;
|
|
38
|
+
} catch {
|
|
39
|
+
DevToolsBridge = null;
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
28
43
|
export interface NeetoCommonsRNProviderProps {
|
|
29
44
|
appName: string;
|
|
30
45
|
hostNavigationRef?: React.RefObject<NavigationContainerRef<
|
|
@@ -33,6 +48,12 @@ export interface NeetoCommonsRNProviderProps {
|
|
|
33
48
|
hostStrings?: HostStrings;
|
|
34
49
|
hostRootReducer: Record<string, Reducer>;
|
|
35
50
|
blacklistedReducers?: string[];
|
|
51
|
+
/**
|
|
52
|
+
* The host app's react-query client. Dev-only: when provided, the
|
|
53
|
+
* Rozenite TanStack Query DevTools panel streams this client's cache.
|
|
54
|
+
* Network and Storage panels work without it.
|
|
55
|
+
*/
|
|
56
|
+
queryClient?: QueryClient;
|
|
36
57
|
children?: ReactNode;
|
|
37
58
|
}
|
|
38
59
|
|
|
@@ -42,6 +63,7 @@ export const NeetoCommonsRNProvider = ({
|
|
|
42
63
|
hostStrings,
|
|
43
64
|
hostRootReducer,
|
|
44
65
|
blacklistedReducers,
|
|
66
|
+
queryClient,
|
|
45
67
|
children,
|
|
46
68
|
}: NeetoCommonsRNProviderProps) => {
|
|
47
69
|
const prevReducersRef = useRef<Record<string, Reducer> | null>(null);
|
|
@@ -69,6 +91,7 @@ export const NeetoCommonsRNProvider = ({
|
|
|
69
91
|
<Provider store={Store}>
|
|
70
92
|
<PersistGate loading={null} persistor={Persistor}>
|
|
71
93
|
<LocalizationProvider hostStrings={hostStrings}>
|
|
94
|
+
{DevToolsBridge ? <DevToolsBridge queryClient={queryClient} /> : null}
|
|
72
95
|
{children}
|
|
73
96
|
</LocalizationProvider>
|
|
74
97
|
</PersistGate>
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import React, { useCallback } from "react";
|
|
2
2
|
import { Linking, StyleSheet } from "react-native";
|
|
3
3
|
|
|
4
|
+
import Left from "@bigbinary/neeto-icons-rn/icons/Left";
|
|
4
5
|
import {
|
|
5
6
|
Container,
|
|
6
7
|
ParentView,
|
|
@@ -9,7 +10,6 @@ import {
|
|
|
9
10
|
lightTheme,
|
|
10
11
|
} from "@bigbinary/neetoui-rn";
|
|
11
12
|
import Config from "react-native-config";
|
|
12
|
-
import Icon from "react-native-remix-icon";
|
|
13
13
|
|
|
14
14
|
import { HyperlinkText } from "../../components/HyperlinkText";
|
|
15
15
|
import { useLocalization } from "../../contexts/localization";
|
|
@@ -64,7 +64,7 @@ export const IncompleteOnboardingInfo = ({
|
|
|
64
64
|
style={styles.backButton}
|
|
65
65
|
onPress={onBackPress}
|
|
66
66
|
>
|
|
67
|
-
<
|
|
67
|
+
<Left size={moderateScale(24)} />
|
|
68
68
|
</Touchable>
|
|
69
69
|
</Container>
|
|
70
70
|
<Container style={styles.body}>
|