├── .eslintignore
├── .eslintrc.js
├── .github
├── ISSUE_TEMPLATE
│ ├── bug_report.md
│ └── feature_request.md
├── dependabot.yml
└── workflows
│ ├── publish-beta.yml
│ ├── publish.yml
│ ├── quality.yml
│ ├── tag.yml
│ └── tests.yml
├── .gitignore
├── .npmignore
├── .prettierrc.js
├── LICENSE
├── README.md
├── babel.config.js
├── docs
├── api.md
├── custom-layouts.md
├── jest-testing.md
├── modal-usage.md
├── navigation-usage.md
├── quick-start.md
└── toast.gif
├── index.ts
├── jest.config.js
├── jest.setup.js
├── package.json
├── scripts
└── dispatch-publish-beta.sh
├── src
├── Toast.tsx
├── ToastUI.tsx
├── __helpers__
│ └── PanResponder.ts
├── __tests__
│ ├── Toast.test.tsx
│ ├── ToastUI.test.tsx
│ └── useToast.test.ts
├── components
│ ├── AnimatedContainer.styles.tsx
│ ├── AnimatedContainer.tsx
│ ├── BaseToast.styles.ts
│ ├── BaseToast.tsx
│ ├── ErrorToast.tsx
│ ├── InfoToast.tsx
│ ├── SuccessToast.tsx
│ └── __tests__
│ │ ├── AnimatedContainer.test.tsx
│ │ ├── BaseToast.test.tsx
│ │ ├── ErrorToast.test.tsx
│ │ ├── InfoToast.test.tsx
│ │ └── SuccessToast.test.tsx
├── contexts
│ ├── LoggerContext.tsx
│ ├── __tests__
│ │ └── LoggerContext.test.tsx
│ └── index.ts
├── hooks
│ ├── __tests__
│ │ ├── useKeyboard.test.ts
│ │ ├── usePanResponder.test.ts
│ │ ├── useSlideAnimation.test.ts
│ │ ├── useTimeout.test.ts
│ │ └── useViewDimensions.test.ts
│ ├── index.ts
│ ├── useKeyboard.ts
│ ├── usePanResponder.ts
│ ├── useSlideAnimation.ts
│ ├── useTimeout.ts
│ └── useViewDimensions.ts
├── types
│ └── index.ts
├── useToast.ts
└── utils
│ ├── __tests__
│ ├── number.test.ts
│ └── obj.test.ts
│ ├── array.ts
│ ├── func.ts
│ ├── number.ts
│ ├── obj.ts
│ ├── platform.ts
│ └── test-id.ts
├── tsconfig.json
└── yarn.lock
/.eslintignore:
--------------------------------------------------------------------------------
1 | babel.config.js
2 | .eslintrc.js
--------------------------------------------------------------------------------
/.eslintrc.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | root: true,
3 | extends: ['backpacker-react-ts'],
4 | rules: {
5 | 'import/no-extraneous-dependencies': 'off',
6 | '@typescript-eslint/explicit-module-boundary-types': 'off'
7 | }
8 | };
9 |
--------------------------------------------------------------------------------
/.github/ISSUE_TEMPLATE/bug_report.md:
--------------------------------------------------------------------------------
1 | ---
2 | name: Bug report
3 | about: Create a report to help us improve
4 | title: ''
5 | labels: ''
6 | assignees: ''
7 |
8 | ---
9 |
10 | **Describe the bug**
11 | A clear and concise description of what the bug is.
12 |
13 | **Steps to reproduce**
14 | Steps to reproduce the behavior:
15 | 1. Go to '...'
16 | 2. Tap on '....'
17 | 3. Scroll down to '....'
18 | 4. See error
19 |
20 | **Expected behavior**
21 | A clear and concise description of what you expected to happen.
22 |
23 | **Screenshots**
24 | If applicable, add screenshots to help explain your problem.
25 |
26 | **Code sample**
27 | To help debugging, please add a code sample that reproduces the issue.
28 |
29 | **Environment (please complete the following information):**
30 | - OS: [iOS, Android, Web]
31 | - react-native-toast-message version: [e.g. v2.0.0]
32 | - react-native version [e.g. v0.65.0]
33 |
34 | **Additional context**
35 | Add any other context about the problem here.
36 |
--------------------------------------------------------------------------------
/.github/ISSUE_TEMPLATE/feature_request.md:
--------------------------------------------------------------------------------
1 | ---
2 | name: Feature request
3 | about: Suggest an idea for this project
4 | title: ''
5 | labels: ''
6 | assignees: ''
7 |
8 | ---
9 |
10 | **Is your feature request related to a problem? Please describe.**
11 | A clear and concise description of what the problem is. Ex. I'm always frustrated when [...]
12 |
13 | **Describe the solution you'd like**
14 | A clear and concise description of what you want to happen.
15 |
16 | **Describe alternatives you've considered**
17 | A clear and concise description of any alternative solutions or features you've considered.
18 |
19 | **Additional context**
20 | Add any other context or screenshots about the feature request here.
21 |
--------------------------------------------------------------------------------
/.github/dependabot.yml:
--------------------------------------------------------------------------------
1 | version: 2
2 | updates:
3 | - package-ecosystem: npm
4 | directory: '/'
5 | schedule:
6 | interval: monthly
7 | time: '03:00'
8 | open-pull-requests-limit: 10
9 | reviewers:
10 | - calintamas
11 |
--------------------------------------------------------------------------------
/.github/workflows/publish-beta.yml:
--------------------------------------------------------------------------------
1 | name: Publish a new beta version
2 |
3 | on:
4 | repository_dispatch:
5 | types: [publish-beta]
6 |
7 | jobs:
8 | test:
9 | runs-on: ubuntu-latest
10 | steps:
11 | - uses: actions/checkout@v2
12 | with:
13 | ref: ${{ github.event.client_payload.ref }}
14 |
15 | - name: Setup Node.js
16 | uses: actions/setup-node@v1
17 | with:
18 | node-version: 16
19 |
20 | - name: Get yarn cache directory path
21 | id: yarn-cache-dir-path
22 | run: echo "::set-output name=dir::$(yarn cache dir)"
23 |
24 | - name: Restore yarn cache
25 | uses: actions/cache@v4
26 | id: yarn-cache # use this to check for `cache-hit` (`steps.yarn-cache.outputs.cache-hit != 'true'`)
27 | with:
28 | path: ${{ steps.yarn-cache-dir-path.outputs.dir }}
29 | key: ${{ runner.os }}-yarn-${{ hashFiles('**/yarn.lock') }}
30 | restore-keys: |
31 | ${{ runner.os }}-yarn-
32 |
33 | - name: Install dependencies
34 | run: yarn --prefer-offline
35 |
36 | - name: Setup React Native environment
37 | run: |
38 | yarn add react@17.0.2
39 | yarn add react-native@0.64.2
40 |
41 | - name: Run tests
42 | run: yarn test --coverage
43 |
44 | publish-beta:
45 | runs-on: ubuntu-latest
46 | needs: test
47 | steps:
48 | - uses: actions/checkout@v2
49 | with:
50 | ref: ${{ github.event.client_payload.ref }}
51 |
52 | - name: Setup Node.js
53 | uses: actions/setup-node@v1
54 | with:
55 | node-version: 16
56 |
57 | - name: Install dependencies
58 | run: yarn
59 |
60 | - name: Build
61 | run: yarn build
62 |
63 | - id: publish
64 | uses: JS-DevTools/npm-publish@v1
65 | with:
66 | token: ${{ secrets.NPM_TOKEN }}
67 | tag: 'beta'
68 |
--------------------------------------------------------------------------------
/.github/workflows/publish.yml:
--------------------------------------------------------------------------------
1 | name: build
2 |
3 | on:
4 | push:
5 | branches:
6 | - main
7 |
8 | jobs:
9 | test:
10 | runs-on: ubuntu-latest
11 | steps:
12 | - name: Checkout
13 | uses: actions/checkout@v1
14 |
15 | - name: Setup Node.js
16 | uses: actions/setup-node@v1
17 | with:
18 | node-version: 14
19 |
20 | - name: Get yarn cache directory path
21 | id: yarn-cache-dir-path
22 | run: echo "::set-output name=dir::$(yarn cache dir)"
23 |
24 | - name: Restore yarn cache
25 | uses: actions/cache@v4
26 | id: yarn-cache # use this to check for `cache-hit` (`steps.yarn-cache.outputs.cache-hit != 'true'`)
27 | with:
28 | path: ${{ steps.yarn-cache-dir-path.outputs.dir }}
29 | key: ${{ runner.os }}-yarn-${{ hashFiles('**/yarn.lock') }}
30 | restore-keys: |
31 | ${{ runner.os }}-yarn-
32 |
33 | - name: Install dependencies
34 | run: yarn --prefer-offline
35 |
36 | - name: Setup React Native environment
37 | run: |
38 | yarn add react@17.0.2
39 | yarn add react-native@0.64.2
40 |
41 | - name: Run tests
42 | run: yarn test --coverage
43 |
44 | publish:
45 | runs-on: ubuntu-latest
46 | needs: test
47 | steps:
48 | - uses: actions/checkout@v1
49 |
50 | - uses: actions/setup-node@v1
51 | with:
52 | node-version: 14
53 |
54 | - name: Install dependencies
55 | run: yarn
56 |
57 | - name: Build
58 | run: yarn build
59 |
60 | - id: publish
61 | uses: JS-DevTools/npm-publish@v1
62 | with:
63 | token: ${{ secrets.NPM_TOKEN }}
64 |
--------------------------------------------------------------------------------
/.github/workflows/quality.yml:
--------------------------------------------------------------------------------
1 | name: Run code quality checks
2 |
3 | on:
4 | push:
5 | branches:
6 | - main
7 | pull_request:
8 | branches:
9 | - main
10 |
11 | jobs:
12 | quality:
13 | runs-on: ubuntu-latest
14 | steps:
15 | - name: Checkout
16 | uses: actions/checkout@v1
17 |
18 | - name: Setup Node.js
19 | uses: actions/setup-node@v1
20 | with:
21 | node-version: 14
22 |
23 | - name: Get yarn cache directory path
24 | id: yarn-cache-dir-path
25 | run: echo "::set-output name=dir::$(yarn cache dir)"
26 |
27 | - name: Restore yarn cache
28 | uses: actions/cache@v4
29 | id: yarn-cache # use this to check for `cache-hit` (`steps.yarn-cache.outputs.cache-hit != 'true'`)
30 | with:
31 | path: ${{ steps.yarn-cache-dir-path.outputs.dir }}
32 | key: ${{ runner.os }}-yarn-${{ hashFiles('**/yarn.lock') }}
33 | restore-keys: |
34 | ${{ runner.os }}-yarn-
35 |
36 | - name: Install dependencies
37 | run: yarn --prefer-offline
38 |
39 | - name: Setup React Native environment
40 | run: |
41 | yarn add react@17.0.2
42 | yarn add react-native@0.64.2
43 |
44 | - name: Run code quality checks
45 | run: yarn quality
46 |
--------------------------------------------------------------------------------
/.github/workflows/tag.yml:
--------------------------------------------------------------------------------
1 | name: Create Tag
2 |
3 | on:
4 | push:
5 | branches:
6 | - main
7 |
8 | jobs:
9 | build:
10 | runs-on: ubuntu-latest
11 | steps:
12 | - uses: actions/checkout@v2
13 | - uses: Klemensas/action-autotag@stable
14 | with:
15 | GITHUB_TOKEN: '${{ secrets.GITHUB_TOKEN }}'
16 | tag_prefix: 'v'
17 |
--------------------------------------------------------------------------------
/.github/workflows/tests.yml:
--------------------------------------------------------------------------------
1 | name: Run tests
2 |
3 | on:
4 | push:
5 | branches:
6 | - main
7 | pull_request:
8 | branches:
9 | - main
10 |
11 | jobs:
12 | test:
13 | runs-on: ubuntu-latest
14 | steps:
15 | - name: Checkout
16 | uses: actions/checkout@v1
17 |
18 | - name: Setup Node.js
19 | uses: actions/setup-node@v1
20 | with:
21 | node-version: 14
22 |
23 | - name: Get yarn cache directory path
24 | id: yarn-cache-dir-path
25 | run: echo "::set-output name=dir::$(yarn cache dir)"
26 |
27 | - name: Restore yarn cache
28 | uses: actions/cache@v4
29 | id: yarn-cache # use this to check for `cache-hit` (`steps.yarn-cache.outputs.cache-hit != 'true'`)
30 | with:
31 | path: ${{ steps.yarn-cache-dir-path.outputs.dir }}
32 | key: ${{ runner.os }}-yarn-${{ hashFiles('**/yarn.lock') }}
33 | restore-keys: |
34 | ${{ runner.os }}-yarn-
35 |
36 | - name: Install dependencies
37 | run: yarn --prefer-offline
38 |
39 | - name: Setup React Native environment
40 | run: |
41 | yarn add react@17.0.2
42 | yarn add react-native@0.64.2
43 |
44 | - name: Run tests
45 | run: yarn test --coverage
46 |
47 | - name: Coveralls
48 | uses: coverallsapp/github-action@master
49 | with:
50 | github-token: ${{ secrets.GITHUB_TOKEN }}
51 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | .DS_Store
2 | .git
3 | node_modules
4 | .idea
5 | coverage
6 | lib
7 |
--------------------------------------------------------------------------------
/.npmignore:
--------------------------------------------------------------------------------
1 | **/__tests__/*
--------------------------------------------------------------------------------
/.prettierrc.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | bracketSameLine: true,
3 | jsxSingleQuote: true,
4 | singleQuote: true,
5 | trailingComma: 'none'
6 | };
7 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2019 Calin Tamas
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # react-native-toast-message
2 |
3 | [](https://www.npmjs.com/package/react-native-toast-message)
4 | [](https://www.npmjs.com/package/react-native-toast-message)
5 | [](https://github.com/calintamas/react-native-toast-message/actions/workflows/publish.yml?query=workflow%3Abuild)
6 | [](https://coveralls.io/github/calintamas/react-native-toast-message?branch=main)
7 | [](https://github.com/prettier/prettier)
8 |
9 | Animated toast message component for React Native.
10 |
11 | 
12 |
13 | ## Features
14 |
15 | - 🚀 Imperative API
16 | - 📦 Very lightweight (~40 kB)
17 | - ⌨️ Keyboard-aware
18 | - 🎨 Customizable layouts
19 | - 🔧 Flexible config
20 |
21 | ## Documentation
22 |
23 | > This is the documentation for `react-native-toast-message@v2`, which has a similar API to v1, but contains a few important changes. [Read the complete changelog](https://github.com/calintamas/react-native-toast-message/releases/tag/v2.0.0).
24 |
25 | - [Quick start](./docs/quick-start.md)
26 | - [API](./docs/api.md)
27 | - [Create custom layouts](./docs/custom-layouts.md)
28 | - FAQ
29 | - [How to show the Toast inside a Modal?](./docs/modal-usage.md)
30 | - [How to render the Toast when using a Navigation library?](./docs/navigation-usage.md)
31 | - [How to mock the library for testing with jest?](./docs/jest-testing.md)
32 |
33 | ## License
34 |
35 | MIT
36 |
--------------------------------------------------------------------------------
/babel.config.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | presets: ['module:metro-react-native-babel-preset']
3 | };
4 |
--------------------------------------------------------------------------------
/docs/api.md:
--------------------------------------------------------------------------------
1 | # API
2 |
3 | The `Toast` API consists of:
4 |
5 | 1. [methods](#methods) that can be called directly on the `Toast` object (in an _imperative_ way)
6 | 1. [props](#props) that can be passed to the `Toast` component instance; they act as defaults for all Toasts that are shown
7 |
8 | ## methods
9 |
10 | ### `show(options = {})`
11 |
12 | To show a Toast, call the `show()` method and pass the `options` that suit your needs. Everything is optional, unless specified otherwise:
13 |
14 | ```js
15 | import Toast from 'react-native-toast-message'
16 |
17 | Toast.show({
18 | type: 'info',
19 | text1: 'This is an info message'
20 | });
21 | ```
22 |
23 | The complete set of **options** is described below:
24 |
25 | | option | description | type | default value |
26 | | ---------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ----------------- | ------------- |
27 | | `type` | Toast type. Default available values: `success`, `error`, `info`. [Learn how to extend / overwrite Toast types](./custom-layouts.md) | `string` | `success` |
28 | | `text1` | First line of text | `string` | |
29 | | `text2` | Second line of text | `string` | |
30 | | `position` | Toast position | `top` or `bottom` | `top` |
31 | | `visibilityTime` | Number of milliseconds after which Toast automatically hides. Has effect only in conjunction with `autoHide` prop set to `true` | `number` | `4000` |
32 | | `autoHide` | When `true`, the visible Toast automatically hides after a certain number of milliseconds, specified by the `visibilityTime` prop | `boolean` | `true` |
33 | | `topOffset` | Offset from the top of the screen (in px). Has effect only when `position` is `top` | `number` | `40` |
34 | | `bottomOffset` | Offset from the bottom of the screen (in px). Has effect only when `position` is `bottom` | `number` | `40` |
35 | | `keyboardOffset` | Offset from the Keyboard (in px). Has effect only when `position` is `bottom` and Keyboard is visible (iOS only) | `number` | `10` |
36 | | `onShow` | Called when the Toast is shown | `() => void` | |
37 | | `onHide` | Called when the Toast hides | `() => void` | |
38 | | `onPress` | Called on Toast press | `() => void` | |
39 | | `props` | Any custom props passed to the specified Toast type. Has effect only when there is a custom Toast type (configured via the `config` prop on the Toast instance) that uses the `props` parameter | `any` | |
40 |
41 | ### `hide()`
42 |
43 | To hide the current visible Toast, call the `hide()` method:
44 |
45 | ```js
46 | Toast.hide();
47 | ```
48 |
49 | If an `onHide` callback was set (via `show()`, or as a default `prop` on the Toast component instance), it will be called now.
50 |
51 | ## props
52 |
53 | The following set of `props` can be passed to the `Toast` component instance to specify certain **defaults for all Toasts that are shown**:
54 |
55 | | prop | description | type | default value |
56 | | ---------------- | --------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------- | ------------- |
57 | | `config` | Layout configuration for custom Toast types | [`ToastConfig`](../src/types/index.ts) | |
58 | | `type` | Default Toast type | `string` | `success` |
59 | | `position` | Default Toast position | `top` or `bottom` | `top` |
60 | | `visibilityTime` | Number of milliseconds after which Toast automatically hides. Has effect only in conjunction with `autoHide` prop set to `true` | `number` | `4000` |
61 | | `autoHide` | When `true`, the visible Toast automatically hides after a certain number of milliseconds, specified by the `visibilityTime` prop | `boolean` | `true` |
62 | | `swipeable` | When `true`, the Toast can be swiped to dismiss | `boolean` | `true` |
63 | | `topOffset` | Offset from the top of the screen (in px). Has effect only when `position` is `top` | `number` | `40` |
64 | | `bottomOffset` | Offset from the bottom of the screen (in px). Has effect only when `position` is `bottom` | `number` | `40` |
65 | | `keyboardOffset` | Offset from the Keyboard (in px). Has effect only when `position` is `bottom` and Keyboard is visible (iOS only) | `number` | `10` |
66 | | `onShow` | Called when any Toast is shown | `() => void` | |
67 | | `onHide` | Called when any Toast hides | `() => void` | |
68 | | `onPress` | Called on any Toast press | `() => void` | |
69 |
70 | For example, to make sure all your Toasts are displayed at the bottom of the screen:
71 |
72 | ```js
73 | // App.jsx
74 | import Toast from 'react-native-toast-message';
75 |
76 | export function App(props) {
77 | return (
78 | <>
79 | {/* ... */}
80 |
84 | >
85 | );
86 | }
87 | ```
88 |
--------------------------------------------------------------------------------
/docs/custom-layouts.md:
--------------------------------------------------------------------------------
1 | # Create custom layouts
2 |
3 | If you want to add custom Toast types - or overwrite the existing ones - you can add a [`config` prop](./api.md#props) when rendering the `Toast` component in your app's entry point.
4 |
5 | When creating the `config`, you can either:
6 |
7 | 1. Use any of the default `BaseToast`, `SuccessToast`, `ErrorToast` or `InfoToast` components and adjust their layout
8 | 1. Create Toast layouts from scratch
9 |
10 | ```js
11 | // App.jsx
12 | import Toast, { BaseToast, ErrorToast } from 'react-native-toast-message';
13 |
14 | /*
15 | 1. Create the config
16 | */
17 | const toastConfig = {
18 | /*
19 | Overwrite 'success' type,
20 | by modifying the existing `BaseToast` component
21 | */
22 | success: (props) => (
23 |
32 | ),
33 | /*
34 | Overwrite 'error' type,
35 | by modifying the existing `ErrorToast` component
36 | */
37 | error: (props) => (
38 |
47 | ),
48 | /*
49 | Or create a completely new type - `tomatoToast`,
50 | building the layout from scratch.
51 |
52 | I can consume any custom `props` I want.
53 | They will be passed when calling the `show` method (see below)
54 | */
55 | tomatoToast: ({ text1, props }) => (
56 |
57 | {text1}
58 | {props.uuid}
59 |
60 | )
61 | };
62 |
63 | /*
64 | 2. Pass the config as prop to the Toast component instance
65 | */
66 | export function App(props) {
67 | return (
68 | <>
69 | {...}
70 |
71 | >
72 | );
73 | }
74 | ```
75 |
76 | Then just use the library as before.
77 |
78 | For example, if I want to show the new `tomatoToast` type I just created above:
79 |
80 | ```js
81 | Toast.show({
82 | type: 'tomatoToast',
83 | // And I can pass any custom props I want
84 | props: { uuid: 'bba1a7d0-6ab2-4a0a-a76e-ebbe05ae6d70' }
85 | });
86 | ```
87 |
88 | All the available props on `BaseToast`, `SuccessToast`, `ErrorToast` or `InfoToast` components can be found here: [BaseToastProps](../src/types/index.ts#L86-L103).
89 |
--------------------------------------------------------------------------------
/docs/jest-testing.md:
--------------------------------------------------------------------------------
1 | # How to mock the library for testing with [jest](https://jestjs.io)?
2 |
3 | ```js
4 | jest.mock('react-native-toast-message', () => ({
5 | show: jest.fn(),
6 | hide: jest.fn()
7 | }));
8 | ```
9 |
--------------------------------------------------------------------------------
/docs/modal-usage.md:
--------------------------------------------------------------------------------
1 | # How to show the Toast inside a Modal?
2 |
3 | ## How are `refs` tracked
4 |
5 | By default, when you render a `` instance in your App's entry point (root), a `ref` is created and tracked internally.
6 |
7 | ```js
8 | // App.jsx
9 | import Toast from 'react-native-toast-message'
10 |
11 | export function App(props) {
12 | return (
13 | <>
14 | {/* ... */}
15 | {/* A `ref` pointing to this Toast instance is created */}
16 |
17 | >
18 | );
19 | }
20 | ```
21 |
22 | Under the hood, this `ref` is used when you imperatively call `Toast.show()` or `Toast.hide()`.
23 |
24 | ## Showing a Toast inside a Modal
25 |
26 | When you have a [Modal](https://reactnative.dev/docs/modal), things get different. This `Modal` component is [_above_ React's root `View`](https://stackoverflow.com/questions/39766350/bring-view-on-top-of-modal-using-zindex-style-with-react-native), so the only way to show something _on top of the modal_ is to render it inside the `Modal` itself.
27 |
28 | This means **you need a new instance** of `` rendered inside your `Modal` (as well as keeping the existing `` instance outside, in your App's entry point).
29 |
30 | ```diff
31 | // App.jsx
32 | import { Modal } from 'react-native'
33 | import Toast from 'react-native-toast-message'
34 |
35 | export function App(props) {
36 | const [isModalVisible, setIsModalVisible] = React.useState(false);
37 |
38 | return (
39 | <>
40 | {/* ... */}
41 |
42 |
43 | +
44 |
45 | >
46 | );
47 | }
48 | ```
49 |
50 | Everything else works as usual; you can show and hide Toasts using the imperative API: `Toast.show()` or `Toast.hide()`. When the `Modal` is visible, the `ref` from inside the `Modal` will be used, otherwise the one outside.
51 |
52 | > The `ref` is tracked automatically; whichever `` instance last had its `ref` set will be used when showing/hiding.
53 |
54 | ### Notes regarding `react-native-modal` or `NativeStackNavigator`
55 |
56 | The same requirements as above apply when using [react-native-modal](https://github.com/react-native-modal/react-native-modal) or a [NativeStackNavigator](https://reactnavigation.org/docs/native-stack-navigator#presentation) with `presentation: 'modal'`:
57 |
58 | ```js
59 | <>
60 | {/* This `Toast` will show when neither the native stack screen nor `Modal` are presented */}
61 |
62 |
63 |
64 | {/* This `Toast` will show when the `NativeStackNavigator.Screen` is visible, but the `Modal` is NOT visible. */}
65 |
66 |
67 |
68 | {/* This `Toast` will show when both the `NativeStackNavigator.Screen` and the `Modal` are visible. */}
69 |
70 |
71 |
72 | >
73 | ```
74 |
--------------------------------------------------------------------------------
/docs/navigation-usage.md:
--------------------------------------------------------------------------------
1 | # How to render the Toast when using a Navigation library?
2 |
3 | 1. Usage with [react-navigation](https://reactnavigation.org)
4 |
5 | ## Usage with [react-navigation](https://reactnavigation.org)
6 |
7 | To have the Toast visible on top of the navigation `View` hierarchy, render it as the **last child** in the `View` hierarchy (along the root Navigation component):
8 |
9 | ```js
10 | import Toast from 'react-native-toast-message'
11 | import { NavigationContainer } from '@react-navigation/native';
12 |
13 | export function App() {
14 | return (
15 | <>
16 |
17 | {...}
18 |
19 |
20 | >
21 | );
22 | }
23 | ```
24 |
--------------------------------------------------------------------------------
/docs/quick-start.md:
--------------------------------------------------------------------------------
1 | # Quick start
2 |
3 | ## Install
4 |
5 | ```sh
6 | yarn add react-native-toast-message
7 | # or
8 | npm install --save react-native-toast-message
9 | ```
10 |
11 | ## Usage
12 |
13 | Render the `Toast` component in your app's entry file, as the **LAST CHILD** in the `View` hierarchy (along with any other components that might be rendered there):
14 |
15 | ```js
16 | // App.jsx
17 | import Toast from 'react-native-toast-message';
18 |
19 | export function App(props) {
20 | return (
21 | <>
22 | {/* ... */}
23 |
24 | >
25 | );
26 | }
27 | ```
28 |
29 | Then use it anywhere in your app (even outside React components), by calling [any `Toast` method](./api.md#methods) directly:
30 |
31 | ```js
32 | // Foo.jsx
33 | import Toast from 'react-native-toast-message';
34 | import { Button } from 'react-native'
35 |
36 | export function Foo(props) {
37 | const showToast = () => {
38 | Toast.show({
39 | type: 'success',
40 | text1: 'Hello',
41 | text2: 'This is some something 👋'
42 | });
43 | }
44 |
45 | return (
46 |
50 | )
51 | }
52 | ```
53 |
54 | ## What's next
55 |
56 | Explore the following topics:
57 |
58 | - [Using the Toast API](./api.md)
59 | - [Create custom layouts](./custom-layouts.md)
60 |
--------------------------------------------------------------------------------
/docs/toast.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/calintamas/react-native-toast-message/aaf5a1cf27f1c42bab3c4e7b97a34448a10ac058/docs/toast.gif
--------------------------------------------------------------------------------
/index.ts:
--------------------------------------------------------------------------------
1 | export { Toast as default } from './src/Toast';
2 | export { BaseToast } from './src/components/BaseToast';
3 | export { SuccessToast } from './src/components/SuccessToast';
4 | export { ErrorToast } from './src/components/ErrorToast';
5 | export { InfoToast } from './src/components/InfoToast';
6 | export * from './src/types';
7 |
--------------------------------------------------------------------------------
/jest.config.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | preset: 'react-native',
3 | testEnvironment: 'node',
4 | collectCoverage: true,
5 | collectCoverageFrom: ['src/**/*.{ts,tsx}'],
6 | setupFilesAfterEnv: [
7 | '@testing-library/jest-native/extend-expect',
8 | './jest.setup.js'
9 | ],
10 | testPathIgnorePatterns: ['/__helpers__/']
11 | };
12 |
--------------------------------------------------------------------------------
/jest.setup.js:
--------------------------------------------------------------------------------
1 | /* eslint-env jest */
2 |
3 | jest.mock('react-native/Libraries/Animated/NativeAnimatedHelper');
4 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "react-native-toast-message",
3 | "version": "2.3.0",
4 | "description": "Toast message component for React Native",
5 | "main": "./lib/index.js",
6 | "types": "./lib/index.d.ts",
7 | "files": [
8 | "/lib"
9 | ],
10 | "repository": {
11 | "type": "git",
12 | "url": "git+https://github.com/calintamas/react-native-toast-message.git"
13 | },
14 | "keywords": [
15 | "react-native",
16 | "toast"
17 | ],
18 | "scripts": {
19 | "prepare": "husky install",
20 | "build": "rm -rf ./lib && tsc",
21 | "prettier": "./node_modules/.bin/prettier --write",
22 | "lint": "./node_modules/.bin/eslint --fix",
23 | "lint-staged": "./node_modules/.bin/lint-staged",
24 | "test": "./node_modules/.bin/jest",
25 | "yalc:push": "yarn build && yalc publish --push",
26 | "quality": "yarn lint && tsc --noEmit"
27 | },
28 | "author": "Calin Tamas ",
29 | "license": "MIT",
30 | "devDependencies": {
31 | "@babel/core": "^7.15.8",
32 | "@testing-library/jest-native": "^4.0.4",
33 | "@testing-library/react-hooks": "^8.0.0",
34 | "@testing-library/react-native": "^9.0.0",
35 | "@types/jest": "^28.1.0",
36 | "@types/react-native": "^0.67.3",
37 | "eslint-config-backpacker-react-ts": "^0.3.0",
38 | "husky": "^8.0.1",
39 | "import-sort-style-module": "^6.0.0",
40 | "jest": "^28.1.0",
41 | "lint-staged": "^13.0.0",
42 | "metro-react-native-babel-preset": "^0.71.0",
43 | "prettier": "^2.4.1",
44 | "prettier-plugin-import-sort": "^0.0.7",
45 | "react-test-renderer": "^17.0.2",
46 | "typescript": "^4.4.3",
47 | "yalc": "^1.0.0-pre.53"
48 | },
49 | "peerDependencies": {
50 | "react": "*",
51 | "react-native": "*"
52 | },
53 | "importSort": {
54 | ".js, .jsx, .ts, .tsx": {
55 | "style": "module",
56 | "parser": "typescript"
57 | }
58 | },
59 | "lint-staged": {
60 | "*.{js,jsx,ts,tsx,md}": "yarn prettier",
61 | "*.{js,jsx,ts,tsx}": "yarn lint"
62 | }
63 | }
64 |
--------------------------------------------------------------------------------
/scripts/dispatch-publish-beta.sh:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env bash
2 |
3 | # Run this script to trigger the "Publish a new beta version" workflow
4 | #
5 | # To trigger the "Repository Dispatch Event", you need to authenticate to Github with a personal access token
6 | # https://help.github.com/en/github/authenticating-to-github/creating-a-personal-access-token-for-the-command-line#creating-a-token
7 |
8 | ## Before running, configure the following params:
9 | # Github repo
10 | REPO=calintamas/react-native-toast-message
11 | # Github username of the user triggering the workflow
12 | USERNAME=calintamas
13 | # Branch from which the workflow will run
14 | BRANCH=\"v2.1.0\"
15 |
16 | PAYLOAD="{\"ref\": $BRANCH}"
17 |
18 | curl https://api.github.com/repos/$REPO/dispatches \
19 | -XPOST \
20 | -u $USERNAME \
21 | -H "Content-Type: application/json" \
22 | -d "{\"event_type\": \"publish-beta\", \"client_payload\": $PAYLOAD}"
23 |
--------------------------------------------------------------------------------
/src/Toast.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 |
3 | import { LoggerProvider } from './contexts';
4 | import { ToastUI } from './ToastUI';
5 | import {
6 | ToastHideParams,
7 | ToastProps,
8 | ToastRef,
9 | ToastShowParams
10 | } from './types';
11 | import { useToast } from './useToast';
12 |
13 | const ToastRoot = React.forwardRef((props: ToastProps, ref) => {
14 | const { config, ...defaultOptions } = props;
15 | const { show, hide, isVisible, options, data } = useToast({
16 | defaultOptions
17 | });
18 |
19 | // This must use useCallback to ensure the ref doesn't get set to null and then a new ref every render.
20 | React.useImperativeHandle(
21 | ref,
22 | React.useCallback(
23 | () => ({
24 | show,
25 | hide
26 | }),
27 | [hide, show]
28 | )
29 | );
30 |
31 | return (
32 |
40 | );
41 | });
42 |
43 | type ToastRefObj = {
44 | current: ToastRef | null;
45 | };
46 |
47 | let refs: ToastRefObj[] = [];
48 |
49 | /**
50 | * Adds a ref to the end of the array, which will be used to show the toasts until its ref becomes null.
51 | *
52 | * @param newRef the new ref, which must be stable for the life of the Toast instance.
53 | */
54 | function addNewRef(newRef: ToastRef) {
55 | refs.push({
56 | current: newRef
57 | });
58 | }
59 |
60 | /**
61 | * Removes the passed in ref from the file-level refs array using a strict equality check.
62 | *
63 | * @param oldRef the exact ref object to remove from the refs array.
64 | */
65 | function removeOldRef(oldRef: ToastRef | null) {
66 | refs = refs.filter((r) => r.current !== oldRef);
67 | }
68 |
69 | export function Toast(props: ToastProps) {
70 | const toastRef = React.useRef(null);
71 |
72 | /*
73 | This must use `useCallback` to ensure the ref doesn't get set to null and then a new ref every render.
74 | Failure to do so will cause whichever Toast *renders or re-renders* last to be the instance that is used,
75 | rather than being the Toast that was *mounted* last.
76 | */
77 | const setRef = React.useCallback((ref: ToastRef | null) => {
78 | // Since we know there's a ref, we'll update `refs` to use it.
79 | if (ref) {
80 | // store the ref in this toast instance to be able to remove it from the array later when the ref becomes null.
81 | toastRef.current = ref;
82 | addNewRef(ref);
83 | } else {
84 | // remove the this toast's ref, wherever it is in the array.
85 | removeOldRef(toastRef.current);
86 | }
87 | }, []);
88 |
89 | return (
90 |
91 |
92 |
93 | );
94 | }
95 |
96 | /**
97 | * Get the active Toast instance `ref`, by priority.
98 | * The "highest" Toast in the `View` hierarchy has the highest priority.
99 | *
100 | * For example, a Toast inside a `Modal`, would have had its ref set later than a Toast inside App's Root.
101 | * Therefore, the library knows that it is currently visible on top of the App's Root
102 | * and will thus use the `Modal`'s Toast when showing/hiding.
103 | *
104 | * ```js
105 | * <>
106 | *
107 | *
108 | *
109 | *
110 | * >
111 | * ```
112 | */
113 | function getRef() {
114 | const reversePriority = [...refs].reverse();
115 | const activeRef = reversePriority.find((ref) => ref?.current !== null);
116 | if (!activeRef) {
117 | return null;
118 | }
119 | return activeRef.current;
120 | }
121 |
122 | Toast.show = (params: ToastShowParams) => {
123 | getRef()?.show(params);
124 | };
125 |
126 | Toast.hide = (params?: ToastHideParams) => {
127 | getRef()?.hide(params);
128 | };
129 |
--------------------------------------------------------------------------------
/src/ToastUI.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 |
3 | import { AnimatedContainer } from './components/AnimatedContainer';
4 | import { ErrorToast } from './components/ErrorToast';
5 | import { InfoToast } from './components/InfoToast';
6 | import { SuccessToast } from './components/SuccessToast';
7 | import {
8 | ToastConfig,
9 | ToastData,
10 | ToastHideParams,
11 | ToastOptions,
12 | ToastShowParams
13 | } from './types';
14 |
15 | export type ToastUIProps = {
16 | isVisible: boolean;
17 | options: Required;
18 | data: ToastData;
19 | show: (params: ToastShowParams) => void;
20 | hide: (params: ToastHideParams) => void;
21 | config?: ToastConfig;
22 | };
23 |
24 | const defaultToastConfig: ToastConfig = {
25 | success: (props) => ,
26 | error: (props) => ,
27 | info: (props) =>
28 | };
29 |
30 | function renderComponent({
31 | data,
32 | options,
33 | config,
34 | isVisible,
35 | show,
36 | hide
37 | }: ToastUIProps) {
38 | const { text1, text2 } = data;
39 | const { type, onPress, text1Style, text2Style, position, props } = options;
40 |
41 | const toastConfig = {
42 | ...defaultToastConfig,
43 | ...config
44 | };
45 | const ToastComponent = toastConfig[type];
46 |
47 | if (!ToastComponent) {
48 | throw new Error(
49 | `Toast type: '${type}' does not exist. You can add it via the 'config' prop on the Toast instance. Learn more: https://github.com/calintamas/react-native-toast-message/blob/master/README.md`
50 | );
51 | }
52 |
53 | return ToastComponent({
54 | position,
55 | type,
56 | isVisible,
57 | text1,
58 | text2,
59 | text1Style,
60 | text2Style,
61 | show,
62 | hide,
63 | onPress,
64 | props
65 | });
66 | }
67 |
68 | export function ToastUI(props: ToastUIProps) {
69 | const { isVisible, options, hide } = props;
70 | const { position, topOffset, bottomOffset, keyboardOffset, avoidKeyboard, swipeable } = options;
71 |
72 | return (
73 |
82 | {renderComponent(props)}
83 |
84 | );
85 | }
86 |
--------------------------------------------------------------------------------
/src/__helpers__/PanResponder.ts:
--------------------------------------------------------------------------------
1 | import {
2 | GestureResponderHandlers,
3 | PanResponder,
4 | PanResponderCallbacks,
5 | PanResponderGestureState
6 | } from 'react-native';
7 |
8 | export const mockGestureValues: PanResponderGestureState = {
9 | moveY: 0,
10 | moveX: 0,
11 | y0: 0,
12 | x0: 0,
13 | dx: 0,
14 | dy: 10,
15 | stateID: 123,
16 | vx: 0,
17 | vy: 0,
18 | numberActiveTouches: 1,
19 | _accountsForMovesUpTo: 1
20 | };
21 |
22 | export function mockPanResponder() {
23 | jest
24 | .spyOn(PanResponder, 'create')
25 | .mockImplementation(
26 | ({
27 | onMoveShouldSetPanResponder,
28 | onMoveShouldSetPanResponderCapture,
29 | onPanResponderMove,
30 | onPanResponderRelease
31 | }: PanResponderCallbacks) => ({
32 | panHandlers: {
33 | onMoveShouldSetResponder: onMoveShouldSetPanResponder,
34 | onMoveShouldSetResponderCapture: onMoveShouldSetPanResponderCapture,
35 | onResponderMove: onPanResponderMove,
36 | onResponderRelease: onPanResponderRelease
37 | } as GestureResponderHandlers
38 | })
39 | );
40 | }
41 |
--------------------------------------------------------------------------------
/src/__tests__/Toast.test.tsx:
--------------------------------------------------------------------------------
1 | /* eslint-env jest */
2 |
3 | import { act, fireEvent, render, waitFor } from '@testing-library/react-native';
4 | import React from 'react';
5 | import { Button, Modal, Text } from 'react-native';
6 |
7 | import { Toast } from '../Toast';
8 |
9 | /*
10 | The Modal component is automatically mocked by RN and apparently contains a bug which makes the Modal
11 | (and its children) to always be visible in the test tree.
12 |
13 | This fixes the issue:
14 | */
15 | jest.mock('react-native/Libraries/Modal/Modal', () => {
16 | const ActualModal = jest.requireActual('react-native/Libraries/Modal/Modal');
17 | return (props) => ;
18 | });
19 |
20 | jest.mock('react-native/Libraries/LogBox/LogBox');
21 |
22 | describe('test Toast component', () => {
23 | it('creates imperative handle', () => {
24 | const onShow = jest.fn();
25 | const onHide = jest.fn();
26 |
27 | render();
28 |
29 | expect(Toast.show).toBeDefined();
30 | expect(Toast.hide).toBeDefined();
31 |
32 | act(() => {
33 | Toast.show({
34 | text1: 'test'
35 | });
36 | });
37 | expect(onShow).toHaveBeenCalled();
38 | act(() => {
39 | Toast.hide();
40 | });
41 | expect(onHide).toHaveBeenCalled();
42 | });
43 |
44 | it('shows Toast inside a Modal', async () => {
45 | const onShow = jest.fn();
46 | const onHide = jest.fn();
47 |
48 | const onShowInsideModal = jest.fn();
49 | const onHideInsideModal = jest.fn();
50 |
51 | const ModalWrapper = () => {
52 | const [isVisible, setIsVisible] = React.useState(false);
53 | return (
54 | <>
55 |
56 | Outside modal
57 |