@bigbinary/neeto-commons-rn 2.0.2 → 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.
Files changed (82) 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/providers/NeetoCommonsRNProvider.d.ts +8 -1
  32. package/lib/typescript/providers/NeetoCommonsRNProvider.d.ts.map +1 -1
  33. package/lib/typescript/screens/authentication/LoginScreen.d.ts.map +1 -1
  34. package/lib/typescript/screens/authentication/OTPVerificationScreen.d.ts.map +1 -1
  35. package/lib/typescript/screens/authentication/Organization.d.ts.map +1 -1
  36. package/lib/typescript/screens/authentication/SubDomainScreen.d.ts.map +1 -1
  37. package/lib/typescript/screens/profile/ProfileScreen.d.ts.map +1 -1
  38. package/lib/typescript/screens/team_members/ProductSelect.d.ts.map +1 -1
  39. package/lib/typescript/screens/team_members/index.d.ts.map +1 -1
  40. package/lib/typescript/screens/team_members/utils.d.ts.map +1 -1
  41. package/lib/typescript/store/index.d.ts +3 -2
  42. package/lib/typescript/store/index.d.ts.map +1 -1
  43. package/lib/typescript/utils/apiHelpers.d.ts.map +1 -1
  44. package/metro-devtools.js +48 -0
  45. package/package.json +485 -16
  46. package/src/api/app_version.ts +42 -2
  47. package/src/components/AppVersion.ts +46 -30
  48. package/src/components/DevToolsBridge.ts +40 -0
  49. package/src/components/Header.tsx +3 -1
  50. package/src/components/NotificationIcon.tsx +1 -1
  51. package/src/components/ToggleSwitch.tsx +2 -1
  52. package/src/components/WorkspaceSwitcherPane.tsx +1 -1
  53. package/src/components/constants.ts +4 -0
  54. package/src/config/axios/constants.ts +3 -0
  55. package/src/config/axios/groupingKey.ts +25 -0
  56. package/src/config/axios/index.ts +7 -4
  57. package/src/config/bugsnag.ts +11 -1
  58. package/src/constants/index.ts +13 -0
  59. package/src/contexts/localization.tsx +1 -1
  60. package/src/helpers/notifications/firebaseMessaging.android.ts +21 -0
  61. package/src/helpers/notifications/firebaseMessaging.ts +37 -0
  62. package/src/helpers/notifications/getDeviceToken.ts +1 -2
  63. package/src/helpers/notifications/index.ts +1 -0
  64. package/src/helpers/notifications/registerBackgroundHandlers.ts +1 -1
  65. package/src/helpers/notifications/usePushNotification.ts +1 -1
  66. package/src/helpers/scale.ts +21 -3
  67. package/src/helpers/utils.ts +2 -1
  68. package/src/hooks/query/useAppVersionApi.ts +9 -0
  69. package/src/hooks/query/useRolesApi.ts +3 -1
  70. package/src/providers/NeetoCommonsRNProvider.tsx +23 -0
  71. package/src/screens/authentication/IncompleteOnboardingInfo.tsx +2 -2
  72. package/src/screens/authentication/LoginScreen.tsx +50 -39
  73. package/src/screens/authentication/OTPVerificationScreen.tsx +17 -41
  74. package/src/screens/authentication/Organization.tsx +2 -6
  75. package/src/screens/authentication/SubDomainScreen.tsx +38 -70
  76. package/src/screens/profile/ProfileScreen.tsx +2 -1
  77. package/src/screens/team_members/Card.tsx +1 -1
  78. package/src/screens/team_members/ProductSelect.tsx +6 -7
  79. package/src/screens/team_members/index.tsx +2 -1
  80. package/src/screens/team_members/utils.ts +3 -1
  81. package/src/store/index.ts +68 -1
  82. package/src/utils/apiHelpers.ts +4 -1
