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/build.gradle b/android/app/build.gradle index 089d89f9afe1..a61340620405 100644 --- a/android/app/build.gradle +++ b/android/app/build.gradle @@ -106,8 +106,8 @@ android { minSdkVersion rootProject.ext.minSdkVersion targetSdkVersion rootProject.ext.targetSdkVersion multiDexEnabled rootProject.ext.multiDexEnabled - versionCode 1001031505 - versionName "1.3.15-5" + versionCode 1001031605 + versionName "1.3.16-5" } splits { diff --git a/android/app/src/main/AndroidManifest.xml b/android/app/src/main/AndroidManifest.xml index 4202dd4162fb..05302f43ef8d 100644 --- a/android/app/src/main/AndroidManifest.xml +++ b/android/app/src/main/AndroidManifest.xml @@ -8,10 +8,10 @@ + 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..9bae001c2b53 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,10 +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$/], + isStaging: envFile === '.env.staging', }), new FontPreloadPlugin({ extensions: ['woff2'], @@ -173,18 +171,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 +187,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/Info.plist b/ios/NewExpensify/Info.plist index beb91c4abb0d..d66f9cfc3664 100644 --- a/ios/NewExpensify/Info.plist +++ b/ios/NewExpensify/Info.plist @@ -17,7 +17,7 @@ CFBundlePackageType APPL CFBundleShortVersionString - 1.3.15 + 1.3.16 CFBundleSignature ???? CFBundleURLTypes @@ -30,7 +30,7 @@ CFBundleVersion - 1.3.15.5 + 1.3.16.5 ITSAppUsesNonExemptEncryption LSApplicationQueriesSchemes 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/ios/NewExpensifyTests/Info.plist b/ios/NewExpensifyTests/Info.plist index 98bb06c2fb27..0ccf71908353 100644 --- a/ios/NewExpensifyTests/Info.plist +++ b/ios/NewExpensifyTests/Info.plist @@ -15,10 +15,10 @@ CFBundlePackageType BNDL CFBundleShortVersionString - 1.3.15 + 1.3.16 CFBundleSignature ???? CFBundleVersion - 1.3.15.5 + 1.3.16.5 diff --git a/ios/Podfile.lock b/ios/Podfile.lock index 75cca4de91b3..43c64fa15997 100644 --- a/ios/Podfile.lock +++ b/ios/Podfile.lock @@ -670,7 +670,7 @@ PODS: - React-Core - RNReactNativeHapticFeedback (1.14.0): - React-Core - - RNReanimated (3.0.0-rc.10): + - RNReanimated (3.1.0): - DoubleConversion - FBLazyVector - FBReactNativeSpec @@ -1124,7 +1124,7 @@ SPEC CHECKSUMS: RNLocalize: d4b8af4e442d4bcca54e68fc687a2129b4d71a81 RNPermissions: dcdb7b99796bbeda6975a6e79ad519c41b251b1c RNReactNativeHapticFeedback: 1e3efeca9628ff9876ee7cdd9edec1b336913f8c - RNReanimated: fbc356493970e3acddc15586b1bccb5eab3ff1ec + RNReanimated: b1220a0e5168745283ff5d53bfc7d2144b2cee1b RNScreens: 0df01424e9e0ed7827200d6ed1087ddd06c493f9 RNSVG: 53c661b76829783cdaf9b7a57258f3d3b4c28315 SDWebImage: a7f831e1a65eb5e285e3fb046a23fcfbf08e696d diff --git a/jest/setup.js b/jest/setup.js index 407059251579..228f3a22f33b 100644 --- a/jest/setup.js +++ b/jest/setup.js @@ -1,3 +1,4 @@ +import 'setimmediate'; import 'react-native-gesture-handler/jestSetup'; import * as reanimatedJestUtils from 'react-native-reanimated/src/reanimated2/jestUtils'; import setupMockImages from './setupMockImages'; diff --git a/package-lock.json b/package-lock.json index 0b8f86ea8e97..17d8159aef7c 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "new.expensify", - "version": "1.3.15-5", + "version": "1.3.16-5", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "new.expensify", - "version": "1.3.15-5", + "version": "1.3.16-5", "hasInstallScript": true, "license": "MIT", "dependencies": { @@ -34,13 +34,14 @@ "@react-navigation/drawer": "github:Expensify/react-navigation#react-navigation-drawer-v6.5.0-alpha1-gitpkg", "@react-navigation/native": "6.0.13", "@react-navigation/stack": "6.3.1", + "@react-ng/bounds-observer": "^0.2.1", "@ua/react-native-airship": "^15.2.3", "awesome-phonenumber": "^5.4.0", "babel-plugin-transform-remove-console": "^6.9.4", "babel-polyfill": "^6.26.0", "dom-serializer": "^0.2.2", "domhandler": "^4.3.0", - "expensify-common": "git+ssh://git@github.com/Expensify/expensify-common.git#879df466155918ec0e0a6b36176af4211302b184", + "expensify-common": "git+ssh://git@github.com/Expensify/expensify-common.git#92088df062e60cba227fd1d6fa59ebfcde27d460", "fbjs": "^3.0.2", "html-entities": "^1.3.1", "htmlparser2": "^7.2.0", @@ -67,7 +68,7 @@ "react-native-dev-menu": "^4.1.1", "react-native-device-info": "^10.3.0", "react-native-document-picker": "^8.0.0", - "react-native-fast-image": "git+https://github.com/Expensify/react-native-fast-image.git#afedf204bfc253d18f08fdcc5356a2bb82f6a87c", + "react-native-fast-image": "^8.6.3", "react-native-gesture-handler": "2.9.0", "react-native-google-places-autocomplete": "git+https://github.com/Expensify/react-native-google-places-autocomplete.git#6f436a06a3018cb49750bb110b89df75f6a865d5", "react-native-haptic-feedback": "^1.13.0", @@ -85,7 +86,7 @@ "react-native-plaid-link-sdk": "^10.0.0", "react-native-qrcode-svg": "^6.2.0", "react-native-quick-sqlite": "^8.0.0-beta.2", - "react-native-reanimated": "3.0.0-rc.10", + "react-native-reanimated": "3.1.0", "react-native-render-html": "6.3.1", "react-native-safe-area-context": "4.4.1", "react-native-screens": "3.17.0", @@ -154,7 +155,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", @@ -171,6 +171,7 @@ "react-native-performance-flipper-reporter": "^2.0.0", "react-native-svg-transformer": "^1.0.0", "react-test-renderer": "18.1.0", + "setimmediate": "^1.0.5", "shellcheck": "^1.1.0", "style-loader": "^2.0.0", "time-analytics-webpack-plugin": "^0.1.17", @@ -2594,6 +2595,11 @@ "@hapi/hoek": "^9.0.0" } }, + "node_modules/@html-ng/bounding-client-rect-observer": { + "version": "0.1.3", + "resolved": "https://registry.npmjs.org/@html-ng/bounding-client-rect-observer/-/bounding-client-rect-observer-0.1.3.tgz", + "integrity": "sha512-RV1Lz23ckbpOgU1bNGxxTS4XTCEFGxiXoEmi8EOHtzTVzS+AEMkoqxllugn6IHEMqNkbcHipURRupEJe8Dsp1g==" + }, "node_modules/@humanwhocodes/config-array": { "version": "0.5.0", "dev": true, @@ -7266,6 +7272,38 @@ "react-native-screens": ">= 3.0.0" } }, + "node_modules/@react-ng/bounds-observer": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/@react-ng/bounds-observer/-/bounds-observer-0.2.1.tgz", + "integrity": "sha512-i0h7x0qOLJz+JKxhOpngHFob6PH2Qmra85aQ0e/viS1yYgidoBvPJHn8WPGn5LXff98fE+fPhngsaD7FSbxcwQ==", + "dependencies": { + "@html-ng/bounding-client-rect-observer": "^0.1.3", + "@types/react": "^18.0.31", + "@types/react-dom": "^18.0.11", + "react": "^18.2.0", + "react-dom": "^18.2.0" + } + }, + "node_modules/@react-ng/bounds-observer/node_modules/react-dom": { + "version": "18.2.0", + "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-18.2.0.tgz", + "integrity": "sha512-6IMTriUmvsjHUjNtEDudZfuDQUoWXVxKHhlEGSk81n4YFS+r/Kl99wXiwlVXtPBtJenozv2P+hxDsw9eA7Xo6g==", + "dependencies": { + "loose-envify": "^1.1.0", + "scheduler": "^0.23.0" + }, + "peerDependencies": { + "react": "^18.2.0" + } + }, + "node_modules/@react-ng/bounds-observer/node_modules/scheduler": { + "version": "0.23.0", + "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.23.0.tgz", + "integrity": "sha512-CtuThmgHNg7zIZWAXi3AsyIzA3n4xx7aNyjwC2VJldO2LMVDhFK+63xGqq6CsJH4rTAt6/M+N4GhZiDYPx9eUw==", + "dependencies": { + "loose-envify": "^1.1.0" + } + }, "node_modules/@sentry/browser": { "version": "7.11.1", "license": "BSD-3-Clause", @@ -15136,8 +15174,7 @@ }, "node_modules/@types/prop-types": { "version": "15.7.5", - "license": "MIT", - "peer": true + "license": "MIT" }, "node_modules/@types/qs": { "version": "6.9.7", @@ -15157,15 +15194,23 @@ "license": "MIT" }, "node_modules/@types/react": { - "version": "18.0.24", - "license": "MIT", - "peer": true, + "version": "18.2.6", + "resolved": "https://registry.npmjs.org/@types/react/-/react-18.2.6.tgz", + "integrity": "sha512-wRZClXn//zxCFW+ye/D2qY65UsYP1Fpex2YXorHc8awoNamkMZSvBxwxdYVInsHOZZd2Ppq8isnSzJL5Mpf8OA==", "dependencies": { "@types/prop-types": "*", "@types/scheduler": "*", "csstype": "^3.0.2" } }, + "node_modules/@types/react-dom": { + "version": "18.2.4", + "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-18.2.4.tgz", + "integrity": "sha512-G2mHoTMTL4yoydITgOGwWdWMVd8sNgyEP85xVmMKAPUBwQWm9wBPQUmvbeF4V3WBY1P7mmL4BkjQ0SqUpf1snw==", + "dependencies": { + "@types/react": "*" + } + }, "node_modules/@types/react-native": { "version": "0.70.6", "license": "MIT", @@ -15189,8 +15234,7 @@ }, "node_modules/@types/scheduler": { "version": "0.16.2", - "license": "MIT", - "peer": true + "license": "MIT" }, "node_modules/@types/seedrandom": { "version": "2.4.30", @@ -23476,8 +23520,8 @@ }, "node_modules/expensify-common": { "version": "1.0.0", - "resolved": "git+ssh://git@github.com/Expensify/expensify-common.git#879df466155918ec0e0a6b36176af4211302b184", - "integrity": "sha512-0XZtJOzpH5cwaWMeW/25ZEazELbFd65Q/SPqoOyEhSWx/rarhZZNGYyQlcfHfj+c1wk1eSUEVL1nEIT9VOAUNg==", + "resolved": "git+ssh://git@github.com/Expensify/expensify-common.git#92088df062e60cba227fd1d6fa59ebfcde27d460", + "integrity": "sha512-xFCTXFz295JjSSYOjcDvR1FeN6rx+t2vCz64ahqm+TohI0eCWWJSzdUq/2jvrDm72e509rjdYwYcLWy4PumeYA==", "license": "MIT", "dependencies": { "classnames": "2.3.1", @@ -25293,19 +25337,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, @@ -34898,8 +34929,6 @@ }, "node_modules/react-native-fast-image": { "version": "8.6.3", - "resolved": "git+ssh://git@github.com/Expensify/react-native-fast-image.git#afedf204bfc253d18f08fdcc5356a2bb82f6a87c", - "integrity": "sha512-jfARLcakkTtqh9jVkGWkJ+3zlpMmcoGH/6LHdnLuestM90Tn3cTPjWfWQloCYJmFwK/O2f4m1SHp1BOs0VR+cw==", "license": "(MIT AND Apache-2.0)", "peerDependencies": { "react": "^17 || ^18", @@ -35157,23 +35186,31 @@ } }, "node_modules/react-native-reanimated": { - "version": "3.0.0-rc.10", - "license": "MIT", + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/react-native-reanimated/-/react-native-reanimated-3.1.0.tgz", + "integrity": "sha512-8YJR7yHnrqK6yKWzkGLVEawi1WZqJ9bGIehKEnE8zG58yLrSwUZe1T220XTbftpkA3r37Sy0kJJ/HOOiaIU+HQ==", "dependencies": { "@babel/plugin-transform-object-assign": "^7.16.7", "@babel/preset-typescript": "^7.16.7", - "convert-source-map": "^1.7.0", - "invariant": "^2.2.4", - "lodash.isequal": "^4.5.0", - "setimmediate": "^1.0.5", - "string-hash-64": "^1.0.3" + "convert-source-map": "^2.0.0", + "invariant": "^2.2.4" }, "peerDependencies": { "@babel/core": "^7.0.0-0", + "@babel/plugin-proposal-nullish-coalescing-operator": "^7.0.0-0", + "@babel/plugin-proposal-optional-chaining": "^7.0.0-0", + "@babel/plugin-transform-arrow-functions": "^7.0.0-0", + "@babel/plugin-transform-shorthand-properties": "^7.0.0-0", + "@babel/plugin-transform-template-literals": "^7.0.0-0", "react": "*", "react-native": "*" } }, + "node_modules/react-native-reanimated/node_modules/convert-source-map": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", + "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==" + }, "node_modules/react-native-render-html": { "version": "6.3.1", "license": "BSD-2-Clause", @@ -37485,7 +37522,8 @@ }, "node_modules/setimmediate": { "version": "1.0.5", - "license": "MIT" + "resolved": "https://registry.npmjs.org/setimmediate/-/setimmediate-1.0.5.tgz", + "integrity": "sha512-MATJdZp8sLqDl/68LfQmbP8zKPLQNV6BIZoIgrscFDQ+RsvK/BxeDQOgyxKKoh0y/8h3BqVFnCqQ/gd+reiIXA==" }, "node_modules/setprototypeof": { "version": "1.2.0", @@ -38361,10 +38399,6 @@ "safe-buffer": "~5.1.0" } }, - "node_modules/string-hash-64": { - "version": "1.0.3", - "license": "MIT" - }, "node_modules/string-length": { "version": "4.0.2", "license": "MIT", @@ -43233,6 +43267,11 @@ "@hapi/hoek": "^9.0.0" } }, + "@html-ng/bounding-client-rect-observer": { + "version": "0.1.3", + "resolved": "https://registry.npmjs.org/@html-ng/bounding-client-rect-observer/-/bounding-client-rect-observer-0.1.3.tgz", + "integrity": "sha512-RV1Lz23ckbpOgU1bNGxxTS4XTCEFGxiXoEmi8EOHtzTVzS+AEMkoqxllugn6IHEMqNkbcHipURRupEJe8Dsp1g==" + }, "@humanwhocodes/config-array": { "version": "0.5.0", "dev": true, @@ -46542,6 +46581,37 @@ "warn-once": "^0.1.0" } }, + "@react-ng/bounds-observer": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/@react-ng/bounds-observer/-/bounds-observer-0.2.1.tgz", + "integrity": "sha512-i0h7x0qOLJz+JKxhOpngHFob6PH2Qmra85aQ0e/viS1yYgidoBvPJHn8WPGn5LXff98fE+fPhngsaD7FSbxcwQ==", + "requires": { + "@html-ng/bounding-client-rect-observer": "^0.1.3", + "@types/react": "^18.0.31", + "@types/react-dom": "^18.0.11", + "react": "^18.2.0", + "react-dom": "^18.2.0" + }, + "dependencies": { + "react-dom": { + "version": "18.2.0", + "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-18.2.0.tgz", + "integrity": "sha512-6IMTriUmvsjHUjNtEDudZfuDQUoWXVxKHhlEGSk81n4YFS+r/Kl99wXiwlVXtPBtJenozv2P+hxDsw9eA7Xo6g==", + "requires": { + "loose-envify": "^1.1.0", + "scheduler": "^0.23.0" + } + }, + "scheduler": { + "version": "0.23.0", + "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.23.0.tgz", + "integrity": "sha512-CtuThmgHNg7zIZWAXi3AsyIzA3n4xx7aNyjwC2VJldO2LMVDhFK+63xGqq6CsJH4rTAt6/M+N4GhZiDYPx9eUw==", + "requires": { + "loose-envify": "^1.1.0" + } + } + } + }, "@sentry/browser": { "version": "7.11.1", "requires": { @@ -51733,8 +51803,7 @@ "dev": true }, "@types/prop-types": { - "version": "15.7.5", - "peer": true + "version": "15.7.5" }, "@types/qs": { "version": "6.9.7", @@ -51751,14 +51820,23 @@ "dev": true }, "@types/react": { - "version": "18.0.24", - "peer": true, + "version": "18.2.6", + "resolved": "https://registry.npmjs.org/@types/react/-/react-18.2.6.tgz", + "integrity": "sha512-wRZClXn//zxCFW+ye/D2qY65UsYP1Fpex2YXorHc8awoNamkMZSvBxwxdYVInsHOZZd2Ppq8isnSzJL5Mpf8OA==", "requires": { "@types/prop-types": "*", "@types/scheduler": "*", "csstype": "^3.0.2" } }, + "@types/react-dom": { + "version": "18.2.4", + "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-18.2.4.tgz", + "integrity": "sha512-G2mHoTMTL4yoydITgOGwWdWMVd8sNgyEP85xVmMKAPUBwQWm9wBPQUmvbeF4V3WBY1P7mmL4BkjQ0SqUpf1snw==", + "requires": { + "@types/react": "*" + } + }, "@types/react-native": { "version": "0.70.6", "peer": true, @@ -51778,8 +51856,7 @@ "dev": true }, "@types/scheduler": { - "version": "0.16.2", - "peer": true + "version": "0.16.2" }, "@types/seedrandom": { "version": "2.4.30", @@ -57324,9 +57401,9 @@ } }, "expensify-common": { - "version": "git+ssh://git@github.com/Expensify/expensify-common.git#879df466155918ec0e0a6b36176af4211302b184", - "integrity": "sha512-0XZtJOzpH5cwaWMeW/25ZEazELbFd65Q/SPqoOyEhSWx/rarhZZNGYyQlcfHfj+c1wk1eSUEVL1nEIT9VOAUNg==", - "from": "expensify-common@git+ssh://git@github.com/Expensify/expensify-common.git#879df466155918ec0e0a6b36176af4211302b184", + "version": "git+ssh://git@github.com/Expensify/expensify-common.git#92088df062e60cba227fd1d6fa59ebfcde27d460", + "integrity": "sha512-xFCTXFz295JjSSYOjcDvR1FeN6rx+t2vCz64ahqm+TohI0eCWWJSzdUq/2jvrDm72e509rjdYwYcLWy4PumeYA==", + "from": "expensify-common@git+ssh://git@github.com/Expensify/expensify-common.git#92088df062e60cba227fd1d6fa59ebfcde27d460", "requires": { "classnames": "2.3.1", "clipboard": "2.0.4", @@ -58522,11 +58599,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, @@ -64945,9 +65017,7 @@ } }, "react-native-fast-image": { - "version": "git+ssh://git@github.com/Expensify/react-native-fast-image.git#afedf204bfc253d18f08fdcc5356a2bb82f6a87c", - "integrity": "sha512-jfARLcakkTtqh9jVkGWkJ+3zlpMmcoGH/6LHdnLuestM90Tn3cTPjWfWQloCYJmFwK/O2f4m1SHp1BOs0VR+cw==", - "from": "react-native-fast-image@git+https://github.com/Expensify/react-native-fast-image.git#afedf204bfc253d18f08fdcc5356a2bb82f6a87c", + "version": "8.6.3", "requires": {} }, "react-native-flipper": { @@ -65088,15 +65158,21 @@ "requires": {} }, "react-native-reanimated": { - "version": "3.0.0-rc.10", + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/react-native-reanimated/-/react-native-reanimated-3.1.0.tgz", + "integrity": "sha512-8YJR7yHnrqK6yKWzkGLVEawi1WZqJ9bGIehKEnE8zG58yLrSwUZe1T220XTbftpkA3r37Sy0kJJ/HOOiaIU+HQ==", "requires": { "@babel/plugin-transform-object-assign": "^7.16.7", "@babel/preset-typescript": "^7.16.7", - "convert-source-map": "^1.7.0", - "invariant": "^2.2.4", - "lodash.isequal": "^4.5.0", - "setimmediate": "^1.0.5", - "string-hash-64": "^1.0.3" + "convert-source-map": "^2.0.0", + "invariant": "^2.2.4" + }, + "dependencies": { + "convert-source-map": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", + "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==" + } } }, "react-native-render-html": { @@ -66562,7 +66638,9 @@ } }, "setimmediate": { - "version": "1.0.5" + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/setimmediate/-/setimmediate-1.0.5.tgz", + "integrity": "sha512-MATJdZp8sLqDl/68LfQmbP8zKPLQNV6BIZoIgrscFDQ+RsvK/BxeDQOgyxKKoh0y/8h3BqVFnCqQ/gd+reiIXA==" }, "setprototypeof": { "version": "1.2.0" @@ -67175,9 +67253,6 @@ "safe-buffer": "~5.1.0" } }, - "string-hash-64": { - "version": "1.0.3" - }, "string-length": { "version": "4.0.2", "requires": { diff --git a/package.json b/package.json index b8512f51b2fe..5506d4bc9f51 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "new.expensify", - "version": "1.3.15-5", + "version": "1.3.16-5", "author": "Expensify, Inc.", "homepage": "https://new.expensify.com", "description": "New Expensify is the next generation of Expensify: a reimagination of payments based atop a foundation of chat.", @@ -69,13 +69,14 @@ "@react-navigation/drawer": "github:Expensify/react-navigation#react-navigation-drawer-v6.5.0-alpha1-gitpkg", "@react-navigation/native": "6.0.13", "@react-navigation/stack": "6.3.1", + "@react-ng/bounds-observer": "^0.2.1", "@ua/react-native-airship": "^15.2.3", "awesome-phonenumber": "^5.4.0", "babel-plugin-transform-remove-console": "^6.9.4", "babel-polyfill": "^6.26.0", "dom-serializer": "^0.2.2", "domhandler": "^4.3.0", - "expensify-common": "git+ssh://git@github.com/Expensify/expensify-common.git#879df466155918ec0e0a6b36176af4211302b184", + "expensify-common": "git+ssh://git@github.com/Expensify/expensify-common.git#92088df062e60cba227fd1d6fa59ebfcde27d460", "fbjs": "^3.0.2", "html-entities": "^1.3.1", "htmlparser2": "^7.2.0", @@ -102,7 +103,7 @@ "react-native-dev-menu": "^4.1.1", "react-native-device-info": "^10.3.0", "react-native-document-picker": "^8.0.0", - "react-native-fast-image": "git+https://github.com/Expensify/react-native-fast-image.git#afedf204bfc253d18f08fdcc5356a2bb82f6a87c", + "react-native-fast-image": "^8.6.3", "react-native-gesture-handler": "2.9.0", "react-native-google-places-autocomplete": "git+https://github.com/Expensify/react-native-google-places-autocomplete.git#6f436a06a3018cb49750bb110b89df75f6a865d5", "react-native-haptic-feedback": "^1.13.0", @@ -120,7 +121,7 @@ "react-native-plaid-link-sdk": "^10.0.0", "react-native-qrcode-svg": "^6.2.0", "react-native-quick-sqlite": "^8.0.0-beta.2", - "react-native-reanimated": "3.0.0-rc.10", + "react-native-reanimated": "3.1.0", "react-native-render-html": "6.3.1", "react-native-safe-area-context": "4.4.1", "react-native-screens": "3.17.0", @@ -189,7 +190,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", @@ -206,6 +206,7 @@ "react-native-performance-flipper-reporter": "^2.0.0", "react-native-svg-transformer": "^1.0.0", "react-test-renderer": "18.1.0", + "setimmediate": "^1.0.5", "shellcheck": "^1.1.0", "style-loader": "^2.0.0", "time-analytics-webpack-plugin": "^0.1.17", @@ -220,6 +221,17 @@ "overrides": { "react-native": "$react-native" }, + "electronmon": { + "patterns": [ + "!node_modules", + "!node_modules/**/*", + "!**/*.map", + "!ios/**", + "!android/**", + "*.test.*", + "*.spec.*" + ] + }, "engines": { "node": "16.15.1", "npm": "8.11.0" diff --git a/patches/react-native-fast-image+8.6.3.patch b/patches/react-native-fast-image+8.6.3.patch new file mode 100644 index 000000000000..f01b87b7fd91 --- /dev/null +++ b/patches/react-native-fast-image+8.6.3.patch @@ -0,0 +1,300 @@ +diff --git a/node_modules/react-native-fast-image/android/src/main/java/com/dylanvann/fastimage/BitmapSizeDecoder.java b/node_modules/react-native-fast-image/android/src/main/java/com/dylanvann/fastimage/BitmapSizeDecoder.java +new file mode 100644 +index 0000000..2bd58b8 +--- /dev/null ++++ b/node_modules/react-native-fast-image/android/src/main/java/com/dylanvann/fastimage/BitmapSizeDecoder.java +@@ -0,0 +1,44 @@ ++package com.dylanvann.fastimage; ++ ++import android.graphics.BitmapFactory; ++ ++import androidx.annotation.NonNull; ++import androidx.annotation.Nullable; ++import androidx.exifinterface.media.ExifInterface; ++ ++import com.bumptech.glide.load.Options; ++import com.bumptech.glide.load.ResourceDecoder; ++import com.bumptech.glide.load.engine.Resource; ++import com.bumptech.glide.load.resource.SimpleResource; ++ ++import java.io.IOException; ++import java.io.InputStream; ++ ++public class BitmapSizeDecoder implements ResourceDecoder { ++ ++ @Override ++ public boolean handles(@NonNull InputStream source, @NonNull Options options) throws IOException { ++ return true; ++ } ++ ++ @Nullable ++ @Override ++ public Resource decode(@NonNull InputStream source, int width, int height, @NonNull Options options) throws IOException { ++ BitmapFactory.Options bitmapOptions = new BitmapFactory.Options(); ++ bitmapOptions.inJustDecodeBounds = true; ++ BitmapFactory.decodeStream(source, null, bitmapOptions); ++ ++ // BitmapFactory#decodeStream leaves stream's position where ever it was after reading the encoded data ++ // https://developer.android.com/reference/android/graphics/BitmapFactory#decodeStream(java.io.InputStream) ++ // so we need to rewind the stream to be able to read image header with exif values ++ source.reset(); ++ ++ int orientation = new ExifInterface(source).getAttributeInt(ExifInterface.TAG_ORIENTATION, ExifInterface.ORIENTATION_UNDEFINED); ++ if (orientation == ExifInterface.ORIENTATION_ROTATE_90 || orientation == ExifInterface.ORIENTATION_ROTATE_270) { ++ int tmpWidth = bitmapOptions.outWidth; ++ bitmapOptions.outWidth = bitmapOptions.outHeight; ++ bitmapOptions.outHeight = tmpWidth; ++ } ++ return new SimpleResource(bitmapOptions); ++ } ++} +\ No newline at end of file +diff --git a/node_modules/react-native-fast-image/android/src/main/java/com/dylanvann/fastimage/BitmapSizeTranscoder.java b/node_modules/react-native-fast-image/android/src/main/java/com/dylanvann/fastimage/BitmapSizeTranscoder.java +new file mode 100644 +index 0000000..7d208d1 +--- /dev/null ++++ b/node_modules/react-native-fast-image/android/src/main/java/com/dylanvann/fastimage/BitmapSizeTranscoder.java +@@ -0,0 +1,23 @@ ++package com.dylanvann.fastimage; ++ ++import android.graphics.BitmapFactory; ++ ++import androidx.annotation.NonNull; ++import androidx.annotation.Nullable; ++ ++import com.bumptech.glide.load.Options; ++import com.bumptech.glide.load.engine.Resource; ++import com.bumptech.glide.load.resource.SimpleResource; ++import com.bumptech.glide.load.resource.transcode.ResourceTranscoder; ++ ++public class BitmapSizeTranscoder implements ResourceTranscoder { ++ @Nullable ++ @Override ++ public Resource transcode(@NonNull Resource toTranscode, @NonNull Options options) { ++ BitmapFactory.Options bitmap = toTranscode.get(); ++ Size size = new Size(); ++ size.width = bitmap.outWidth; ++ size.height = bitmap.outHeight; ++ return new SimpleResource(size); ++ } ++} +\ No newline at end of file +diff --git a/node_modules/react-native-fast-image/android/src/main/java/com/dylanvann/fastimage/FastImageOkHttpProgressGlideModule.java b/node_modules/react-native-fast-image/android/src/main/java/com/dylanvann/fastimage/FastImageOkHttpProgressGlideModule.java +index 811292a..f60b87c 100644 +--- a/node_modules/react-native-fast-image/android/src/main/java/com/dylanvann/fastimage/FastImageOkHttpProgressGlideModule.java ++++ b/node_modules/react-native-fast-image/android/src/main/java/com/dylanvann/fastimage/FastImageOkHttpProgressGlideModule.java +@@ -2,6 +2,7 @@ package com.dylanvann.fastimage; + + import android.content.Context; + import androidx.annotation.NonNull; ++import android.graphics.BitmapFactory; + + import com.bumptech.glide.Glide; + import com.bumptech.glide.Registry; +@@ -47,6 +48,9 @@ public class FastImageOkHttpProgressGlideModule extends LibraryGlideModule { + .build(); + OkHttpUrlLoader.Factory factory = new OkHttpUrlLoader.Factory(client); + registry.replace(GlideUrl.class, InputStream.class, factory); ++ // Decoder + Transcoder pair for InputStream -> Size ++ registry.prepend(InputStream.class, BitmapFactory.Options.class, new BitmapSizeDecoder()); ++ registry.register(BitmapFactory.Options.class, Size.class, new BitmapSizeTranscoder()); + } + + private static Interceptor createInterceptor(final ResponseProgressListener listener) { +diff --git a/node_modules/react-native-fast-image/android/src/main/java/com/dylanvann/fastimage/FastImageRequestListener.java b/node_modules/react-native-fast-image/android/src/main/java/com/dylanvann/fastimage/FastImageRequestListener.java +index dbeb813..bf8f21c 100644 +--- a/node_modules/react-native-fast-image/android/src/main/java/com/dylanvann/fastimage/FastImageRequestListener.java ++++ b/node_modules/react-native-fast-image/android/src/main/java/com/dylanvann/fastimage/FastImageRequestListener.java +@@ -22,13 +22,6 @@ public class FastImageRequestListener implements RequestListener { + this.key = key; + } + +- private static WritableMap mapFromResource(Drawable resource) { +- WritableMap resourceData = new WritableNativeMap(); +- resourceData.putInt("width", resource.getIntrinsicWidth()); +- resourceData.putInt("height", resource.getIntrinsicHeight()); +- return resourceData; +- } +- + @Override + public boolean onLoadFailed(@androidx.annotation.Nullable GlideException e, Object model, Target target, boolean isFirstResource) { + FastImageOkHttpProgressGlideModule.forget(key); +@@ -53,7 +46,6 @@ public class FastImageRequestListener implements RequestListener { + ThemedReactContext context = (ThemedReactContext) view.getContext(); + RCTEventEmitter eventEmitter = context.getJSModule(RCTEventEmitter.class); + int viewId = view.getId(); +- eventEmitter.receiveEvent(viewId, REACT_ON_LOAD_EVENT, mapFromResource(resource)); + eventEmitter.receiveEvent(viewId, REACT_ON_LOAD_END_EVENT, new WritableNativeMap()); + return false; + } +diff --git a/node_modules/react-native-fast-image/android/src/main/java/com/dylanvann/fastimage/FastImageViewWithUrl.java b/node_modules/react-native-fast-image/android/src/main/java/com/dylanvann/fastimage/FastImageViewWithUrl.java +index 34fcf89..1339f5c 100644 +--- a/node_modules/react-native-fast-image/android/src/main/java/com/dylanvann/fastimage/FastImageViewWithUrl.java ++++ b/node_modules/react-native-fast-image/android/src/main/java/com/dylanvann/fastimage/FastImageViewWithUrl.java +@@ -2,6 +2,7 @@ package com.dylanvann.fastimage; + + import static com.dylanvann.fastimage.FastImageRequestListener.REACT_ON_ERROR_EVENT; + ++import androidx.annotation.NonNull; + import android.annotation.SuppressLint; + import android.content.Context; + import android.graphics.drawable.Drawable; +@@ -9,16 +10,24 @@ import android.graphics.drawable.Drawable; + import androidx.annotation.Nullable; + import androidx.appcompat.widget.AppCompatImageView; + ++import com.bumptech.glide.Glide; + import com.bumptech.glide.RequestBuilder; + import com.bumptech.glide.RequestManager; ++import com.bumptech.glide.load.DataSource; ++import com.bumptech.glide.load.engine.GlideException; + import com.bumptech.glide.load.model.GlideUrl; + import com.bumptech.glide.request.Request; ++import com.bumptech.glide.request.RequestListener; ++import com.bumptech.glide.request.target.SimpleTarget; ++import com.bumptech.glide.request.target.Target; ++import com.bumptech.glide.request.transition.Transition; + import com.facebook.react.bridge.ReadableMap; + import com.facebook.react.bridge.WritableMap; + import com.facebook.react.bridge.WritableNativeMap; + import com.facebook.react.uimanager.ThemedReactContext; + import com.facebook.react.uimanager.events.RCTEventEmitter; + ++import java.io.File; + import java.util.ArrayList; + import java.util.Collections; + import java.util.List; +@@ -124,9 +133,34 @@ class FastImageViewWithUrl extends AppCompatImageView { + RCTEventEmitter eventEmitter = context.getJSModule(RCTEventEmitter.class); + int viewId = this.getId(); + +- eventEmitter.receiveEvent(viewId, +- FastImageViewManager.REACT_ON_LOAD_START_EVENT, +- new WritableNativeMap()); ++ // Request the URL from cache to see if it exists there and if so pass the cache ++ // path as an argument in the onLoadStart event ++ requestManager ++ .asFile() ++ .load(glideUrl) ++ .onlyRetrieveFromCache(true) ++ .listener(new RequestListener() { ++ @Override ++ public boolean onLoadFailed(@Nullable GlideException e, Object model, Target target, boolean isFirstResource) { ++ WritableNativeMap result = new WritableNativeMap(); ++ result.putNull("cachePath"); ++ eventEmitter.receiveEvent(viewId, ++ FastImageViewManager.REACT_ON_LOAD_START_EVENT, ++ result); ++ return false; ++ } ++ ++ @Override ++ public boolean onResourceReady(File resource, Object model, Target target, DataSource dataSource, boolean isFirstResource) { ++ WritableNativeMap result = new WritableNativeMap(); ++ result.putString("cachePath", resource.getAbsolutePath()); ++ eventEmitter.receiveEvent(viewId, ++ FastImageViewManager.REACT_ON_LOAD_START_EVENT, ++ result); ++ return false; ++ } ++ }) ++ .submit(); + } + + if (requestManager != null) { +@@ -148,6 +182,25 @@ class FastImageViewWithUrl extends AppCompatImageView { + builder.listener(new FastImageRequestListener(key)); + + builder.into(this); ++ ++ // Used specifically to handle the `onLoad` event for the image ++ RCTEventEmitter eventEmitter = context.getJSModule(RCTEventEmitter.class); ++ int viewId = this.getId(); ++ requestManager ++ .as(Size.class) ++ .load(imageSource == null ? null : imageSource.getSourceForLoad()) ++ .into(new SimpleTarget() { ++ @Override ++ public void onResourceReady(@NonNull Size resource, @Nullable Transition transition) { ++ WritableMap resourceData = new WritableNativeMap(); ++ resourceData.putInt("width", resource.width); ++ resourceData.putInt("height", resource.height); ++ eventEmitter.receiveEvent(viewId, ++ "onFastImageLoad", ++ resourceData ++ ); ++ } ++ }); + } + } + +diff --git a/node_modules/react-native-fast-image/android/src/main/java/com/dylanvann/fastimage/Size.java b/node_modules/react-native-fast-image/android/src/main/java/com/dylanvann/fastimage/Size.java +new file mode 100644 +index 0000000..2fe8a47 +--- /dev/null ++++ b/node_modules/react-native-fast-image/android/src/main/java/com/dylanvann/fastimage/Size.java +@@ -0,0 +1,6 @@ ++package com.dylanvann.fastimage; ++ ++public class Size { ++ int width; ++ int height; ++} +\ No newline at end of file +diff --git a/node_modules/react-native-fast-image/dist/index.d.ts b/node_modules/react-native-fast-image/dist/index.d.ts +index 5abb7c9..a2672c6 100644 +--- a/node_modules/react-native-fast-image/dist/index.d.ts ++++ b/node_modules/react-native-fast-image/dist/index.d.ts +@@ -27,6 +27,11 @@ export declare type Source = { + priority?: Priority; + cache?: Cache; + }; ++export interface OnLoadStartEvent { ++ nativeEvent: { ++ cachePath: string | null; ++ }; ++} + export interface OnLoadEvent { + nativeEvent: { + width: number; +@@ -57,7 +62,7 @@ export interface FastImageProps extends AccessibilityProps, ViewProps { + defaultSource?: ImageRequireSource; + resizeMode?: ResizeMode; + fallback?: boolean; +- onLoadStart?(): void; ++ onLoadStart?(event: OnLoadStartEvent): void; + onProgress?(event: OnProgressEvent): void; + onLoad?(event: OnLoadEvent): void; + onError?(): void; +diff --git a/node_modules/react-native-fast-image/ios/FastImage/FFFastImageView.m b/node_modules/react-native-fast-image/ios/FastImage/FFFastImageView.m +index f710081..391ef92 100644 +--- a/node_modules/react-native-fast-image/ios/FastImage/FFFastImageView.m ++++ b/node_modules/react-native-fast-image/ios/FastImage/FFFastImageView.m +@@ -54,7 +54,6 @@ - (void) setOnFastImageError: (RCTDirectEventBlock)onFastImageError { + - (void) setOnFastImageLoadStart: (RCTDirectEventBlock)onFastImageLoadStart { + if (_source && !self.hasSentOnLoadStart) { + _onFastImageLoadStart = onFastImageLoadStart; +- onFastImageLoadStart(@{}); + self.hasSentOnLoadStart = YES; + } else { + _onFastImageLoadStart = onFastImageLoadStart; +@@ -188,7 +187,18 @@ - (void) reloadImage { + } + + if (self.onFastImageLoadStart) { +- self.onFastImageLoadStart(@{}); ++ NSString* cachePath = [[SDImageCache sharedImageCache] cachePathForKey:url]; ++ BOOL isCached = [[SDImageCache sharedImageCache] diskImageDataExistsWithKey:url]; ++ if (isCached) { ++ self.onFastImageLoadStart(@{ ++ @"cachePath": cachePath ++ }); ++ } ++ else { ++ self.onFastImageLoadStart(@{ ++ @"cachePath": [NSNull null] ++ }); ++ } + self.hasSentOnLoadStart = YES; + } else { + self.hasSentOnLoadStart = NO; diff --git a/patches/react-native-reanimated+3.1.0.patch b/patches/react-native-reanimated+3.1.0.patch new file mode 100644 index 000000000000..6dc6e0d3bc9b --- /dev/null +++ b/patches/react-native-reanimated+3.1.0.patch @@ -0,0 +1,177 @@ +diff --git a/node_modules/react-native-reanimated/ios/REANodesManager.mm b/node_modules/react-native-reanimated/ios/REANodesManager.mm +index 26bb253..4108293 100644 +--- a/node_modules/react-native-reanimated/ios/REANodesManager.mm ++++ b/node_modules/react-native-reanimated/ios/REANodesManager.mm +@@ -85,19 +85,77 @@ - (void)runSyncUIUpdatesWithObserver:(id)observer + + @end + +-@interface REANodesManager () ++#ifndef RCT_NEW_ARCH_ENABLED + ++@interface REASyncUpdateObserver : NSObject + @end + ++@implementation REASyncUpdateObserver { ++ volatile void (^_mounting)(void); ++ volatile BOOL _waitTimedOut; ++ dispatch_semaphore_t _semaphore; ++} ++ ++- (instancetype)init ++{ ++ self = [super init]; ++ if (self) { ++ _mounting = nil; ++ _waitTimedOut = NO; ++ _semaphore = dispatch_semaphore_create(0); ++ } ++ return self; ++} ++ ++- (void)dealloc ++{ ++ RCTAssert(_mounting == nil, @"Mouting block was set but never executed. This may lead to UI inconsistencies"); ++} ++ ++- (void)unblockUIThread ++{ ++ RCTAssertUIManagerQueue(); ++ dispatch_semaphore_signal(_semaphore); ++} ++ ++- (void)waitAndMountWithTimeout:(NSTimeInterval)timeout ++{ ++ RCTAssertMainQueue(); ++ long result = dispatch_semaphore_wait(_semaphore, dispatch_time(DISPATCH_TIME_NOW, timeout * NSEC_PER_SEC)); ++ if (result != 0) { ++ @synchronized(self) { ++ _waitTimedOut = YES; ++ } ++ } ++ if (_mounting) { ++ _mounting(); ++ _mounting = nil; ++ } ++} ++ ++- (BOOL)uiManager:(RCTUIManager *)manager performMountingWithBlock:(RCTUIManagerMountingBlock)block ++{ ++ RCTAssertUIManagerQueue(); ++ @synchronized(self) { ++ if (_waitTimedOut) { ++ return NO; ++ } else { ++ _mounting = block; ++ return YES; ++ } ++ } ++} ++ ++@end ++ ++#endif ++ + @implementation REANodesManager { + CADisplayLink *_displayLink; + BOOL _wantRunUpdates; + NSMutableArray *_onAnimationCallbacks; + BOOL _tryRunBatchUpdatesSynchronously; + REAEventHandler _eventHandler; +- volatile void (^_mounting)(void); +- NSObject *_syncLayoutUpdatesWaitLock; +- volatile BOOL _syncLayoutUpdatesWaitTimedOut; + NSMutableDictionary *_componentUpdateBuffer; + NSMutableDictionary *_viewRegistry; + #ifdef RCT_NEW_ARCH_ENABLED +@@ -125,7 +183,6 @@ - (nonnull instancetype)initWithModule:(REAModule *)reanimatedModule + _operationsInBatch = [NSMutableDictionary new]; + _componentUpdateBuffer = [NSMutableDictionary new]; + _viewRegistry = [_uiManager valueForKey:@"_viewRegistry"]; +- _syncLayoutUpdatesWaitLock = [NSObject new]; + } + + _displayLink = [CADisplayLink displayLinkWithTarget:self selector:@selector(onAnimationFrame:)]; +@@ -241,19 +298,6 @@ - (void)onAnimationFrame:(CADisplayLink *)displayLink + } + } + +-- (BOOL)uiManager:(RCTUIManager *)manager performMountingWithBlock:(RCTUIManagerMountingBlock)block +-{ +- RCTAssert(_mounting == nil, @"Mouting block is expected to not be set"); +- @synchronized(_syncLayoutUpdatesWaitLock) { +- if (_syncLayoutUpdatesWaitTimedOut) { +- return NO; +- } else { +- _mounting = block; +- return YES; +- } +- } +-} +- + - (void)performOperations + { + #ifdef RCT_NEW_ARCH_ENABLED +@@ -268,8 +312,7 @@ - (void)performOperations + _tryRunBatchUpdatesSynchronously = NO; + + __weak __typeof__(self) weakSelf = self; +- dispatch_semaphore_t semaphore = dispatch_semaphore_create(0); +- _syncLayoutUpdatesWaitTimedOut = NO; ++ REASyncUpdateObserver *syncUpdateObserver = [REASyncUpdateObserver new]; + RCTExecuteOnUIManagerQueue(^{ + __typeof__(self) strongSelf = weakSelf; + if (strongSelf == nil) { +@@ -278,7 +321,7 @@ - (void)performOperations + BOOL canUpdateSynchronously = trySynchronously && ![strongSelf.uiManager hasEnqueuedUICommands]; + + if (!canUpdateSynchronously) { +- dispatch_semaphore_signal(semaphore); ++ [syncUpdateObserver unblockUIThread]; + } + + for (int i = 0; i < copiedOperationsQueue.count; i++) { +@@ -286,8 +329,8 @@ - (void)performOperations + } + + if (canUpdateSynchronously) { +- [strongSelf.uiManager runSyncUIUpdatesWithObserver:strongSelf]; +- dispatch_semaphore_signal(semaphore); ++ [strongSelf.uiManager runSyncUIUpdatesWithObserver:syncUpdateObserver]; ++ [syncUpdateObserver unblockUIThread]; + } + // In case canUpdateSynchronously=true we still have to send uiManagerWillPerformMounting event + // to observers because some components (e.g. TextInput) update their UIViews only on that event. +@@ -298,17 +341,7 @@ - (void)performOperations + // from CADisplayLink but it is easier to hardcode it for the time being. + // The reason why we use frame duration here is that if takes longer than one frame to complete layout tasks + // there is no point of synchronizing layout with the UI interaction as we get that one frame delay anyways. +- long result = dispatch_semaphore_wait(semaphore, dispatch_time(DISPATCH_TIME_NOW, 16 * NSEC_PER_MSEC)); +- if (result != 0) { +- @synchronized(_syncLayoutUpdatesWaitLock) { +- _syncLayoutUpdatesWaitTimedOut = YES; +- } +- } +- } +- +- if (_mounting) { +- _mounting(); +- _mounting = nil; ++ [syncUpdateObserver waitAndMountWithTimeout:0.016]; + } + } + _wantRunUpdates = NO; +diff --git a/node_modules/react-native-reanimated/mock.js b/node_modules/react-native-reanimated/mock.js +index 68b20d2..b088001 100644 +--- a/node_modules/react-native-reanimated/mock.js ++++ b/node_modules/react-native-reanimated/mock.js +@@ -41,6 +41,9 @@ const Reanimated = { + createAnimatedComponent: (Component) => Component, + addWhitelistedUIProps: NOOP, + addWhitelistedNativeProps: NOOP, ++ ++ // used by react-navigation fork ++ isConfigured: () => true, + }; + + module.exports = { diff --git a/src/CONST.js b/src/CONST.js index 47fb10ec576a..a04c20192037 100755 --- a/src/CONST.js +++ b/src/CONST.js @@ -282,6 +282,7 @@ const CONST = { MX: 'MX', AU: 'AU', CA: 'CA', + GB: 'GB', }, DESKTOP_DEEPLINK_APP_STATE: { CHECKING: 'checking', @@ -559,6 +560,7 @@ const CONST = { }, STATE: { SUBMITTED: 'SUBMITTED', + PROCESSING: 'PROCESSING', }, STATE_NUM: { OPEN: 0, @@ -960,6 +962,7 @@ const CONST = { ELSEWHERE: 'Elsewhere', EXPENSIFY: 'Expensify', PAYPAL_ME: 'PayPal.me', + VBBA: 'ACH', }, MONEY_REQUEST_TYPE: { SEND: 'send', diff --git a/src/Expensify.js b/src/Expensify.js index f6831443d907..4fc000302ce7 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,11 @@ 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 shouldInit = isNavigationReady && (!isAuthenticated || props.isSidebarLoaded); + const shouldHideSplash = shouldInit && !isSplashHidden; const initializeClient = () => { if (!Visibility.isVisible()) { @@ -105,6 +108,10 @@ function Expensify(props) { Navigation.setIsNavigationReady(); }, []); + const onSplashHide = useCallback(() => { + setIsSplashHidden(true); + }, []); + useLayoutEffect(() => { // Initialize this client as being an active client ActiveClientManager.init(); @@ -158,20 +165,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 +172,7 @@ function Expensify(props) { return ( - {!isSplashShown && ( + {shouldInit && ( <> @@ -204,6 +197,8 @@ function Expensify(props) { onReady={setNavigationReady} authenticated={isAuthenticated} /> + + {shouldHideSplash && } ); } diff --git a/src/ONYXKEYS.js b/src/ONYXKEYS.js index a5e3c97740da..1805db98f1d5 100755 --- a/src/ONYXKEYS.js +++ b/src/ONYXKEYS.js @@ -91,6 +91,9 @@ export default { // A unique identifier that each user has that's used to send notifications NVP_PRIVATE_PUSH_NOTIFICATION_ID: 'private_pushNotificationID', + // The NVP with the last payment method used per policy + NVP_LAST_PAYMENT_METHOD: 'nvp_lastPaymentMethod', + // Does this user have push notifications enabled for this device? PUSH_NOTIFICATIONS_ENABLED: 'pushNotificationsEnabled', @@ -194,6 +197,7 @@ export default { CLOSE_ACCOUNT_FORM: 'closeAccount', PROFILE_SETTINGS_FORM: 'profileSettingsForm', DISPLAY_NAME_FORM: 'displayNameForm', + ROOM_NAME_FORM: 'roomNameForm', LEGAL_NAME_FORM: 'legalNameForm', WORKSPACE_INVITE_MESSAGE_FORM: 'workspaceInviteMessageForm', DATE_OF_BIRTH_FORM: 'dateOfBirthForm', diff --git a/src/ROUTES.js b/src/ROUTES.js index 272b0f9b6c70..2dc2338f2e05 100644 --- a/src/ROUTES.js +++ b/src/ROUTES.js @@ -19,6 +19,7 @@ const SETTINGS_CONTACT_METHODS = 'settings/profile/contact-methods'; export default { BANK_ACCOUNT: 'bank-account', + BANK_ACCOUNT_NEW: 'bank-account/new', BANK_ACCOUNT_WITH_STEP_TO_OPEN: 'bank-account/:stepToOpen?', BANK_ACCOUNT_PERSONAL: 'bank-account/personal', getBankAccountRoute: (stepToOpen = '', policyID = '') => `bank-account/${stepToOpen}?policyID=${policyID}`, @@ -90,9 +91,9 @@ export default { IOU_SEND_ADD_BANK_ACCOUNT: `${IOU_SEND}/add-bank-account`, IOU_SEND_ADD_DEBIT_CARD: `${IOU_SEND}/add-debit-card`, IOU_SEND_ENABLE_PAYMENTS: `${IOU_SEND}/enable-payments`, - getIouRequestCurrencyRoute: (reportID) => `${IOU_REQUEST_CURRENCY}/${reportID}`, - getIouBillCurrencyRoute: (reportID) => `${IOU_BILL_CURRENCY}/${reportID}`, - getIouSendCurrencyRoute: (reportID) => `${IOU_SEND_CURRENCY}/${reportID}`, + getIouRequestCurrencyRoute: (reportID, currency, backTo) => `${IOU_REQUEST_CURRENCY}/${reportID}?currency=${currency}&backTo=${backTo}`, + getIouBillCurrencyRoute: (reportID, currency, backTo) => `${IOU_BILL_CURRENCY}/${reportID}?currency=${currency}&backTo=${backTo}`, + getIouSendCurrencyRoute: (reportID, currency, backTo) => `${IOU_SEND_CURRENCY}/${reportID}?currency=${currency}&backTo=${backTo}`, IOU_DETAILS, IOU_DETAILS_ADD_BANK_ACCOUNT: `${IOU_DETAILS}/add-bank-account`, IOU_DETAILS_ADD_DEBIT_CARD: `${IOU_DETAILS}/add-debit-card`, @@ -123,7 +124,11 @@ export default { REPORT_WITH_ID_DETAILS: 'r/:reportID/details', getReportDetailsRoute: (reportID) => `r/${reportID}/details`, REPORT_SETTINGS: 'r/:reportID/settings', + REPORT_SETTINGS_ROOM_NAME: 'r/:reportID/settings/room-name', + REPORT_SETTINGS_NOTIFICATION_PREFERENCES: 'r/:reportID/settings/notification-preferences', getReportSettingsRoute: (reportID) => `r/${reportID}/settings`, + getReportSettingsRoomNameRoute: (reportID) => `r/${reportID}/settings/room-name`, + getReportSettingsNotificationPreferencesRoute: (reportID) => `r/${reportID}/settings/notification-preferences`, TRANSITION_FROM_OLD_DOT: 'transition', VALIDATE_LOGIN: 'v/:accountID/:validateCode', GET_ASSISTANCE: 'get-assistance/:taskID', diff --git a/src/components/AddPaymentMethodMenu.js b/src/components/AddPaymentMethodMenu.js index 61927aefb868..2581bde85b4e 100644 --- a/src/components/AddPaymentMethodMenu.js +++ b/src/components/AddPaymentMethodMenu.js @@ -12,11 +12,16 @@ import PopoverMenu from './PopoverMenu'; import paypalMeDataPropTypes from './paypalMeDataPropTypes'; const propTypes = { + /** Should the component be visible? */ isVisible: PropTypes.bool.isRequired, + + /** Callback to execute when the component closes. */ onClose: PropTypes.func.isRequired, + + /** Anchor position for the AddPaymentMenu. */ anchorPosition: PropTypes.shape({ - top: PropTypes.number, - left: PropTypes.number, + horizontal: PropTypes.number, + vertical: PropTypes.number, }), /** Account details for PayPal.Me */ @@ -43,7 +48,7 @@ const AddPaymentMethodMenu = (props) => ( isVisible={props.isVisible} onClose={props.onClose} anchorPosition={props.anchorPosition} - onItemSelected={() => props.onClose()} + onItemSelected={props.onClose} menuItems={[ { text: props.translate('common.bankAccount'), diff --git a/src/components/AddressSearch/index.js b/src/components/AddressSearch/index.js index 52ca14dbf701..5219c5b4855f 100644 --- a/src/components/AddressSearch/index.js +++ b/src/components/AddressSearch/index.js @@ -119,6 +119,7 @@ const AddressSearch = (props) => { postal_town: postalTown, postal_code: zipCode, administrative_area_level_1: state, + administrative_area_level_2: stateFallback, country, } = GooglePlacesUtils.getAddressComponents(addressComponents, { street_number: 'long_name', @@ -129,6 +130,7 @@ const AddressSearch = (props) => { postal_town: 'long_name', postal_code: 'long_name', administrative_area_level_1: 'short_name', + administrative_area_level_2: 'long_name', country: 'short_name', }); @@ -164,6 +166,12 @@ const AddressSearch = (props) => { values.state = longStateName; } + // UK addresses return countries (e.g. England) in the state field (administrative_area_level_1) + // So we use a secondary field (administrative_area_level_2) as a fallback + if (country === CONST.COUNTRY.GB) { + values.state = stateFallback; + } + // Not all pages define the Address Line 2 field, so in that case we append any additional address details // (e.g. Apt #) to Address Line 1 if (subpremise && typeof props.renamedInputKeys.street2 === 'undefined') { diff --git a/src/components/AvatarCropModal/ImageCropView.js b/src/components/AvatarCropModal/ImageCropView.js index 7605ad83da5b..3ad72bd3d674 100644 --- a/src/components/AvatarCropModal/ImageCropView.js +++ b/src/components/AvatarCropModal/ImageCropView.js @@ -52,16 +52,23 @@ const defaultProps = { const ImageCropView = (props) => { const containerStyle = StyleUtils.getWidthAndHeightStyle(props.containerSize, props.containerSize); + const originalImageHeight = props.originalImageHeight; + const originalImageWidth = props.originalImageWidth; + const rotation = props.rotation; + const translateX = props.translateX; + const translateY = props.translateY; + const scale = props.scale; + // A reanimated memoized style, which updates when the image's size or scale changes. const imageStyle = useAnimatedStyle(() => { - const height = props.originalImageHeight.value; - const width = props.originalImageWidth.value; + const height = originalImageHeight.value; + const width = originalImageWidth.value; const aspectRatio = height > width ? height / width : width / height; - const rotate = interpolate(props.rotation.value, [0, 360], [0, 360]); + const rotate = interpolate(rotation.value, [0, 360], [0, 360]); return { - transform: [{translateX: props.translateX.value}, {translateY: props.translateY.value}, {scale: props.scale.value * aspectRatio}, {rotate: `${rotate}deg`}], + transform: [{translateX: translateX.value}, {translateY: translateY.value}, {scale: scale.value * aspectRatio}, {rotate: `${rotate}deg`}], }; - }, [props.originalImageHeight, props.originalImageWidth]); + }, [originalImageHeight, originalImageWidth, rotation, translateX, translateY, scale]); // We're preventing text selection with ControlSelection.blockElement to prevent safari // default behaviour of cursor - I-beam cursor on drag. See https://github.com/Expensify/App/issues/13688 diff --git a/src/components/AvatarCropModal/Slider.js b/src/components/AvatarCropModal/Slider.js index 29bbd378f3ba..2877b3a9c917 100644 --- a/src/components/AvatarCropModal/Slider.js +++ b/src/components/AvatarCropModal/Slider.js @@ -26,12 +26,13 @@ const defaultProps = { // This component can't be written using class since reanimated API uses hooks. const Slider = (props) => { + const sliderValue = props.sliderValue; const [tooltipIsVisible, setTooltipIsVisible] = useState(true); // A reanimated memoized style, which tracks // a translateX shared value and updates the slider position. const rSliderStyle = useAnimatedStyle(() => ({ - transform: [{translateX: props.sliderValue.value}], + transform: [{translateX: sliderValue.value}], })); // We're preventing text selection with ControlSelection.blockElement to prevent safari diff --git a/src/components/AvatarWithImagePicker.js b/src/components/AvatarWithImagePicker.js index f65696ec3541..29424233891d 100644 --- a/src/components/AvatarWithImagePicker.js +++ b/src/components/AvatarWithImagePicker.js @@ -294,6 +294,7 @@ class AvatarWithImagePicker extends React.Component { onItemSelected={() => this.setState({isMenuVisible: false})} menuItems={this.createMenuItems(openPicker)} anchorPosition={this.props.anchorPosition} + anchorAlignment={this.props.anchorAlignment} /> )} diff --git a/src/components/Button/index.js b/src/components/Button/index.js index 65107cfe9392..f9200d085b28 100644 --- a/src/components/Button/index.js +++ b/src/components/Button/index.js @@ -109,6 +109,9 @@ const propTypes = { /** Accessibility label for the component */ accessibilityLabel: PropTypes.string, + + /** A ref to forward the button */ + forwardedRef: PropTypes.oneOfType([PropTypes.func, PropTypes.shape({current: PropTypes.oneOfType([PropTypes.instanceOf(React.Component), PropTypes.func])})]), }; const defaultProps = { @@ -141,6 +144,7 @@ const defaultProps = { shouldEnableHapticFeedback: false, nativeID: '', accessibilityLabel: '', + forwardedRef: undefined, }; class Button extends Component { @@ -240,6 +244,7 @@ class Button extends Component { render() { return ( { if (e && e.type === 'click') { e.currentTarget.blur(); @@ -303,4 +308,15 @@ class Button extends Component { Button.propTypes = propTypes; Button.defaultProps = defaultProps; -export default compose(withNavigationFallback, withNavigationFocus)(Button); +export default compose( + withNavigationFallback, + withNavigationFocus, +)( + React.forwardRef((props, ref) => ( + - -); - -ButtonWithDropdown.propTypes = propTypes; -ButtonWithDropdown.defaultProps = defaultProps; -ButtonWithDropdown.displayName = 'ButtonWithDropdown'; -export default ButtonWithDropdown; diff --git a/src/components/ButtonWithDropdownMenu.js b/src/components/ButtonWithDropdownMenu.js new file mode 100644 index 000000000000..a518ce647c75 --- /dev/null +++ b/src/components/ButtonWithDropdownMenu.js @@ -0,0 +1,139 @@ +import React, {useState, useRef, useEffect} from 'react'; +import PropTypes from 'prop-types'; +import {View} from 'react-native'; +import _ from 'underscore'; +import useWindowDimensions from '../hooks/useWindowDimensions'; +import styles from '../styles/styles'; +import Button from './Button'; +import PopoverMenu from './PopoverMenu'; +import Icon from './Icon'; +import * as Expensicons from './Icon/Expensicons'; +import themeColors from '../styles/themes/default'; +import CONST from '../CONST'; + +const propTypes = { + /** Text to display for the menu header */ + menuHeaderText: PropTypes.string, + + /** Callback to execute when the main button is pressed */ + onPress: PropTypes.func.isRequired, + + /** Whether we should show a loading state for the main button */ + isLoading: PropTypes.bool, + + /** Should the confirmation button be disabled? */ + isDisabled: PropTypes.bool, + + /** Additional styles to add to the component */ + style: PropTypes.oneOfType([PropTypes.arrayOf(PropTypes.object), PropTypes.object]), + + /** Menu options to display */ + /** e.g. [{text: 'Pay with Expensify', icon: Wallet}, {text: 'PayPal', icon: PayPal}] */ + options: PropTypes.arrayOf( + PropTypes.shape({ + value: PropTypes.string.isRequired, + text: PropTypes.string.isRequired, + icon: PropTypes.elementType, + iconWidth: PropTypes.number, + iconHeight: PropTypes.number, + iconDescription: PropTypes.string, + }), + ).isRequired, +}; + +const defaultProps = { + isLoading: false, + isDisabled: false, + menuHeaderText: '', + style: [], +}; + +const ButtonWithDropdownMenu = (props) => { + const [selectedItemIndex, setSelectedItemIndex] = useState(0); + const [isMenuVisible, setIsMenuVisible] = useState(false); + const [popoverAnchorPosition, setPopoverAnchorPosition] = useState(null); + const {width: windowWidth, height: windowHeight} = useWindowDimensions(); + const caretButton = useRef(null); + useEffect(() => { + if (!caretButton.current) { + return; + } + caretButton.current.measureInWindow((x, y, w, h) => { + setPopoverAnchorPosition({ + horizontal: x + w, + vertical: y + h, + }); + }); + }, [windowWidth, windowHeight]); + + const selectedItem = props.options[selectedItemIndex]; + return ( + + {props.options.length > 1 ? ( + + + + ) : ( +