@grupalia/rn-ui-kit 0.15.0 → 0.16.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 (126) hide show
  1. package/lib/commonjs/assets/illustrations/camera.svg +87 -0
  2. package/lib/commonjs/components/CameraImageInput.js +478 -0
  3. package/lib/commonjs/components/CameraImageInput.js.map +1 -0
  4. package/lib/commonjs/components/CameraWrapperModal.js +255 -0
  5. package/lib/commonjs/components/CameraWrapperModal.js.map +1 -0
  6. package/lib/commonjs/components/FormikCameraImageInput.js +37 -0
  7. package/lib/commonjs/components/FormikCameraImageInput.js.map +1 -0
  8. package/lib/commonjs/components/ImagePickerBottomSheet.js +100 -0
  9. package/lib/commonjs/components/ImagePickerBottomSheet.js.map +1 -0
  10. package/lib/commonjs/components/PhotoPickerModal.js +98 -0
  11. package/lib/commonjs/components/PhotoPickerModal.js.map +1 -0
  12. package/lib/commonjs/components/Toasts.js +188 -0
  13. package/lib/commonjs/components/Toasts.js.map +1 -0
  14. package/lib/commonjs/components/index.js +66 -0
  15. package/lib/commonjs/components/index.js.map +1 -1
  16. package/lib/commonjs/components/svgs/Camera.js +17 -0
  17. package/lib/commonjs/components/svgs/Camera.js.map +1 -0
  18. package/lib/commonjs/hooks/index.js +41 -0
  19. package/lib/commonjs/hooks/index.js.map +1 -0
  20. package/lib/commonjs/hooks/useInternetConnectionStatus.js +182 -0
  21. package/lib/commonjs/hooks/useInternetConnectionStatus.js.map +1 -0
  22. package/lib/commonjs/types/svg.d.js +2 -0
  23. package/lib/commonjs/types/svg.d.js.map +1 -0
  24. package/lib/commonjs/utils/fileDirectoryUtils.js +19 -0
  25. package/lib/commonjs/utils/fileDirectoryUtils.js.map +1 -0
  26. package/lib/commonjs/utils/index.js +22 -0
  27. package/lib/commonjs/utils/index.js.map +1 -1
  28. package/lib/commonjs/utils/timeConstants.js +15 -0
  29. package/lib/commonjs/utils/timeConstants.js.map +1 -0
  30. package/lib/module/assets/illustrations/camera.svg +87 -0
  31. package/lib/module/components/CameraImageInput.js +471 -0
  32. package/lib/module/components/CameraImageInput.js.map +1 -0
  33. package/lib/module/components/CameraWrapperModal.js +250 -0
  34. package/lib/module/components/CameraWrapperModal.js.map +1 -0
  35. package/lib/module/components/FormikCameraImageInput.js +32 -0
  36. package/lib/module/components/FormikCameraImageInput.js.map +1 -0
  37. package/lib/module/components/ImagePickerBottomSheet.js +95 -0
  38. package/lib/module/components/ImagePickerBottomSheet.js.map +1 -0
  39. package/lib/module/components/PhotoPickerModal.js +93 -0
  40. package/lib/module/components/PhotoPickerModal.js.map +1 -0
  41. package/lib/module/components/Toasts.js +182 -0
  42. package/lib/module/components/Toasts.js.map +1 -0
  43. package/lib/module/components/index.js +6 -0
  44. package/lib/module/components/index.js.map +1 -1
  45. package/lib/module/components/svgs/Camera.js +12 -0
  46. package/lib/module/components/svgs/Camera.js.map +1 -0
  47. package/lib/module/hooks/index.js +6 -0
  48. package/lib/module/hooks/index.js.map +1 -0
  49. package/lib/module/hooks/useInternetConnectionStatus.js +177 -0
  50. package/lib/module/hooks/useInternetConnectionStatus.js.map +1 -0
  51. package/lib/module/types/svg.d.js +2 -0
  52. package/lib/module/types/svg.d.js.map +1 -0
  53. package/lib/module/utils/fileDirectoryUtils.js +14 -0
  54. package/lib/module/utils/fileDirectoryUtils.js.map +1 -0
  55. package/lib/module/utils/index.js +2 -0
  56. package/lib/module/utils/index.js.map +1 -1
  57. package/lib/module/utils/timeConstants.js +12 -0
  58. package/lib/module/utils/timeConstants.js.map +1 -0
  59. package/lib/typescript/commonjs/components/CameraImageInput.d.ts +41 -0
  60. package/lib/typescript/commonjs/components/CameraImageInput.d.ts.map +1 -0
  61. package/lib/typescript/commonjs/components/CameraWrapperModal.d.ts +18 -0
  62. package/lib/typescript/commonjs/components/CameraWrapperModal.d.ts.map +1 -0
  63. package/lib/typescript/commonjs/components/FormikCameraImageInput.d.ts +14 -0
  64. package/lib/typescript/commonjs/components/FormikCameraImageInput.d.ts.map +1 -0
  65. package/lib/typescript/commonjs/components/ImagePickerBottomSheet.d.ts +15 -0
  66. package/lib/typescript/commonjs/components/ImagePickerBottomSheet.d.ts.map +1 -0
  67. package/lib/typescript/commonjs/components/PhotoPickerModal.d.ts +19 -0
  68. package/lib/typescript/commonjs/components/PhotoPickerModal.d.ts.map +1 -0
  69. package/lib/typescript/commonjs/components/Toasts.d.ts +3 -0
  70. package/lib/typescript/commonjs/components/Toasts.d.ts.map +1 -0
  71. package/lib/typescript/commonjs/components/index.d.ts +6 -0
  72. package/lib/typescript/commonjs/components/index.d.ts.map +1 -1
  73. package/lib/typescript/commonjs/components/svgs/Camera.d.ts +9 -0
  74. package/lib/typescript/commonjs/components/svgs/Camera.d.ts.map +1 -0
  75. package/lib/typescript/commonjs/hooks/index.d.ts +4 -0
  76. package/lib/typescript/commonjs/hooks/index.d.ts.map +1 -0
  77. package/lib/typescript/commonjs/hooks/useInternetConnectionStatus.d.ts +20 -0
  78. package/lib/typescript/commonjs/hooks/useInternetConnectionStatus.d.ts.map +1 -0
  79. package/lib/typescript/commonjs/utils/fileDirectoryUtils.d.ts +3 -0
  80. package/lib/typescript/commonjs/utils/fileDirectoryUtils.d.ts.map +1 -0
  81. package/lib/typescript/commonjs/utils/index.d.ts +2 -0
  82. package/lib/typescript/commonjs/utils/index.d.ts.map +1 -1
  83. package/lib/typescript/commonjs/utils/timeConstants.d.ts +10 -0
  84. package/lib/typescript/commonjs/utils/timeConstants.d.ts.map +1 -0
  85. package/lib/typescript/module/components/CameraImageInput.d.ts +41 -0
  86. package/lib/typescript/module/components/CameraImageInput.d.ts.map +1 -0
  87. package/lib/typescript/module/components/CameraWrapperModal.d.ts +18 -0
  88. package/lib/typescript/module/components/CameraWrapperModal.d.ts.map +1 -0
  89. package/lib/typescript/module/components/FormikCameraImageInput.d.ts +14 -0
  90. package/lib/typescript/module/components/FormikCameraImageInput.d.ts.map +1 -0
  91. package/lib/typescript/module/components/ImagePickerBottomSheet.d.ts +15 -0
  92. package/lib/typescript/module/components/ImagePickerBottomSheet.d.ts.map +1 -0
  93. package/lib/typescript/module/components/PhotoPickerModal.d.ts +19 -0
  94. package/lib/typescript/module/components/PhotoPickerModal.d.ts.map +1 -0
  95. package/lib/typescript/module/components/Toasts.d.ts +3 -0
  96. package/lib/typescript/module/components/Toasts.d.ts.map +1 -0
  97. package/lib/typescript/module/components/index.d.ts +6 -0
  98. package/lib/typescript/module/components/index.d.ts.map +1 -1
  99. package/lib/typescript/module/components/svgs/Camera.d.ts +9 -0
  100. package/lib/typescript/module/components/svgs/Camera.d.ts.map +1 -0
  101. package/lib/typescript/module/hooks/index.d.ts +4 -0
  102. package/lib/typescript/module/hooks/index.d.ts.map +1 -0
  103. package/lib/typescript/module/hooks/useInternetConnectionStatus.d.ts +20 -0
  104. package/lib/typescript/module/hooks/useInternetConnectionStatus.d.ts.map +1 -0
  105. package/lib/typescript/module/utils/fileDirectoryUtils.d.ts +3 -0
  106. package/lib/typescript/module/utils/fileDirectoryUtils.d.ts.map +1 -0
  107. package/lib/typescript/module/utils/index.d.ts +2 -0
  108. package/lib/typescript/module/utils/index.d.ts.map +1 -1
  109. package/lib/typescript/module/utils/timeConstants.d.ts +10 -0
  110. package/lib/typescript/module/utils/timeConstants.d.ts.map +1 -0
  111. package/package.json +27 -2
  112. package/src/components/CameraImageInput.tsx +610 -0
  113. package/src/components/CameraWrapperModal.tsx +309 -0
  114. package/src/components/FormikCameraImageInput.tsx +39 -0
  115. package/src/components/ImagePickerBottomSheet.tsx +109 -0
  116. package/src/components/PhotoPickerModal.tsx +116 -0
  117. package/src/components/Toasts.tsx +200 -0
  118. package/src/components/index.ts +6 -0
  119. package/src/components/svgs/Camera.tsx +14 -0
  120. package/src/hooks/index.ts +3 -0
  121. package/src/hooks/useBreakpoints.ts +20 -0
  122. package/src/hooks/useInternetConnectionStatus.ts +221 -0
  123. package/src/hooks/useIsAboveBreakpoint.ts +8 -0
  124. package/src/utils/fileDirectoryUtils.ts +12 -0
  125. package/src/utils/index.ts +2 -0
  126. package/src/utils/timeConstants.ts +19 -0
