@synonymdev/react-native-toast 0.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 (50) hide show
  1. package/API.md +210 -0
  2. package/DEVELOPMENT.md +35 -0
  3. package/README.md +62 -0
  4. package/SynonymReactNativeToast.podspec +17 -0
  5. package/android/build.gradle +24 -0
  6. package/android/src/main/AndroidManifest.xml +1 -0
  7. package/android/src/main/java/com/synonymdev/reactnativetoast/NativeToastModule.kt +332 -0
  8. package/android/src/main/java/com/synonymdev/reactnativetoast/NativeToastPackage.kt +15 -0
  9. package/ios/NativeToast.m +530 -0
  10. package/lib/commonjs/defaults.js +105 -0
  11. package/lib/commonjs/defaults.js.map +1 -0
  12. package/lib/commonjs/index.js +25 -0
  13. package/lib/commonjs/index.js.map +1 -0
  14. package/lib/commonjs/nativeModule.js +15 -0
  15. package/lib/commonjs/nativeModule.js.map +1 -0
  16. package/lib/commonjs/package.json +1 -0
  17. package/lib/commonjs/types.js +2 -0
  18. package/lib/commonjs/types.js.map +1 -0
  19. package/lib/module/defaults.js +97 -0
  20. package/lib/module/defaults.js.map +1 -0
  21. package/lib/module/index.js +9 -0
  22. package/lib/module/index.js.map +1 -0
  23. package/lib/module/nativeModule.js +10 -0
  24. package/lib/module/nativeModule.js.map +1 -0
  25. package/lib/module/package.json +1 -0
  26. package/lib/module/types.js +2 -0
  27. package/lib/module/types.js.map +1 -0
  28. package/lib/typescript/commonjs/defaults.d.ts +9 -0
  29. package/lib/typescript/commonjs/defaults.d.ts.map +1 -0
  30. package/lib/typescript/commonjs/index.d.ts +6 -0
  31. package/lib/typescript/commonjs/index.d.ts.map +1 -0
  32. package/lib/typescript/commonjs/nativeModule.d.ts +3 -0
  33. package/lib/typescript/commonjs/nativeModule.d.ts.map +1 -0
  34. package/lib/typescript/commonjs/package.json +1 -0
  35. package/lib/typescript/commonjs/types.d.ts +57 -0
  36. package/lib/typescript/commonjs/types.d.ts.map +1 -0
  37. package/lib/typescript/module/defaults.d.ts +9 -0
  38. package/lib/typescript/module/defaults.d.ts.map +1 -0
  39. package/lib/typescript/module/index.d.ts +6 -0
  40. package/lib/typescript/module/index.d.ts.map +1 -0
  41. package/lib/typescript/module/nativeModule.d.ts +3 -0
  42. package/lib/typescript/module/nativeModule.d.ts.map +1 -0
  43. package/lib/typescript/module/package.json +1 -0
  44. package/lib/typescript/module/types.d.ts +57 -0
  45. package/lib/typescript/module/types.d.ts.map +1 -0
  46. package/package.json +68 -0
  47. package/src/defaults.ts +110 -0
  48. package/src/index.ts +25 -0
  49. package/src/nativeModule.ts +12 -0
  50. package/src/types.ts +87 -0
