diff --git a/android/app/src/main/java/me/rainbow/MainApplication.java b/android/app/src/main/java/me/rainbow/MainApplication.java index a415a0d3713..fc9d96c517e 100644 --- a/android/app/src/main/java/me/rainbow/MainApplication.java +++ b/android/app/src/main/java/me/rainbow/MainApplication.java @@ -18,6 +18,7 @@ import me.rainbow.NativeModules.Internals.InternalPackage; import me.rainbow.NativeModules.RNBip39.RNBip39Package; import me.rainbow.NativeModules.RNBackHandler.RNBackHandlerPackage; +import me.rainbow.NativeModules.SystemNavigationBar.SystemNavigationBarPackage; import me.rainbow.NativeModules.RNReview.RNReviewPackage; import me.rainbow.NativeModules.RNStartTime.RNStartTimePackage; import me.rainbow.NativeModules.RNTextAnimatorPackage.RNTextAnimatorPackage; @@ -53,6 +54,7 @@ protected List getPackages() { // Packages that cannot be autolinked yet can be added manually here, for example: packages.add(new RNBip39Package()); packages.add(new RNReviewPackage()); + packages.add(new SystemNavigationBarPackage()); packages.add(new RNBackHandlerPackage()); packages.add(new RNTextAnimatorPackage()); packages.add(new RNZoomableButtonPackage()); @@ -61,8 +63,7 @@ protected List getPackages() { packages.add(new RNStartTimePackage(MainApplication.START_MARK)); packages.add(new RNHapticsPackage()); - - return packages; + return packages; } @Override diff --git a/android/app/src/main/java/me/rainbow/NativeModules/SystemNavigationBar/SystemNavigationBarModule.java b/android/app/src/main/java/me/rainbow/NativeModules/SystemNavigationBar/SystemNavigationBarModule.java new file mode 100644 index 00000000000..a0c2aec95e6 --- /dev/null +++ b/android/app/src/main/java/me/rainbow/NativeModules/SystemNavigationBar/SystemNavigationBarModule.java @@ -0,0 +1,176 @@ +// Created by react-native-create-bridge + +package me.rainbow.NativeModules.SystemNavigationBar; + +import android.animation.ArgbEvaluator; +import android.animation.ValueAnimator; +import android.annotation.TargetApi; +import android.graphics.Color; +import android.os.Build; +import android.app.Activity; +import android.view.View; +import android.view.Window; +import android.view.WindowManager; +import androidx.annotation.UiThread; +import com.facebook.react.bridge.Arguments; +import com.facebook.react.bridge.Promise; +import com.facebook.react.bridge.ReactApplicationContext; +import com.facebook.react.bridge.ReactContextBaseJavaModule; +import com.facebook.react.bridge.ReactMethod; +import com.facebook.react.bridge.UiThreadUtil; +import com.facebook.react.bridge.WritableMap; +import java.util.HashMap; +import java.util.Map; +import com.facebook.react.uimanager.IllegalViewOperationException; +import static com.facebook.react.bridge.UiThreadUtil.runOnUiThread; + +public class SystemNavigationBarModule extends ReactContextBaseJavaModule { + public static final String REACT_CLASS = "NavigationBar"; + private static final String ERROR_NO_ACTIVITY = "E_NO_ACTIVITY"; + private static final String ERROR_NO_ACTIVITY_MESSAGE = "Tried to change the navigation bar while not attached to an Activity"; + private static final String ERROR_API_LEVEL = "API_LEVEl"; + private static final String ERROR_API_LEVEL_MESSAGE = "Only Android Oreo and above is supported"; + private static ReactApplicationContext reactContext = null; + private static final int UI_FLAG_HIDE_NAV_BAR = View.SYSTEM_UI_FLAG_LAYOUT_STABLE + | View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION + | View.SYSTEM_UI_FLAG_HIDE_NAVIGATION // hide nav bar + | View.SYSTEM_UI_FLAG_IMMERSIVE_STICKY; + + public SystemNavigationBarModule(ReactApplicationContext context) { + // Pass in the context to the constructor and save it so you can emit events + // https://facebook.github.io/react-native/docs/native-modules-android.html#the-toast-module + super(context); + reactContext = context; + } + + public void setNavigationBarTheme(Activity activity, Boolean light) { + if (activity != null && Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { + Window window = activity.getWindow(); + int flags = window.getDecorView().getSystemUiVisibility(); + if (light) { + flags |= View.SYSTEM_UI_FLAG_LIGHT_NAVIGATION_BAR; + } else { + flags &= ~View.SYSTEM_UI_FLAG_LIGHT_NAVIGATION_BAR; + } + window.getDecorView().setSystemUiVisibility(flags); + } + } + + + @Override + public String getName() { + // Tell React the name of the module + // https://facebook.github.io/react-native/docs/native-modules-android.html#the-toast-module + return REACT_CLASS; + } + + @Override + public Map getConstants() { + // Export any constants to be used in your native module + // https://facebook.github.io/react-native/docs/native-modules-android.html#the-toast-module + final Map constants = new HashMap<>(); + constants.put("EXAMPLE_CONSTANT", "example"); + + return constants; + } + + @ReactMethod + public void changeNavigationBarColor(final String color, final Boolean light, final Boolean animated, final Promise promise) { + final WritableMap map = Arguments.createMap(); + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { + if (getCurrentActivity() != null) { + try { + final Window window = getCurrentActivity().getWindow(); + runOnUiThread(new Runnable() { + @Override + public void run() { + if (color.equals("transparent") || color.equals("translucent")) { + window.clearFlags(WindowManager.LayoutParams.FLAG_LAYOUT_NO_LIMITS); + window.clearFlags(WindowManager.LayoutParams.FLAG_TRANSLUCENT_NAVIGATION); + if (color.equals("transparent")) { + window.setFlags(WindowManager.LayoutParams.FLAG_LAYOUT_NO_LIMITS, WindowManager.LayoutParams.FLAG_LAYOUT_NO_LIMITS); + } else { + window.setFlags(WindowManager.LayoutParams.FLAG_TRANSLUCENT_NAVIGATION, WindowManager.LayoutParams.FLAG_TRANSLUCENT_NAVIGATION); + } + setNavigationBarTheme(getCurrentActivity(), light); + map.putBoolean("success", true); + promise.resolve(map); + return; + } else { + window.clearFlags(WindowManager.LayoutParams.FLAG_LAYOUT_NO_LIMITS); + window.clearFlags(WindowManager.LayoutParams.FLAG_TRANSLUCENT_NAVIGATION); + } + if (animated) { + Integer colorFrom = window.getNavigationBarColor(); + Integer colorTo = Color.parseColor(String.valueOf(color)); + //window.setNavigationBarColor(colorTo); + ValueAnimator colorAnimation = ValueAnimator.ofObject(new ArgbEvaluator(), colorFrom, colorTo); + colorAnimation.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() { + @Override + public void onAnimationUpdate(ValueAnimator animator) { + window.setNavigationBarColor((Integer) animator.getAnimatedValue()); + } + }); + colorAnimation.start(); + } else { + window.setNavigationBarColor(Color.parseColor(String.valueOf(color))); + } + setNavigationBarTheme(getCurrentActivity(), light); + WritableMap map = Arguments.createMap(); + map.putBoolean("success", true); + promise.resolve(map); + } + }); + } catch (IllegalViewOperationException e) { + map.putBoolean("success", false); + promise.reject("error", e); + } + + } else { + promise.reject(ERROR_NO_ACTIVITY, new Throwable(ERROR_NO_ACTIVITY_MESSAGE)); + + } + } else { + promise.reject(ERROR_API_LEVEL, new Throwable(ERROR_API_LEVEL_MESSAGE)); + } + } + + @ReactMethod + public void hideNavigationBar(Promise promise) { + try { + runOnUiThread(new Runnable() { + @Override + public void run() { + if (getCurrentActivity() != null) { + View decorView = getCurrentActivity().getWindow().getDecorView(); + decorView.setSystemUiVisibility(UI_FLAG_HIDE_NAV_BAR); + } + } + }); + } catch (IllegalViewOperationException e) { + WritableMap map = Arguments.createMap(); + map.putBoolean("success", false); + promise.reject("error", e); + } + } + + @ReactMethod + public void showNavigationBar(Promise promise) { + try { + runOnUiThread(new Runnable() { + @Override + public void run() { + if (getCurrentActivity() != null) { + View decorView = getCurrentActivity().getWindow().getDecorView(); + int uiOptions = View.SYSTEM_UI_FLAG_VISIBLE; + decorView.setSystemUiVisibility(uiOptions); + } + } + }); + } catch (IllegalViewOperationException e) { + WritableMap map = Arguments.createMap(); + map.putBoolean("success", false); + promise.reject("error", e); + } + } +} \ No newline at end of file diff --git a/android/app/src/main/java/me/rainbow/NativeModules/SystemNavigationBar/SystemNavigationBarPackage.java b/android/app/src/main/java/me/rainbow/NativeModules/SystemNavigationBar/SystemNavigationBarPackage.java new file mode 100644 index 00000000000..f7b56ced301 --- /dev/null +++ b/android/app/src/main/java/me/rainbow/NativeModules/SystemNavigationBar/SystemNavigationBarPackage.java @@ -0,0 +1,28 @@ +package me.rainbow.NativeModules.SystemNavigationBar; + +import androidx.annotation.NonNull; + +import com.facebook.react.ReactPackage; +import com.facebook.react.bridge.NativeModule; +import com.facebook.react.bridge.ReactApplicationContext; +import com.facebook.react.uimanager.ViewManager; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + +public class SystemNavigationBarPackage implements ReactPackage { + @NonNull + @Override + public List createNativeModules(@NonNull ReactApplicationContext reactContext) { + List modules = new ArrayList<>(); + modules.add(new SystemNavigationBarModule(reactContext)); + return modules; + } + + @NonNull + @Override + public List createViewManagers(@NonNull ReactApplicationContext reactContext) { + return Collections.emptyList(); + } +} \ No newline at end of file diff --git a/src/components/PromoSheet.tsx b/src/components/PromoSheet.tsx index 42a404aa8a4..2de38981c00 100644 --- a/src/components/PromoSheet.tsx +++ b/src/components/PromoSheet.tsx @@ -1,5 +1,10 @@ import React, { useCallback, useEffect, useReducer } from 'react'; -import { ImageSourcePropType, StatusBar, ImageBackground } from 'react-native'; +import { + ImageSourcePropType, + Dimensions, + StatusBar, + ImageBackground, +} from 'react-native'; import LinearGradient from 'react-native-linear-gradient'; import MaskedView from '@react-native-masked-view/masked-view'; import { SheetActionButton, SheetHandle, SlackSheet } from '@/components/sheet'; diff --git a/src/components/sheet/SlackSheet.js b/src/components/sheet/SlackSheet.js index 07e7795744b..2329026d2b2 100644 --- a/src/components/sheet/SlackSheet.js +++ b/src/components/sheet/SlackSheet.js @@ -68,7 +68,9 @@ const Content = styled.ScrollView.attrs(({ limitScrollViewContent }) => ({ scrollEventThrottle: 16, }))(({ contentHeight, deviceHeight, backgroundColor, removeTopPadding }) => ({ backgroundColor: backgroundColor, - ...(contentHeight ? { height: deviceHeight + contentHeight } : {}), + ...(contentHeight + ? { height: deviceHeight + contentHeight } + : { height: '100%' }), paddingTop: removeTopPadding ? 0 : SheetHandleFixedToTopHeight, width: '100%', })); diff --git a/src/handlers/localstorage/theme.ts b/src/handlers/localstorage/theme.ts index 49fd2ec2acd..3451490a31d 100644 --- a/src/handlers/localstorage/theme.ts +++ b/src/handlers/localstorage/theme.ts @@ -1,7 +1,21 @@ +import { IS_ANDROID } from '@/env'; import { getGlobal, saveGlobal } from './common'; +import { NativeModules } from 'react-native'; +import { colors } from '@/styles'; +import { isUsingButtonNavigation } from '@/helpers/statusBarHelper'; + +const { NavigationBar } = NativeModules; const THEME = 'theme'; +export const getColorForThemeAndNavigationStyle = (theme: string) => { + if (!isUsingButtonNavigation()) { + return 'transparent'; + } + + return theme === 'dark' ? '#191A1C' : colors.white; +}; + /** * @desc get theme * @return {String} @@ -11,4 +25,14 @@ export const getTheme = () => getGlobal(THEME, 'light'); /** * @desc save theme */ -export const saveTheme = (theme: any) => saveGlobal(THEME, theme); +export const saveTheme = (theme: string) => { + if (IS_ANDROID) { + NavigationBar.changeNavigationBarColor( + getColorForThemeAndNavigationStyle(theme), + theme === 'light', + true + ); + } + + return saveGlobal(THEME, theme); +}; diff --git a/src/helpers/statusBarHelper.ts b/src/helpers/statusBarHelper.ts index 46e1f3f64fe..52a22d6849f 100644 --- a/src/helpers/statusBarHelper.ts +++ b/src/helpers/statusBarHelper.ts @@ -1,4 +1,9 @@ -import { ColorValue, StatusBar, StatusBarAnimation } from 'react-native'; +import { + ColorValue, + StatusBar, + StatusBarAnimation, + Dimensions, +} from 'react-native'; export const setTranslucent = (translucent: boolean): void => { StatusBar.setTranslucent(translucent); @@ -35,3 +40,10 @@ export const setDarkContent = (isAnimated = true) => { barStyle: 'dark-content', }); }; + +export const isUsingButtonNavigation = () => { + const deviceHeight = Dimensions.get('screen').height; + const windowHeight = Dimensions.get('window').height; + const bottomNavBarHeight = deviceHeight - windowHeight; + return bottomNavBarHeight > 80; +}; diff --git a/src/navigation/SwipeNavigator.js b/src/navigation/SwipeNavigator.js index a109ff3caa4..9713364ed52 100644 --- a/src/navigation/SwipeNavigator.js +++ b/src/navigation/SwipeNavigator.js @@ -1,6 +1,6 @@ import { BlurView } from '@react-native-community/blur'; import { createMaterialTopTabNavigator } from '@react-navigation/material-top-tabs'; -import React, { useLayoutEffect, useState } from 'react'; +import React, { useLayoutEffect, useMemo, useState } from 'react'; import Animated, { useAnimatedStyle, useSharedValue, @@ -46,6 +46,7 @@ import config from '@/model/config'; import SectionListScrollToTopProvider, { useSectionListScrollToTopContext, } from './SectionListScrollToTopContext'; +import { isUsingButtonNavigation } from '@/helpers/statusBarHelper'; const HORIZONTAL_TAB_BAR_INSET = 6; @@ -62,7 +63,18 @@ const animationConfig = { }; const Swipe = createMaterialTopTabNavigator(); -const HEADER_HEIGHT = IS_IOS ? 82 : 62; + +const getHeaderHeight = () => { + if (IS_IOS) { + return 82; + } + + if (!isUsingButtonNavigation()) { + return 72; + } + + return 48; +}; const TabBar = ({ state, @@ -76,6 +88,7 @@ const TabBar = ({ const { width: deviceWidth } = useDimensions(); const { colors, isDarkMode } = useTheme(); + const HEADER_HEIGHT = useMemo(() => getHeaderHeight(), []); const showPointsTab = IS_DEV || IS_TEST || config.points_enabled; const NUMBER_OF_TABS = showPointsTab ? 4 : 3; diff --git a/src/screens/AddCash/index.tsx b/src/screens/AddCash/index.tsx index b1cfb305430..75b893b4f4a 100644 --- a/src/screens/AddCash/index.tsx +++ b/src/screens/AddCash/index.tsx @@ -51,7 +51,7 @@ export function AddCashSheet() { const skeletonColor = useBackgroundColor('surfaceSecondaryElevated'); const sheetHeight = IS_IOS ? deviceHeight - insets.top - : deviceHeight - statusBarHeight; + : deviceHeight + statusBarHeight; const { isLoading, data: providers, error } = useQuery( ['f2c', 'providers'], diff --git a/src/screens/ProfileSheet.tsx b/src/screens/ProfileSheet.tsx index d56523bfc1a..16d22050548 100644 --- a/src/screens/ProfileSheet.tsx +++ b/src/screens/ProfileSheet.tsx @@ -1,6 +1,6 @@ import { useRoute } from '@react-navigation/native'; import React, { createContext, useEffect, useMemo } from 'react'; -import { StatusBar } from 'react-native'; +import { Dimensions } from 'react-native'; import RecyclerAssetList2 from '../components/asset-list/RecyclerAssetList2'; import ProfileSheetHeader from '../components/ens-profile/ProfileSheetHeader'; import Skeleton from '../components/skeleton/Skeleton'; @@ -125,11 +125,15 @@ export default function ProfileSheet() { } function AndroidWrapper({ children }: { children: React.ReactElement }) { + const screenHeight = Dimensions.get('screen').height; + const windowHeight = Dimensions.get('window').height; + const navbarHeight = screenHeight - windowHeight; + return android ? ( {children} diff --git a/src/screens/mints/MintSheet.tsx b/src/screens/mints/MintSheet.tsx index 0ae8dc3597b..e41440a1940 100644 --- a/src/screens/mints/MintSheet.tsx +++ b/src/screens/mints/MintSheet.tsx @@ -7,7 +7,7 @@ import React, { useRef, useState, } from 'react'; -import { Linking, View } from 'react-native'; +import { Linking, StatusBar, View } from 'react-native'; import { useSharedValue } from 'react-native-reanimated'; import useWallets from '../../hooks/useWallets'; import { GasSpeedButton } from '@/components/gas'; @@ -77,6 +77,7 @@ import { useDispatch } from 'react-redux'; import { QuantityButton } from './components/QuantityButton'; import { estimateGas, getProviderForNetwork } from '@/handlers/web3'; import { getRainbowFeeAddress } from '@/resources/reservoir/utils'; +import { IS_ANDROID, IS_IOS } from '@/env'; const NFT_IMAGE_HEIGHT = 250; // inset * 2 -> 28 *2 @@ -592,9 +593,10 @@ const MintSheet = () => { ? `rgba(22, 22, 22, ${ios ? 0.4 : 1})` : `rgba(26, 26, 26, ${ios ? 0.4 : 1})` } - height={'100%'} + {...(IS_IOS ? { height: '100%' } : {})} ref={sheetRef} scrollEnabled + additionalTopPadding={IS_ANDROID ? StatusBar.currentHeight : false} testID="nft-mint-sheet" yPosition={yPosition} > diff --git a/src/theme/ThemeContext.tsx b/src/theme/ThemeContext.tsx index 37ffc77ae47..859d04631b1 100644 --- a/src/theme/ThemeContext.tsx +++ b/src/theme/ThemeContext.tsx @@ -93,7 +93,6 @@ export const MainThemeProvider = ( // Listening to changes of device appearance while in run-time useEffect(() => { if (colorScheme) { - // setIsDarkMode(colorScheme === Themes.DARK); saveTheme(colorScheme); } }, [colorScheme]);