@@ -5,6 +5,7 @@ import {
5
5
  Container,
6
6
  Input,
7
7
  ParentView,
8
+ ScrollView,
8
9
  Toast,
9
10
  Typography,
10
11
  lightTheme,
@@ -37,7 +38,6 @@ const styles = StyleSheet.create({
37
38
  flex: 1,
38
39
  },
39
40
  body: {
40
- flex: 1,
41
41
  marginHorizontal: 16,
42
42
  },
43
43
  inputWrapper: {
@@ -45,11 +45,15 @@ const styles = StyleSheet.create({
45
45
  marginBottom: 32,
46
46
  marginTop: 18,
47
47
  },
48
+ // `marginTop: auto` inside the grow-to-fill scroll content parks this at
49
+ // the bottom whenever the screen has slack, and lets it flow below the
50
+ // auth buttons when it does not. Absolute positioning used to pin it 50dp
51
+ // from the bottom, which landscape turned into an overlay across the
52
+ // middle of the form.
48
53
  versionText: {
49
- position: "absolute",
50
- left: 0,
51
- right: 0,
52
- bottom: moderateScale(50),
54
+ marginBottom: moderateScale(24),
55
+ marginTop: "auto",
56
+ paddingTop: moderateScale(24),
53
57
  textAlign: "center",
54
58
  },
55
59
  androidVersionText: {
@@ -129,42 +133,49 @@ export const LoginScreen = () => {
129
133
  barStyle="dark-content"
130
134
  style={styles.screen}
131
135
  >
132
- <Container style={styles.body}>
133
- {/*
134
- The v2 `lineHeight` override has no v3 counterpart: ScreenHeader
135
- takes only `title` and a container `style`, and sets the title's
136
- line height itself on Android.
137
- */}
138
- <ScreenHeader title={strings.commons.headers.login} />
139
- <Container style={styles.inputWrapper}>
140
- <Input
141
- autoCapitalize="none"
142
- error={touched?.email ? errors?.email : ""}
143
- keyboardType="email-address"
144
- label={strings.commons.labels.Email}
145
- value={email}
146
- onBlur={() => setFieldValue("email", email.toLowerCase())}
147
- onChangeText={handleChange("email")}
136
+ {/*
137
+ Landscape (and any small viewport with the keyboard up) leaves the
138
+ auth buttons taller than the screen, so the content scrolls instead
139
+ of being clipped.
140
+ */}
141
+ <ScrollView>
142
+ <Container style={styles.body}>
143
+ {/*
144
+ The v2 `lineHeight` override has no v3 counterpart: ScreenHeader
145
+ takes only `title` and a container `style`, and sets the title's
146
+ line height itself on Android.
147
+ */}
148
+ <ScreenHeader title={strings.commons.headers.login} />
149
+ <Container style={styles.inputWrapper}>
150
+ <Input
151
+ autoCapitalize="none"
152
+ error={touched?.email ? errors?.email : ""}
153
+ keyboardType="email-address"
154
+ label={strings.commons.labels.Email}
155
+ value={email}
156
+ onBlur={() => setFieldValue("email", email.toLowerCase())}
157
+ onChangeText={handleChange("email")}
158
+ />
159
+ </Container>
160
+ <AuthOptions
161
+ buttonLoading={isGeneratingOTP}
162
+ handleSubmit={handleSubmit}
148
163
  />
149
164
  </Container>
150
- <AuthOptions
151
- buttonLoading={isGeneratingOTP}
152
- handleSubmit={handleSubmit}
153
- />
154
- </Container>
155
- <Typography
156
- color={lightTheme.colors.palette.grey600}
157
- size="xs"
158
- weight="regular"
159
- style={StyleSheet.flatten([
160
- styles.versionText,
161
- Platform.OS === "android" && styles.androidVersionText,
162
- ])}
163
- >
164
- {Config.APP_DISPLAY_NAME}
165
- {strings.commons.common.version}
166
- {DeviceInfo.getVersion()}
167
- </Typography>
165
+ <Typography
166
+ color={lightTheme.colors.palette.grey600}
167
+ size="xs"
168
+ weight="regular"
169
+ style={StyleSheet.flatten([
170
+ styles.versionText,
171
+ Platform.OS === "android" && styles.androidVersionText,
172
+ ])}
173
+ >
174
+ {Config.APP_DISPLAY_NAME}
175
+ {strings.commons.common.version}
176
+ {DeviceInfo.getVersion()}
177
+ </Typography>
178
+ </ScrollView>
168
179
  </ParentView>
169
180
  );
170
181
  };
@@ -4,15 +4,14 @@ import { StyleSheet } from "react-native";
4
4
  import {
5
5
  Button,
6
6
  Container,
7
+ Header,
7
8
  Input,
8
9
  ParentView,
9
10
  Toast,
10
- Touchable,
11
11
  Typography,
12
12
  lightTheme,
13
13
  } from "@bigbinary/neetoui-rn";
14
14
  import { useFocusEffect } from "@react-navigation/native";
15
- import Icon from "react-native-remix-icon";
16
15
 
17
16
  import { useLocalization } from "../../contexts/localization";
18
17
  import { useAuth } from "../../hooks/query/useAuth";
@@ -20,7 +19,6 @@ import { type CommonsScreenProps } from "../../navigation/types";
20
19
 
21
20
  // eslint-disable-next-line @bigbinary/neeto/no-dangling-constants
22
21
  const RESEND_TIMER = 30;
23
- let interval: ReturnType<typeof setInterval> | null = null;
24
22
 
25
23
  export const OTPVerificationScreen = ({
26
24
  navigation,
@@ -31,6 +29,7 @@ export const OTPVerificationScreen = ({
31
29
  const [startTimer, setStartTimer] = useState(false);
32
30
 
33
31
  const isSubmittingRef = useRef(false);
32
+ const intervalRef = useRef<ReturnType<typeof setInterval> | null>(null);
34
33
 
35
34
  const { email, userId } = params;
36
35
  const { strings } = useLocalization();
@@ -80,10 +79,12 @@ export const OTPVerificationScreen = ({
80
79
 
81
80
  useEffect(() => {
82
81
  if (startTimer) {
83
- interval = setInterval(() => {
82
+ intervalRef.current = setInterval(() => {
84
83
  setResendTimer(prev => {
85
84
  if (prev === 1) {
86
- clearInterval(interval as ReturnType<typeof setInterval>);
85
+ clearInterval(
86
+ intervalRef.current as ReturnType<typeof setInterval>
87
+ );
87
88
  }
88
89
 
89
90
  return prev - 1;
@@ -91,9 +92,10 @@ export const OTPVerificationScreen = ({
91
92
  }, 1000);
92
93
  }
93
94
 
94
- // `interval` is null until the first tick is scheduled; clearing a null
95
- // handle has always been the no-op it is today.
96
- return () => clearInterval(interval as ReturnType<typeof setInterval>);
95
+ // The ref is null until the first tick is scheduled; clearing a null
96
+ // handle is a no-op.
97
+ return () =>
98
+ clearInterval(intervalRef.current as ReturnType<typeof setInterval>);
97
99
  }, [startTimer]);
98
100
 
99
101
  useEffect(() => {
@@ -118,27 +120,11 @@ export const OTPVerificationScreen = ({
118
120
  barStyle="dark-content"
119
121
  style={styles.screen}
120
122
  >
123
+ <Header
124
+ title={strings.commons.headers.otpVerification}
125
+ onBack={() => navigation?.goBack()}
126
+ />
121
127
  <Container style={styles.body}>
122
- <Container style={styles.backRow}>
123
- <Touchable
124
- style={styles.backButton}
125
- onPress={() => navigation?.goBack()}
126
- >
127
- <Icon
128
- color={lightTheme.colors.palette.grey600}
129
- name="arrow-left-s-line"
130
- size={25}
131
- />
132
- </Touchable>
133
- </Container>
134
- <Typography
135
- color={lightTheme.colors.palette.grey800}
136
- style={styles.heading}
137
- variant="header"
138
- weight="semibold"
139
- >
140
- {strings.commons.headers.otpVerification}
141
- </Typography>
142
128
  <Typography
143
129
  color={lightTheme.colors.palette.grey700}
144
130
  size="s"
@@ -201,19 +187,9 @@ const styles = StyleSheet.create({
201
187
  body: {
202
188
  flex: 1,
203
189
  marginHorizontal: 16,
204
- },
205
- backRow: {
206
- justifyContent: "center",
207
- height: 42,
208
- marginBottom: 18,
209
- },
210
- backButton: {
211
- marginLeft: -14,
212
- padding: 2,
213
- width: 42,
214
- },
215
- heading: {
216
- marginBottom: 2,
190
+ // The title moved into the Header, which carries its own bottom spacing;
191
+ // the description now opens the body.
192
+ marginTop: 16,
217
193
  },
218
194
  description: {
219
195
  marginBottom: 24,
@@ -1,6 +1,7 @@
1
1
  import React from "react";
2
2
  import { StyleSheet } from "react-native";
3
3
 
4
+ import Right from "@bigbinary/neeto-icons-rn/icons/Right";
4
5
  import {
5
6
  Avatar,
6
7
  Container,
@@ -8,7 +9,6 @@ import {
8
9
  Typography,
9
10
  lightTheme,
10
11
  } from "@bigbinary/neetoui-rn";
11
- import Icon from "react-native-remix-icon";
12
12
 
13
13
  import { moderateScale } from "../../helpers/scale";
14
14
  import { type Organization as OrganizationEntity } from "../../slices/auth";
@@ -64,11 +64,7 @@ export const Organization = ({
64
64
  ) : (
65
65
  showOrganizationDetails()
66
66
  )}
67
- <Icon
68
- color={lightTheme.colors.primary}
69
- name="arrow-right-s-line"
70
- size={25}
71
- />
67
+ <Right color={lightTheme.colors.primary} size={25} />
72
68
  </Touchable>
73
69
  );
74
70
  };
@@ -4,17 +4,15 @@ import { StyleSheet } from "react-native";
4
4
  import {
5
5
  Container,
6
6
  FlashList,
7
+ Header,
7
8
  ParentView,
8
9
  SearchBar,
9
- Touchable,
10
10
  Typography,
11
11
  lightTheme,
12
12
  } from "@bigbinary/neetoui-rn";
13
- import Icon from "react-native-remix-icon";
14
13
 
15
14
  import { Organization } from "./Organization";
16
15
 
17
- import { LineLoader } from "../../components";
18
16
  import { useLocalization } from "../../contexts/localization";
19
17
  import { moderateScale } from "../../helpers/scale";
20
18
  import { useAuth } from "../../hooks/query/useAuth";
@@ -57,45 +55,35 @@ export const SubDomainScreen = ({
57
55
  org.subdomain.toLowerCase().includes(searchTermLowerCase)
58
56
  );
59
57
 
60
- const renderHeader = () => (
61
- <Container style={styles.header}>
58
+ const renderIntro = () => (
59
+ <Container style={styles.intro}>
62
60
  <Typography
63
- color={lightTheme.colors.palette.grey800}
64
- style={styles.headerTitle}
65
- variant="header"
66
- weight="semibold"
61
+ color={lightTheme.colors.palette.grey600}
62
+ size="s"
63
+ style={styles.introDescription}
64
+ weight="regular"
67
65
  >
68
- {strings.commons.headers.subDomain}
69
- </Typography>
70
- <Container>
71
- <Typography
72
- color={lightTheme.colors.palette.grey600}
73
- size="s"
74
- style={styles.headerDescription}
75
- weight="regular"
76
- >
77
- {strings.commons.subdomain.description1}
78
- <Typography color={lightTheme.colors.text.primary} weight="bold">
79
- {organizations[0].email}
80
- </Typography>
81
- {strings.commons.subdomain.description2}
66
+ {strings.commons.subdomain.description1}
67
+ <Typography color={lightTheme.colors.text.primary} weight="bold">
68
+ {organizations[0].email}
82
69
  </Typography>
83
- {/*
84
- v3's SearchBar owns its text; it has no `value` prop, so the screen
85
- only mirrors it through onChangeText.
86
- */}
87
- <SearchBar
88
- // `formatString` returns a plain string whenever every argument is
89
- // one, which is the case here.
90
- placeholder={
91
- strings.formatString(
92
- strings.commons.placeholders.search,
93
- strings.commons.labels.Workspaces.toLowerCase()
94
- ) as string
95
- }
96
- onChangeText={setSearchTerm}
97
- />
98
- </Container>
70
+ {strings.commons.subdomain.description2}
71
+ </Typography>
72
+ {/*
73
+ v3's SearchBar owns its text; it has no `value` prop, so the screen
74
+ only mirrors it through onChangeText.
75
+ */}
76
+ <SearchBar
77
+ // `formatString` returns a plain string whenever every argument is
78
+ // one, which is the case here.
79
+ placeholder={
80
+ strings.formatString(
81
+ strings.commons.placeholders.search,
82
+ strings.commons.labels.Workspaces.toLowerCase()
83
+ ) as string
84
+ }
85
+ onChangeText={setSearchTerm}
86
+ />
99
87
  </Container>
100
88
  );
101
89
 
@@ -105,22 +93,15 @@ export const SubDomainScreen = ({
105
93
  barStyle="dark-content"
106
94
  style={styles.screen}
107
95
  >
108
- <Container>
109
- <Container style={styles.navBar}>
110
- {/* v3's ripple config dropped `isCentered`; nothing replaces it. */}
111
- <Touchable
112
- hitSlop={10}
113
- rippleConfig={{ shouldOverflowContainer: true }}
114
- style={styles.backButton}
115
- onPress={onBackPress}
116
- >
117
- <Icon name="arrow-left-s-line" size={moderateScale(24)} />
118
- </Touchable>
119
- </Container>
120
- <LineLoader isLoading={isSelectingOrganization} />
121
- </Container>
96
+ {/* Header carries the progress line the screen used to draw with its
97
+ own LineLoader. */}
98
+ <Header
99
+ isLoading={isSelectingOrganization}
100
+ title={strings.commons.headers.subDomain}
101
+ onBack={onBackPress}
102
+ />
122
103
  <Container style={styles.body}>
123
- {renderHeader()}
104
+ {renderIntro()}
124
105
  <FlashList
125
106
  contentContainerStyle={styles.contentContainerStyle}
126
107
  data={filteredOrganizations}
@@ -142,29 +123,16 @@ const styles = StyleSheet.create({
142
123
  screen: {
143
124
  flex: 1,
144
125
  },
145
- navBar: {
146
- alignItems: "center",
147
- flexDirection: "row",
148
- height: moderateScale(44),
149
- paddingHorizontal: moderateScale(16),
150
- },
151
- backButton: {
152
- marginLeft: moderateScale(-8),
153
- },
154
126
  body: {
155
127
  flex: 1,
156
128
  marginHorizontal: moderateScale(16),
157
- marginTop: moderateScale(16),
129
+ marginTop: moderateScale(4),
158
130
  },
159
- header: {
131
+ intro: {
160
132
  justifyContent: "center",
161
133
  marginBottom: 18,
162
134
  },
163
- headerTitle: {
164
- lineHeight: moderateScale(28),
165
- marginBottom: moderateScale(8),
166
- },
167
- headerDescription: {
135
+ introDescription: {
168
136
  marginBottom: moderateScale(8),
169
137
  },
170
138
  flatListStyle: {
@@ -1,7 +1,8 @@
1
1
  import React, { type ReactNode } from "react";
2
2
  import { Linking, ScrollView, StyleSheet } from "react-native";
3
3
 
4
- import { LeftArrow, Help } from "@bigbinary/neeto-icons-rn";
4
+ import Help from "@bigbinary/neeto-icons-rn/icons/Help";
5
+ import LeftArrow from "@bigbinary/neeto-icons-rn/icons/LeftArrow";
5
6
  import {
6
7
  Typography,
7
8
  Container,
@@ -1,7 +1,7 @@
1
1
  import React, { type ComponentType } from "react";
2
2
  import { StyleSheet, View } from "react-native";
3
3
 
4
- import { MenuVertical } from "@bigbinary/neeto-icons-rn";
4
+ import MenuVertical from "@bigbinary/neeto-icons-rn/icons/MenuVertical";
5
5
  import {
6
6
  Avatar as UIAvatar,
7
7
  Container,
@@ -2,7 +2,7 @@ import React, { useState, type ComponentType } from "react";
2
2
  import { Pressable, StyleSheet } from "react-native";
3
3
 
4
4
  import { findIndexBy } from "@bigbinary/neeto-cist";
5
- import { Down } from "@bigbinary/neeto-icons-rn";
5
+ import Down from "@bigbinary/neeto-icons-rn/icons/Down";
6
6
  import {
7
7
  BottomSheet as UIBottomSheet,
8
8
  Container,
@@ -13,7 +13,7 @@ import {
13
13
  type BottomSheetProps,
14
14
  } from "@bigbinary/neetoui-rn";
15
15
  import { type FormikProps } from "formik";
16
- import { mergeRight } from "ramda";
16
+ import mergeRight from "ramda/src/mergeRight";
17
17
 
18
18
  import Checkbox from "./Checkbox";
19
19
  import { APP_NAME } from "./constants";
@@ -32,11 +32,10 @@ interface RoleSheetItem {
32
32
  value: string;
33
33
  }
34
34
 
35
- // `isVisible` starts out null here, which v3 narrowed to `boolean`.
35
+ // Pins the sheet's row type — `data` can be undefined mid-close, which
36
+ // blocks inference at the call site.
36
37
  const BottomSheet = UIBottomSheet as ComponentType<
37
- Omit<BottomSheetProps<RoleSheetItem>, "isVisible"> & {
38
- isVisible: boolean | null;
39
- }
38
+ BottomSheetProps<RoleSheetItem>
40
39
  >;
41
40
 
42
41
  interface ProductSelectProps {
@@ -52,7 +51,7 @@ const ProductSelect = ({
52
51
  setFieldValue,
53
52
  }: ProductSelectProps) => {
54
53
  const otherProducts = values?.otherProducts ?? true;
55
- const [openDropdown, setOpenDropdown] = useState<boolean | null>(null);
54
+ const [openDropdown, setOpenDropdown] = useState(false);
56
55
  const [selectedAppName, setSelectedAppName] = useState<string | null>(null);
57
56
 
58
57
  const updateProduct = (appName: string, object: Partial<ProductRole>) => {
@@ -1,7 +1,8 @@
1
1
  import React, { useCallback, useState } from "react";
2
2
  import { StyleSheet } from "react-native";
3
3
 
4
- import { Edit, Delete } from "@bigbinary/neeto-icons-rn";
4
+ import Delete from "@bigbinary/neeto-icons-rn/icons/Delete";
5
+ import Edit from "@bigbinary/neeto-icons-rn/icons/Edit";
5
6
  import {
6
7
  Container,
7
8
  Divider,
@@ -1,5 +1,7 @@
1
1
  import { _findBy, notEquals } from "@bigbinary/neeto-cist";
2
- import { equals, includes, move } from "ramda";
2
+ import equals from "ramda/src/equals";
3
+ import includes from "ramda/src/includes";
4
+ import move from "ramda/src/move";
3
5
 
4
6
  import { ADMIN_ROLE_NAME, ADMIN_ROLES, APP_NAME } from "./constants";
5
7
 
@@ -1,5 +1,9 @@
1
1
  import AsyncStorage from "@react-native-async-storage/async-storage";
2
- import { configureStore } from "@reduxjs/toolkit";
2
+ import {
3
+ configureStore,
4
+ type StoreEnhancer,
5
+ type UnknownAction,
6
+ } from "@reduxjs/toolkit";
3
7
  import {
4
8
  persistStore,
5
9
  persistReducer,
@@ -27,6 +31,67 @@ const persistConfig: PersistConfig<CommonsState> = {
27
31
 
28
32
  const persistedReducer = persistReducer(persistConfig, rootReducer);
29
33
 
34
+ // The enhancer must exist when configureStore runs — at module-import time —
35
+ // so the store wires it itself; host apps could never inject it early enough.
36
+ // The guarded require keeps hosts without the plugin working (it is a dev
37
+ // tool the host may not install), and metro strips the whole branch from
38
+ // release bundles because __DEV__ is inlined before dependency collection.
39
+ const devToolsEnhancers = () => {
40
+ if (__DEV__) {
41
+ try {
42
+ // The require is untyped on purpose (consistent-type-imports forbids
43
+ // import() annotations); cast just the surface used here.
44
+ const { rozeniteDevToolsEnhancer } =
45
+ require("@rozenite/redux-devtools-plugin") as {
46
+ rozeniteDevToolsEnhancer: (options: {
47
+ name?: string;
48
+ maxAge?: number;
49
+ actionSanitizer?: (action: UnknownAction, id: number) => unknown;
50
+ stateSanitizer?: (state: unknown, index: number) => unknown;
51
+ }) => StoreEnhancer;
52
+ };
53
+
54
+ // The workspace switcher keeps a per-organization auth token in the
55
+ // store (every other credential lives in the keychain). The DevTools
56
+ // bridge rides metro's websocket, so those tokens must not cross it.
57
+ const redactOrganizationTokens = (state: unknown) => {
58
+ const snapshot = state as CommonsState | undefined;
59
+ if (!snapshot?.auth?.organizations?.length) return state;
60
+
61
+ return {
62
+ ...snapshot,
63
+ auth: {
64
+ ...snapshot.auth,
65
+ organizations: snapshot.auth.organizations.map(organization => ({
66
+ ...organization,
67
+ auth_token: "<<redacted>>",
68
+ })),
69
+ },
70
+ };
71
+ };
72
+
73
+ return [
74
+ rozeniteDevToolsEnhancer({
75
+ name: "neeto-commons-rn store",
76
+ // The whole persisted tree rides through REHYDRATE; big snapshots
77
+ // can OOM the Android DevTools panel, so cap history and elide the
78
+ // payload.
79
+ maxAge: 25,
80
+ actionSanitizer: action =>
81
+ action.type === REHYDRATE
82
+ ? { ...action, payload: "<<elided: persisted state>>" }
83
+ : action,
84
+ stateSanitizer: redactOrganizationTokens,
85
+ }),
86
+ ];
87
+ } catch {
88
+ return [];
89
+ }
90
+ }
91
+
92
+ return [];
93
+ };
94
+
30
95
  const baseStore = configureStore({
31
96
  reducer: persistedReducer,
32
97
  middleware: getDefaultMiddleware =>
@@ -35,6 +100,8 @@ const baseStore = configureStore({
35
100
  ignoredActions: [FLUSH, REHYDRATE, PAUSE, PERSIST, PURGE, REGISTER],
36
101
  },
37
102
  }),
103
+ enhancers: getDefaultEnhancers =>
104
+ getDefaultEnhancers().concat(devToolsEnhancers()),
38
105
  });
39
106
  const Persistor = persistStore(baseStore);
40
107
 
@@ -1,7 +1,10 @@
1
+ // `appName` doubles as user-facing copy ("Update neetoDesk"), so it keeps its
2
+ // camel case. Hosts are case-insensitive, but the mixed-case spelling shows up
3
+ // verbatim in request logs and Bugsnag error groups, so lowercase it here.
1
4
  export const appVersionsUrl = ({
2
5
  appName,
3
6
  environment,
4
7
  }: {
5
8
  appName: string;
6
9
  environment: string;
7
- }) => `https://api.${appName}.${environment}/app_versions`;
10
+ }) => `https://api.${appName.toLowerCase()}.${environment}/app_versions`;