package/API.md ADDED
@@ -0,0 +1,210 @@
1
+ # API
2
+
3
+ ## `showToast(options)`
4
+
5
+ Displays a native toast notification.
6
+
7
+ ```ts
8
+ import { showToast } from '@synonymdev/react-native-toast';
9
+
10
+ showToast({
11
+ type: 'success',
12
+ title: 'Saved',
13
+ description: 'Your changes are ready.',
14
+ });
15
+ ```
16
+
17
+ ## Toast Options
18
+
19
+ ```ts
20
+ type ToastOptions = {
21
+ type: 'success' | 'info' | 'warning' | 'error';
22
+ title: string;
23
+ description?: string;
24
+ autoHide?: boolean;
25
+ dismissible?: boolean;
26
+ haptics?: boolean;
27
+ durationMs?: number;
28
+ style?: NativeToastStyle;
29
+ };
30
+ ```
31
+
32
+ | Option | Default | Description |
33
+ | --- | --- | --- |
34
+ | `type` | Required | Visual variant for the toast. |
35
+ | `title` | Required | Primary toast text. |
36
+ | `description` | `undefined` | Secondary toast text. |
37
+ | `autoHide` | `true` | Whether the toast dismisses itself after `durationMs`. |
38
+ | `dismissible` | `true` | Whether the toast can be dismissed with an upward swipe. |
39
+ | `haptics` | `false` | Whether to trigger native haptic feedback when the toast is shown. |
40
+ | `durationMs` | `4000` | Auto-hide delay in milliseconds. Native implementations enforce a minimum visible duration. |
41
+ | `style` | `undefined` | Native style overrides. |
42
+
43
+ Set `autoHide: false` to keep a toast visible until another toast replaces it or the user dismisses it.
44
+
45
+ ## Global Configuration
46
+
47
+ Use `configureToast` once during app startup to customize the base style or built-in variant styles for every toast.
48
+
49
+ ```ts
50
+ import { configureToast, resetToastConfig } from '@synonymdev/react-native-toast';
51
+
52
+ configureToast({
53
+ options: {
54
+ haptics: true,
55
+ dismissible: true,
56
+ durationMs: 4000,
57
+ },
58
+ defaults: {
59
+ borderRadius: 16,
60
+ paddingHorizontal: 24,
61
+ },
62
+ variants: {
63
+ success: {
64
+ backgroundColor: '#047857',
65
+ borderColor: '#34D399',
66
+ },
67
+ error: {
68
+ backgroundColor: '#991B1B',
69
+ borderColor: '#FCA5A5',
70
+ },
71
+ },
72
+ });
73
+
74
+ resetToastConfig();
75
+ ```
76
+
77
+ ```ts
78
+ type NativeToastVariantStyles = Partial<
79
+ Record<NativeToastVariant, NativeToastStyle>
80
+ >;
81
+
82
+ type NativeToastDefaultOptions = Pick<
83
+ ToastOptions,
84
+ 'autoHide' | 'dismissible' | 'haptics' | 'durationMs'
85
+ >;
86
+
87
+ type NativeToastConfig = {
88
+ defaults?: NativeToastStyle;
89
+ variants?: NativeToastVariantStyles;
90
+ options?: NativeToastDefaultOptions;
91
+ };
92
+ ```
93
+
94
+ Configured behavior options are merged in this order:
95
+
96
+ 1. Configured `options`.
97
+ 2. Per-toast options.
98
+
99
+ Per-toast options always win.
100
+
101
+ Configured styles are merged in this order:
102
+
103
+ 1. Built-in default style.
104
+ 2. Configured `defaults`.
105
+ 3. Built-in variant style.
106
+ 4. Configured variant style.
107
+ 5. Per-toast `style`.
108
+
109
+ ## Styling
110
+
111
+ Pass `style` to override the native toast appearance.
112
+
113
+ ```ts
114
+ showToast({
115
+ type: 'info',
116
+ title: 'New message',
117
+ description: 'You have a new notification.',
118
+ style: {
119
+ backgroundColor: '#111111',
120
+ iosBlurEffect: 'systemThinMaterialDark',
121
+ iosBlurAmount: 20,
122
+ iosBlurTintOpacity: 0.72,
123
+ borderColor: '#333333',
124
+ borderRadius: 12,
125
+ borderWidth: 1,
126
+ paddingHorizontal: 20,
127
+ paddingVertical: 16,
128
+ width: 320,
129
+ maxWidth: 420,
130
+ titleColor: '#FFFFFF',
131
+ titleFontSize: 15,
132
+ descriptionColor: '#D4D4D4',
133
+ descriptionFontSize: 13,
134
+ shadowColor: '#000000',
135
+ shadowOpacity: 0.35,
136
+ shadowRadius: 16,
137
+ shadowOffsetY: 8,
138
+ shadowElevation: 8,
139
+ animation: 'slide-fade',
140
+ animationDurationMs: 200,
141
+ },
142
+ });
143
+ ```
144
+
145
+ ## Style Options
146
+
147
+ ```ts
148
+ type NativeToastBlurEffect =
149
+ | 'none'
150
+ | 'extraLight'
151
+ | 'light'
152
+ | 'dark'
153
+ | 'regular'
154
+ | 'prominent'
155
+ | 'systemUltraThinMaterial'
156
+ | 'systemThinMaterial'
157
+ | 'systemMaterial'
158
+ | 'systemThickMaterial'
159
+ | 'systemChromeMaterial'
160
+ | 'systemUltraThinMaterialLight'
161
+ | 'systemThinMaterialLight'
162
+ | 'systemMaterialLight'
163
+ | 'systemThickMaterialLight'
164
+ | 'systemChromeMaterialLight'
165
+ | 'systemUltraThinMaterialDark'
166
+ | 'systemThinMaterialDark'
167
+ | 'systemMaterialDark'
168
+ | 'systemThickMaterialDark'
169
+ | 'systemChromeMaterialDark';
170
+
171
+ type NativeToastStyle = {
172
+ backgroundColor?: string;
173
+ iosBlurEffect?: NativeToastBlurEffect;
174
+ iosBlurAmount?: number;
175
+ iosBlurTintOpacity?: number;
176
+ borderRadius?: number;
177
+ borderColor?: string;
178
+ borderWidth?: number;
179
+ shadowColor?: string;
180
+ shadowOpacity?: number;
181
+ shadowRadius?: number;
182
+ shadowOffsetX?: number;
183
+ shadowOffsetY?: number;
184
+ shadowElevation?: number;
185
+ animation?: 'none' | 'fade' | 'slide' | 'slide-fade';
186
+ animationDurationMs?: number;
187
+ width?: number;
188
+ maxWidth?: number;
189
+ marginHorizontal?: number;
190
+ padding?: number;
191
+ paddingHorizontal?: number;
192
+ paddingVertical?: number;
193
+ paddingTop?: number;
194
+ paddingRight?: number;
195
+ paddingBottom?: number;
196
+ paddingLeft?: number;
197
+ titleColor?: string;
198
+ titleFontFamily?: string;
199
+ titleFontSize?: number;
200
+ titleFontWeight?: 'normal' | 'medium' | 'semibold' | 'bold';
201
+ descriptionColor?: string;
202
+ descriptionFontFamily?: string;
203
+ descriptionFontSize?: number;
204
+ descriptionFontWeight?: 'normal' | 'medium' | 'semibold' | 'bold';
205
+ };
206
+ ```
207
+
208
+ `iosBlurEffect` uses native `UIBlurEffect` on iOS and is ignored by Android. `iosBlurAmount` controls the custom blur radius and defaults to `20`. `iosBlurTintOpacity` controls the opacity of the `backgroundColor` tint layered over the blur and defaults to `0.72`.
209
+
210
+ Supported animations are `none`, `fade`, `slide`, and `slide-fade`.
package/DEVELOPMENT.md ADDED
@@ -0,0 +1,35 @@
1
+ # Development
2
+
3
+ ## Example App
4
+
5
+ The `example` directory contains a small React Native app that links this package locally and demonstrates the main toast flows.
6
+
7
+ ```sh
8
+ cd example
9
+ yarn install
10
+ yarn pods
11
+ yarn start
12
+ ```
13
+
14
+ Run it from another terminal:
15
+
16
+ ```sh
17
+ yarn ios
18
+ yarn android
19
+ ```
20
+
21
+ ## Package Commands
22
+
23
+ Run tests:
24
+
25
+ ```sh
26
+ yarn test
27
+ ```
28
+
29
+ Build the package:
30
+
31
+ ```sh
32
+ yarn build
33
+ ```
34
+
35
+ The React Native entry points at `src/index.ts` for local app development. Published JavaScript and TypeScript declarations are emitted to `lib`.
package/README.md ADDED
@@ -0,0 +1,62 @@
1
+ # @synonymdev/react-native-toast
2
+
3
+ Native toast notifications for React Native.
4
+
5
+ Renders toasts from native iOS and Android code so notifications can appear above native sheet presentation surfaces.
6
+
7
+ Designed for use with native sheet implementations like React Navigation `formSheet`, [software-mansion-labs/react-native-bottom-sheet](https://github.com/software-mansion-labs/react-native-bottom-sheet) and [lodev09/react-native-true-sheet](https://github.com/lodev09/react-native-true-sheet).
8
+
9
+ ## Demo
10
+
11
+ <video src="https://github.com/synonymdev/react-native-toast/raw/main/media/demo.mov" controls width="300">
12
+ <a href="./media/demo.mov">Watch the demo</a>
13
+ </video>
14
+
15
+ ## Features
16
+
17
+ - Native iOS and Android rendering.
18
+ - Appears above native sheet presentation surfaces.
19
+ - Simple `showToast(options)` JavaScript API.
20
+ - Built-in `success`, `info`, `warning`, and `error` variants.
21
+ - Typed options for behavior, haptics, blur, and styling.
22
+ - Global configuration for default behavior and variant styling.
23
+
24
+ ## Why?
25
+
26
+ React Native toast components usually render inside the JavaScript view hierarchy. That works for normal screens, but it can fall behind native sheet presentation layers.
27
+
28
+ This package sends a fully resolved toast payload to a small native module, then renders the toast from native iOS and Android code. That makes it useful for app-level feedback that should remain visible across native sheet presentations.
29
+
30
+ On Android, React Native `Modal` and modal-backed sheet libraries create their own native window. This toast currently renders in the Activity window, so those modal surfaces can appear above it.
31
+
32
+ ## Installation
33
+
34
+ ```sh
35
+ yarn add @synonymdev/react-native-toast
36
+ ```
37
+
38
+ ## Usage
39
+
40
+ ```ts
41
+ import { showToast } from '@synonymdev/react-native-toast';
42
+
43
+ showToast({
44
+ type: 'success',
45
+ title: 'Saved',
46
+ description: 'Your changes are ready.',
47
+ });
48
+ ```
49
+
50
+ Configure shared defaults once during app startup:
51
+
52
+ ```ts
53
+ import { configureToast } from '@synonymdev/react-native-toast';
54
+
55
+ configureToast({
56
+ options: {
57
+ haptics: true,
58
+ },
59
+ });
60
+ ```
61
+
62
+ See [API.md](./API.md) for configuration, styling, and the full type reference. Local development and example app instructions live in [DEVELOPMENT.md](./DEVELOPMENT.md).
@@ -0,0 +1,17 @@
1
+ require 'json'
2
+
3
+ package = JSON.parse(File.read(File.join(__dir__, 'package.json')))
4
+
5
+ Pod::Spec.new do |s|
6
+ s.name = 'SynonymReactNativeToast'
7
+ s.version = package['version']
8
+ s.summary = package['description']
9
+ s.license = package['license']
10
+ s.authors = { 'Synonym' => 'engineering@synonym.to' }
11
+ s.homepage = 'https://github.com/synonymdev'
12
+ s.platforms = { :ios => '13.0' }
13
+ s.source = { :git => 'https://github.com/synonymdev/react-native-toast.git', :tag => s.version.to_s }
14
+ s.source_files = 'ios/**/*.{h,m,mm,swift}'
15
+
16
+ s.dependency 'React-Core'
17
+ end
@@ -0,0 +1,24 @@
1
+ buildscript {
2
+ ext.safeExtGet = { prop, fallback ->
3
+ rootProject.ext.has(prop) ? rootProject.ext.get(prop) : fallback
4
+ }
5
+ }
6
+
7
+ plugins {
8
+ id "com.android.library"
9
+ id "org.jetbrains.kotlin.android"
10
+ }
11
+
12
+ android {
13
+ namespace "com.synonymdev.reactnativetoast"
14
+ compileSdk safeExtGet("compileSdkVersion", 36)
15
+
16
+ defaultConfig {
17
+ minSdkVersion safeExtGet("minSdkVersion", 24)
18
+ targetSdkVersion safeExtGet("targetSdkVersion", 36)
19
+ }
20
+ }
21
+
22
+ dependencies {
23
+ implementation "com.facebook.react:react-android"
24
+ }
@@ -0,0 +1 @@
1
+ <manifest xmlns:android="http://schemas.android.com/apk/res/android" />
@@ -0,0 +1,332 @@
1
+ package com.synonymdev.reactnativetoast
2
+
3
+ import android.graphics.Color
4
+ import android.graphics.Typeface
5
+ import android.graphics.drawable.GradientDrawable
6
+ import android.os.Build
7
+ import android.util.TypedValue
8
+ import android.view.Gravity
9
+ import android.view.HapticFeedbackConstants
10
+ import android.view.MotionEvent
11
+ import android.view.View
12
+ import android.view.ViewGroup
13
+ import android.view.WindowManager
14
+ import android.widget.FrameLayout
15
+ import android.widget.LinearLayout
16
+ import android.widget.TextView
17
+ import com.facebook.react.bridge.ReactApplicationContext
18
+ import com.facebook.react.bridge.ReactContextBaseJavaModule
19
+ import com.facebook.react.bridge.ReactMethod
20
+ import com.facebook.react.bridge.ReadableMap
21
+ import kotlin.math.roundToInt
22
+
23
+ class NativeToastModule(private val reactContext: ReactApplicationContext) :
24
+ ReactContextBaseJavaModule(reactContext) {
25
+ override fun getName(): String = "NativeToast"
26
+
27
+ @ReactMethod
28
+ fun show(options: ReadableMap) {
29
+ val title = options.getString("title").orEmpty()
30
+ val description = options.getString("description").orEmpty()
31
+ val variant = options.getString("type") ?: "info"
32
+ val autoHide = if (options.hasKey("autoHide") && !options.isNull("autoHide")) options.getBoolean("autoHide") else true
33
+ val dismissible = if (options.hasKey("dismissible") && !options.isNull("dismissible")) options.getBoolean("dismissible") else true
34
+ val haptics = if (options.hasKey("haptics") && !options.isNull("haptics")) options.getBoolean("haptics") else false
35
+ val durationMs = if (options.hasKey("durationMs")) options.getDouble("durationMs") else 4000.0
36
+ val style = if (options.hasKey("style") && !options.isNull("style")) options.getMap("style") else null
37
+ val animation = string(style, "animation", "slide-fade")
38
+ val animationDurationMs = number(style, "animationDurationMs", 200.0).coerceAtLeast(0.0).toLong()
39
+
40
+ if (title.isBlank() && description.isBlank()) {
41
+ return
42
+ }
43
+
44
+ reactContext.runOnUiQueueThread {
45
+ val activity = reactContext.currentActivity ?: return@runOnUiQueueThread
46
+ val root = activity.window.decorView as? ViewGroup ?: return@runOnUiQueueThread
47
+
48
+ root.findViewWithTag<View>(TOAST_TAG)?.let {
49
+ it.animate().cancel()
50
+ root.removeView(it)
51
+ }
52
+ root.clipChildren = false
53
+ root.clipToPadding = false
54
+
55
+ val paddingTop = padding(style, "paddingTop", "paddingVertical", 12.0)
56
+ val paddingRight = padding(style, "paddingRight", "paddingHorizontal", 16.0)
57
+ val paddingBottom = padding(style, "paddingBottom", "paddingVertical", 12.0)
58
+ val paddingLeft = padding(style, "paddingLeft", "paddingHorizontal", 16.0)
59
+ val marginHorizontal = dp(number(style, "marginHorizontal", 16.0))
60
+ val availableWidth = (screenWidth() - (marginHorizontal * 2)).coerceAtLeast(0)
61
+ val toastWidth = toastWidth(style, availableWidth)
62
+
63
+ val toast = LinearLayout(activity).apply {
64
+ tag = TOAST_TAG
65
+ alpha = if (animationUsesFade(animation)) 0f else 1f
66
+ clipToOutline = true
67
+ gravity = Gravity.CENTER_VERTICAL
68
+ orientation = LinearLayout.VERTICAL
69
+ visibility = View.INVISIBLE
70
+ setPadding(dp(paddingLeft), dp(paddingTop), dp(paddingRight), dp(paddingBottom))
71
+ isClickable = dismissible
72
+ importantForAccessibility = View.IMPORTANT_FOR_ACCESSIBILITY_NO_HIDE_DESCENDANTS
73
+ background =
74
+ GradientDrawable().apply {
75
+ setColor(color(style, "backgroundColor", Color.rgb(51, 51, 51)))
76
+ cornerRadius = dp(number(style, "borderRadius", 12.0)).toFloat()
77
+ setStroke(dp(number(style, "borderWidth", 1.0)), color(style, "borderColor", Color.rgb(68, 68, 68)))
78
+ }
79
+ elevation = dp(number(style, "shadowElevation", 8.0)).toFloat()
80
+ translationZ = elevation
81
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
82
+ val shadowColor = color(style, "shadowColor", Color.BLACK)
83
+ outlineAmbientShadowColor = shadowColor
84
+ outlineSpotShadowColor = shadowColor
85
+ }
86
+ }
87
+
88
+ if (title.isNotBlank()) {
89
+ toast.addView(
90
+ TextView(activity).apply {
91
+ text = title
92
+ maxLines = 2
93
+ setTextColor(color(style, "titleColor", Color.WHITE))
94
+ setTextSize(TypedValue.COMPLEX_UNIT_SP, number(style, "titleFontSize", 15.0).toFloat())
95
+ typeface = typeface(style, "titleFontFamily", "titleFontWeight", Typeface.BOLD)
96
+ includeFontPadding = false
97
+ },
98
+ LinearLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT),
99
+ )
100
+ }
101
+
102
+ if (description.isNotBlank()) {
103
+ toast.addView(
104
+ TextView(activity).apply {
105
+ text = description
106
+ maxLines = 3
107
+ setTextColor(color(style, "descriptionColor", Color.rgb(204, 204, 204)))
108
+ setTextSize(TypedValue.COMPLEX_UNIT_SP, number(style, "descriptionFontSize", 13.0).toFloat())
109
+ typeface = typeface(style, "descriptionFontFamily", "descriptionFontWeight", Typeface.NORMAL)
110
+ includeFontPadding = false
111
+ },
112
+ LinearLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT).apply {
113
+ topMargin = if (title.isNotBlank()) dp(2.0) else 0
114
+ },
115
+ )
116
+ }
117
+
118
+ val layoutParams =
119
+ FrameLayout.LayoutParams(toastWidth, ViewGroup.LayoutParams.WRAP_CONTENT, Gravity.TOP or Gravity.CENTER_HORIZONTAL)
120
+ .apply {
121
+ topMargin = statusBarHeight() + dp(12.0)
122
+ }
123
+
124
+ root.addView(toast, layoutParams)
125
+ if (haptics) {
126
+ triggerHaptic(toast, variant)
127
+ }
128
+
129
+ toast.post {
130
+ val dismissedTranslation = if (animationUsesSlide(animation)) -(toast.height + layoutParams.topMargin + dp(8.0)).toFloat() else 0f
131
+ toast.translationY = dismissedTranslation
132
+ toast.visibility = View.VISIBLE
133
+ if (dismissible) {
134
+ configureDragDismiss(toast, root, dismissedTranslation, animationDurationMs)
135
+ }
136
+
137
+ toast
138
+ .animate()
139
+ .alpha(1f)
140
+ .translationY(0f)
141
+ .setDuration(animationDurationMs)
142
+ .withEndAction {
143
+ if (autoHide) {
144
+ toast.postDelayed(
145
+ {
146
+ toast
147
+ .animate()
148
+ .alpha(if (animationUsesFade(animation)) 0f else 1f)
149
+ .translationY(dismissedTranslation)
150
+ .setDuration(animationDurationMs)
151
+ .withEndAction {
152
+ if (toast.parent === root) {
153
+ root.removeView(toast)
154
+ }
155
+ }
156
+ .start()
157
+ },
158
+ durationMs.coerceAtLeast(1500.0).toLong(),
159
+ )
160
+ }
161
+ }
162
+ .start()
163
+ }
164
+ }
165
+ }
166
+
167
+ private fun triggerHaptic(view: View, variant: String) {
168
+ val feedbackConstant =
169
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
170
+ when (variant) {
171
+ "success" -> HapticFeedbackConstants.CONFIRM
172
+ "warning" -> HapticFeedbackConstants.LONG_PRESS
173
+ "error" -> HapticFeedbackConstants.REJECT
174
+ else -> HapticFeedbackConstants.CONTEXT_CLICK
175
+ }
176
+ } else {
177
+ when (variant) {
178
+ "warning", "error" -> HapticFeedbackConstants.LONG_PRESS
179
+ else -> HapticFeedbackConstants.KEYBOARD_TAP
180
+ }
181
+ }
182
+
183
+ view.isHapticFeedbackEnabled = true
184
+ view.performHapticFeedback(feedbackConstant)
185
+ }
186
+
187
+ private fun color(style: ReadableMap?, key: String, fallback: Int): Int {
188
+ val value = if (style?.hasKey(key) == true && !style.isNull(key)) style.getString(key) else null
189
+ return try {
190
+ if (value.isNullOrBlank()) fallback else Color.parseColor(value)
191
+ } catch (_: IllegalArgumentException) {
192
+ fallback
193
+ }
194
+ }
195
+
196
+ private fun number(style: ReadableMap?, key: String, fallback: Double): Double {
197
+ return if (style?.hasKey(key) == true && !style.isNull(key)) style.getDouble(key) else fallback
198
+ }
199
+
200
+ private fun string(style: ReadableMap?, key: String, fallback: String): String {
201
+ return if (style?.hasKey(key) == true && !style.isNull(key)) style.getString(key) ?: fallback else fallback
202
+ }
203
+
204
+ private fun animationUsesFade(animation: String): Boolean {
205
+ return animation != "none" && animation != "slide"
206
+ }
207
+
208
+ private fun animationUsesSlide(animation: String): Boolean {
209
+ return animation == "slide" || animation == "slide-fade"
210
+ }
211
+
212
+ private fun optionalNumber(style: ReadableMap?, key: String): Double? {
213
+ return if (style?.hasKey(key) == true && !style.isNull(key)) style.getDouble(key) else null
214
+ }
215
+
216
+ private fun toastWidth(style: ReadableMap?, availableWidth: Int): Int {
217
+ val width = optionalNumber(style, "width")?.let { dp(it).coerceIn(0, availableWidth) }
218
+ if (width != null) {
219
+ return width
220
+ }
221
+
222
+ val maxWidth = optionalNumber(style, "maxWidth")?.let { dp(it).coerceIn(0, availableWidth) }
223
+ return maxWidth ?: availableWidth
224
+ }
225
+
226
+ private fun padding(style: ReadableMap?, edgeKey: String, axisKey: String, fallback: Double): Double {
227
+ return when {
228
+ style?.hasKey(edgeKey) == true && !style.isNull(edgeKey) -> style.getDouble(edgeKey)
229
+ style?.hasKey(axisKey) == true && !style.isNull(axisKey) -> style.getDouble(axisKey)
230
+ style?.hasKey("padding") == true && !style.isNull("padding") -> style.getDouble("padding")
231
+ else -> fallback
232
+ }
233
+ }
234
+
235
+ private fun typeface(style: ReadableMap?, familyKey: String, weightKey: String, fallbackStyle: Int): Typeface {
236
+ val family = if (style?.hasKey(familyKey) == true && !style.isNull(familyKey)) style.getString(familyKey) else null
237
+ val fontStyle =
238
+ when (if (style?.hasKey(weightKey) == true && !style.isNull(weightKey)) style.getString(weightKey) else null) {
239
+ "bold", "semibold" -> Typeface.BOLD
240
+ "medium" -> Typeface.NORMAL
241
+ "normal" -> Typeface.NORMAL
242
+ else -> fallbackStyle
243
+ }
244
+
245
+ return if (family.isNullOrBlank()) {
246
+ Typeface.defaultFromStyle(fontStyle)
247
+ } else {
248
+ Typeface.create(family, fontStyle)
249
+ }
250
+ }
251
+
252
+ private fun configureDragDismiss(
253
+ toast: View,
254
+ root: ViewGroup,
255
+ dismissedTranslation: Float,
256
+ animationDurationMs: Long,
257
+ ) {
258
+ var downRawY = 0f
259
+
260
+ toast.setOnTouchListener { view, event ->
261
+ when (event.actionMasked) {
262
+ MotionEvent.ACTION_DOWN -> {
263
+ view.animate().cancel()
264
+ downRawY = event.rawY
265
+ view.parent?.requestDisallowInterceptTouchEvent(true)
266
+ true
267
+ }
268
+ MotionEvent.ACTION_MOVE -> {
269
+ val offsetY = (event.rawY - downRawY).coerceAtMost(0f)
270
+ view.translationY = offsetY
271
+ view.alpha = (1f + (offsetY / view.height.coerceAtLeast(1))).coerceAtLeast(0.35f)
272
+ true
273
+ }
274
+ MotionEvent.ACTION_UP, MotionEvent.ACTION_CANCEL -> {
275
+ val offsetY = (event.rawY - downRawY).coerceAtMost(0f)
276
+ val shouldDismiss = offsetY < -dp(40.0)
277
+ view.parent?.requestDisallowInterceptTouchEvent(false)
278
+
279
+ if (shouldDismiss) {
280
+ view
281
+ .animate()
282
+ .alpha(0f)
283
+ .translationY(dismissedTranslation)
284
+ .setDuration(160L)
285
+ .withEndAction {
286
+ if (view.parent === root) {
287
+ root.removeView(view)
288
+ }
289
+ }
290
+ .start()
291
+ } else {
292
+ view
293
+ .animate()
294
+ .alpha(1f)
295
+ .translationY(0f)
296
+ .setDuration(animationDurationMs.coerceAtMost(160L))
297
+ .start()
298
+ }
299
+ true
300
+ }
301
+ else -> false
302
+ }
303
+ }
304
+ }
305
+
306
+ private fun dp(value: Double): Int {
307
+ return TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, value.toFloat(), reactContext.resources.displayMetrics)
308
+ .roundToInt()
309
+ }
310
+
311
+ private fun statusBarHeight(): Int {
312
+ val resourceId = reactContext.resources.getIdentifier("status_bar_height", "dimen", "android")
313
+ return if (resourceId > 0) reactContext.resources.getDimensionPixelSize(resourceId) else 0
314
+ }
315
+
316
+ private fun screenWidth(): Int {
317
+ val activity = reactContext.currentActivity ?: return reactContext.resources.displayMetrics.widthPixels
318
+ return if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.R) {
319
+ activity.windowManager.currentWindowMetrics.bounds.width()
320
+ } else {
321
+ @Suppress("DEPRECATION")
322
+ val display = (activity.getSystemService(android.content.Context.WINDOW_SERVICE) as WindowManager).defaultDisplay
323
+ @Suppress("DEPRECATION")
324
+ val metrics = android.util.DisplayMetrics().also { display.getMetrics(it) }
325
+ metrics.widthPixels
326
+ }
327
+ }
328
+
329
+ private companion object {
330
+ const val TOAST_TAG = 900413
331
+ }
332
+ }
@@ -0,0 +1,15 @@
1
+ package com.synonymdev.reactnativetoast
2
+
3
+ import com.facebook.react.ReactPackage
4
+ import com.facebook.react.bridge.NativeModule
5
+ import com.facebook.react.bridge.ReactApplicationContext
6
+ import com.facebook.react.uimanager.ViewManager
7
+
8
+ class NativeToastPackage : ReactPackage {
9
+ override fun createNativeModules(reactContext: ReactApplicationContext): List<NativeModule> =
10
+ listOf(NativeToastModule(reactContext))
11
+
12
+ override fun createViewManagers(
13
+ reactContext: ReactApplicationContext
14
+ ): List<ViewManager<*, *>> = emptyList()
15
+ }