@bigbinary/neeto-commons-rn 2.0.3 → 2.2.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.
Files changed (91) hide show
  1. package/align-deps-preset.js +84 -0
  2. package/lib/typescript/api/app_version.d.ts.map +1 -1
  3. package/lib/typescript/components/AppVersion.d.ts.map +1 -1
  4. package/lib/typescript/components/DevToolsBridge.d.ts +8 -0
  5. package/lib/typescript/components/DevToolsBridge.d.ts.map +1 -0
  6. package/lib/typescript/components/Header.d.ts.map +1 -1
  7. package/lib/typescript/components/ToggleSwitch.d.ts.map +1 -1
  8. package/lib/typescript/components/constants.d.ts +1 -0
  9. package/lib/typescript/components/constants.d.ts.map +1 -1
  10. package/lib/typescript/config/axios/constants.d.ts +1 -0
  11. package/lib/typescript/config/axios/constants.d.ts.map +1 -1
  12. package/lib/typescript/config/axios/groupingKey.d.ts +12 -0
  13. package/lib/typescript/config/axios/groupingKey.d.ts.map +1 -0
  14. package/lib/typescript/config/axios/index.d.ts.map +1 -1
  15. package/lib/typescript/config/bugsnag.d.ts +1 -1
  16. package/lib/typescript/config/bugsnag.d.ts.map +1 -1
  17. package/lib/typescript/constants/index.d.ts +10 -0
  18. package/lib/typescript/constants/index.d.ts.map +1 -1
  19. package/lib/typescript/helpers/notifications/firebaseMessaging.android.d.ts +10 -0
  20. package/lib/typescript/helpers/notifications/firebaseMessaging.android.d.ts.map +1 -0
  21. package/lib/typescript/helpers/notifications/firebaseMessaging.d.ts +24 -0
  22. package/lib/typescript/helpers/notifications/firebaseMessaging.d.ts.map +1 -0
  23. package/lib/typescript/helpers/notifications/getDeviceToken.d.ts.map +1 -1
  24. package/lib/typescript/helpers/notifications/index.d.ts +1 -0
  25. package/lib/typescript/helpers/notifications/index.d.ts.map +1 -1
  26. package/lib/typescript/helpers/scale.d.ts +10 -0
  27. package/lib/typescript/helpers/scale.d.ts.map +1 -1
  28. package/lib/typescript/helpers/utils.d.ts.map +1 -1
  29. package/lib/typescript/hooks/query/useAppVersionApi.d.ts.map +1 -1
  30. package/lib/typescript/hooks/query/useRolesApi.d.ts.map +1 -1
  31. package/lib/typescript/initializers/constants.d.ts +4 -0
  32. package/lib/typescript/initializers/constants.d.ts.map +1 -0
  33. package/lib/typescript/initializers/index.d.ts +2 -2
  34. package/lib/typescript/initializers/index.d.ts.map +1 -1
  35. package/lib/typescript/initializers/logRocket.d.ts +11 -1
  36. package/lib/typescript/initializers/logRocket.d.ts.map +1 -1
  37. package/lib/typescript/providers/NeetoCommonsRNProvider.d.ts +8 -1
  38. package/lib/typescript/providers/NeetoCommonsRNProvider.d.ts.map +1 -1
  39. package/lib/typescript/screens/authentication/LoginScreen.d.ts.map +1 -1
  40. package/lib/typescript/screens/authentication/OTPVerificationScreen.d.ts.map +1 -1
  41. package/lib/typescript/screens/authentication/Organization.d.ts.map +1 -1
  42. package/lib/typescript/screens/authentication/SubDomainScreen.d.ts.map +1 -1
  43. package/lib/typescript/screens/profile/ProfileScreen.d.ts.map +1 -1
  44. package/lib/typescript/screens/team_members/ProductSelect.d.ts.map +1 -1
  45. package/lib/typescript/screens/team_members/index.d.ts.map +1 -1
  46. package/lib/typescript/screens/team_members/utils.d.ts.map +1 -1
  47. package/lib/typescript/store/index.d.ts +3 -2
  48. package/lib/typescript/store/index.d.ts.map +1 -1
  49. package/lib/typescript/utils/apiHelpers.d.ts.map +1 -1
  50. package/metro-devtools.js +48 -0
  51. package/package.json +485 -16
  52. package/src/api/app_version.ts +42 -2
  53. package/src/components/AppVersion.ts +46 -30
  54. package/src/components/DevToolsBridge.ts +40 -0
  55. package/src/components/Header.tsx +3 -1
  56. package/src/components/NotificationIcon.tsx +1 -1
  57. package/src/components/ToggleSwitch.tsx +2 -1
  58. package/src/components/WorkspaceSwitcherPane.tsx +1 -1
  59. package/src/components/constants.ts +4 -0
  60. package/src/config/axios/constants.ts +3 -0
  61. package/src/config/axios/groupingKey.ts +25 -0
  62. package/src/config/axios/index.ts +7 -4
  63. package/src/config/bugsnag.ts +17 -1
  64. package/src/constants/index.ts +13 -0
  65. package/src/contexts/localization.tsx +1 -1
  66. package/src/helpers/notifications/firebaseMessaging.android.ts +21 -0
  67. package/src/helpers/notifications/firebaseMessaging.ts +37 -0
  68. package/src/helpers/notifications/getDeviceToken.ts +1 -2
  69. package/src/helpers/notifications/index.ts +1 -0
  70. package/src/helpers/notifications/registerBackgroundHandlers.ts +1 -1
  71. package/src/helpers/notifications/usePushNotification.ts +1 -1
  72. package/src/helpers/scale.ts +21 -3
  73. package/src/helpers/utils.ts +2 -1
  74. package/src/hooks/query/useAppVersionApi.ts +9 -0
  75. package/src/hooks/query/useRolesApi.ts +3 -1
  76. package/src/initializers/constants.ts +3 -0
  77. package/src/initializers/index.ts +12 -2
  78. package/src/initializers/logRocket.ts +111 -20
  79. package/src/providers/NeetoCommonsRNProvider.tsx +23 -0
  80. package/src/screens/authentication/IncompleteOnboardingInfo.tsx +2 -2
  81. package/src/screens/authentication/LoginScreen.tsx +50 -39
  82. package/src/screens/authentication/OTPVerificationScreen.tsx +17 -41
  83. package/src/screens/authentication/Organization.tsx +2 -6
  84. package/src/screens/authentication/SubDomainScreen.tsx +38 -70
  85. package/src/screens/profile/ProfileScreen.tsx +2 -1
  86. package/src/screens/team_members/Card.tsx +1 -1
  87. package/src/screens/team_members/ProductSelect.tsx +6 -7
  88. package/src/screens/team_members/index.tsx +2 -1
  89. package/src/screens/team_members/utils.ts +3 -1
  90. package/src/store/index.ts +68 -1
  91. package/src/utils/apiHelpers.ts +4 -1
