react-native-earl-toastify 1.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.
Files changed (54) hide show
  1. package/CHANGELOG.md +71 -0
  2. package/LICENSE +21 -0
  3. package/README.md +513 -0
  4. package/dist/Toast.d.ts +12 -0
  5. package/dist/Toast.d.ts.map +1 -0
  6. package/dist/Toast.js +143 -0
  7. package/dist/Toast.js.map +1 -0
  8. package/dist/ToastContainer.d.ts +12 -0
  9. package/dist/ToastContainer.d.ts.map +1 -0
  10. package/dist/ToastContainer.js +46 -0
  11. package/dist/ToastContainer.js.map +1 -0
  12. package/dist/ToastProvider.d.ts +22 -0
  13. package/dist/ToastProvider.d.ts.map +1 -0
  14. package/dist/ToastProvider.js +163 -0
  15. package/dist/ToastProvider.js.map +1 -0
  16. package/dist/animations.d.ts +54 -0
  17. package/dist/animations.d.ts.map +1 -0
  18. package/dist/animations.js +118 -0
  19. package/dist/animations.js.map +1 -0
  20. package/dist/icons.d.ts +12 -0
  21. package/dist/icons.d.ts.map +1 -0
  22. package/dist/icons.js +110 -0
  23. package/dist/icons.js.map +1 -0
  24. package/dist/index.d.ts +10 -0
  25. package/dist/index.d.ts.map +1 -0
  26. package/dist/index.js +14 -0
  27. package/dist/index.js.map +1 -0
  28. package/dist/styles.d.ts +63 -0
  29. package/dist/styles.d.ts.map +1 -0
  30. package/dist/styles.js +182 -0
  31. package/dist/styles.js.map +1 -0
  32. package/dist/types.d.ts +100 -0
  33. package/dist/types.d.ts.map +1 -0
  34. package/dist/types.js +2 -0
  35. package/dist/types.js.map +1 -0
  36. package/dist/useToast.d.ts +40 -0
  37. package/dist/useToast.d.ts.map +1 -0
  38. package/dist/useToast.js +48 -0
  39. package/dist/useToast.js.map +1 -0
  40. package/dist/useToastAnimation.d.ts +24 -0
  41. package/dist/useToastAnimation.d.ts.map +1 -0
  42. package/dist/useToastAnimation.js +59 -0
  43. package/dist/useToastAnimation.js.map +1 -0
  44. package/package.json +58 -0
  45. package/src/Toast.tsx +250 -0
  46. package/src/ToastContainer.tsx +70 -0
  47. package/src/ToastProvider.tsx +271 -0
  48. package/src/animations.ts +161 -0
  49. package/src/icons.tsx +152 -0
  50. package/src/index.ts +41 -0
  51. package/src/styles.ts +208 -0
  52. package/src/types.ts +157 -0
  53. package/src/useToast.ts +53 -0
  54. package/src/useToastAnimation.ts +108 -0
