react-native-inappbrowser icon indicating copy to clipboard operation
react-native-inappbrowser copied to clipboard

Normal open function returns incredibly slowly when canceling under certain circumstances on Android

Open TheWirv opened this issue 4 years ago • 11 comments

Hi, I have the strange problem that, after canceling on Android, the InAppBrowser.open function takes a very long time to return with the result ({type: "cancel"}) under certain circumstances. The worst thing is that I can't really pin down said circumstances. All I know for sure is that, in contrast, InAppBrowser.openAuth returns instantly after being canceled.

I made a video of it all and uploaded it to YouTube. What I did was the following:

  • InAppBrowser.open imprint link (dark theme); then close and wait
  • InAppBrowser.open privacy policy link (dark theme); then close and wait
  • Switch to different tab => theme changes
  • InAppBrowser.open (the same) imprint link (light theme); then close and wait (a long time)
  • Add some products to cart and select payment method
  • Check out => InAppBrowser.openAuth PayPal (light theme); then close

In App Browser anomaly showcase

@jdnichollsc Do you have an inkling of an idea what could be the cause of this delay? It's almost ten seconds. Additionally, it has the effect that during that duration any other call to open fails, or rather the open call from before finally returns with {type: "cancel"} which, I guess, directly cancels the new call.

The code is rather distributed and would therefore be difficult to share, but if that would be of substantial help I could pull out a few snippets, I guess.

Which platform(s) does your issue occur on?

  • iOS/Android/both: Only an issue on Android. On iOS, both open and openAuth return in an instant, no matter what.
  • iOS/Android versions: Android 10
  • emulator or device. What type of device? Emulator and Samsung Galaxy S10

Please, provide the following version numbers that your issue occurs with:

  • CLI: 4.8.0 (I ran npx react-native --version to fetch it, instead of react-native --version)
  • Plugin(s):
"dependencies": {
  "@react-native-community/async-storage": "^1.9.0",
  "@react-native-community/blur": "^3.4.1",
  "@react-native-community/masked-view": "^0.1.7",
  "@react-native-community/netinfo": "^5.5.1",
  "@react-native-community/viewpager": "^3.3.0",
  "@react-navigation/bottom-tabs": "^5.2.4",
  "@react-navigation/drawer": "^5.3.4",
  "@react-navigation/native": "^5.1.3",
  "@react-navigation/stack": "^5.2.6",
  "color": "^3.1.2",
  "react": "^16.13.1",
  "react-native": "^0.62.2",
  "react-native-gesture-handler": "^1.6.0",
  "react-native-inappbrowser-reborn": "^3.4.0",
  "react-native-iphone-x-helper": "^1.2.1",
  "react-native-linear-gradient": "^2.5.6",
  "react-native-localize": "^1.3.4",
  "react-native-reanimated": "1.8.0",
  "react-native-safe-area-context": "^0.7.3",
  "react-native-screens": "^2.0.0",
  "react-native-splash-screen": "^3.2.0",
  "react-native-svg": "^12.0.3",
  "react-native-vector-icons": "^6.6.0",
  "react-redux": "^7.2.0",
  "redux": "^4.0.5",
  "redux-thunk": "^2.3.0"
},
"devDependencies": {
  "@babel/cli": "^7.8.4",
  "@babel/core": "^7.8.6",
  "@babel/preset-env": "^7.8.6",
  "@babel/runtime": "^7.8.4",
  "@react-native-community/eslint-config": "^0.0.6",
  "@react-native-community/eslint-plugin": "^1.0.0",
  "babel-jest": "^25.1.0",
  "babel-plugin-lodash": "^3.3.4",
  "babel-plugin-module-resolver": "^4.0.0",
  "babel-plugin-transform-remove-console": "^6.9.4",
  "eslint": "^6.8.0",
  "eslint-config-prettier": "^6.10.0",
  "eslint-import-resolver-babel-module": "^5.1.2",
  "eslint-plugin-import": "^2.20.1",
  "eslint-plugin-prettier": "^3.1.2",
  "jest": "^25.1.0",
  "metro-react-native-babel-preset": "^0.58.0",
  "patch-package": "^6.2.2",
  "postinstall-postinstall": "^2.0.0",
  "prettier": "^1.19.1",
  "react-test-renderer": "^16.13.1"
}

TheWirv avatar May 06 '20 14:05 TheWirv

Can you share your code with the options you're using?

jdnichollsc avatar May 06 '20 15:05 jdnichollsc