@@ -0,0 +1,200 @@
1
+ import Constants from 'expo-constants';
2
+ import React, { useEffect, useRef, useState } from 'react';
3
+ import { resolveValue, useToaster } from 'react-hot-toast/headless';
4
+ import type { Toast, ToastType } from 'react-hot-toast/headless';
5
+ import { Animated, LayoutChangeEvent } from 'react-native';
6
+ import { CheckIcon, ExclamationTriangleIcon } from 'react-native-heroicons/outline';
7
+
8
+ import BaseIcon from './BaseIcon';
9
+ import BaseText from './BaseText';
10
+ import { View, ActivityIndicator } from '../hoc-components';
11
+ import utilityColors from '../styles/utility-colors';
12
+
13
+ interface ToastProps {
14
+ t: Toast;
15
+ updateHeight: (height: number) => void;
16
+ offset: number;
17
+ }
18
+
19
+ function DefaultIcon({ type }: { type: ToastType }): JSX.Element | null {
20
+ switch (type) {
21
+ case 'success':
22
+ return (
23
+ <View className="mr-2 flex items-center justify-center rounded-full bg-white p-0.5">
24
+ <BaseIcon
25
+ icon={CheckIcon}
26
+ size={12}
27
+ strokeWidth={2}
28
+ color="fg-success-primary"
29
+ />
30
+ </View>
31
+ );
32
+ case 'error':
33
+ return (
34
+ <View className="mr-2">
35
+ <BaseIcon
36
+ icon={ExclamationTriangleIcon}
37
+ size={16}
38
+ strokeWidth={2}
39
+ color="fg-white"
40
+ />
41
+ </View>
42
+ );
43
+ default:
44
+ return null;
45
+ }
46
+ }
47
+
48
+ function ToastIcon({ t }: { t: Toast }) {
49
+ if (t.type === 'loading') {
50
+ return (
51
+ <ActivityIndicator
52
+ color={utilityColors['utility-gray-400'].light}
53
+ size="small"
54
+ style={{ marginRight: 8 }}
55
+ />
56
+ );
57
+ }
58
+
59
+ if (t.icon) {
60
+ return (
61
+ <BaseText className="mr-2">
62
+ {t.icon}
63
+ {' '}
64
+ </BaseText>
65
+ );
66
+ }
67
+
68
+ return <DefaultIcon type={t.type} />;
69
+ }
70
+
71
+ function Toast({ t, updateHeight, offset }: ToastProps) {
72
+ const fadeAnim = useRef(new Animated.Value(0.5)).current;
73
+ const posAnim = useRef(new Animated.Value(-80)).current;
74
+
75
+ useEffect(() => {
76
+ Animated.timing(fadeAnim, {
77
+ useNativeDriver: true,
78
+ toValue: t.visible ? 1 : 0,
79
+ duration: 300,
80
+ }).start();
81
+ }, [fadeAnim, t.visible]);
82
+
83
+ useEffect(() => {
84
+ Animated.spring(posAnim, {
85
+ useNativeDriver: true,
86
+ toValue: t.visible ? offset : -80,
87
+ }).start();
88
+ }, [posAnim, offset, t.visible]);
89
+
90
+ const backgroundColor = (): string => {
91
+ switch (t.type) {
92
+ case 'error': return utilityColors['utility-error-500'].light;
93
+ case 'success': return utilityColors['utility-success-500'].light;
94
+ case 'loading': return utilityColors['utility-gray-50'].light;
95
+ default: return utilityColors['utility-brand-500'].light;
96
+ }
97
+ };
98
+
99
+ const textColor = (): string => {
100
+ switch (t.type) {
101
+ case 'loading': return utilityColors['utility-gray-500'].light;
102
+ default: return 'white';
103
+ }
104
+ };
105
+
106
+ const [isMultiline, setIsMultiline] = useState(false);
107
+ const containerWidth = useRef(0);
108
+
109
+ const handleTextLayout = (event: LayoutChangeEvent) => {
110
+ const { height } = event.nativeEvent.layout;
111
+ const singleLineTextHeight = 40;
112
+
113
+ setIsMultiline(height > singleLineTextHeight);
114
+ };
115
+
116
+ const message = resolveValue(t.message, t);
117
+
118
+ return (
119
+ <Animated.View
120
+ style={{
121
+ position: 'absolute',
122
+ left: 0,
123
+ right: 0,
124
+ zIndex: t.visible ? 9999 : undefined,
125
+ alignItems: 'center',
126
+ opacity: fadeAnim,
127
+ transform: [
128
+ {
129
+ translateY: posAnim,
130
+ },
131
+ ],
132
+ }}
133
+ >
134
+ <View
135
+ onLayout={(event: LayoutChangeEvent) => {
136
+ updateHeight(event.nativeEvent.layout.height);
137
+ containerWidth.current = event.nativeEvent.layout.width;
138
+ }}
139
+ key={t.id}
140
+ style={{
141
+ margin: Constants.statusBarHeight + 10,
142
+ backgroundColor: backgroundColor(),
143
+ borderRadius: 30,
144
+ borderWidth: t.type === 'loading' ? 1 : 0,
145
+ borderColor: utilityColors['utility-gray-200'].light,
146
+ flexDirection: 'row',
147
+ maxWidth: '80%',
148
+ minWidth: 200,
149
+ alignItems: 'center',
150
+ alignSelf: 'center',
151
+ paddingVertical: 8,
152
+ paddingHorizontal: 24,
153
+ }}
154
+ >
155
+ <ToastIcon t={t} />
156
+ {typeof message === 'string' ? (
157
+ <BaseText
158
+ onLayout={handleTextLayout}
159
+ style={{
160
+ color: textColor(),
161
+ padding: 4,
162
+ alignSelf: 'center',
163
+ flexGrow: 1,
164
+ textAlign: isMultiline ? 'left' : 'center',
165
+ }}
166
+ >
167
+ {message}
168
+ </BaseText>
169
+ ) : message}
170
+ </View>
171
+ </Animated.View>
172
+ );
173
+ }
174
+
175
+ export default function Toasts() {
176
+ const { toasts, handlers } = useToaster();
177
+
178
+ return (
179
+ <View
180
+ style={{
181
+ position: 'absolute',
182
+ top: 0,
183
+ left: 0,
184
+ right: 0,
185
+ zIndex: 9999,
186
+ }}
187
+ >
188
+ {toasts.map((t) => (
189
+ <Toast
190
+ key={t.id}
191
+ t={t}
192
+ updateHeight={(height) => handlers.updateHeight(t.id, height)}
193
+ offset={handlers.calculateOffset(t, {
194
+ reverseOrder: false,
195
+ })}
196
+ />
197
+ ))}
198
+ </View>
199
+ );
200
+ }
@@ -17,3 +17,9 @@ export { default as FormikDateInput } from './FormikDateInput';
17
17
  export { default as BaseSwitch } from './BaseSwitch';
