rn-toastify 1.0.12 → 2.0.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.
@@ -0,0 +1,84 @@
1
+ import React, { useEffect } from 'react';
2
+ import { View, StyleSheet } from 'react-native';
3
+ import Animated, {
4
+ useSharedValue,
5
+ useAnimatedStyle,
6
+ withTiming,
7
+ withDelay,
8
+ withSequence,
9
+ Easing,
10
+ } from 'react-native-reanimated';
11
+
12
+ /**
13
+ * WarningIcon — Animated exclamation mark with subtle shake.
14
+ * Bar + dot that fade in, followed by a quick shake.
15
+ */
16
+ const WarningIcon = ({ size = 22, color = '#D97706', animated = true }) => {
17
+ const opacity = useSharedValue(animated ? 0 : 1);
18
+ const shake = useSharedValue(0);
19
+
20
+ useEffect(() => {
21
+ if (animated) {
22
+ opacity.value = withDelay(
23
+ 100,
24
+ withTiming(1, { duration: 350, easing: Easing.out(Easing.cubic) })
25
+ );
26
+ shake.value = withDelay(
27
+ 350,
28
+ withSequence(
29
+ withTiming(-2.5, { duration: 40 }),
30
+ withTiming(2.5, { duration: 40 }),
31
+ withTiming(-1.5, { duration: 40 }),
32
+ withTiming(1.5, { duration: 40 }),
33
+ withTiming(0, { duration: 40 })
34
+ )
35
+ );
36
+ }
37
+ }, [animated]);
38
+
39
+ const contentStyle = useAnimatedStyle(() => ({
40
+ opacity: opacity.value,
41
+ transform: [{ translateX: shake.value }],
42
+ }));
43
+
44
+ const barW = Math.max(2, size * 0.12);
45
+ const barH = size * 0.35;
46
+ const dotSize = Math.max(3, size * 0.16);
47
+
48
+ return (
49
+ <View style={[styles.container, { width: size, height: size }]}>
50
+ <Animated.View style={[styles.content, contentStyle]}>
51
+ <View
52
+ style={{
53
+ width: barW,
54
+ height: barH,
55
+ backgroundColor: color,
56
+ borderRadius: barW,
57
+ marginBottom: size * 0.08,
58
+ }}
59
+ />
60
+ <View
61
+ style={{
62
+ width: dotSize,
63
+ height: dotSize,
64
+ borderRadius: dotSize / 2,
65
+ backgroundColor: color,
66
+ }}
67
+ />
68
+ </Animated.View>
69
+ </View>
70
+ );
71
+ };
72
+
73
+ const styles = StyleSheet.create({
74
+ container: {
75
+ alignItems: 'center',
76
+ justifyContent: 'center',
77
+ },
78
+ content: {
79
+ alignItems: 'center',
80
+ justifyContent: 'center',
81
+ },
82
+ });
83
+
84
+ export default React.memo(WarningIcon);
@@ -0,0 +1,5 @@
1
+ export { default as CheckIcon } from './CheckIcon';
2
+ export { default as CrossIcon } from './CrossIcon';
3
+ export { default as InfoIcon } from './InfoIcon';
4
+ export { default as WarningIcon } from './WarningIcon';
5
+ export { default as LoadingSpinner } from './LoadingSpinner';
@@ -1,145 +1,224 @@
1
- import React, { useEffect, useState } from 'react';
2
- import { View, StyleSheet, Appearance, StatusBar, Platform } from 'react-native';
3
- import Toast from '../Toast';
4
- import toastManagerInstance from './ToastManager';
5
- import {
6
- heightPercentageToDP as hp,
7
- widthPercentageToDP as wp,
8
- } from '../utils/Pixel/Index';
9
-
10
- const ToastContainer = ({ theme: forcedTheme } = {}) => {
11
- const [toasts, setToasts] = useState([]);
12
- const [theme, setTheme] = useState(forcedTheme ?? (Appearance.getColorScheme() || 'light'));
13
-
14
- useEffect(() => {
15
- const handleShow = (toast) => {
16
- setToasts((prevToasts) => [...prevToasts, toast]);
17
- };
18
-
19
- const handleRemove = (id) => {
20
- setToasts((prevToasts) => prevToasts.filter((toast) => toast.id !== id));
21
- };
22
-
23
- toastManagerInstance.on('show', handleShow);
24
- toastManagerInstance.on('remove', handleRemove);
25
-
26
- let appearanceSub;
27
- if (!forcedTheme && Appearance && Appearance.addChangeListener) {
28
- appearanceSub = Appearance.addChangeListener(({ colorScheme }) => {
29
- setTheme(colorScheme ?? 'light');
30
- });
31
- }
32
-
33
- return () => {
34
- toastManagerInstance.off('show', handleShow);
35
- toastManagerInstance.off('remove', handleRemove);
36
- if (appearanceSub && appearanceSub.remove) appearanceSub.remove();
37
- };
38
- }, [forcedTheme]);
39
-
40
- // Calculate safe top margin for status bar
41
- const getTopMargin = () => {
42
- if (Platform.OS === 'android') {
43
- return StatusBar.currentHeight || 0;
44
- }
45
- // For iOS, use a default safe area top margin
46
- return 44; // Standard iOS notch/status bar height
47
- };
48
-
49
- // Separate toasts by position
50
- const topToasts = toasts.filter(toast => toast?.options?.position === 'top');
51
- const centerToasts = toasts.filter(toast => toast?.options?.position === 'center');
52
- const bottomToasts = toasts.filter(toast => toast?.options?.position === 'bottom');
53
-
54
- const topMargin = getTopMargin();
55
-
56
- return (
57
- <>
58
- {/* Top positioned toasts */}
59
- <View
60
- style={[styles.topContainer, { top: topMargin + hp(1) }]}
61
- pointerEvents="box-none"
62
- >
63
- {topToasts.map((toast, index) => (
64
- <Toast
65
- key={toast.id}
66
- visible={true}
67
- duration={toast?.options?.duration}
68
- position="top"
69
- theme={theme}
70
- style={[toast?.options?.style || {}, { marginTop: index * (hp(7) + hp(1)) }]}
71
- onHide={() => toastManagerInstance.remove(toast.id)}
72
- >
73
- {toast.content}
74
- </Toast>
75
- ))}
76
- </View>
77
-
78
- {/* Center positioned toasts */}
79
- <View style={styles.centerContainer} pointerEvents="box-none">
80
- {centerToasts.map((toast, index) => (
81
- <Toast
82
- key={toast.id}
83
- visible={true}
84
- duration={toast?.options?.duration}
85
- position="center"
86
- theme={theme}
87
- style={[toast?.options?.style || {}, { marginTop: index * (hp(7) + hp(1)) }]}
88
- onHide={() => toastManagerInstance.remove(toast.id)}
89
- >
90
- {toast.content}
91
- </Toast>
92
- ))}
93
- </View>
94
-
95
- {/* Bottom positioned toasts */}
96
- <View style={styles.bottomContainer} pointerEvents="box-none">
97
- {bottomToasts.map((toast, index) => (
98
- <Toast
99
- key={toast.id}
100
- visible={true}
101
- duration={toast?.options?.duration}
102
- position="bottom"
103
- theme={theme}
104
- style={[toast?.options?.style || {}, { bottom: hp(2) + index * (hp(7) + hp(1)) }]}
105
- onHide={() => toastManagerInstance.remove(toast.id)}
106
- >
107
- {toast.content}
108
- </Toast>
109
- ))}
110
- </View>
111
- </>
112
- );
113
- };
114
-
115
- const styles = StyleSheet.create({
116
- topContainer: {
117
- position: 'absolute',
118
- left: 0,
119
- right: 0,
120
- zIndex: 9999,
121
- pointerEvents: 'box-none',
122
- alignItems: 'center',
123
- },
124
- centerContainer: {
125
- position: 'absolute',
126
- top: '50%',
127
- left: 0,
128
- right: 0,
129
- zIndex: 9999,
130
- pointerEvents: 'box-none',
131
- alignItems: 'center',
132
- transform: [{ translateY: -hp(3.5) }]
133
- },
134
- bottomContainer: {
135
- position: 'absolute',
136
- bottom: 0,
137
- left: 0,
138
- right: 0,
139
- zIndex: 9999,
140
- pointerEvents: 'box-none',
141
- alignItems: 'center',
142
- },
143
- });
144
-
1
+ import React, { useEffect, useState, useCallback, useMemo } from 'react';
2
+ import { View, StyleSheet, Appearance, Platform, Keyboard } from 'react-native';
3
+ import Toast from '../Toast';
4
+ import toastManagerInstance from './ToastManager';
5
+ import {
6
+ heightPercentageToDP as hp,
7
+ widthPercentageToDP as wp,
8
+ } from '../utils/Pixel/Index';
9
+
10
+ const ToastContainer = ({
11
+ theme: forcedTheme,
12
+ maxVisible = 3,
13
+ defaultPosition = 'top',
14
+ topOffset = 0,
15
+ bottomOffset = 0,
16
+ swipeable = true,
17
+ } = {}) => {
18
+ const [toasts, setToasts] = useState([]);
19
+ const [theme, setTheme] = useState(forcedTheme ?? (Appearance?.getColorScheme?.() || 'light'));
20
+ const [keyboardHeight, setKeyboardHeight] = useState(0);
21
+
22
+ useEffect(() => {
23
+ // Configure the manager with our settings
24
+ toastManagerInstance.configure({ maxVisible });
25
+ }, [maxVisible]);
26
+
27
+ useEffect(() => {
28
+ const handleShow = (toast) => {
29
+ setToasts((prev) => {
30
+ // Prevent duplicate IDs
31
+ const filtered = prev.filter((t) => t.id !== toast.id);
32
+ return [...filtered, toast];
33
+ });
34
+ };
35
+
36
+ const handleRemove = (id) => {
37
+ setToasts((prev) => prev.filter((toast) => toast.id !== id));
38
+ };
39
+
40
+ const handleUpdate = (updatedToast) => {
41
+ setToasts((prev) =>
42
+ prev.map((t) => (t.id === updatedToast.id ? updatedToast : t))
43
+ );
44
+ };
45
+
46
+ toastManagerInstance.on('show', handleShow);
47
+ toastManagerInstance.on('remove', handleRemove);
48
+ toastManagerInstance.on('update', handleUpdate);
49
+
50
+ // Theme listener
51
+ let appearanceSub;
52
+ if (!forcedTheme && Appearance?.addChangeListener) {
53
+ appearanceSub = Appearance.addChangeListener(({ colorScheme }) => {
54
+ setTheme(colorScheme ?? 'light');
55
+ });
56
+ }
57
+
58
+ // Keyboard listener for bottom toasts
59
+ const keyboardShowSub = Keyboard.addListener(
60
+ Platform.OS === 'ios' ? 'keyboardWillShow' : 'keyboardDidShow',
61
+ (e) => setKeyboardHeight(e.endCoordinates.height)
62
+ );
63
+ const keyboardHideSub = Keyboard.addListener(
64
+ Platform.OS === 'ios' ? 'keyboardWillHide' : 'keyboardDidHide',
65
+ () => setKeyboardHeight(0)
66
+ );
67
+
68
+ return () => {
69
+ toastManagerInstance.off('show', handleShow);
70
+ toastManagerInstance.off('remove', handleRemove);
71
+ toastManagerInstance.off('update', handleUpdate);
72
+ if (appearanceSub?.remove) appearanceSub.remove();
73
+ keyboardShowSub.remove();
74
+ keyboardHideSub.remove();
75
+ };
76
+ }, [forcedTheme]);
77
+
78
+ // Calculate safe top margin
79
+ const getTopMargin = useCallback(() => {
80
+ if (Platform.OS === 'android') {
81
+ // Use StatusBar height if available
82
+ try {
83
+ const { StatusBar } = require('react-native');
84
+ return (StatusBar.currentHeight || 0) + topOffset;
85
+ } catch {
86
+ return topOffset;
87
+ }
88
+ }
89
+ // iOS: default safe area
90
+ return 50 + topOffset;
91
+ }, [topOffset]);
92
+
93
+ // Separate toasts by position
94
+ const { topToasts, centerToasts, bottomToasts } = useMemo(() => {
95
+ const top = [];
96
+ const center = [];
97
+ const bottom = [];
98
+
99
+ toasts.forEach((toast) => {
100
+ const pos = toast?.options?.position || defaultPosition;
101
+ if (pos === 'top') top.push(toast);
102
+ else if (pos === 'center') center.push(toast);
103
+ else bottom.push(toast);
104
+ });
105
+
106
+ return { topToasts: top, centerToasts: center, bottomToasts: bottom };
107
+ }, [toasts, defaultPosition]);
108
+
109
+ const topMargin = getTopMargin();
110
+ const toastSpacing = hp(1.2);
111
+ const toastHeight = hp(8.5);
112
+
113
+ return (
114
+ <>
115
+ {/* Top positioned toasts */}
116
+ {topToasts.length > 0 && (
117
+ <View
118
+ style={[styles.topContainer, { top: topMargin }]}
119
+ pointerEvents="box-none"
120
+ >
121
+ {topToasts.map((toast, index) => (
122
+ <Toast
123
+ key={toast.id}
124
+ visible={true}
125
+ duration={toast?.options?.duration}
126
+ position="top"
127
+ theme={theme}
128
+ style={[
129
+ toast?.options?.style || {},
130
+ { marginTop: index * (toastHeight + toastSpacing) },
131
+ ]}
132
+ onHide={() => toastManagerInstance.remove(toast.id)}
133
+ >
134
+ {toast.content}
135
+ </Toast>
136
+ ))}
137
+ </View>
138
+ )}
139
+
140
+ {/* Center positioned toasts */}
141
+ {centerToasts.length > 0 && (
142
+ <View style={styles.centerContainer} pointerEvents="box-none">
143
+ {centerToasts.map((toast, index) => (
144
+ <Toast
145
+ key={toast.id}
146
+ visible={true}
147
+ duration={toast?.options?.duration}
148
+ position="center"
149
+ theme={theme}
150
+ style={[
151
+ toast?.options?.style || {},
152
+ { marginTop: index * (toastHeight + toastSpacing) },
153
+ ]}
154
+ onHide={() => toastManagerInstance.remove(toast.id)}
155
+ >
156
+ {toast.content}
157
+ </Toast>
158
+ ))}
159
+ </View>
160
+ )}
161
+
162
+ {/* Bottom positioned toasts */}
163
+ {bottomToasts.length > 0 && (
164
+ <View
165
+ style={[
166
+ styles.bottomContainer,
167
+ { bottom: bottomOffset + keyboardHeight },
168
+ ]}
169
+ pointerEvents="box-none"
170
+ >
171
+ {bottomToasts.map((toast, index) => (
172
+ <Toast
173
+ key={toast.id}
174
+ visible={true}
175
+ duration={toast?.options?.duration}
176
+ position="bottom"
177
+ theme={theme}
178
+ style={[
179
+ toast?.options?.style || {},
180
+ { bottom: hp(2) + index * (toastHeight + toastSpacing) },
181
+ ]}
182
+ onHide={() => toastManagerInstance.remove(toast.id)}
183
+ >
184
+ {toast.content}
185
+ </Toast>
186
+ ))}
187
+ </View>
188
+ )}
189
+ </>
190
+ );
191
+ };
192
+
193
+ const styles = StyleSheet.create({
194
+ topContainer: {
195
+ position: 'absolute',
196
+ left: 0,
197
+ right: 0,
198
+ zIndex: 9999,
199
+ pointerEvents: 'box-none',
200
+ alignItems: 'center',
201
+ },
202
+ centerContainer: {
203
+ position: 'absolute',
204
+ top: 0,
205
+ bottom: 0,
206
+ left: 0,
207
+ right: 0,
208
+ zIndex: 9999,
209
+ pointerEvents: 'box-none',
210
+ alignItems: 'center',
211
+ justifyContent: 'center',
212
+ },
213
+ bottomContainer: {
214
+ position: 'absolute',
215
+ bottom: 0,
216
+ left: 0,
217
+ right: 0,
218
+ zIndex: 9999,
219
+ pointerEvents: 'box-none',
220
+ alignItems: 'center',
221
+ },
222
+ });
223
+
145
224
  export default React.memo(ToastContainer);
