@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.
- package/lib/commonjs/assets/illustrations/camera.svg +87 -0
- package/lib/commonjs/components/CameraImageInput.js +478 -0
- package/lib/commonjs/components/CameraImageInput.js.map +1 -0
- package/lib/commonjs/components/CameraWrapperModal.js +255 -0
- package/lib/commonjs/components/CameraWrapperModal.js.map +1 -0
- package/lib/commonjs/components/FormikCameraImageInput.js +37 -0
- package/lib/commonjs/components/FormikCameraImageInput.js.map +1 -0
- package/lib/commonjs/components/ImagePickerBottomSheet.js +100 -0
- package/lib/commonjs/components/ImagePickerBottomSheet.js.map +1 -0
- package/lib/commonjs/components/PhotoPickerModal.js +98 -0
- package/lib/commonjs/components/PhotoPickerModal.js.map +1 -0
- package/lib/commonjs/components/Toasts.js +188 -0
- package/lib/commonjs/components/Toasts.js.map +1 -0
- package/lib/commonjs/components/index.js +66 -0
- package/lib/commonjs/components/index.js.map +1 -1
- package/lib/commonjs/components/svgs/Camera.js +17 -0
- package/lib/commonjs/components/svgs/Camera.js.map +1 -0
- package/lib/commonjs/hooks/index.js +41 -0
- package/lib/commonjs/hooks/index.js.map +1 -0
- package/lib/commonjs/hooks/useInternetConnectionStatus.js +182 -0
- package/lib/commonjs/hooks/useInternetConnectionStatus.js.map +1 -0
- package/lib/commonjs/types/svg.d.js +2 -0
- package/lib/commonjs/types/svg.d.js.map +1 -0
- package/lib/commonjs/utils/fileDirectoryUtils.js +19 -0
- package/lib/commonjs/utils/fileDirectoryUtils.js.map +1 -0
- package/lib/commonjs/utils/index.js +22 -0
- package/lib/commonjs/utils/index.js.map +1 -1
- package/lib/commonjs/utils/timeConstants.js +15 -0
- package/lib/commonjs/utils/timeConstants.js.map +1 -0
- package/lib/module/assets/illustrations/camera.svg +87 -0
- package/lib/module/components/CameraImageInput.js +471 -0
- package/lib/module/components/CameraImageInput.js.map +1 -0
- package/lib/module/components/CameraWrapperModal.js +250 -0
- package/lib/module/components/CameraWrapperModal.js.map +1 -0
- package/lib/module/components/FormikCameraImageInput.js +32 -0
- package/lib/module/components/FormikCameraImageInput.js.map +1 -0
- package/lib/module/components/ImagePickerBottomSheet.js +95 -0
- package/lib/module/components/ImagePickerBottomSheet.js.map +1 -0
- package/lib/module/components/PhotoPickerModal.js +93 -0
- package/lib/module/components/PhotoPickerModal.js.map +1 -0
- package/lib/module/components/Toasts.js +182 -0
- package/lib/module/components/Toasts.js.map +1 -0
- package/lib/module/components/index.js +6 -0
- package/lib/module/components/index.js.map +1 -1
- package/lib/module/components/svgs/Camera.js +12 -0
- package/lib/module/components/svgs/Camera.js.map +1 -0
- package/lib/module/hooks/index.js +6 -0
- package/lib/module/hooks/index.js.map +1 -0
- package/lib/module/hooks/useInternetConnectionStatus.js +177 -0
- package/lib/module/hooks/useInternetConnectionStatus.js.map +1 -0
- package/lib/module/types/svg.d.js +2 -0
- package/lib/module/types/svg.d.js.map +1 -0
- package/lib/module/utils/fileDirectoryUtils.js +14 -0
- package/lib/module/utils/fileDirectoryUtils.js.map +1 -0
- package/lib/module/utils/index.js +2 -0
- package/lib/module/utils/index.js.map +1 -1
- package/lib/module/utils/timeConstants.js +12 -0
- package/lib/module/utils/timeConstants.js.map +1 -0
- package/lib/typescript/commonjs/components/CameraImageInput.d.ts +41 -0
- package/lib/typescript/commonjs/components/CameraImageInput.d.ts.map +1 -0
- package/lib/typescript/commonjs/components/CameraWrapperModal.d.ts +18 -0
- package/lib/typescript/commonjs/components/CameraWrapperModal.d.ts.map +1 -0
- package/lib/typescript/commonjs/components/FormikCameraImageInput.d.ts +14 -0
- package/lib/typescript/commonjs/components/FormikCameraImageInput.d.ts.map +1 -0
- package/lib/typescript/commonjs/components/ImagePickerBottomSheet.d.ts +15 -0
- package/lib/typescript/commonjs/components/ImagePickerBottomSheet.d.ts.map +1 -0
- package/lib/typescript/commonjs/components/PhotoPickerModal.d.ts +19 -0
- package/lib/typescript/commonjs/components/PhotoPickerModal.d.ts.map +1 -0
- package/lib/typescript/commonjs/components/Toasts.d.ts +3 -0
- package/lib/typescript/commonjs/components/Toasts.d.ts.map +1 -0
- package/lib/typescript/commonjs/components/index.d.ts +6 -0
- package/lib/typescript/commonjs/components/index.d.ts.map +1 -1
- package/lib/typescript/commonjs/components/svgs/Camera.d.ts +9 -0
- package/lib/typescript/commonjs/components/svgs/Camera.d.ts.map +1 -0
- package/lib/typescript/commonjs/hooks/index.d.ts +4 -0
- package/lib/typescript/commonjs/hooks/index.d.ts.map +1 -0
- package/lib/typescript/commonjs/hooks/useInternetConnectionStatus.d.ts +20 -0
- package/lib/typescript/commonjs/hooks/useInternetConnectionStatus.d.ts.map +1 -0
- package/lib/typescript/commonjs/utils/fileDirectoryUtils.d.ts +3 -0
- package/lib/typescript/commonjs/utils/fileDirectoryUtils.d.ts.map +1 -0
- package/lib/typescript/commonjs/utils/index.d.ts +2 -0
- package/lib/typescript/commonjs/utils/index.d.ts.map +1 -1
- package/lib/typescript/commonjs/utils/timeConstants.d.ts +10 -0
- package/lib/typescript/commonjs/utils/timeConstants.d.ts.map +1 -0
- package/lib/typescript/module/components/CameraImageInput.d.ts +41 -0
- package/lib/typescript/module/components/CameraImageInput.d.ts.map +1 -0
- package/lib/typescript/module/components/CameraWrapperModal.d.ts +18 -0
- package/lib/typescript/module/components/CameraWrapperModal.d.ts.map +1 -0
- package/lib/typescript/module/components/FormikCameraImageInput.d.ts +14 -0
- package/lib/typescript/module/components/FormikCameraImageInput.d.ts.map +1 -0
- package/lib/typescript/module/components/ImagePickerBottomSheet.d.ts +15 -0
- package/lib/typescript/module/components/ImagePickerBottomSheet.d.ts.map +1 -0
- package/lib/typescript/module/components/PhotoPickerModal.d.ts +19 -0
- package/lib/typescript/module/components/PhotoPickerModal.d.ts.map +1 -0
- package/lib/typescript/module/components/Toasts.d.ts +3 -0
- package/lib/typescript/module/components/Toasts.d.ts.map +1 -0
- package/lib/typescript/module/components/index.d.ts +6 -0
- package/lib/typescript/module/components/index.d.ts.map +1 -1
- package/lib/typescript/module/components/svgs/Camera.d.ts +9 -0
- package/lib/typescript/module/components/svgs/Camera.d.ts.map +1 -0
- package/lib/typescript/module/hooks/index.d.ts +4 -0
- package/lib/typescript/module/hooks/index.d.ts.map +1 -0
- package/lib/typescript/module/hooks/useInternetConnectionStatus.d.ts +20 -0
- package/lib/typescript/module/hooks/useInternetConnectionStatus.d.ts.map +1 -0
- package/lib/typescript/module/utils/fileDirectoryUtils.d.ts +3 -0
- package/lib/typescript/module/utils/fileDirectoryUtils.d.ts.map +1 -0
- package/lib/typescript/module/utils/index.d.ts +2 -0
- package/lib/typescript/module/utils/index.d.ts.map +1 -1
- package/lib/typescript/module/utils/timeConstants.d.ts +10 -0
- package/lib/typescript/module/utils/timeConstants.d.ts.map +1 -0
- package/package.json +27 -2
- package/src/components/CameraImageInput.tsx +610 -0
- package/src/components/CameraWrapperModal.tsx +309 -0
- package/src/components/FormikCameraImageInput.tsx +39 -0
- package/src/components/ImagePickerBottomSheet.tsx +109 -0
- package/src/components/PhotoPickerModal.tsx +116 -0
- package/src/components/Toasts.tsx +200 -0
- package/src/components/index.ts +6 -0
- package/src/components/svgs/Camera.tsx +14 -0
- package/src/hooks/index.ts +3 -0
- package/src/hooks/useBreakpoints.ts +20 -0
- package/src/hooks/useInternetConnectionStatus.ts +221 -0
- package/src/hooks/useIsAboveBreakpoint.ts +8 -0
- package/src/utils/fileDirectoryUtils.ts +12 -0
- package/src/utils/index.ts +2 -0
- 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
|
+
}
|
package/src/components/index.ts
CHANGED
|
@@ -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,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 };
|
package/src/utils/index.ts
CHANGED
|
@@ -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
|
+
};
|