18
18
  export { default as PressableOpacity } from './PressableOpacity';
19
19
  export { default as BaseText } from './BaseText';
20
+ export { default as Toasts } from './Toasts';
21
+ export { default as CameraWrapperModal, CameraWrapperModalImage } from './CameraWrapperModal';
22
+ export { default as ImagePickerBottomSheet, ImagePickerBottomSheetImage } from './ImagePickerBottomSheet';
23
+ export { default as PhotoPickerModal } from './PhotoPickerModal';
24
+ export { default as CameraImageInput, CameraImageInputProps, CameraImage } from './CameraImageInput';
25
+ export { default as FormikCameraImageInput } from './FormikCameraImageInput';
@@ -0,0 +1,14 @@
1
+ import { styled } from 'nativewind';
2
+ import { ComponentProps } from 'react';
3
+
4
+ import CameraSvg from '../../assets/illustrations/camera.svg';
5
+
6
+ type Props = ComponentProps<typeof CameraSvg>;
7
+
8
+ function Camera(props: Props) {
9
+ return (
10
+ <CameraSvg {...props} />
11
+ );
12
+ }
13
+
14
+ export default styled(Camera);
@@ -0,0 +1,3 @@
1
+ export * from './useBreakpoints';
2
+ export * from './useIsAboveBreakpoint';
3
+ export { default as useInternetConnectionStatus } from './useInternetConnectionStatus';
@@ -0,0 +1,20 @@
1
+ import { useMemo } from 'react';
2
+ import { useWindowDimensions } from 'react-native';
3
+
4
+ export const BREAKPOINTS = {
5
+ xs: 378,
6
+ sm: 436,
7
+ base: 494,
8
+ };
9
+
10
+ export type Breakpoint = keyof typeof BREAKPOINTS;
11
+
12
+ export function useBreakpoints(): Breakpoint {
13
+ const { width } = useWindowDimensions();
14
+
15
+ return useMemo(() => {
16
+ if (width < BREAKPOINTS.sm) return 'xs';
17
+ if (width < BREAKPOINTS.base) return 'sm';
18
+ return 'base';
19
+ }, [width]);
20
+ }
@@ -0,0 +1,221 @@
1
+ import AsyncStorage from '@react-native-async-storage/async-storage';
2
+ import { useNetInfo } from '@react-native-community/netinfo';
3
+ import {
4
+ useState, useEffect, useCallback, useRef, useMemo,
5
+ } from 'react';
6
+
7
+ import { MS_IN_A_SECOND } from '../utils/timeConstants';
8
+
9
+ export interface InternetConnectionStatus {
10
+ isConnected: boolean | null;
11
+ isLoading: boolean;
12
+ lastChecked: Date | null;
13
+ isWeakConnection: boolean;
14
+ }
15
+
16
+ export interface InternetConnectionStatusProps {
17
+ checkInterval?: number;
18
+ timeout?: number;
19
+ checkHealth: () => Promise<{ ok: boolean }>;
20
+ }
21
+
22
+ interface CachedInternetConnectionStatus {
23
+ hasInternetAccess: boolean;
24
+ timestamp: number;
25
+ }
26
+
27
+ const DEFAULT_SETTINGS: Required<Omit<InternetConnectionStatusProps, 'checkHealth'>> = {
28
+ checkInterval: 15 * MS_IN_A_SECOND,
29
+ timeout: 5 * MS_IN_A_SECOND,
30
+ };
31
+
32
+ const CACHE_KEY = 'internet_connectivity_status';
33
+ const CACHE_EXPIRY_TIME = 10 * MS_IN_A_SECOND;
34
+ const STRONG_CONNECTION_THRESHOLD = 20;
35
+
36
+ let ongoingNetworkTest: Promise<boolean> | null = null;
37
+
38
+ // Global event broadcaster for syncing all instances
39
+ const globalListeners = new Set<(hasInternetAccess: boolean | null) => void>();
40
+
41
+ function broadcastConnectionChange(hasInternetAccess: boolean | null) {
42
+ globalListeners.forEach((listener) => {
43
+ try {
44
+ listener(hasInternetAccess);
45
+ } catch {
46
+ // Ignore listener errors
47
+ }
48
+ });
49
+ }
50
+
51
+ async function getCachedStatus() {
52
+ try {
53
+ const cached = await AsyncStorage.getItem(CACHE_KEY);
54
+ if (!cached) return null;
55
+
56
+ const parsedCache: CachedInternetConnectionStatus = JSON.parse(cached);
57
+ const isExpired = Date.now() - parsedCache.timestamp > CACHE_EXPIRY_TIME;
58
+
59
+ if (isExpired) {
60
+ await AsyncStorage.removeItem(CACHE_KEY);
61
+ return null;
62
+ }
63
+
64
+ return parsedCache;
65
+ } catch {
66
+ return null;
67
+ }
68
+ }
69
+
70
+ async function setCachedStatus(internetAccess: boolean) {
71
+ try {
72
+ const cacheData: CachedInternetConnectionStatus = {
73
+ hasInternetAccess: internetAccess,
74
+ timestamp: Date.now(),
75
+ };
76
+ await AsyncStorage.setItem(CACHE_KEY, JSON.stringify(cacheData));
77
+ } catch {
78
+ // Silently fail if we can't write to cache
79
+ }
80
+ }
81
+
82
+ export default function useInternetConnectionStatus(props: InternetConnectionStatusProps) {
83
+ const { checkInterval, timeout, checkHealth } = { ...DEFAULT_SETTINGS, ...props };
84
+
85
+ const netInfo = useNetInfo();
86
+
87
+ const [successfulHealthCheck, setSuccessfulHealthCheck] = useState<boolean | null>(null);
88
+ const [isLoading, setIsLoading] = useState(false);
89
+ const [lastChecked, setLastChecked] = useState<Date | null>(null);
90
+
91
+ const intervalRef = useRef<NodeJS.Timeout | null>(null);
92
+ const abortControllerRef = useRef<AbortController | null>(null);
93
+
94
+ const cleanupIntervals = useCallback(() => {
95
+ if (intervalRef.current) {
96
+ clearInterval(intervalRef.current);
97
+ intervalRef.current = null;
98
+ }
99
+ if (abortControllerRef.current) {
100
+ abortControllerRef.current.abort();
101
+ abortControllerRef.current = null;
102
+ }
103
+ }, []);
104
+
105
+ const performNetworkTest = useCallback(async (): Promise<boolean> => {
106
+ if (abortControllerRef.current) {
107
+ abortControllerRef.current.abort();
108
+ }
109
+
110
+ const abortController = new AbortController();
111
+ abortControllerRef.current = abortController;
112
+
113
+ const timeoutId = setTimeout(() => {
114
+ abortController.abort();
115
+ }, timeout);
116
+
117
+ try {
118
+ const response = await checkHealth();
119
+
120
+ clearTimeout(timeoutId);
121
+ const isConnected = response.ok;
122
+
123
+ await setCachedStatus(isConnected);
124
+
125
+ setSuccessfulHealthCheck(isConnected);
126
+ setLastChecked(new Date());
127
+
128
+ broadcastConnectionChange(isConnected);
129
+
130
+ return isConnected;
131
+ } catch (err) {
132
+ clearTimeout(timeoutId);
133
+ if (err instanceof Error && err.name === 'AbortError') return false;
134
+
135
+ await setCachedStatus(false);
136
+
137
+ setSuccessfulHealthCheck(false);
138
+ setLastChecked(new Date());
139
+
140
+ broadcastConnectionChange(false);
141
+
142
+ return false;
143
+ } finally {
144
+ abortControllerRef.current = null;
145
+ }
146
+ }, [timeout]); // eslint-disable-line react-hooks/exhaustive-deps
147
+
148
+ const requestCachedStatusOrPerformNetworkTest = useCallback(async () => {
149
+ const cachedStatus = await getCachedStatus();
150
+ if (cachedStatus) {
151
+ setSuccessfulHealthCheck(cachedStatus.hasInternetAccess);
152
+ setLastChecked(new Date(cachedStatus.timestamp));
153
+
154
+ return cachedStatus.hasInternetAccess;
155
+ }
156
+
157
+ setIsLoading(true);
158
+
159
+ if (ongoingNetworkTest) {
160
+ try {
161
+ const result = await ongoingNetworkTest;
162
+ return result;
163
+ } catch {
164
+ return false;
165
+ } finally {
166
+ setIsLoading(false);
167
+ }
168
+ }
169
+
170
+ ongoingNetworkTest = performNetworkTest();
171
+
172
+ try {
173
+ const result = await ongoingNetworkTest;
174
+ return result;
175
+ } catch {
176
+ return false;
177
+ } finally {
178
+ setIsLoading(false);
179
+ ongoingNetworkTest = null;
180
+ }
181
+ }, [performNetworkTest]);
182
+
183
+ // Setup
184
+ useEffect(() => {
185
+ cleanupIntervals();
186
+
187
+ if (checkInterval > 0) {
188
+ requestCachedStatusOrPerformNetworkTest();
189
+
190
+ intervalRef.current = setInterval(() => {
191
+ requestCachedStatusOrPerformNetworkTest();
192
+ }, checkInterval);
193
+ }
194
+
195
+ return cleanupIntervals;
196
+ }, [requestCachedStatusOrPerformNetworkTest, cleanupIntervals, checkInterval]);
197
+
198
+ const { isConnected, type, details } = netInfo;
199
+
200
+ const result = useMemo(() => ({
201
+ isConnected: successfulHealthCheck,
202
+ isLoading,
203
+ lastChecked,
204
+ isWeakConnection: isConnected === true && type === 'wifi' && details.strength && details.strength < STRONG_CONNECTION_THRESHOLD,
205
+ }), [successfulHealthCheck, isLoading, lastChecked, isConnected, type, details]);
206
+
207
+ // Listen for changes from other instances
208
+ useEffect(() => {
209
+ const listener = (globalHasInternetAccess: boolean | null) => {
210
+ if (successfulHealthCheck !== globalHasInternetAccess) {
211
+ setSuccessfulHealthCheck(globalHasInternetAccess);
212
+ AsyncStorage.removeItem(CACHE_KEY).catch(() => {});
213
+ }
214
+ };
215
+
216
+ globalListeners.add(listener);
217
+ return () => { globalListeners.delete(listener); };
218
+ }, [successfulHealthCheck]);
219
+
220
+ return result;
221
+ }
@@ -0,0 +1,8 @@
1
+ import { useWindowDimensions } from 'react-native';
2
+
3
+ import { BREAKPOINTS, Breakpoint } from './useBreakpoints';
4
+
5
+ export function useIsAboveBreakpoint(minBreakpoint: Breakpoint): boolean {
6
+ const { width } = useWindowDimensions();
7
+ return width >= BREAKPOINTS[minBreakpoint];
8
+ }
@@ -0,0 +1,12 @@
1
+ import * as FileSystem from 'expo-file-system';
2
+
3
+ const ensureDirExists = async (dirPath: string) => {
4
+ const dirInfo = await FileSystem.getInfoAsync(dirPath);
5
+ if (!dirInfo.exists) {
6
+ await FileSystem.makeDirectoryAsync(dirPath, { intermediates: true });
7
+ }
8
+
9
+ return dirPath;
10
+ };
11
+
12
+ export { ensureDirExists };
@@ -1 +1,3 @@
1
1
  export * from './clabe';
