diff --git a/__mocks__/react-native.js b/__mocks__/react-native.js index 47f4b7968566..d3f5ecfc0a5c 100644 --- a/__mocks__/react-native.js +++ b/__mocks__/react-native.js @@ -24,6 +24,7 @@ jest.doMock('react-native', () => { BootSplash: { getVisibilityStatus: jest.fn(), hide: jest.fn(), + navigationBarHeight: 0, }, StartupTimer: {stop: jest.fn()}, }, diff --git a/android/app/src/main/java/com/expensify/chat/bootsplash/BootSplashDialog.java b/android/app/src/main/java/com/expensify/chat/bootsplash/BootSplashDialog.java index db7c84bc9630..f5b1ceff60e2 100644 --- a/android/app/src/main/java/com/expensify/chat/bootsplash/BootSplashDialog.java +++ b/android/app/src/main/java/com/expensify/chat/bootsplash/BootSplashDialog.java @@ -6,7 +6,6 @@ import android.view.Window; import android.view.WindowManager.LayoutParams; import androidx.annotation.NonNull; -import com.expensify.chat.R; public class BootSplashDialog extends Dialog { @@ -27,7 +26,6 @@ protected void onCreate(Bundle savedInstanceState) { if (window != null) { window.setLayout(LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT); - window.setWindowAnimations(R.style.Theme_SplashScreen_Dialog); } super.onCreate(savedInstanceState); diff --git a/android/app/src/main/java/com/expensify/chat/bootsplash/BootSplashModule.java b/android/app/src/main/java/com/expensify/chat/bootsplash/BootSplashModule.java index c6b597975433..c286ebf7a935 100644 --- a/android/app/src/main/java/com/expensify/chat/bootsplash/BootSplashModule.java +++ b/android/app/src/main/java/com/expensify/chat/bootsplash/BootSplashModule.java @@ -1,9 +1,13 @@ package com.expensify.chat.bootsplash; +import android.annotation.SuppressLint; import android.app.Activity; +import android.content.Context; import android.content.DialogInterface; +import android.content.res.Resources; import android.os.Build; import android.view.View; +import android.view.ViewConfiguration; import android.view.ViewTreeObserver; import android.window.SplashScreen; import android.window.SplashScreenView; @@ -18,6 +22,9 @@ import com.facebook.react.bridge.UiThreadUtil; import com.facebook.react.common.ReactConstants; import com.facebook.react.module.annotations.ReactModule; +import com.facebook.react.uimanager.PixelUtil; +import java.util.HashMap; +import java.util.Map; import java.util.Timer; import java.util.TimerTask; @@ -25,6 +32,7 @@ public class BootSplashModule extends ReactContextBaseJavaModule { public static final String NAME = "BootSplash"; + private static final BootSplashQueue mPromiseQueue = new BootSplashQueue<>(); private static boolean mShouldKeepOnScreen = true; @Nullable @@ -39,6 +47,24 @@ public String getName() { return NAME; } + @Override + public Map getConstants() { + final HashMap constants = new HashMap<>(); + final Context context = getReactApplicationContext(); + final Resources resources = context.getResources(); + + @SuppressLint({"DiscouragedApi", "InternalInsetResource"}) final int heightResId = + resources.getIdentifier("navigation_bar_height", "dimen", "android"); + + final float height = + heightResId > 0 && !ViewConfiguration.get(context).hasPermanentMenuKey() + ? Math.round(PixelUtil.toDIPFromPixel(resources.getDimensionPixelSize(heightResId))) + : 0; + + constants.put("navigationBarHeight", height); + return constants; + } + protected static void init(@Nullable final Activity activity) { if (activity == null) { FLog.w(ReactConstants.TAG, NAME + ": Ignored initialization, current activity is null."); @@ -68,13 +94,14 @@ public boolean onPreDraw() { }); if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) { - // This is not called on Android 12 when activity is started using Android studio / notifications + // This is not called on Android 12 when activity is started using intent + // (Android studio / CLI / notification / widget…) activity .getSplashScreen() .setOnExitAnimationListener(new SplashScreen.OnExitAnimationListener() { @Override public void onSplashScreenExit(@NonNull SplashScreenView view) { - view.remove(); // Remove it without animation + view.remove(); // Remove it immediately, without animation } }); } @@ -96,35 +123,39 @@ public void run() { }); } - private void waitAndHide() { - final Timer timer = new Timer(); + private void clearPromiseQueue() { + while (!mPromiseQueue.isEmpty()) { + Promise promise = mPromiseQueue.shift(); - timer.schedule(new TimerTask() { - @Override - public void run() { - hide(); - timer.cancel(); - } - }, 250); + if (promise != null) + promise.resolve(true); + } } - @ReactMethod - public void hide() { + private void hideAndClearPromiseQueue() { UiThreadUtil.runOnUiThread(new Runnable() { @Override public void run() { final Activity activity = getReactApplicationContext().getCurrentActivity(); - if (activity == null || activity.isFinishing()) { - waitAndHide(); - return; - } + if (mShouldKeepOnScreen || activity == null || activity.isFinishing()) { + final Timer timer = new Timer(); - if (mDialog != null) { + timer.schedule(new TimerTask() { + @Override + public void run() { + timer.cancel(); + hideAndClearPromiseQueue(); + } + }, 100); + } else if (mDialog == null) { + clearPromiseQueue(); + } else { mDialog.setOnDismissListener(new DialogInterface.OnDismissListener() { @Override public void onDismiss(DialogInterface dialog) { mDialog = null; + clearPromiseQueue(); } }); @@ -134,8 +165,14 @@ public void onDismiss(DialogInterface dialog) { }); } + @ReactMethod + public void hide(final Promise promise) { + mPromiseQueue.push(promise); + hideAndClearPromiseQueue(); + } + @ReactMethod public void getVisibilityStatus(final Promise promise) { - promise.resolve(mDialog != null ? "visible" : "hidden"); + promise.resolve(mShouldKeepOnScreen || mDialog != null ? "visible" : "hidden"); } } diff --git a/android/app/src/main/java/com/expensify/chat/bootsplash/BootSplashQueue.java b/android/app/src/main/java/com/expensify/chat/bootsplash/BootSplashQueue.java new file mode 100644 index 000000000000..4e35a066708c --- /dev/null +++ b/android/app/src/main/java/com/expensify/chat/bootsplash/BootSplashQueue.java @@ -0,0 +1,28 @@ +package com.expensify.chat.bootsplash; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import java.util.Vector; + +/** + * Represents a first-in-first-out (FIFO) thread safe queue of objects. + * Its source code is based on Java internal Stack. + */ +public class BootSplashQueue extends Vector { + + @Nullable + public synchronized T shift() { + if (size() == 0) { + return null; + } + + T item = elementAt(0); + removeElementAt(0); + + return item; + } + + public void push(@NonNull T item) { + addElement(item); + } +} diff --git a/android/app/src/main/res/anim/fade_out.xml b/android/app/src/main/res/anim/fade_out.xml deleted file mode 100644 index 049a8e36ddad..000000000000 --- a/android/app/src/main/res/anim/fade_out.xml +++ /dev/null @@ -1,7 +0,0 @@ - - - diff --git a/android/app/src/main/res/mipmap-hdpi/bootsplash_logo.png b/android/app/src/main/res/mipmap-hdpi/bootsplash_logo.png index 5fc519ee898a..95124d59275e 100644 Binary files a/android/app/src/main/res/mipmap-hdpi/bootsplash_logo.png and b/android/app/src/main/res/mipmap-hdpi/bootsplash_logo.png differ diff --git a/android/app/src/main/res/mipmap-mdpi/bootsplash_logo.png b/android/app/src/main/res/mipmap-mdpi/bootsplash_logo.png index 2cacc654e77c..c6b62d8cac9b 100644 Binary files a/android/app/src/main/res/mipmap-mdpi/bootsplash_logo.png and b/android/app/src/main/res/mipmap-mdpi/bootsplash_logo.png differ diff --git a/android/app/src/main/res/mipmap-xhdpi/bootsplash_logo.png b/android/app/src/main/res/mipmap-xhdpi/bootsplash_logo.png index 600f8bd7f0fb..a3f54d63e0ee 100644 Binary files a/android/app/src/main/res/mipmap-xhdpi/bootsplash_logo.png and b/android/app/src/main/res/mipmap-xhdpi/bootsplash_logo.png differ diff --git a/android/app/src/main/res/mipmap-xxhdpi/bootsplash_logo.png b/android/app/src/main/res/mipmap-xxhdpi/bootsplash_logo.png index b341ad440d37..06b2bfc8447b 100644 Binary files a/android/app/src/main/res/mipmap-xxhdpi/bootsplash_logo.png and b/android/app/src/main/res/mipmap-xxhdpi/bootsplash_logo.png differ diff --git a/android/app/src/main/res/mipmap-xxxhdpi/bootsplash_logo.png b/android/app/src/main/res/mipmap-xxxhdpi/bootsplash_logo.png index 7959763c13c2..c49a0f3bb854 100644 Binary files a/android/app/src/main/res/mipmap-xxxhdpi/bootsplash_logo.png and b/android/app/src/main/res/mipmap-xxxhdpi/bootsplash_logo.png differ diff --git a/android/app/src/main/res/values/colors.xml b/android/app/src/main/res/values/colors.xml index b4d8c2181b0b..07a41cec581f 100644 --- a/android/app/src/main/res/values/colors.xml +++ b/android/app/src/main/res/values/colors.xml @@ -1,5 +1,6 @@ #03D47C + #061B09 #FFFFFF #03D47C #0b1b34 diff --git a/android/app/src/main/res/values/styles.xml b/android/app/src/main/res/values/styles.xml index 19d4257a8a77..c789cdfef09f 100644 --- a/android/app/src/main/res/values/styles.xml +++ b/android/app/src/main/res/values/styles.xml @@ -6,7 +6,7 @@ - - diff --git a/assets/images/new-expensify-dark.svg b/assets/images/new-expensify-dark.svg new file mode 100644 index 000000000000..567cc667e972 --- /dev/null +++ b/assets/images/new-expensify-dark.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/config/webpack/webpack.common.js b/config/webpack/webpack.common.js index 7c6d1b9de3a9..12ecd9168fa2 100644 --- a/config/webpack/webpack.common.js +++ b/config/webpack/webpack.common.js @@ -1,11 +1,11 @@ const path = require('path'); +const fs = require('fs'); const {IgnorePlugin, DefinePlugin, ProvidePlugin, EnvironmentPlugin} = require('webpack'); const {CleanWebpackPlugin} = require('clean-webpack-plugin'); const HtmlWebpackPlugin = require('html-webpack-plugin'); const CopyPlugin = require('copy-webpack-plugin'); const dotenv = require('dotenv'); const {BundleAnalyzerPlugin} = require('webpack-bundle-analyzer'); -const HtmlInlineScriptPlugin = require('html-inline-script-webpack-plugin'); const FontPreloadPlugin = require('webpack-font-preload-plugin'); const CustomVersionFilePlugin = require('./CustomVersionFilePlugin'); @@ -52,7 +52,6 @@ const webpackConfig = ({envFile = '.env', platform = 'web'}) => ({ devtool: 'source-map', entry: { main: ['babel-polyfill', './index.js'], - splash: ['./web/splash/splash.js'], }, output: { filename: '[name]-[contenthash].bundle.js', @@ -73,11 +72,9 @@ const webpackConfig = ({envFile = '.env', platform = 'web'}) => ({ new HtmlWebpackPlugin({ template: 'web/index.html', filename: 'index.html', + splashLogo: fs.readFileSync(path.resolve(__dirname, `../../assets/images/new-expensify${mapEnvToLogoSuffix(envFile)}.svg`), 'utf-8'), usePolyfillIO: platform === 'web', }), - new HtmlInlineScriptPlugin({ - scriptMatchPattern: [/splash.+[.]js$/], - }), new FontPreloadPlugin({ extensions: ['woff2'], }), @@ -173,18 +170,6 @@ const webpackConfig = ({envFile = '.env', platform = 'web'}) => ({ }, ], }, - { - test: /splash.css$/i, - use: [ - { - loader: 'style-loader', - options: { - insert: 'head', - injectType: 'singletonStyleTag', - }, - }, - ], - }, { test: /\.css$/i, use: ['style-loader', 'css-loader'], @@ -201,7 +186,6 @@ const webpackConfig = ({envFile = '.env', platform = 'web'}) => ({ }, resolve: { alias: { - logo$: path.resolve(__dirname, `../../assets/images/new-expensify${mapEnvToLogoSuffix(envFile)}.svg`), 'react-native-config': 'react-web-config', 'react-native$': '@expensify/react-native-web', 'react-native-web': '@expensify/react-native-web', diff --git a/ios/NewExpensify/Images.xcassets/BootSplashLogo.imageset/bootsplash_logo.png b/ios/NewExpensify/Images.xcassets/BootSplashLogo.imageset/bootsplash_logo.png index 8fbef1c5ab06..6b031c1bd43d 100644 Binary files a/ios/NewExpensify/Images.xcassets/BootSplashLogo.imageset/bootsplash_logo.png and b/ios/NewExpensify/Images.xcassets/BootSplashLogo.imageset/bootsplash_logo.png differ diff --git a/ios/NewExpensify/Images.xcassets/BootSplashLogo.imageset/bootsplash_logo@2x.png b/ios/NewExpensify/Images.xcassets/BootSplashLogo.imageset/bootsplash_logo@2x.png index 186a2f85e1dd..d1a1700c1c03 100644 Binary files a/ios/NewExpensify/Images.xcassets/BootSplashLogo.imageset/bootsplash_logo@2x.png and b/ios/NewExpensify/Images.xcassets/BootSplashLogo.imageset/bootsplash_logo@2x.png differ diff --git a/ios/NewExpensify/Images.xcassets/BootSplashLogo.imageset/bootsplash_logo@3x.png b/ios/NewExpensify/Images.xcassets/BootSplashLogo.imageset/bootsplash_logo@3x.png index e208d1e0f8ab..32c8c76a2a37 100644 Binary files a/ios/NewExpensify/Images.xcassets/BootSplashLogo.imageset/bootsplash_logo@3x.png and b/ios/NewExpensify/Images.xcassets/BootSplashLogo.imageset/bootsplash_logo@3x.png differ diff --git a/ios/NewExpensify/RCTBootSplash.m b/ios/NewExpensify/RCTBootSplash.m index 5e32ffc659ff..bceac70efdcf 100644 --- a/ios/NewExpensify/RCTBootSplash.m +++ b/ios/NewExpensify/RCTBootSplash.m @@ -10,8 +10,9 @@ #import "RCTBootSplash.h" +static NSMutableArray *_resolverQueue = nil; static RCTRootView *_rootView = nil; -static bool _hideHasBeenCalled = false; +static bool _nativeHidden = false; @implementation RCTBootSplash @@ -39,11 +40,22 @@ + (void)initWithStoryboard:(NSString * _Nonnull)storyboardName UIStoryboard *storyboard = [UIStoryboard storyboardWithName:storyboardName bundle:nil]; UIView *loadingView = [[storyboard instantiateInitialViewController] view]; - if (_hideHasBeenCalled) + if ([self resolverQueueExists]) return; [_rootView setLoadingView:loadingView]; + [NSTimer scheduledTimerWithTimeInterval:0.35 + repeats:NO + block:^(NSTimer * _Nonnull timer) { + // wait for native iOS launch screen to fade out + _nativeHidden = true; + + // hide has been called before native launch screen fade out + if ([self resolverQueueExists]) + [self hideAndClearResolverQueue]; + }]; + [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(onContentDidAppear) name:RCTContentDidAppearNotification @@ -59,28 +71,29 @@ + (bool)isHidden { return _rootView == nil || _rootView.loadingView == nil || [_rootView.loadingView isHidden]; } -+ (void)hideWithFade:(bool)fade { - if ([self isHidden]) ++ (bool)resolverQueueExists { + return _resolverQueue != nil; +} + ++ (void)clearResolverQueue { + if (![self resolverQueueExists]) return; - if (fade) { - dispatch_async(dispatch_get_main_queue(), ^{ - [UIView transitionWithView:_rootView - duration:0.250 - options:UIViewAnimationOptionTransitionCrossDissolve - animations:^{ - _rootView.loadingView.hidden = YES; - } - completion:^(__unused BOOL finished) { - [_rootView.loadingView removeFromSuperview]; - _rootView.loadingView = nil; - }]; - }); - } else { + while ([_resolverQueue count] > 0) { + RCTPromiseResolveBlock resolve = [_resolverQueue objectAtIndex:0]; + [_resolverQueue removeObjectAtIndex:0]; + resolve(@(true)); + } +} + ++ (void)hideAndClearResolverQueue { + if (![self isHidden]) { _rootView.loadingView.hidden = YES; [_rootView.loadingView removeFromSuperview]; _rootView.loadingView = nil; } + + [RCTBootSplash clearResolverQueue]; } + (void)onContentDidAppear { @@ -89,28 +102,36 @@ + (void)onContentDidAppear { block:^(NSTimer * _Nonnull timer) { [timer invalidate]; - _hideHasBeenCalled = true; - [self hideWithFade:true]; + if (_resolverQueue == nil) + _resolverQueue = [[NSMutableArray alloc] init]; + + [self hideAndClearResolverQueue]; }]; [[NSNotificationCenter defaultCenter] removeObserver:self]; } + (void)onJavaScriptDidFailToLoad { - [self hideWithFade:false]; + [self hideAndClearResolverQueue]; [[NSNotificationCenter defaultCenter] removeObserver:self]; } -RCT_EXPORT_METHOD(hide) { - if (!_hideHasBeenCalled && !RCTRunningInAppExtension()) { - _hideHasBeenCalled = true; - [RCTBootSplash hideWithFade:true]; - } +RCT_EXPORT_METHOD(hide:(RCTPromiseResolveBlock)resolve + rejecter:(RCTPromiseRejectBlock)reject) { + if (_resolverQueue == nil) + _resolverQueue = [[NSMutableArray alloc] init]; + + [_resolverQueue addObject:resolve]; + + if ([RCTBootSplash isHidden] || RCTRunningInAppExtension()) + return [RCTBootSplash clearResolverQueue]; + + if (_nativeHidden) + return [RCTBootSplash hideAndClearResolverQueue]; } -RCT_REMAP_METHOD(getVisibilityStatus, - getVisibilityStatusWithResolver:(RCTPromiseResolveBlock)resolve - rejecter:(RCTPromiseRejectBlock)reject) { +RCT_EXPORT_METHOD(getVisibilityStatus:(RCTPromiseResolveBlock)resolve + rejecter:(RCTPromiseRejectBlock)reject) { resolve([RCTBootSplash isHidden] ? @"hidden" : @"visible"); } diff --git a/package-lock.json b/package-lock.json index 309b307a979d..71e39c0119f2 100644 --- a/package-lock.json +++ b/package-lock.json @@ -154,7 +154,6 @@ "eslint-plugin-react-hooks": "^4.6.0", "eslint-plugin-storybook": "^0.5.13", "flipper-plugin-bridgespy-client": "^0.1.9", - "html-inline-script-webpack-plugin": "^3.1.0", "html-webpack-plugin": "^5.5.0", "jest": "29.4.1", "jest-circus": "29.4.1", @@ -25292,19 +25291,6 @@ "version": "2.0.2", "license": "MIT" }, - "node_modules/html-inline-script-webpack-plugin": { - "version": "3.1.0", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=14.0.0", - "npm": ">=6.0.0" - }, - "peerDependencies": { - "html-webpack-plugin": "^5.0.0", - "webpack": "^5.0.0" - } - }, "node_modules/html-minifier-terser": { "version": "6.1.0", "dev": true, @@ -58519,11 +58505,6 @@ "html-escaper": { "version": "2.0.2" }, - "html-inline-script-webpack-plugin": { - "version": "3.1.0", - "dev": true, - "requires": {} - }, "html-minifier-terser": { "version": "6.1.0", "dev": true, diff --git a/package.json b/package.json index 14d422c794cd..f1f4b0492d35 100644 --- a/package.json +++ b/package.json @@ -189,7 +189,6 @@ "eslint-plugin-react-hooks": "^4.6.0", "eslint-plugin-storybook": "^0.5.13", "flipper-plugin-bridgespy-client": "^0.1.9", - "html-inline-script-webpack-plugin": "^3.1.0", "html-webpack-plugin": "^5.5.0", "jest": "29.4.1", "jest-circus": "29.4.1", diff --git a/src/Expensify.js b/src/Expensify.js index f6831443d907..638bb6119bb9 100644 --- a/src/Expensify.js +++ b/src/Expensify.js @@ -27,6 +27,7 @@ import Navigation from './libs/Navigation/Navigation'; import DeeplinkWrapper from './components/DeeplinkWrapper'; import PopoverReportActionContextMenu from './pages/home/report/ContextMenu/PopoverReportActionContextMenu'; import * as ReportActionContextMenu from './pages/home/report/ContextMenu/ReportActionContextMenu'; +import SplashScreenHider from './components/SplashScreenHider'; import KeyboardShortcutsModal from './components/KeyboardShortcutsModal'; // This lib needs to be imported, but it has nothing to export since all it contains is an Onyx connection @@ -86,9 +87,10 @@ function Expensify(props) { const appStateChangeListener = useRef(null); const [isNavigationReady, setIsNavigationReady] = useState(false); const [isOnyxMigrated, setIsOnyxMigrated] = useState(false); - const [isSplashShown, setIsSplashShown] = useState(true); + const [isSplashHidden, setIsSplashHidden] = useState(false); const isAuthenticated = useMemo(() => Boolean(lodashGet(props.session, 'authToken', null)), [props.session]); + const shouldHideSplash = isNavigationReady && !isSplashHidden && (!isAuthenticated || props.isSidebarLoaded); const initializeClient = () => { if (!Visibility.isVisible()) { @@ -105,6 +107,10 @@ function Expensify(props) { Navigation.setIsNavigationReady(); }, []); + const onSplashHide = useCallback(() => { + setIsSplashHidden(true); + }, []); + useLayoutEffect(() => { // Initialize this client as being an active client ActiveClientManager.init(); @@ -158,20 +164,6 @@ function Expensify(props) { // eslint-disable-next-line react-hooks/exhaustive-deps -- we don't want this effect to run again }, []); - useEffect(() => { - if (!isNavigationReady || !isSplashShown) { - return; - } - - const shouldHideSplash = !isAuthenticated || props.isSidebarLoaded; - - if (shouldHideSplash) { - BootSplash.hide(); - - setIsSplashShown(false); - } - }, [props.isSidebarLoaded, isNavigationReady, isSplashShown, isAuthenticated]); - // Display a blank page until the onyx migration completes if (!isOnyxMigrated) { return null; @@ -179,7 +171,7 @@ function Expensify(props) { return ( - {!isSplashShown && ( + {shouldHideSplash && ( <> @@ -204,6 +196,8 @@ function Expensify(props) { onReady={setNavigationReady} authenticated={isAuthenticated} /> + + {shouldHideSplash && } ); } diff --git a/src/components/SplashScreenHider/index.js b/src/components/SplashScreenHider/index.js new file mode 100644 index 000000000000..cf8745715572 --- /dev/null +++ b/src/components/SplashScreenHider/index.js @@ -0,0 +1,28 @@ +import {useEffect} from 'react'; +import PropTypes from 'prop-types'; +import BootSplash from '../../libs/BootSplash'; + +const propTypes = { + /** Splash screen has been hidden */ + onHide: PropTypes.func, +}; + +const defaultProps = { + onHide: () => {}, +}; + +const SplashScreenHider = (props) => { + const {onHide} = props; + + useEffect(() => { + BootSplash.hide().then(() => onHide()); + }, [onHide]); + + return null; +}; + +SplashScreenHider.displayName = 'SplashScreenHider'; +SplashScreenHider.propTypes = propTypes; +SplashScreenHider.defaultProps = defaultProps; + +export default SplashScreenHider; diff --git a/src/components/SplashScreenHider/index.native.js b/src/components/SplashScreenHider/index.native.js new file mode 100644 index 000000000000..8e544b7da2a1 --- /dev/null +++ b/src/components/SplashScreenHider/index.native.js @@ -0,0 +1,87 @@ +import {useCallback, useRef} from 'react'; +import PropTypes from 'prop-types'; +import {StatusBar, StyleSheet} from 'react-native'; +import Reanimated, {useSharedValue, withTiming, Easing, useAnimatedStyle, runOnJS} from 'react-native-reanimated'; +import BootSplash from '../../libs/BootSplash'; +import Logo from '../../../assets/images/new-expensify-dark.svg'; +import styles from '../../styles/styles'; + +const propTypes = { + /** Splash screen has been hidden */ + onHide: PropTypes.func, +}; + +const defaultProps = { + onHide: () => {}, +}; + +const SplashScreenHider = (props) => { + const {onHide} = props; + + const opacity = useSharedValue(1); + const scale = useSharedValue(1); + + const opacityStyle = useAnimatedStyle(() => ({ + opacity: opacity.value, + })); + const scaleStyle = useAnimatedStyle(() => ({ + transform: [{scale: scale.value}], + })); + + const hideHasBeenCalled = useRef(false); + + const hide = useCallback(() => { + // hide can only be called once + if (hideHasBeenCalled.current) { + return; + } + + hideHasBeenCalled.current = true; + + BootSplash.hide().then(() => { + scale.value = withTiming(0, { + duration: 200, + easing: Easing.back(2), + }); + + opacity.value = withTiming( + 0, + { + duration: 250, + easing: Easing.out(Easing.ease), + }, + () => runOnJS(onHide)(), + ); + }); + }, [opacity, scale, onHide]); + + return ( + + + + + + ); +}; + +SplashScreenHider.displayName = 'SplashScreenHider'; +SplashScreenHider.propTypes = propTypes; +SplashScreenHider.defaultProps = defaultProps; + +export default SplashScreenHider; diff --git a/src/libs/BootSplash/index.js b/src/libs/BootSplash/index.js index f4cdb7a7427f..b9b8692f687c 100644 --- a/src/libs/BootSplash/index.js +++ b/src/libs/BootSplash/index.js @@ -1,4 +1,28 @@ +import Log from '../Log'; + +function resolveAfter(delay) { + return new Promise((resolve) => setTimeout(resolve, delay)); +} + +function hide() { + Log.info('[BootSplash] hiding splash screen', false); + + return document.fonts.ready.then(() => { + const splash = document.getElementById('splash'); + splash.style.opacity = 0; + + return resolveAfter(250).then(() => { + splash.parentNode.removeChild(splash); + }); + }); +} + +function getVisibilityStatus() { + return Promise.resolve(document.getElementById('splash') ? 'visible' : 'hidden'); +} + export default { - hide: () => {}, - getVisibilityStatus: () => Promise.resolve('hidden'), + hide, + getVisibilityStatus, + navigationBarHeight: 0, }; diff --git a/src/libs/BootSplash/index.native.js b/src/libs/BootSplash/index.native.js index a228422733be..942b3cadb74a 100644 --- a/src/libs/BootSplash/index.native.js +++ b/src/libs/BootSplash/index.native.js @@ -5,10 +5,11 @@ const BootSplash = NativeModules.BootSplash; function hide() { Log.info('[BootSplash] hiding splash screen', false); - BootSplash.hide(); + return BootSplash.hide(); } export default { hide, getVisibilityStatus: BootSplash.getVisibilityStatus, + navigationBarHeight: BootSplash.navigationBarHeight || 0, }; diff --git a/src/libs/Navigation/NavigationRoot.js b/src/libs/Navigation/NavigationRoot.js index 4bc5a839da55..32bf58e011fe 100644 --- a/src/libs/Navigation/NavigationRoot.js +++ b/src/libs/Navigation/NavigationRoot.js @@ -5,9 +5,7 @@ import {useFlipper} from '@react-navigation/devtools'; import Navigation, {navigationRef} from './Navigation'; import linkingConfig from './linkingConfig'; import AppNavigator from './AppNavigator'; -import FullScreenLoadingIndicator from '../../components/FullscreenLoadingIndicator'; import themeColors from '../../styles/themes/default'; -import styles from '../../styles/styles'; import Log from '../Log'; // https://reactnavigation.org/docs/themes @@ -52,7 +50,6 @@ const NavigationRoot = (props) => { useFlipper(navigationRef); return ( } onStateChange={parseAndLogRoute} onReady={props.onReady} theme={navigationTheme} diff --git a/src/styles/styles.js b/src/styles/styles.js index 8763ecc765dd..f9bb9e718a83 100644 --- a/src/styles/styles.js +++ b/src/styles/styles.js @@ -3219,6 +3219,7 @@ const styles = { whisper: { backgroundColor: themeColors.cardBG, }, + contextMenuItemPopoverMaxWidth: { maxWidth: 375, }, @@ -3244,6 +3245,12 @@ const styles = { backgroundColor: themeColors.highlightBG, }, + splashScreenHider: { + backgroundColor: themeColors.splashBG, + alignItems: 'center', + justifyContent: 'center', + }, + headerEnvBadge: { marginLeft: 0, marginBottom: 2, diff --git a/src/styles/themes/default.js b/src/styles/themes/default.js index 86a89b38e695..54af4b52ce50 100644 --- a/src/styles/themes/default.js +++ b/src/styles/themes/default.js @@ -4,6 +4,7 @@ import colors from '../colors'; const darkTheme = { // Figma keys appBG: colors.greenAppBackground, + splashBG: colors.green, highlightBG: colors.greenHighlightBackground, border: colors.greenBorders, borderLighter: colors.greenBordersLighter, diff --git a/web/index.html b/web/index.html index aa7735079384..728948dc8323 100644 --- a/web/index.html +++ b/web/index.html @@ -78,6 +78,36 @@ -webkit-text-fill-color: #ffffff; caret-color: #ffffff; } + + @media screen and (min-width: 480px) { + .splash-logo > svg { + width: 104px; + height: 104px; + } + } + + @media screen and (max-width: 479px) { + .splash-logo > svg { + width: 52px; + height: 52px; + } + } + + #splash { + position: absolute; + bottom: 0; + left: 0; + right: 0; + top: 0; + background-color: #061B09; + width: 100%; + height: 100%; + display: flex; + justify-content: center; + align-items: center; + transition-duration: 250ms; + transition-property: opacity; + } @@ -90,7 +120,9 @@
- +
diff --git a/web/splash/splash.css b/web/splash/splash.css deleted file mode 100644 index 45a82396e981..000000000000 --- a/web/splash/splash.css +++ /dev/null @@ -1,25 +0,0 @@ -@media screen and (min-width: 480px) { - .splash-logo > svg { - width: 104px; - height: 104px; - } -} -@media screen and (max-width: 479px) { - .splash-logo > svg { - width: 52px; - height: 52px; - } -} - -#splash { - position: absolute; - top: 0; left: 0; - right: 0; background-color: white; - width: 100%; - height: 100%; - display: flex; - justify-content: center; - align-items: center; - transition-duration: 250ms; - transition-property: opacity; -} diff --git a/web/splash/splash.js b/web/splash/splash.js deleted file mode 100644 index 8ed06fc32270..000000000000 --- a/web/splash/splash.js +++ /dev/null @@ -1,41 +0,0 @@ -import './splash.css'; -import newExpensifyLogo from 'logo?raw'; -import themeColors from '../../src/styles/themes/default'; - -let areFontsReady = false; -document.fonts.ready.then(() => { - areFontsReady = true; -}); - -document.addEventListener( - 'DOMContentLoaded', - () => { - const minMilisecondsToWait = 1.5 * 1000; - let passedMiliseconds = 0; - let isRootMounted = false; - const splash = document.getElementById('splash'); - const splashLogo = document.querySelector('.splash-logo'); - const root = document.getElementById('root'); - splash.style.backgroundColor = themeColors.appBG; - - // Set app background color for overflow scrolling - const body = document.querySelector('body'); - body.style.backgroundColor = themeColors.appBG; - - splashLogo.innerHTML = newExpensifyLogo; - - const intervalId = setInterval(() => { - passedMiliseconds += 250; - isRootMounted = root.children.length > 0; - if (passedMiliseconds >= minMilisecondsToWait && isRootMounted && areFontsReady) { - clearInterval(intervalId); - splash.style.opacity = 0; - - setTimeout(() => { - splash.parentNode.removeChild(splash); - }, 250); - } - }, 250); - }, - false, -);