Can you share your code with the options you're using?

I'll try..

So, on the one hand, I've built a wrapper class around the InAppBrowser for ease-of-use. Info: The Platform.setOnIosAndAndroid() is basically just an

if (Platform.OS === 'ios') {
   // return first param
} else if (Platform.OS === 'android') {
  // return second param
}

Here it goes:

import {Linking, Alert} from 'react-native';
import InAppBrowser from 'react-native-inappbrowser-reborn';
// Module registry and config
import {theme} from 'root/app-config';
// Utils
import {Platform} from '@cineorder/utils/helpers';
// Themes and styles
import {Colors} from '@cineorder/themes';

class Browser {
  constructor() {
    this.isAvailable = null;
  }

  checkForAvailability = async () => {
    this.isAvailable = await InAppBrowser.isAvailable();
  };

  openLink = async (url, auth = false, redirectUrl?, options?) => {
    try {
      let result;

      if (this.isAvailable) {
        console.log(
          `opening ${Platform.setOnIosAndAndroid(
            'Safari View Controller',
            'Chrome Custom Tab',
          )}...`,
        );

        let browserConfig = Platform.setOnIosAndAndroid(
          {
            dismissButtonStyle: auth ? 'cancel' : 'close',
            readerMode: false,
            animated: true,
            modalEnabled: true,
          },
          {
            showTitle: false,
            toolbarColor: Colors[options?.theme ?? theme].background.regular,
            secondaryToolbarColor: Colors.snow,
            enableUrlBarHiding: true,
            enableDefaultShare: false,
            forceCloseOnRedirection: false,
            animations: {
              startEnter: 'slide_in_down',
              startExit: 'slide_out_up',
              endEnter: 'slide_in_up',
              endExit: 'slide_out_down',
            },
          },
        );

        if (options) {
          browserConfig = {
            ...browserConfig,
            ...options,
          };
        }

        if (auth) {
          console.log('opening Link in auth window...');
          result = await InAppBrowser.openAuth(url, redirectUrl, browserConfig);
          console.log('result:', result);
        } else {
          console.log('opening Link in normal window...');
          result = await InAppBrowser.open(url, browserConfig);
          console.log('result:', result);
        }
      } else {
        console.log(
          `unfortunately, not able to open ${Platform.setOnIosAndAndroid(
            'Safari View Controller',
            'Chrome Custom Tab',
          )}...`,
        );

        Linking.openURL(url);
      }

      return result;
    } catch (e) {
      console.error('An error occured in InAppBrowser:', e.message);
      Alert.alert('Error occured in InAppBrowser', e.message);
    }
  };
}

export default new Browser();

Under normal circumstances, the code for the actual opening of the browser windows is written like this:

if (auth) {
  console.log('opening Link in auth window...');
  return await InAppBrowser.openAuth(url, redirectUrl, browserConfig);
}

console.log('opening Link in normal window...');
return await InAppBrowser.open(url, browserConfig);

But for debugging purposes I funneled the result into a variable and console.logged it.

Then, when calling it, I call it like this. Analogous to the aforementioned Platform.setOnIosAndAndroid(), Platform.setOnAndroid() is basically just

if (Platform.OS === 'android') {
  // return param
} else {
  return undefined
}

Here's the call:

Browser.openLink(
  props.children.url,
  false,
  null,
  Platform.setOnAndroid({
    toolbarColor: props.children.browserColor ?? Colors[theme].primary,
    theme,
  }),
);

I console.logged the calls and the options for the first three calls (which were all without auth) are as follows:

{
  animations: {
    startEnter: 'slide_in_down',
    startExit: 'slide_out_up',
    endEnter: 'slide_in_up',
    endExit: 'slide_out_down',
  },
  enableDefaultShare: false,
  enableUrlBarHiding: true,
  forceCloseOnRedirection: false,
  secondaryToolbarColor: 'white',
  showTitle: false,
  theme: /* 'light' or 'dark', depending on the calling screen */
  toolbarColor: '#28A3D9',
}

The options object for the auth call is almost identical:

{
  animations: {
    startEnter: 'slide_in_down',
    startExit: 'slide_out_up',
    endEnter: 'slide_in_up',
    endExit: 'slide_out_down',
  },
  enableDefaultShare: false,
  enableUrlBarHiding: true,
  forceCloseOnRedirection: false,
  secondaryToolbarColor: 'white',
  showTitle: false,
  theme: 'light',
  toolbarColor: '#0070BA', // PayPal blue; only difference
}