2
+ export * from './fileDirectoryUtils';
3
+ export * from './timeConstants';
@@ -0,0 +1,19 @@
1
+ const MS_IN_A_SECOND = 1_000;
2
+ const HOURS_IN_A_DAY = 24;
3
+ const MINUTES_IN_A_HOUR = 60;
4
+ const SECONDS_IN_A_MINUTE = 60;
5
+ const DAYS_IN_A_WEEK = 7;
6
+ const MS_IN_A_DAY = MS_IN_A_SECOND * SECONDS_IN_A_MINUTE * MINUTES_IN_A_HOUR * HOURS_IN_A_DAY;
7
+ const MS_IN_A_MINUTE = MS_IN_A_SECOND * SECONDS_IN_A_MINUTE;
8
+ const MS_IN_A_WEEK = MS_IN_A_DAY * DAYS_IN_A_WEEK;
9
+
10
+ export {
11
+ MS_IN_A_SECOND,
12
+ HOURS_IN_A_DAY,
13
+ MINUTES_IN_A_HOUR,
14
+ SECONDS_IN_A_MINUTE,
15
+ DAYS_IN_A_WEEK,
16
+ MS_IN_A_DAY,
17
+ MS_IN_A_MINUTE,
18
+ MS_IN_A_WEEK,
19
+ };