@@ -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
- const fetch = () => {
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
- return axios.get<AppVersionInfo>(appVersionsUrl({ appName, environment }));
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 { data: appVersionData, refetch } = useFetchAppVersionApi();
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
- if (Platform.OS === "android") {
64
- const {
65
- android_minimum_supported_version: androidMinimumSupportedVersion,
66
- android_current_version: androidCurrentVersion,
67
- } = data;
68
- if (androidMinimumSupportedVersion && androidCurrentVersion) {
69
- if (compare(getVersion(), androidMinimumSupportedVersion, "<")) {
70
- setIsMajorUpdate(true);
71
- } else if (compare(getVersion(), androidCurrentVersion, "<")) {
72
- setIsMinorUpdate(true);
73
- }
74
- }
75
- } else {
76
- const {
77
- ios_minimum_supported_version: iOSMinimumSupportedVersion,
78
- ios_current_version: iOSCurrentVersion,
79
- } = data;
80
- if (iOSMinimumSupportedVersion && iOSCurrentVersion) {
81
- if (compare(getVersion(), iOSMinimumSupportedVersion, "<")) {
82
- setIsMajorUpdate(true);
83
- } else if (compare(getVersion(), iOSCurrentVersion, "<")) {
84
- setIsMinorUpdate(true);
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 === "active") {
146
- refetch();
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 { HamburgerMenu, MenuVertical, Search } from "@bigbinary/neeto-icons-rn";
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,7 @@
1
1
  import React from "react";
2
2
  import { StyleSheet } from "react-native";
3
3
 
4
- import { Notification } from "@bigbinary/neeto-icons-rn";
4
+ import Notification from "@bigbinary/neeto-icons-rn/icons/Notification";
5
5
  import {
6
6
  Container,
7
7
  Touchable,
@@ -1,7 +1,8 @@
1
1
  import React, { useEffect, useRef } from "react";
2
2
  import { Animated, StyleSheet } from "react-native";
3
3
 
4
- import { Check, Close } from "@bigbinary/neeto-icons-rn";
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";
@@ -1,7 +1,7 @@
1
1
  import React from "react";
2
2
  import { SafeAreaView, StyleSheet } from "react-native";
3
3
 
4
- import { Close } from "@bigbinary/neeto-icons-rn";
4
+ import Close from "@bigbinary/neeto-icons-rn/icons/Close";
5
5
  import {
6
6
  Container,
7
7
  FlashList,
@@ -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
- report.context = `Axios Request Failed: ${config?.method?.toUpperCase()} ${
207
- config?.url
208
- }`;
209
- report.groupingHash = `axios-${config?.method}-${config?.url}`;
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
@@ -6,9 +6,16 @@ import {
6
6
  isNetworkError,
7
7
  } from "./axios/networkErrorHandler";
8
8
 
9
+ import { getLogRocketSessionURL } from "../initializers/logRocket";
9
10
  import { Store } from "../store";
10
11
 
11
- export const bugsnagErrorHandler = (err: NotifiableError, origin: string) => {
12
+ export const bugsnagErrorHandler = (
13
+ err: NotifiableError,
14
+ origin: string,
15
+ // Pass a hash for failures that would otherwise fragment into a group per
16
+ // resource or per status code.
17
+ groupingHash?: string
18
+ ) => {
12
19
  if (!__DEV__) {
13
20
  Bugsnag.notify(err, event => {
14
21
  if (isNetworkError(err)) {
@@ -18,6 +25,10 @@ export const bugsnagErrorHandler = (err: NotifiableError, origin: string) => {
18
25
  event.severity = "warning";
19
26
  event.context = origin;
20
27
 
28
+ if (groupingHash) {
29
+ event.groupingHash = groupingHash;
30
+ }
31
+
21
32
  return true;
22
33
  });
23
34
  }
@@ -40,6 +51,11 @@ export const configureBugsnag = () => {
40
51
  return false;
41
52
  }
42
53
 
54
+ const sessionURL = getLogRocketSessionURL();
55
+ if (sessionURL) {
56
+ event.addMetadata("LogRocket", { sessionURL });
57
+ }
58
+
43
59
  return true;
44
60
  },
45
61
  plugins: [new BugsnagPluginReactNavigation()],
@@ -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;
@@ -8,7 +8,7 @@ import React, {
8
8
  type ReactNode,
9
9
  } from "react";
10
10
 
11
- import { mergeDeepRight } from "ramda";
11
+ import mergeDeepRight from "ramda/src/mergeDeepRight";
12
12
 
13
13
  import { getItem, setItem } from "../helpers/asyncStore";
14
14
  import {
@@ -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,
@@ -1,4 +1,22 @@
1
- import { moderateScale as baseModerateScale } from "react-native-size-matters";
1
+ import { UnistylesRuntime } from "react-native-unistyles";
2
2
 
3
- export const moderateScale = (size: number, factor?: number) =>
4
- Math.round(baseModerateScale(size, factor));
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
+ };
@@ -1,7 +1,8 @@
1
1
  import { Linking } from "react-native";
2
2
 
3
3
  import { isNotPresent } from "@bigbinary/neeto-cist";
4
- import { equals, intersection } from "ramda";
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 { groupBy, prop, reverse } from "ramda";
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
 
@@ -0,0 +1,3 @@
1
+ export const SAMPLE_STORAGE_KEY = "LOG_ROCKET_SAMPLED";
2
+ export const DEFAULT_SAMPLE_RATE = 10;
3
+ export const SAMPLE_DECISION_TTL_MS = 30 * 24 * 60 * 60 * 1000;
@@ -1,3 +1,13 @@
1
- import { logRocket } from "./logRocket";
1
+ import {
2
+ getLogRocketSessionURL,
3
+ identifyLogRocketUser,
4
+ initLogRocket,
5
+ shutdownLogRocket,
6
+ } from "./logRocket";
2
7
 
3
- export { logRocket };
8
+ export {
9
+ getLogRocketSessionURL,
10
+ identifyLogRocketUser,
11
+ initLogRocket,
12
+ shutdownLogRocket,
13
+ };