I hope this helps. If you need anything else, let me know!

TheWirv avatar May 06 '20 16:05 TheWirv

This is not an issue on iOS. I have updated the initial post.

TheWirv avatar May 06 '20 16:05 TheWirv

Did some more digging and it turns out that it doesn't have to do anything with whether open or openAuth is being called. Opening the browser only returns incredibly slowly on that one screen. Is it possible that it takes so long because that screen's component is much more complex than the other screens'? It holds a Tab View (utilizing a Native ViewPager) with approx. 10 sub-views, each consisting of a vertically scrolling FlatList, which, in turn, each consists of a number of product cards in a 2-col-layout.

TheWirv avatar May 07 '20 18:05 TheWirv

@TheWirv I don't think because this plugin don't have React components, it's only a wrapper to call a native component of the device by just calling native code, also without using EventEmitters or something like that

jdnichollsc avatar May 07 '20 18:05 jdnichollsc

@TheWirv I don't think because this plugin don't have React components, it's only a wrapper to call a native component of the device by just calling native code, also without using EventEmitters or something like that

I thought exactly the same. On the other hand, I suspected the open and openAuth functions, which are also JavaScript after all. Maybe they are taking so much time to return whereas the native component has already exited long before.

And if it's not that, what could be the reason? I could easily look a bit more into it, if you can give me some direction as to where exactly I should check.

TheWirv avatar May 07 '20 18:05 TheWirv

Do you have this configuration? https://github.com/proyecto26/react-native-inappbrowser/blob/master/example/android/app/src/main/AndroidManifest.xml#L18 android:launchMode="singleTask"

jdnichollsc avatar May 07 '20 20:05 jdnichollsc

Yes, this is my AndroidManifest.xml:

<manifest xmlns:android="http://schemas.android.com/apk/res/android"
  xmlns:tools="http://schemas.android.com/tools"
  package="com.compeso.cineorder">

  <uses-permission android:name="android.permission.INTERNET" />
  <uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
  <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
  <uses-permission android:name="android.permission.DOWNLOAD_WITHOUT_NOTIFICATION" />
  <uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />

  <application
    android:name=".MainApplication"
    android:label="@string/app_name"
    android:icon="@mipmap/ic_launcher"
    android:roundIcon="@mipmap/ic_launcher_round"
    android:allowBackup="false"
    android:theme="@style/AppTheme">
    <activity
      android:name=".SplashActivity"
      android:theme="@style/SplashTheme"
      android:label="@string/app_name">
      <intent-filter>
        <action android:name="android.intent.action.MAIN" />
        <category android:name="android.intent.category.LAUNCHER" />
      </intent-filter>
    </activity>
    <activity
      android:name=".MainActivity"
      android:launchMode="singleTask"
      android:label="@string/app_name"
      android:screenOrientation="portrait"
      android:configChanges="keyboard|keyboardHidden|orientation|screenSize|uiMode"
      android:windowSoftInputMode="stateHidden|adjustNothing"
      android:exported="true">
      <intent-filter>
        <action android:name="android.intent.action.DOWNLOAD_COMPLETE"/>
      </intent-filter>
      <intent-filter>
        <action android:name="android.intent.action.VIEW" />
        <category android:name="android.intent.category.DEFAULT" />
        <category android:name="android.intent.category.BROWSABLE" />
        <data android:scheme="cineorder" />
      </intent-filter>
    </activity>
    <activity android:name="com.facebook.react.devsupport.DevSettingsActivity" />
  <provider
    android:name="androidx.core.content.FileProvider"
    android:authorities="${applicationId}"
    android:grantUriPermissions="true"
    android:exported="false"
    tools:replace="android:authorities">
    <meta-data
      android:name="android.support.FILE_PROVIDER_PATHS"
      android:resource="@xml/passkit_file_paths"
      tools:replace="android:resource" />
  </provider>
  </application>

</manifest>

TheWirv avatar May 07 '20 20:05 TheWirv

mmm very odd, and without a demo project I can't reproduce that issue here :(

jdnichollsc avatar May 07 '20 21:05 jdnichollsc

I'll see if there's any possibility for you to reproduce this issue. This is not an open source project so unfortunately, I can't just give you the code base to play with. :/

TheWirv avatar May 07 '20 21:05 TheWirv

Can you share your code with the options you're using?

I'll try..

So, on the one hand, I've built a wrapper class around the InAppBrowser for ease-of-use. Info: The Platform.setOnIosAndAndroid() is basically just an