@@ -1,36 +1,150 @@
1
- import { EventEmitter } from 'events';
2
-
3
- class ToastManager extends EventEmitter {
4
- // show accepts an optional id so callers (like promise) can control the id lifecycle
5
- show(content, options = {}, id = null) {
6
- const _id = id ?? Date.now().toString();
7
- const defaultOptions = {
8
- duration: 1000,
9
- position: 'bottom',
10
- style: {},
11
- };
12
- this.emit('show', { id: _id, content, options: { ...defaultOptions, ...options } });
13
- return _id;
14
- }
15
-
16
- remove(id) {
17
- this.emit('remove', id);
18
- }
19
-
20
- async promise(promise, { loading, success, error }) {
21
- const id = Date.now().toString();
22
- // show the loading toast using the same id so we can remove it later
23
- this.show(loading, { duration: Infinity, position: 'top' }, id);
24
- try {
25
- await promise;
26
- this.remove(id);
27
- this.show(success, { duration: 1000, position: 'top' });
28
- } catch (err) {
29
- this.remove(id);
30
- this.show(error, { duration: 1000, position: 'top' });
31
- }
32
- }
33
- }
34
-
35
- const toastManagerInstance = new ToastManager();
36
- export default toastManagerInstance;
1
+ import { EventEmitter } from 'events';
2
+ import { TOAST_DEFAULTS } from '../utils/theme';
3
+
4
+ let _idCounter = 0;
5
+ const generateId = () => `toast_${++_idCounter}_${Date.now()}`;
6
+
7
+ class ToastManager extends EventEmitter {
8
+ constructor() {
9
+ super();
10
+ this.setMaxListeners(20);
11
+ this._activeToasts = new Map();
12
+ this._queue = [];
13
+ this._maxVisible = TOAST_DEFAULTS.maxVisible;
14
+ }
15
+
16
+ /**
17
+ * Configure the toast manager.
18
+ * @param {{ maxVisible?: number }} config
19
+ */
20
+ configure(config = {}) {
21
+ if (config.maxVisible !== undefined) {
22
+ this._maxVisible = config.maxVisible;
23
+ }
24
+ }
25
+
26
+ /**
27
+ * Show a toast notification.
28
+ * @param {React.ReactNode} content - The toast content to render
29
+ * @param {Object} options - Toast options (duration, position, style)
30
+ * @param {string|null} id - Optional custom ID for the toast
31
+ * @returns {string} The toast ID
32
+ */
33
+ show(content, options = {}, id = null) {
34
+ const _id = id ?? generateId();
35
+ const defaultOptions = {
36
+ duration: TOAST_DEFAULTS.duration,
37
+ position: TOAST_DEFAULTS.position,
38
+ style: {},
39
+ };
40
+
41
+ const toast = {
42
+ id: _id,
43
+ content,
44
+ options: { ...defaultOptions, ...options },
45
+ createdAt: Date.now(),
46
+ };
47
+
48
+ // If we've hit the max, queue the toast or dismiss the oldest
49
+ if (this._activeToasts.size >= this._maxVisible) {
50
+ // Remove the oldest toast to make room
51
+ const oldest = this._activeToasts.keys().next().value;
52
+ if (oldest) {
53
+ this.remove(oldest);
54
+ }
55
+ }
56
+
57
+ this._activeToasts.set(_id, toast);
58
+ this.emit('show', toast);
59
+ return _id;
60
+ }
61
+
62
+ /**
63
+ * Remove a toast by ID.
64
+ * @param {string} id
65
+ */
66
+ remove(id) {
67
+ this._activeToasts.delete(id);
68
+ this.emit('remove', id);
69
+
70
+ // Process queue if there are waiting toasts
71
+ if (this._queue.length > 0 && this._activeToasts.size < this._maxVisible) {
72
+ const next = this._queue.shift();
73
+ if (next) {
74
+ this.show(next.content, next.options, next.id);
75
+ }
76
+ }
77
+ }
78
+
79
+ /**
80
+ * Dismiss all active toasts.
81
+ */
82
+ dismissAll() {
83
+ const ids = Array.from(this._activeToasts.keys());
84
+ ids.forEach((id) => this.remove(id));
85
+ this._queue = [];
86
+ }
87
+
88
+ /**
89
+ * Update an existing toast's content.
90
+ * @param {string} id - The toast ID to update
91
+ * @param {React.ReactNode} content - New content
92
+ * @param {Object} options - New options (merged with existing)
93
+ */
94
+ update(id, content, options = {}) {
95
+ if (this._activeToasts.has(id)) {
96
+ const existing = this._activeToasts.get(id);
97
+ const updated = {
98
+ ...existing,
99
+ content: content ?? existing.content,
100
+ options: { ...existing.options, ...options },
101
+ };
102
+ this._activeToasts.set(id, updated);
103
+ this.emit('update', updated);
104
+ }
105
+ }
106
+
107
+ /**
108
+ * Check if a toast is currently active.
109
+ * @param {string} id
110
+ * @returns {boolean}
111
+ */
112
+ isActive(id) {
113
+ return this._activeToasts.has(id);
114
+ }
115
+
116
+ /**
117
+ * Handle promise states with loading → success/error toasts.
118
+ * @param {Promise} promise
119
+ * @param {{ loading: React.ReactNode, success: React.ReactNode|Function, error: React.ReactNode|Function }} messages
120
+ * @param {Object} options
121
+ */
122
+ async promise(promise, { loading, success, error }, options = {}) {
123
+ const id = generateId();
124
+ // Show the loading toast
125
+ this.show(loading, { ...options, duration: Infinity, position: options.position || 'top' }, id);
126
+
127
+ try {
128
+ const result = await promise;
129
+ this.remove(id);
130
+ // Support function callbacks for dynamic messages
131
+ const successContent = typeof success === 'function' ? success(result) : success;
132
+ this.show(successContent, {
133
+ duration: options.successDuration || TOAST_DEFAULTS.duration,
134
+ position: options.position || 'top',
135
+ });
136
+ return result;
137
+ } catch (err) {
138
+ this.remove(id);
139
+ const errorContent = typeof error === 'function' ? error(err) : error;
140
+ this.show(errorContent, {
141
+ duration: options.errorDuration || TOAST_DEFAULTS.duration,
142
+ position: options.position || 'top',
143
+ });
144
+ throw err;
145
+ }
146
+ }
147
+ }
148
+
149
+ const toastManagerInstance = new ToastManager();
150
+ export default toastManagerInstance;