Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Unable to navigate away from Auth Component when Android OS destroys Activity #360

Closed
AlteaDown opened this issue Jul 15, 2019 · 4 comments

Comments

@AlteaDown
Copy link

Issue

The library can't seem to handle cases where Activity.onDestroy() is called when authenticating. Authentication is performed in a WebView separate from the Android Activity that is running RN, so that means when we move to it, the RN Activity is put on the backstack, and Android is now free to have the Activity.onDestroy() called on the Activity.

When Activity.onDestroy() is called on the RN Activity, and once authentication succeeds, the library attempts to launch the previous activity again (but it was destroyed so it needs to be recreated). So we end up with a new Activity/Component that is completely unrelated to the authorize call we originally made.

Some time after the new activity is done loading, our original call to authorize() will get a response from the token endpoint. However, the Component is detached, and unable to handle rendering. This means that if you want to navigate to a new Component, such as the rest of your app, you can not. The only way I can imagine being able to handle this is, first being aware that this situation can arise, when it does arise, and then to actually know that the state has changed on the new Component, you would have to use a reactive structure like a RxJS Subject that will emit to the new Component to allow it to handle this case, when it occurs.

You can recreate this scenario easily by using the "Don't Keep Activities" setting in your android Device's Developer Settings, or by using a low performance/low memory Android device.

Example code

import React from 'react'
import {Button, Text} from 'native-base'
import {Image, ImageStyle, StyleSheet, View, ViewStyle} from 'react-native'
import {NavigationParams, NavigationScreenProps} from 'react-navigation'
import {authorize} from 'react-native-app-auth'
import Config from 'react-native-config'

type State = {
  showLogin: Boolean,
}

export default class LoginComponent extends React.Component<NavigationScreenProps, State> {
  constructor(props: Readonly<NavigationScreenProps<NavigationParams, State>>) {
    super(props)

    this.state = {
      showLogin: true,
    }
  }

  async authWithBackend() {
    this.setState({
      showLogin: false,
    })

    // use the client to make the auth request and receive the authState
    try {
      const authorizationResponse: any = await authorize({
        clientId: 'f3d259ddd3ed8ff3843839b',
        clientSecret: '4c7f6f8fa93d59c45502c0ae8c4a95b',
        redirectUrl: 'io.viamo.clipboard://',
        scopes: [],
        serviceConfiguration: {
          authorizationEndpoint: Config.OAUTH_ISSUER_URL,
          tokenEndpoint: Config.OAUTH_TOKEN_URL,
        },
        usePKCE: false,
      })

      this.props.navigation.navigate('MainComponent')
    } catch (error) {
      console.error(error)
    }
  }

  private loginButton() {
    if (this.state.showLogin) {
      return (
        <Button style={styles.signInButton} onPress={() => this.authWithBackend()}>
          <Text>
            Sign In
          </Text>
        </Button>
      )
    } else {
      return null
    }
  }

  render() {
    return (
      <View style={styles.root}>
        <Image source={{uri: 'viamo_logo'}} style={styles.logo}/>
        {this.loginButton()}
      </View>
    )
  }

  // noinspection JSUnusedGlobalSymbols Used by NavigationController
  static navigationOptions = {
    header: null,
  }
}

interface IStyles {
  root: ViewStyle,
  signInButton: ViewStyle,
  logo: ImageStyle;
}

const styles = StyleSheet.create<IStyles>({
  root: {
    flex: 1,
    flexDirection: 'column',
    alignItems: 'center',
    justifyContent: 'space-evenly',
  },
  signInButton: {
    alignSelf: 'center',
    paddingLeft: 32,
    paddingRight: 32,
  },
  logo: {
    width: '85%',

    // Without height undefined it won't work :( (https://stackoverflow.com/a/53482563)
    height: undefined,

    // figure out your image aspect ratio
    aspectRatio: 488 / 171,
  },
})

Environment

  • Your Identity Provider: `e.g. Custom/Viamo.io
  • Platform that you're experiencing the issue on: Android
  • Are you using Expo?: No
@aeirola
Copy link
Contributor

aeirola commented Jul 1, 2020

We are having similar issues on certain production user devices where the app just seems to restart after the authentication flow is complete, leaving the user in an unauthenticated state. Wasn't able to reproduce this myself earlier, but now setting the "Don't Keep Activities" developer option makes the bug really apparent.

This seems to be quite a large omission of the library. Not supporting completion of the authentication flow from a cold start seems to have caused quite a lot of different bug reports in the repo, but none seem to have fixed the underlying issue of handling the case where the original activity has been destroyed.

For reference, here is a screencast of the issue reproduced in the example app:
Screen Recording 2020-07-01 at 9 02 09 mov
(the second attempt succeeds by random chance since the app activity didn't have time to close before the authentication completed)

Just to make sure that this is not an issue with the underlying AppAuth-Android library, I verified that it works with their sample app:
Screen Recording 2020-07-01 at 9 33 02 mov

@aeirola
Copy link
Contributor

aeirola commented Jul 1, 2020

For some additional context about why Android kills activities see https://developer.android.com/guide/components/activities/activity-lifecycle#asem. Here is also a longer developer point of view for how to think about testing and handling killed activities https://medium.com/mindorks/hey-android-please-keep-my-activities-7bf96ccc0a38

@aeirola
Copy link
Contributor

aeirola commented Jul 1, 2020

Playing around with the example app, I noticed that the React Native JS runtime seems to persist between the separate Android activities. As @AlteaDown mentioned earlier, this means that the original promise is still resolved, but the React component handling the result is not attached anymore. But this also means that the issue could likely be worked around by using some global JavaScript variable to transfer authentication responses between separate instances of the React Native app root component.

This would of course doesn't work in cases where the whole application is closed in the background, but I guess this is a bit rarer case.

Nonetheless, it would be nice for the library to provide some support on how to best handle these situations.

@carbonrobot
Copy link
Contributor

Closing and moving discussion to #743 as that captures the issue and links to the upstream repo.

@carbonrobot carbonrobot closed this as not planned Won't fix, can't repro, duplicate, stale May 2, 2024
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

3 participants