if (Platform.OS === 'ios') {
   // return first param
} else if (Platform.OS === 'android') {
  // return second param
}

Here it goes:

import {Linking, Alert} from 'react-native';
import InAppBrowser from 'react-native-inappbrowser-reborn';
// Module registry and config
import {theme} from 'root/app-config';
// Utils
import {Platform} from '@cineorder/utils/helpers';
// Themes and styles
import {Colors} from '@cineorder/themes';

class Browser {
  constructor() {
    this.isAvailable = null;
  }

  checkForAvailability = async () => {
    this.isAvailable = await InAppBrowser.isAvailable();
  };

  openLink = async (url, auth = false, redirectUrl?, options?) => {
    try {
      let result;

      if (this.isAvailable) {
        console.log(
          `opening ${Platform.setOnIosAndAndroid(
            'Safari View Controller',
            'Chrome Custom Tab',
          )}...`,
        );

        let browserConfig = Platform.setOnIosAndAndroid(
          {
            dismissButtonStyle: auth ? 'cancel' : 'close',
            readerMode: false,
            animated: true,
            modalEnabled: true,
          },
          {
            showTitle: false,
            toolbarColor: Colors[options?.theme ?? theme].background.regular,
            secondaryToolbarColor: Colors.snow,
            enableUrlBarHiding: true,
            enableDefaultShare: false,
            forceCloseOnRedirection: false,
            animations: {
              startEnter: 'slide_in_down',
              startExit: 'slide_out_up',
              endEnter: 'slide_in_up',
              endExit: 'slide_out_down',
            },
          },
        );

        if (options) {
          browserConfig = {
            ...browserConfig,
            ...options,
          };
        }

        if (auth) {
          console.log('opening Link in auth window...');
          result = await InAppBrowser.openAuth(url, redirectUrl, browserConfig);
          console.log('result:', result);
        } else {
          console.log('opening Link in normal window...');
          result = await InAppBrowser.open(url, browserConfig);
          console.log('result:', result);
        }
      } else {
        console.log(
          `unfortunately, not able to open ${Platform.setOnIosAndAndroid(
            'Safari View Controller',
            'Chrome Custom Tab',
          )}...`,
        );

        Linking.openURL(url);
      }

      return result;
    } catch (e) {
      console.error('An error occured in InAppBrowser:', e.message);
      Alert.alert('Error occured in InAppBrowser', e.message);
    }
  };
}

export default new Browser();

Under normal circumstances, the code for the actual opening of the browser windows is written like this:

if (auth) {
  console.log('opening Link in auth window...');
  return await InAppBrowser.openAuth(url, redirectUrl, browserConfig);
}

console.log('opening Link in normal window...');
return await InAppBrowser.open(url, browserConfig);

But for debugging purposes I funneled the result into a variable and console.logged it.

Then, when calling it, I call it like this. Analogous to the aforementioned Platform.setOnIosAndAndroid(), Platform.setOnAndroid() is basically just

if (Platform.OS === 'android') {
  // return param
} else {
  return undefined
}

Here's the call:

Browser.openLink(
  props.children.url,
  false,
  null,
  Platform.setOnAndroid({
    toolbarColor: props.children.browserColor ?? Colors[theme].primary,
    theme,
  }),
);

I console.logged the calls and the options for the first three calls (which were all without auth) are as follows:

{
  animations: {
    startEnter: 'slide_in_down',
    startExit: 'slide_out_up',
    endEnter: 'slide_in_up',
    endExit: 'slide_out_down',
  },
  enableDefaultShare: false,
  enableUrlBarHiding: true,
  forceCloseOnRedirection: false,
  secondaryToolbarColor: 'white',
  showTitle: false,
  theme: /* 'light' or 'dark', depending on the calling screen */
  toolbarColor: '#28A3D9',
}

The options object for the auth call is almost identical:

{
  animations: {
    startEnter: 'slide_in_down',
    startExit: 'slide_out_up',
    endEnter: 'slide_in_up',
    endExit: 'slide_out_down',
  },
  enableDefaultShare: false,
  enableUrlBarHiding: true,
  forceCloseOnRedirection: false,
  secondaryToolbarColor: 'white',
  showTitle: false,
  theme: 'light',
  toolbarColor: '#0070BA', // PayPal blue; only difference
}

I hope this helps. If you need anything else, let me know!

I think if (this.isAvailable) should be this if (await this.isAvailable)

akshy78695 avatar Mar 03 '22 05:03 akshy78695