diff --git a/docs/quick-start.md b/docs/quick-start.md index 1f941d0ee..f028c9ad9 100644 --- a/docs/quick-start.md +++ b/docs/quick-start.md @@ -1,13 +1,34 @@ # Quick start -## Install +## Install React Native Project ```sh yarn add react-native-toast-message # or npm install --save react-native-toast-message +cd ios && pod install # for iOS ``` +## Install Expo Project +```sh +npx expo install react-native-toast-message +``` + +## Rebuild the project +Rebuild the project +```sh +# expo projects +npx expo run:android +npx expo run:ios + +# non-expo projects +npx react-native run-android +npx react-native run-ios +``` + +## Expo +- ❌ This library can't be used in the "Expo Go" app because it [requires custom native code](https://docs.expo.dev/workflow/customizing/). + ## Usage 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): diff --git a/ios/SafeAreaModule.h b/ios/SafeAreaModule.h new file mode 100644 index 000000000..1a55dc8ca --- /dev/null +++ b/ios/SafeAreaModule.h @@ -0,0 +1,5 @@ +#import + +@interface RNSafeAreaModule : NSObject + +@end diff --git a/ios/SafeAreaModule.mm b/ios/SafeAreaModule.mm new file mode 100644 index 000000000..ee30f12a4 --- /dev/null +++ b/ios/SafeAreaModule.mm @@ -0,0 +1,27 @@ +#import +#import +@interface SafeAreaModule : NSObject +@end + +@implementation SafeAreaModule +RCT_EXPORT_MODULE(); + +RCT_EXPORT_METHOD(getSafeAreaInsets:(RCTResponseSenderBlock)callback) { + UIViewController *rootViewController = [UIApplication sharedApplication].keyWindow.rootViewController; + UIEdgeInsets safeAreaInsets = UIEdgeInsetsZero; + + if (@available(iOS 11.0, *)) { + safeAreaInsets = rootViewController.view.safeAreaInsets; + } + + NSDictionary *result = @{ + @"top": @(safeAreaInsets.top), + @"bottom": @(safeAreaInsets.bottom), + @"left": @(safeAreaInsets.left), + @"right": @(safeAreaInsets.right) + }; + + callback(@[[NSNull null], result]); +} + +@end diff --git a/package.json b/package.json index 81135fe6d..598bb29f4 100644 --- a/package.json +++ b/package.json @@ -5,7 +5,9 @@ "main": "./lib/index.js", "types": "./lib/index.d.ts", "files": [ - "/lib" + "/lib", + "ios", + "*.podspec" ], "repository": { "type": "git", diff --git a/react-native-toast-message.podspec b/react-native-toast-message.podspec new file mode 100644 index 000000000..1204fa33a --- /dev/null +++ b/react-native-toast-message.podspec @@ -0,0 +1,41 @@ +require "json" + +package = JSON.parse(File.read(File.join(__dir__, "package.json"))) +folly_compiler_flags = '-DFOLLY_NO_CONFIG -DFOLLY_MOBILE=1 -DFOLLY_USE_LIBCPP=1 -Wno-comma -Wno-shorten-64-to-32' + +Pod::Spec.new do |s| + s.name = "react-native-toast-message" + s.version = package["version"] + s.summary = package["description"] + s.homepage = "https://github.com/calintamas/react-native-toast-message" + s.license = package["license"] + s.authors = package["author"] + + s.platforms = { :ios => "9.0" } + s.source = { :git => "https://github.com/calintamas/react-native-toast-message.git", :tag => "#{s.version}" } + + s.source_files = "ios/**/*.{h,m,mm,swift}" + + # Use install_modules_dependencies helper to install the dependencies if React Native version >=0.71.0. + # See https://github.com/facebook/react-native/blob/febf6b7f33fdb4904669f99d795eba4c0f95d7bf/scripts/cocoapods/new_architecture.rb#L79. + if respond_to?(:install_modules_dependencies, true) + install_modules_dependencies(s) + else + s.dependency "React-Core" + + # Don't install the dependencies when we run `pod install` in the old architecture. + if ENV['RCT_NEW_ARCH_ENABLED'] == '1' then + s.compiler_flags = folly_compiler_flags + " -DRCT_NEW_ARCH_ENABLED=1" + s.pod_target_xcconfig = { + "HEADER_SEARCH_PATHS" => "\"$(PODS_ROOT)/boost\"", + "OTHER_CPLUSPLUSFLAGS" => "-DFOLLY_NO_CONFIG -DFOLLY_MOBILE=1 -DFOLLY_USE_LIBCPP=1", + "CLANG_CXX_LANGUAGE_STANDARD" => "c++17" + } + s.dependency "React-Codegen" + s.dependency "RCT-Folly" + s.dependency "RCTRequired" + s.dependency "RCTTypeSafety" + s.dependency "ReactCommon/turbomodule/core" + end + end +end diff --git a/src/__tests__/useToast.test.ts b/src/__tests__/useToast.test.ts index 00a9c3e7c..83ef209b0 100644 --- a/src/__tests__/useToast.test.ts +++ b/src/__tests__/useToast.test.ts @@ -121,8 +121,8 @@ describe('test useToast hook', () => { position: 'bottom', autoHide: false, visibilityTime: 20, - topOffset: 120, - bottomOffset: 130, + topOffset: 40, + bottomOffset: 40, keyboardOffset: 5, onShow: jest.fn(), onHide: jest.fn(), diff --git a/src/hooks/index.ts b/src/hooks/index.ts index 2840571eb..be6f968c4 100644 --- a/src/hooks/index.ts +++ b/src/hooks/index.ts @@ -3,3 +3,4 @@ export * from './useSlideAnimation'; export * from './useTimeout'; export * from './usePanResponder'; export * from './useKeyboard'; +export * from './useSafeArea'; diff --git a/src/hooks/useSafeArea.ts b/src/hooks/useSafeArea.ts new file mode 100644 index 000000000..bc8739664 --- /dev/null +++ b/src/hooks/useSafeArea.ts @@ -0,0 +1,42 @@ +import {NativeModules, StatusBar} from 'react-native' +import {useEffect, useState} from "react"; +import {isIOS} from "../utils/platform"; +import {SCREEN_HEIGHT, WINDOW_HEIGHT} from "../utils/dimension"; + +type TSafeArea = { + top: number + bottom: number + left: number + right: number +} + +type TNativeModulesSafeArea = { + SafeAreaInsetsModule: { + getSafeAreaInsets(p: (error: Error, result: TSafeArea) => void): Promise<{ + top: number; + bottom: number; + left: number; + right: number + }>; + }; +} +export const useSafeArea = () => { + const [safeAreaInsets, setSafeAreaInsets] = useState(null); + useEffect(() => { + if (isIOS()) { + NativeModules?.SafeAreaModule?.getSafeAreaInsets((error: Error, result: TSafeArea) => { + if (error) { + console.error(error); + } else { + setSafeAreaInsets(result) + } + }); + } else { + const statusBarHeight = StatusBar.currentHeight ?? 0 + const bottomInset = SCREEN_HEIGHT - WINDOW_HEIGHT - statusBarHeight; + setSafeAreaInsets({top: statusBarHeight, bottom: bottomInset, left: 0, right: 0}) + } + }, []) + + return {safeAreaInsets} +} diff --git a/src/useToast.ts b/src/useToast.ts index ae494db0a..5e7060fbd 100644 --- a/src/useToast.ts +++ b/src/useToast.ts @@ -1,7 +1,7 @@ import React from 'react'; import { useLogger } from './contexts'; -import { useTimeout } from './hooks'; +import { useSafeArea, useTimeout } from './hooks'; import { ToastData, ToastOptions, ToastProps, ToastShowParams } from './types'; import { noop } from './utils/func'; import { mergeIfDefined } from './utils/obj'; @@ -34,6 +34,7 @@ export function useToast({ defaultOptions }: UseToastParams) { const [isVisible, setIsVisible] = React.useState(false); const [data, setData] = React.useState(DEFAULT_DATA); + const {safeAreaInsets} = useSafeArea() const initialOptions = mergeIfDefined( DEFAULT_OPTIONS, @@ -62,6 +63,8 @@ export function useToast({ defaultOptions }: UseToastParams) { const show = React.useCallback( (params: ToastShowParams) => { log(`Showing with params: ${JSON.stringify(params)}`); + const topOffset = safeAreaInsets && safeAreaInsets.top > 0 ? safeAreaInsets.top : initialOptions.topOffset + const bottomOffset = safeAreaInsets && safeAreaInsets.bottom > 0 ? safeAreaInsets.bottom : initialOptions.bottomOffset const { text1 = DEFAULT_DATA.text1, text2 = DEFAULT_DATA.text2, @@ -69,8 +72,6 @@ export function useToast({ defaultOptions }: UseToastParams) { position = initialOptions.position, autoHide = initialOptions.autoHide, visibilityTime = initialOptions.visibilityTime, - topOffset = initialOptions.topOffset, - bottomOffset = initialOptions.bottomOffset, keyboardOffset = initialOptions.keyboardOffset, onShow = initialOptions.onShow, onHide = initialOptions.onHide, diff --git a/src/utils/dimension.ts b/src/utils/dimension.ts new file mode 100644 index 000000000..f61cb3165 --- /dev/null +++ b/src/utils/dimension.ts @@ -0,0 +1,4 @@ +import {Dimensions} from "react-native"; + +export const {height: WINDOW_HEIGHT} = Dimensions.get('window') +export const {height: SCREEN_HEIGHT} = Dimensions.get('screen')