@@ -0,0 +1,59 @@
1
+ import { useRef, useEffect, useCallback } from "react";
2
+ import { Animated } from "react-native";
3
+ import { getInitialAnimatedValues, createEnterAnimation, createExitAnimation, DEFAULT_ANIMATION_CONFIG, } from "./animations";
4
+ /**
5
+ * Custom hook for managing toast animations
6
+ */
7
+ export const useToastAnimation = (animationIn, animationOut, position, animationDuration = DEFAULT_ANIMATION_CONFIG.duration) => {
8
+ // Get initial values based on enter animation
9
+ const initialValues = getInitialAnimatedValues(animationIn, position);
10
+ // Create animated values
11
+ const opacity = useRef(new Animated.Value(initialValues.opacity)).current;
12
+ const translateX = useRef(new Animated.Value(initialValues.translateX)).current;
13
+ const translateY = useRef(new Animated.Value(initialValues.translateY)).current;
14
+ // Animation config
15
+ const config = {
16
+ duration: animationDuration,
17
+ useNativeDriver: true,
18
+ };
19
+ /**
20
+ * Start enter animation
21
+ */
22
+ const startEnterAnimation = useCallback(() => {
23
+ const animation = createEnterAnimation({ opacity, translateX, translateY }, config);
24
+ animation.start();
25
+ }, [opacity, translateX, translateY, config.duration]);
26
+ /**
27
+ * Start exit animation with optional callback
28
+ */
29
+ const startExitAnimation = useCallback((callback) => {
30
+ const animation = createExitAnimation({ opacity, translateX, translateY }, animationOut, position, config);
31
+ animation.start(({ finished }) => {
32
+ if (finished && callback) {
33
+ callback();
34
+ }
35
+ });
36
+ }, [
37
+ opacity,
38
+ translateX,
39
+ translateY,
40
+ animationOut,
41
+ position,
42
+ config.duration,
43
+ ]);
44
+ // Start enter animation on mount
45
+ useEffect(() => {
46
+ startEnterAnimation();
47
+ }, []);
48
+ // Animated style object
49
+ const animatedStyle = {
50
+ opacity,
51
+ transform: [{ translateX }, { translateY }],
52
+ };
53
+ return {
54
+ animatedStyle,
55
+ startEnterAnimation,
56
+ startExitAnimation,
57
+ };
58
+ };
59
+ //# sourceMappingURL=useToastAnimation.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"useToastAnimation.js","sourceRoot":"","sources":["../src/useToastAnimation.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,MAAM,EAAE,SAAS,EAAE,WAAW,EAAE,MAAM,OAAO,CAAC;AACvD,OAAO,EAAE,QAAQ,EAAE,MAAM,cAAc,CAAC;AAExC,OAAO,EACN,wBAAwB,EACxB,oBAAoB,EACpB,mBAAmB,EACnB,wBAAwB,GACxB,MAAM,cAAc,CAAC;AAkBtB;;GAEG;AACH,MAAM,CAAC,MAAM,iBAAiB,GAAG,CAChC,WAA2B,EAC3B,YAA4B,EAC5B,QAAuB,EACvB,oBAA4B,wBAAwB,CAAC,QAAQ,EACnC,EAAE;IAC5B,8CAA8C;IAC9C,MAAM,aAAa,GAAG,wBAAwB,CAAC,WAAW,EAAE,QAAQ,CAAC,CAAC;IAEtE,yBAAyB;IACzB,MAAM,OAAO,GAAG,MAAM,CAAC,IAAI,QAAQ,CAAC,KAAK,CAAC,aAAa,CAAC,OAAO,CAAC,CAAC,CAAC,OAAO,CAAC;IAC1E,MAAM,UAAU,GAAG,MAAM,CACxB,IAAI,QAAQ,CAAC,KAAK,CAAC,aAAa,CAAC,UAAU,CAAC,CAC5C,CAAC,OAAO,CAAC;IACV,MAAM,UAAU,GAAG,MAAM,CACxB,IAAI,QAAQ,CAAC,KAAK,CAAC,aAAa,CAAC,UAAU,CAAC,CAC5C,CAAC,OAAO,CAAC;IAEV,mBAAmB;IACnB,MAAM,MAAM,GAAG;QACd,QAAQ,EAAE,iBAAiB;QAC3B,eAAe,EAAE,IAAI;KACrB,CAAC;IAEF;;OAEG;IACH,MAAM,mBAAmB,GAAG,WAAW,CAAC,GAAG,EAAE;QAC5C,MAAM,SAAS,GAAG,oBAAoB,CACrC,EAAE,OAAO,EAAE,UAAU,EAAE,UAAU,EAAE,EACnC,MAAM,CACN,CAAC;QACF,SAAS,CAAC,KAAK,EAAE,CAAC;IACnB,CAAC,EAAE,CAAC,OAAO,EAAE,UAAU,EAAE,UAAU,EAAE,MAAM,CAAC,QAAQ,CAAC,CAAC,CAAC;IAEvD;;OAEG;IACH,MAAM,kBAAkB,GAAG,WAAW,CACrC,CAAC,QAAqB,EAAE,EAAE;QACzB,MAAM,SAAS,GAAG,mBAAmB,CACpC,EAAE,OAAO,EAAE,UAAU,EAAE,UAAU,EAAE,EACnC,YAAY,EACZ,QAAQ,EACR,MAAM,CACN,CAAC;QACF,SAAS,CAAC,KAAK,CAAC,CAAC,EAAE,QAAQ,EAAE,EAAE,EAAE;YAChC,IAAI,QAAQ,IAAI,QAAQ,EAAE,CAAC;gBAC1B,QAAQ,EAAE,CAAC;YACZ,CAAC;QACF,CAAC,CAAC,CAAC;IACJ,CAAC,EACD;QACC,OAAO;QACP,UAAU;QACV,UAAU;QACV,YAAY;QACZ,QAAQ;QACR,MAAM,CAAC,QAAQ;KACf,CACD,CAAC;IAEF,iCAAiC;IACjC,SAAS,CAAC,GAAG,EAAE;QACd,mBAAmB,EAAE,CAAC;IACvB,CAAC,EAAE,EAAE,CAAC,CAAC;IAEP,wBAAwB;IACxB,MAAM,aAAa,GAA6C;QAC/D,OAAO;QACP,SAAS,EAAE,CAAC,EAAE,UAAU,EAAE,EAAE,EAAE,UAAU,EAAE,CAAqB;KAC/D,CAAC;IAEF,OAAO;QACN,aAAa;QACb,mBAAmB;QACnB,kBAAkB;KAClB,CAAC;AACH,CAAC,CAAC"}
package/package.json ADDED
@@ -0,0 +1,58 @@
1
+ {
2
+ "name": "react-native-earl-toastify",
3
+ "version": "1.0.0",
4
+ "description": "A beautiful, customizable toast notification library for React Native with animations, multiple toast types, and accessibility support",
5
+ "main": "dist/index.js",
6
+ "module": "dist/index.js",
7
+ "types": "dist/index.d.ts",
8
+ "react-native": "src/index.ts",
9
+ "source": "src/index.ts",
10
+ "files": [
11
+ "src",
12
+ "dist",
13
+ "README.md",
14
+ "CHANGELOG.md"
15
+ ],
16
+ "scripts": {
17
+ "build": "tsc",
18
+ "prepublishOnly": "npm run build",
19
+ "test": "echo \"No tests specified\" && exit 0"
20
+ },
21
+ "keywords": [
22
+ "react-native",
23
+ "toast",
24
+ "toastify",
25
+ "notification",
26
+ "alert",
27
+ "message",
28
+ "snackbar",
29
+ "popup",
30
+ "animated",
31
+ "accessible"
32
+ ],
33
+ "author": "Ordovez, Earl Romeo",
34
+ "license": "MIT",
35
+ "repository": {
36
+ "type": "git",
37
+ "url": "https://github.com/Swif7ify/react-native-earl-toastify.git"
38
+ },
39
+ "bugs": {
40
+ "url": "https://github.com/Swif7ify/react-native-earl-toastify/issues"
41
+ },
42
+ "homepage": "https://github.com/Swif7ify/react-native-earl-toastify#readme",
43
+ "funding": {
44
+ "type": "github",
45
+ "url": "https://github.com/sponsors/Swif7ify"
46
+ },
47
+ "peerDependencies": {
48
+ "react": ">=17.0.0",
49
+ "react-native": ">=0.64.0"
50
+ },
51
+ "devDependencies": {
52
+ "@types/react": "^18.2.0",
53
+ "@types/react-native": "^0.72.0",
54
+ "react": "^18.2.0",
55
+ "react-native": "^0.72.0",
56
+ "typescript": "^5.0.0"
57
+ }
58
+ }
package/src/Toast.tsx ADDED
@@ -0,0 +1,250 @@
1
+ import React, { useEffect, useRef, useCallback } from "react";
2
+ import {
3
+ Animated,
4
+ Text,
5
+ TouchableOpacity,
6
+ View,
7
+ StyleSheet,
8
+ AccessibilityInfo,
9
+ } from "react-native";
10
+ import { Toast as ToastData, ToastPosition, ToastAnimation } from "./types";
11
+ import { useToastAnimation } from "./useToastAnimation";
12
+ import {
13
+ getToastColors,
14
+ baseToastStyle,
15
+ getPositionalStyle,
16
+ titleStyle,
17
+ descriptionStyle,
18
+ messageStyle,
19
+ textContainerStyle,
20
+ iconContainerStyle,
21
+ closeButtonStyle,
22
+ } from "./styles";
23
+ import { DefaultIcons } from "./icons";
24
+
25
+ export interface ToastProps {
26
+ toast: ToastData;
27
+ position: ToastPosition;
28
+ onHide: (id: string) => void;
29
+ }
30
+
31
+ /**
32
+ * Individual Toast component with animations
33
+ */
34
+ export const Toast: React.FC<ToastProps> = ({ toast, position, onHide }) => {
35
+ const {
36
+ id,
37
+ title,
38
+ message,
39
+ type = "default",
40
+ duration = 3000,
41
+ animationIn = "fade",
42
+ animationOut = "fade",
43
+ animationDuration = 300,
44
+ dismissable = true,
45
+ icon,
46
+ hideIcon = false,
47
+ backgroundColor,
48
+ textColor,
49
+ borderColor,
50
+ style,
51
+ textStyle,
52
+ onShow,
53
+ onHide: onHideCallback,
54
+ onPress,
55
+ } = toast;
56
+
57
+ const timerRef = useRef<NodeJS.Timeout | null>(null);
58
+ const isHidingRef = useRef(false);
59
+
60
+ // Get colors based on type
61
+ const colors = getToastColors(type);
62
+ const finalBackgroundColor = backgroundColor || colors.background;
63
+ const finalTextColor = textColor || colors.text;
64
+ const finalBorderColor = borderColor || colors.border;
65
+
66
+ // Animation hook
67
+ const { animatedStyle, startExitAnimation } = useToastAnimation(
68
+ animationIn,
69
+ animationOut,
70
+ position,
71
+ animationDuration,
72
+ );
73
+
74
+ /**
75
+ * Handle hiding the toast
76
+ */
77
+ const handleHide = useCallback(() => {
78
+ if (isHidingRef.current) return;
79
+ isHidingRef.current = true;
80
+
81
+ // Clear timer
82
+ if (timerRef.current) {
83
+ clearTimeout(timerRef.current);
84
+ timerRef.current = null;
85
+ }
86
+
87
+ // Start exit animation
88
+ startExitAnimation(() => {
89
+ onHideCallback?.();
90
+ onHide(id);
91
+ });
92
+ }, [id, onHide, onHideCallback, startExitAnimation]);
93
+
94
+ /**
95
+ * Handle press on toast
96
+ */
97
+ const handlePress = useCallback(() => {
98
+ onPress?.();
99
+ if (dismissable) {
100
+ handleHide();
101
+ }
102
+ }, [onPress, dismissable, handleHide]);
103
+
104
+ // Setup auto-dismiss timer
105
+ useEffect(() => {
106
+ // Call onShow callback
107
+ onShow?.();
108
+
109
+ // Announce to accessibility
110
+ AccessibilityInfo.announceForAccessibility(message);
111
+
112
+ // Setup auto-dismiss if duration > 0
113
+ if (duration > 0) {
114
+ timerRef.current = setTimeout(() => {
115
+ handleHide();
116
+ }, duration);
117
+ }
118
+
119
+ return () => {
120
+ if (timerRef.current) {
121
+ clearTimeout(timerRef.current);
122
+ }
123
+ };
124
+ }, [duration, message, onShow, handleHide]);
125
+
126
+ // Render icon
127
+ const renderIcon = () => {
128
+ if (hideIcon) return null;
129
+
130
+ if (icon) {
131
+ return <View style={iconContainerStyle}>{icon}</View>;
132
+ }
133
+
134
+ // Use default icon based on type
135
+ const DefaultIcon = DefaultIcons[type];
136
+ if (DefaultIcon) {
137
+ return (
138
+ <View style={iconContainerStyle}>
139
+ <DefaultIcon color={colors.icon} size={20} />
140
+ </View>
141
+ );
142
+ }
143
+
144
+ return null;
145
+ };
146
+
147
+ // Render close button
148
+ const renderCloseButton = () => {
149
+ if (!dismissable) return null;
150
+
151
+ return (
152
+ <TouchableOpacity
153
+ style={closeButtonStyle}
154
+ onPress={handleHide}
155
+ accessibilityRole="button"
156
+ accessibilityLabel="Dismiss notification"
157
+ hitSlop={{ top: 10, bottom: 10, left: 10, right: 10 }}
158
+ >
159
+ <DefaultIcons.close color={colors.icon} size={16} />
160
+ </TouchableOpacity>
161
+ );
162
+ };
163
+
164
+ const containerStyles = [
165
+ baseToastStyle,
166
+ getPositionalStyle(position),
167
+ {
168
+ backgroundColor: finalBackgroundColor,
169
+ borderLeftColor: finalBorderColor,
170
+ },
171
+ style,
172
+ ];
173
+
174
+ // Accessibility label combines title and message
175
+ const accessibilityLabel = title ? `${title}. ${message}` : message;
176
+
177
+ // Render text content - title + description or just message
178
+ const renderTextContent = () => {
179
+ if (title) {
180
+ // Has title: render title (larger) + message as description (smaller)
181
+ return (
182
+ <View style={textContainerStyle}>
183
+ <Text
184
+ style={[
185
+ titleStyle,
186
+ { color: finalTextColor },
187
+ textStyle,
188
+ ]}
189
+ numberOfLines={2}
190
+ >
191
+ {title}
192
+ </Text>
193
+ <Text
194
+ style={[descriptionStyle, { color: finalTextColor }]}
195
+ numberOfLines={2}
196
+ >
197
+ {message}
198
+ </Text>
199
+ </View>
200
+ );
201
+ }
202
+ // No title: render message as main text
203
+ return (
204
+ <Text
205
+ style={[messageStyle, { color: finalTextColor }, textStyle]}
206
+ numberOfLines={3}
207
+ >
208
+ {message}
209
+ </Text>
210
+ );
211
+ };
212
+
213
+ return (
214
+ <Animated.View
215
+ style={[
216
+ containerStyles,
217
+ {
218
+ opacity: animatedStyle.opacity,
219
+ transform: animatedStyle.transform,
220
+ },
221
+ ]}
222
+ accessibilityRole="alert"
223
+ accessibilityLiveRegion="polite"
224
+ >
225
+ <TouchableOpacity
226
+ style={styles.content}
227
+ onPress={handlePress}
228
+ activeOpacity={dismissable ? 0.8 : 1}
229
+ disabled={!dismissable && !onPress}
230
+ accessibilityRole="button"
231
+ accessibilityLabel={accessibilityLabel}
232
+ accessibilityHint={
233
+ dismissable ? "Double tap to dismiss" : undefined
234
+ }
235
+ >
236
+ {renderIcon()}
237
+ {renderTextContent()}
238
+ {renderCloseButton()}
239
+ </TouchableOpacity>
240
+ </Animated.View>
241
+ );
242
+ };
243
+
244
+ const styles = StyleSheet.create({
245
+ content: {
246
+ flex: 1,
247
+ flexDirection: "row",
248
+ alignItems: "center",
249
+ },
250
+ });
@@ -0,0 +1,70 @@
1
+ import React, { useMemo } from "react";
2
+ import { View, StyleSheet } from "react-native";
3
+ import { Toast as ToastData, ToastPosition } from "./types";
4
+ import { Toast } from "./Toast";
5
+
6
+ export interface ToastContainerProps {
7
+ toasts: ToastData[];
8
+ position: ToastPosition;
9
+ onHide: (id: string) => void;
10
+ }
11
+
12
+ /**
13
+ * Container component that positions and renders toasts
14
+ */
15
+ export const ToastContainer: React.FC<ToastContainerProps> = ({
16
+ toasts,
17
+ position,
18
+ onHide,
19
+ }) => {
20
+ // Filter toasts for this position
21
+ const positionedToasts = useMemo(() => {
22
+ return toasts.filter((toast) => (toast.position || "top") === position);
23
+ }, [toasts, position]);
24
+
25
+ if (positionedToasts.length === 0) return null;
26
+
27
+ // Get container positioning based on position
28
+ const getContainerPositionStyle = () => {
29
+ switch (position) {
30
+ case "top":
31
+ return { top: 0, paddingTop: 50 }; // 50px for status bar
32
+ case "bottom":
33
+ return { bottom: 0, paddingBottom: 34 }; // 34px for home indicator
34
+ case "center":
35
+ return { top: 0, bottom: 0, justifyContent: "center" as const };
36
+ default:
37
+ return { top: 0, paddingTop: 50 };
38
+ }
39
+ };
40
+
41
+ return (
42
+ <View
43
+ style={[styles.container, getContainerPositionStyle()]}
44
+ pointerEvents="box-none"
45
+ >
46
+ {positionedToasts.map((toast, index) => (
47
+ <View
48
+ key={toast.id}
49
+ style={index > 0 ? styles.toastGap : undefined}
50
+ >
51
+ <Toast toast={toast} position={position} onHide={onHide} />
52
+ </View>
53
+ ))}
54
+ </View>
55
+ );
56
+ };
57
+
58
+ const styles = StyleSheet.create({
59
+ container: {
60
+ position: "absolute",
61
+ left: 0,
62
+ right: 0,
63
+ zIndex: 9999,
64
+ elevation: 9999,
65
+ pointerEvents: "box-none",
66
+ },
67
+ toastGap: {
68
+ marginTop: 8,
69
+ },
70
+ });
@@ -0,0 +1,271 @@
1
+ import React, {
2
+ createContext,
3
+ useState,
4
+ useCallback,
5
+ useMemo,
6
+ ReactNode,
7
+ } from "react";
8
+ import { View, StyleSheet } from "react-native";
9
+ import {
10
+ Toast as ToastData,
11
+ ToastConfig,
12
+ ToastContextValue,
13
+ ToastProviderConfig,
14
+ ToastPosition,
15
+ } from "./types";
16
+ import { ToastContainer } from "./ToastContainer";
17
+
18
+ /**
19
+ * Toast context for accessing toast functions
20
+ */
21
+ export const ToastContext = createContext<ToastContextValue | null>(null);
22
+
23
+ /**
24
+ * Generate unique ID for toasts
25
+ */
26
+ const generateId = (): string => {
27
+ return `toast-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`;
28
+ };
29
+
30
+ /**
31
+ * Default configuration values
32
+ */
33
+ const DEFAULT_CONFIG: Required<ToastProviderConfig> = {
34
+ defaultPosition: "top",
35
+ defaultDuration: 3000,
36
+ defaultAnimationIn: "fade",
37
+ defaultAnimationOut: "fade",
38
+ defaultAnimationDuration: 300,
39
+ maxToasts: 5,
40
+ };
41
+
42
+ export interface ToastProviderProps {
43
+ children: ReactNode;
44
+ config?: ToastProviderConfig;
45
+ }
46
+
47
+ /**
48
+ * ToastProvider - Wrap your app with this component to enable toasts
49
+ *
50
+ * @example
51
+ * ```tsx
52
+ * <ToastProvider config={{ defaultPosition: 'bottom' }}>
53
+ * <App />
54
+ * </ToastProvider>
55
+ * ```
56
+ */
57
+ export const ToastProvider: React.FC<ToastProviderProps> = ({
58
+ children,
59
+ config = {},
60
+ }) => {
61
+ const [toasts, setToasts] = useState<ToastData[]>([]);
62
+
63
+ // Merge config with defaults
64
+ const mergedConfig = useMemo(
65
+ () => ({ ...DEFAULT_CONFIG, ...config }),
66
+ [config],
67
+ );
68
+
69
+ /**
70
+ * Show a toast notification
71
+ */
72
+ const show = useCallback(
73
+ (toastConfig: ToastConfig): string => {
74
+ const id = generateId();
75
+
76
+ const newToast: ToastData = {
77
+ id,
78
+ title: toastConfig.title,
79
+ message: toastConfig.message,
80
+ type: toastConfig.type || "default",
81
+ position: toastConfig.position || mergedConfig.defaultPosition,
82
+ duration: toastConfig.duration ?? mergedConfig.defaultDuration,
83
+ animationIn:
84
+ toastConfig.animationIn || mergedConfig.defaultAnimationIn,
85
+ animationOut:
86
+ toastConfig.animationOut ||
87
+ mergedConfig.defaultAnimationOut,
88
+ animationDuration:
89
+ toastConfig.animationDuration ||
90
+ mergedConfig.defaultAnimationDuration,
91
+ dismissable: toastConfig.dismissable ?? true,
92
+ icon: toastConfig.icon,
93
+ hideIcon: toastConfig.hideIcon,
94
+ backgroundColor: toastConfig.backgroundColor,
95
+ textColor: toastConfig.textColor,
96
+ borderColor: toastConfig.borderColor,
97
+ style: toastConfig.style,
98
+ textStyle: toastConfig.textStyle,
99
+ onShow: toastConfig.onShow,
100
+ onHide: toastConfig.onHide,
101
+ onPress: toastConfig.onPress,
102
+ };
103
+
104
+ setToasts((prev) => {
105
+ // Limit to maxToasts
106
+ const updated = [...prev, newToast];
107
+ if (updated.length > mergedConfig.maxToasts) {
108
+ return updated.slice(-mergedConfig.maxToasts);
109
+ }
110
+ return updated;
111
+ });
112
+
113
+ return id;
114
+ },
115
+ [mergedConfig],
116
+ );
117
+
118
+ /**
119
+ * Helper to parse flexible arguments: (message) or (title, description)
120
+ */
121
+ const parseArgs = (
122
+ titleOrMessage: string,
123
+ descriptionOrConfig?: string | Partial<ToastConfig>,
124
+ config?: Partial<ToastConfig>,
125
+ ): { title?: string; message: string; config?: Partial<ToastConfig> } => {
126
+ if (typeof descriptionOrConfig === "string") {
127
+ // Called as (title, description, config?)
128
+ return {
129
+ title: titleOrMessage,
130
+ message: descriptionOrConfig,
131
+ config,
132
+ };
133
+ } else {
134
+ // Called as (message, config?)
135
+ return {
136
+ message: titleOrMessage,
137
+ config: descriptionOrConfig,
138
+ };
139
+ }
140
+ };
141
+
142
+ /**
143
+ * Show a success toast
144
+ */
145
+ const success = useCallback(
146
+ (
147
+ titleOrMessage: string,
148
+ descriptionOrConfig?: string | Partial<ToastConfig>,
149
+ config?: Partial<ToastConfig>,
150
+ ): string => {
151
+ const {
152
+ title,
153
+ message,
154
+ config: cfg,
155
+ } = parseArgs(titleOrMessage, descriptionOrConfig, config);
156
+ return show({ ...cfg, title, message, type: "success" });
157
+ },
158
+ [show],
159
+ );
160
+
161
+ /**
162
+ * Show a warning toast
163
+ */
164
+ const warning = useCallback(
165
+ (
166
+ titleOrMessage: string,
167
+ descriptionOrConfig?: string | Partial<ToastConfig>,
168
+ config?: Partial<ToastConfig>,
169
+ ): string => {
170
+ const {
171
+ title,
172
+ message,
173
+ config: cfg,
174
+ } = parseArgs(titleOrMessage, descriptionOrConfig, config);
175
+ return show({ ...cfg, title, message, type: "warning" });
176
+ },
177
+ [show],
178
+ );
179
+
180
+ /**
181
+ * Show an error toast
182
+ */
183
+ const error = useCallback(
184
+ (
185
+ titleOrMessage: string,
186
+ descriptionOrConfig?: string | Partial<ToastConfig>,
187
+ config?: Partial<ToastConfig>,
188
+ ): string => {
189
+ const {
190
+ title,
191
+ message,
192
+ config: cfg,
193
+ } = parseArgs(titleOrMessage, descriptionOrConfig, config);
194
+ return show({ ...cfg, title, message, type: "error" });
195
+ },
196
+ [show],
197
+ );
198
+
199
+ /**
200
+ * Show an info toast
201
+ */
202
+ const info = useCallback(
203
+ (
204
+ titleOrMessage: string,
205
+ descriptionOrConfig?: string | Partial<ToastConfig>,
206
+ config?: Partial<ToastConfig>,
207
+ ): string => {
208
+ const {
209
+ title,
210
+ message,
211
+ config: cfg,
212
+ } = parseArgs(titleOrMessage, descriptionOrConfig, config);
213
+ return show({ ...cfg, title, message, type: "info" });
214
+ },
215
+ [show],
216
+ );
217
+
218
+ /**
219
+ * Hide a specific toast by ID
220
+ */
221
+ const hide = useCallback((id: string): void => {
222
+ setToasts((prev) => prev.filter((toast) => toast.id !== id));
223
+ }, []);
224
+
225
+ /**
226
+ * Hide all toasts
227
+ */
228
+ const hideAll = useCallback((): void => {
229
+ setToasts([]);
230
+ }, []);
231
+
232
+ // Context value
233
+ const contextValue = useMemo<ToastContextValue>(
234
+ () => ({
235
+ show,
236
+ success,
237
+ warning,
238
+ error,
239
+ info,
240
+ hide,
241
+ hideAll,
242
+ }),
243
+ [show, success, warning, error, info, hide, hideAll],
244
+ );
245
+
246
+ // Group toasts by position
247
+ const positions: ToastPosition[] = ["top", "center", "bottom"];
248
+
249
+ return (
250
+ <ToastContext.Provider value={contextValue}>
251
+ <View style={styles.container}>
252
+ {children}
253
+ {/* Render toast containers for each position */}
254
+ {positions.map((position) => (
255
+ <ToastContainer
256
+ key={position}
257
+ toasts={toasts}
258
+ position={position}
259
+ onHide={hide}
260
+ />
261
+ ))}
262
+ </View>
263
+ </ToastContext.Provider>
264
+ );
265
+ };
266
+
267
+ const styles = StyleSheet.create({
268
+ container: {
269
+ flex: 1,
270
+ },
271
+ });