aws-amplify / amplify-js

A declarative JavaScript library for application development using cloud services.
https://docs.amplify.aws/lib/q/platform/js
Apache License 2.0
9.43k stars 2.13k forks source link

Cannot read property 'unsubscribe' of undefined - React native Auth.signUp() / autoSignIn #12213

Closed ChristopherGabba closed 11 months ago

ChristopherGabba commented 1 year ago

Before opening, please confirm:

JavaScript Framework

React Native

Amplify APIs

Authentication

Amplify Categories

auth

Environment information

``` TypeError: Cannot read property 'unsubscribe' of undefined, js engine: hermes ```

IMG_EAAF491341AA-1

Describe the bug

  1. I enter in credentials on the sign up screen
  2. The app navigates to the verification screen
  3. I enter in the code provided by authentication email or phone
  4. The listener properly pics up the change in autoSignIn and navigates through the authentication conditional
  5. After a second or two the app crashes.

Expected behavior

The app should not crash after running Auth.signUp() and this error should not occur.

Reproduction steps

  1. Create a react native app with a Sign Up Screen, Verification Code Screen, and a Screen on the other side of the authentication wall to navigate to.
  2. In the main AppNavigator, use a Hub.listen function to catch when a verification code is entered via the autoSign in listener.

Code Snippet

SIGN UP FUNCTION ON SIGN UP SCREEN:

await Auth.signUp({
        username,
        password,
        attributes: {
          preferred_username: username,
          birthdate: birthday,
          given_name: firstName,
          family_name: lastName,
          phone_number: phoneNumber,
        },
        autoSignIn: {
          enabled: true,
        },
      })
        .then(() => {
          navigation.navigate("Verification")
          console.log("successfully logged in! Now lets confirm...")
        })
        .catch((error) => {
          console.log("Sign in error:", error)
        })

VERIFICATION CODE SCREEN:

await Auth.confirmSignUp(username, verificationCode)
          .then((result) => {
            clearAuthenticationStore()
            console.log("successfully logged in completely with", result)
          })
          .catch((error) => {
            console.log("Invalid code:", error)
            setIsLoading(false)
          })
      }

AUTHENTICATION LISTENER CUSTOM HOOK ON MAIN APP NAVIGATOR

import { useEffect, useState } from "react"
import { Hub } from "aws-amplify"
import { useStores } from "app/models"
/**
 * This is a custom hook to look for:
 * 1. Existing auth state
 * 2. Future changes in the auth state
 */

export function useAWSAuthentication() {
  const [isAuthenticated, setIsAuthenticated] = useState(false);

  useEffect(() => {
    const authSubscription = Hub.listen("auth", (data) => {
      const authChange = data?.payload?.event
      switch (authChange) {
        case "autoSignIn":
          console.log("User entered in correct verification code")
          setIsAuthenticated(true);
          break
        case "signIn":
          console.log("user signed in normally")
          setIsAuthenticated(true)
          break
        case "signOut":
          console.log("user signed out")
          setIsAuthenticated(false)
          break
      }
    })

    return authSubscription()
  }, [])

  return { isAuthenticated }
}

Log output

``` // Put your logs below this line ```

aws-exports.js

/* eslint-disable */
// WARNING: DO NOT EDIT. This file is automatically generated by AWS Amplify. It will be overwritten.

const awsmobile = {
    "aws_project_region": "us-east-1",
    "aws_cognito_identity_pool_id": "us-east-1:20e31dce-9d56-4d84-b1c3-8c683ebd8387",
    "aws_cognito_region": "us-east-1",
    "aws_user_pools_id": "us-east-1_MDvyzyZi8",
    "aws_user_pools_web_client_id": "5mprjt5obds1vihis674lmtfh2",
    "oauth": {},
    "aws_cognito_username_attributes": [],
    "aws_cognito_social_providers": [],
    "aws_cognito_signup_attributes": [
        "BIRTHDATE",
        "GIVEN_NAME",
        "FAMILY_NAME",
        "PREFERRED_USERNAME",
        "PHONE_NUMBER"
    ],
    "aws_cognito_mfa_configuration": "OFF",
    "aws_cognito_mfa_types": [
        "SMS"
    ],
    "aws_cognito_password_protection_settings": {
        "passwordPolicyMinLength": 8,
        "passwordPolicyCharacters": [
            "REQUIRES_LOWERCASE",
            "REQUIRES_NUMBERS",
            "REQUIRES_SYMBOLS",
            "REQUIRES_UPPERCASE"
        ]
    },
    "aws_cognito_verification_mechanisms": [
        "PHONE_NUMBER"
    ]
};

export default awsmobile;

Manual configuration

{
  "aws_project_region": "us-east-1",
  "aws_cognito_identity_pool_id": "us-east-1:20e31dce-9d56-4d84-b1c3-8c683ebd8387",
  "aws_cognito_region": "us-east-1",
  "aws_user_pools_id": "us-east-1_MDvyzyZi8",
  "aws_user_pools_web_client_id": "5mprjt5obds1vihis674lmtfh2",
  "oauth": {},
  "aws_cognito_username_attributes": [],
  "aws_cognito_social_providers": [],
  "aws_cognito_signup_attributes": [
    "BIRTHDATE",
    "GIVEN_NAME",
    "FAMILY_NAME",
    "PREFERRED_USERNAME",
    "PHONE_NUMBER"
  ],
  "aws_cognito_mfa_configuration": "OFF",
  "aws_cognito_mfa_types": [
    "SMS"
  ],
  "aws_cognito_password_protection_settings": {
    "passwordPolicyMinLength": 8,
    "passwordPolicyCharacters": [
      "REQUIRES_LOWERCASE",
      "REQUIRES_NUMBERS",
      "REQUIRES_SYMBOLS",
      "REQUIRES_UPPERCASE"
    ]
  },
  "aws_cognito_verification_mechanisms": [
    "PHONE_NUMBER"
  ]
}

Additional configuration

No response

Mobile Device

iPhone12

Mobile Operating System

iOS 16.6.1

Mobile Browser

Safari

Mobile Browser Version

No response

Additional information and screenshots

I'm using an expo development build.

nadetastic commented 1 year ago

Hi @ChristopherGabba, thank you for opening this issue. It looks like you are facing this error when performing signup with auto signin - which seems to be successful, however you get an unsubscribe error. Due to this I think the root of the issue will be somewhere in the useEffect + Hub setup.

Taking a look at the code you shared one thing I noticed is how the Hub.listener is getting cleaned up in your useEffect. Instead of invoking it directly, could you wrap it in a function?

Ex:

// Do this
return () => {
    authSubscription()
}

// Instead of 
return authSubscription() 
ChristopherGabba commented 1 year ago

@nadetastic Thanks for the response Dan. I've tried every combination of the useEffect (cleanup functions, no cleanup functions, different types, etc. )

I've also tried:

// deprecated versions
return () => Hub.remove()
return () => {
    Hub.remove()
}
return () => authSubscription()
return () => {
    authSubscription()
}

The cleanup you proposed above produced a slightly different stack trace but same overall error:

IMG_999BBE592059-1

I spent about 5 hours trying every combination I could think of and the only thing I can determine is that I never get the error if I don't the Hub.listen() function. If I set the token directly without the listener I never get the error. As soon as I uncomment the Hub.listen() and try again, I get this error.

ChristopherGabba commented 1 year ago

Just got a slightly different stack trace on the same code attempt. image

nadetastic commented 1 year ago

@ChristopherGabba Could you verify the version of aws-amplify and react-native (or share your package.json) so I can try and reproduce this issue? I tried this out with the latest version of aws-amplify and a React Web App and don't see any errors, so im curios what could be the issue with React Native+Hub. Also are you able to confirm if this affects Android as well?

ChristopherGabba commented 1 year ago

@nadetastic

// package.json
{
  "name": "reel-feel",
  "version": "0.0.1",
  "private": true,
  "main": "node_modules/expo/AppEntry.js",
  "scripts": {
    "start": "react-native start",
    "ios": "react-native run-ios",
    "android": "react-native run-android --active-arch-only",
    "test:detox": "detox test -c ios.sim.debug",
    "build:detox": "detox build -c ios.sim.debug",
    "ci:test:detox": "detox test -c ios.sim.release -l verbose --cleanup",
    "ci:build:detox": "detox build -c ios.sim.release",
    "compile": "tsc --noEmit -p . --pretty",
    "format": "prettier --write \"app/**/*.{js,jsx,json,md,ts,tsx}\"",
    "lint": "eslint index.js App.js app test --fix --ext .js,.ts,.tsx && npm run format",
    "patch": "patch-package",
    "test": "jest",
    "test:watch": "jest --watch",
    "adb": "adb reverse tcp:9090 tcp:9090 && adb reverse tcp:3000 tcp:3000 && adb reverse tcp:9001 tcp:9001 && adb reverse tcp:8081 tcp:8081",
    "postinstall": "node ./bin/postInstall",
    "bundle:ios": "react-native bundle --entry-file index.js --platform ios --dev false --bundle-output ios/main.jsbundle --assets-dest ios",
    "bundle:android": "react-native bundle --platform android --dev false --entry-file index.js --bundle-output android/app/src/main/assets/index.android.bundle --assets-dest android/app/src/main/res",
    "release:ios": "echo 'Not implemented yet: release:ios. Use Xcode. More info: https://reactnative.dev/docs/next/publishing-to-app-store'",
    "release:android": "cd android && rm -rf app/src/main/res/drawable-* && ./gradlew assembleRelease && cd - && echo 'APK generated in ./android/app/build/outputs/apk/release/app-release.apk'",
    "clean": "npx react-native-clean-project",
    "clean-all": "npx react-native clean-project-auto",
    "expo:start": "expo start",
    "expo:android": "expo start --android",
    "expo:ios": "expo start --ios",
    "expo:web": "expo start --web",
    "expo:build:detox": "detox build -c ios.sim.expo",
    "expo:test:detox": "./bin/downloadExpoApp.sh && detox test --configuration ios.sim.expo"
  },
  "overrides": {
    "react-native": "0.72.5",
    "react-error-overlay": "6.0.9"
  },
  "dependencies": {
    "@aws-amplify/ui-react-native": "^1.2.28",
    "@expo-google-fonts/m-plus-1p": "^0.2.3",
    "@expo-google-fonts/montserrat": "^0.2.3",
    "@expo-google-fonts/space-grotesk": "^0.2.2",
    "@expo/config-plugins": "^7.2.5",
    "@expo/metro-config": "^0.10.7",
    "@gorhom/bottom-sheet": "^4.4.7",
    "@likashefqet/react-native-image-zoom": "^2.1.1",
    "@react-native-async-storage/async-storage": "1.18.2",
    "@react-native-clipboard/clipboard": "^1.11.2",
    "@react-native-community/netinfo": "9.3.10",
    "@react-navigation/bottom-tabs": "^6.3.2",
    "@react-navigation/native": "^6.0.2",
    "@react-navigation/native-stack": "^6.0.2",
    "@shopify/flash-list": "1.4.3",
    "amazon-cognito-identity-js": "^6.3.6",
    "apisauce": "2.1.5",
    "aws-amplify": "^5.3.11",
    "axios": "^1.5.0",
    "cheerio": "^1.0.0-rc.12",
    "date-fns": "^2.29.2",
    "expo": "^49.0.7",
    "expo-application": "~5.3.0",
    "expo-av": "~13.4.1",
    "expo-blur": "~12.4.1",
    "expo-clipboard": "~4.3.1",
    "expo-config-plugin-ios-share-extension": "^0.0.4",
    "expo-constants": "~14.4.2",
    "expo-contacts": "~12.2.0",
    "expo-dev-client": "~2.4.11",
    "expo-device": "~5.4.0",
    "expo-file-system": "~15.4.4",
    "expo-font": "~11.4.0",
    "expo-image-picker": "~14.3.2",
    "expo-linear-gradient": "~12.3.0",
    "expo-linking": "~5.0.2",
    "expo-localization": "~14.3.0",
    "expo-media-library": "~15.4.1",
    "expo-splash-screen": "^0.20.5",
    "expo-status-bar": "~1.6.0",
    "expo-video-thumbnails": "~7.4.0",
    "firebase": "^10.3.1",
    "i18n-js": "3.9.2",
    "jsdom": "^22.1.0",
    "jsdom-jscore-rn": "^0.1.8",
    "lodash.filter": "^4.6.0",
    "mobx": "6.6.0",
    "mobx-react-lite": "3.4.0",
    "mobx-state-tree": "5.1.5",
    "react": "18.2.0",
    "react-native": "0.72.5",
    "react-native-animated-pagination-dots": "^0.1.73",
    "react-native-bootsplash": "^5.0.2",
    "react-native-element-dropdown": "^2.9.0",
    "react-native-fast-image": "^8.6.3",
    "react-native-fs": "^2.20.0",
    "react-native-gesture-handler": "~2.12.0",
    "react-native-get-random-values": "^1.9.0",
    "react-native-pager-view": "6.2.0",
    "react-native-reanimated": "~3.3.0",
    "react-native-receive-sharing-intent": "^2.0.0",
    "react-native-render-html": "^6.3.4",
    "react-native-safe-area-context": "4.6.3",
    "react-native-screens": "~3.22.0",
    "react-native-share-menu": "^6.0.0",
    "react-native-touchable-scale": "^2.2.0",
    "react-native-url-polyfill": "^2.0.0",
    "react-native-vision-camera": "^3.0.0",
    "react-native-volume-manager": "^1.10.0",
    "react-native-webview": "13.2.2",
    "react-native-youtube-iframe": "^2.3.0",
    "reactotron-mst": "3.1.4",
    "reactotron-react-js": "^3.3.7",
    "reactotron-react-native": "5.0.3",
    "typescript": "^4.9.4"
  },
  "devDependencies": {
    "@babel/core": "^7.20.0",
    "@babel/plugin-proposal-export-namespace-from": "^7.18.9",
    "@babel/preset-env": "^7.20.0",
    "@babel/runtime": "^7.20.0",
    "@react-native-community/cli-platform-ios": "^8.0.2",
    "@rnx-kit/metro-config": "^1.3.5",
    "@rnx-kit/metro-resolver-symlinks": "^0.1.26",
    "@types/i18n-js": "3.8.2",
    "@types/jest": "^29.2.1",
    "@types/react": "~18.2.14",
    "@types/react-test-renderer": "^18.0.0",
    "@typescript-eslint/eslint-plugin": "^5.59.0",
    "@typescript-eslint/parser": "^5.59.0",
    "babel-jest": "^29.2.1",
    "babel-loader": "8.2.5",
    "babel-plugin-root-import": "^6.6.0",
    "eslint": "8.17.0",
    "eslint-config-prettier": "8.5.0",
    "eslint-config-standard": "17.0.0",
    "eslint-plugin-import": "2.26.0",
    "eslint-plugin-n": "^15.0.0",
    "eslint-plugin-node": "11.1.0",
    "eslint-plugin-promise": "6.0.0",
    "eslint-plugin-react": "7.30.0",
    "eslint-plugin-react-native": "4.0.0",
    "fbjs-scripts": "3.0.1",
    "jest": "^29.2.1",
    "jest-circus": "29",
    "jest-environment-node": "29",
    "jest-expo": "^49.0.0",
    "metro-config": "0.75.1",
    "metro-react-native-babel-preset": "0.75.1",
    "metro-source-map": "0.75.1",
    "mocha": "^10.2.0",
    "patch-package": "^6.4.7",
    "postinstall-prepare": "1.0.1",
    "prettier": "2.8.8",
    "query-string": "^7.0.1",
    "react-devtools-core": "4.24.7",
    "react-dom": "18.2.0",
    "react-native-web": "~0.19.6",
    "react-test-renderer": "18.2.0",
    "reactotron-core-client": "^2.8.10",
    "regenerator-runtime": "^0.13.4",
    "ts-jest": "29",
    "typescript": "^5.1.3"
  },
  "resolutions": {
    "@types/react": "^18",
    "@types/react-dom": "^18"
  },
  "engines": {
    "node": ">=18"
  },
  "prettier": {
    "printWidth": 100,
    "semi": false,
    "singleQuote": false,
    "trailingComma": "all"
  },
  "eslintConfig": {
    "root": true,
    "parser": "@typescript-eslint/parser",
    "extends": [
      "plugin:@typescript-eslint/recommended",
      "plugin:react/recommended",
      "plugin:react-native/all",
      "standard",
      "prettier"
    ],
    "plugins": [
      "@typescript-eslint",
      "react",
      "react-native"
    ],
    "parserOptions": {
      "ecmaFeatures": {
        "jsx": true
      },
      "project": "./tsconfig.json"
    },
    "settings": {
      "react": {
        "pragma": "React",
        "version": "detect"
      }
    },
    "globals": {
      "__DEV__": false,
      "jasmine": false,
      "beforeAll": false,
      "afterAll": false,
      "beforeEach": false,
      "afterEach": false,
      "test": false,
      "expect": false,
      "describe": false,
      "jest": false,
      "it": false
    },
    "rules": {
      "@typescript-eslint/ban-ts-ignore": 0,
      "@typescript-eslint/ban-ts-comment": 0,
      "@typescript-eslint/explicit-function-return-type": 0,
      "@typescript-eslint/explicit-member-accessibility": 0,
      "@typescript-eslint/explicit-module-boundary-types": 0,
      "@typescript-eslint/indent": 0,
      "@typescript-eslint/member-delimiter-style": 0,
      "@typescript-eslint/no-empty-interface": 0,
      "@typescript-eslint/no-explicit-any": 0,
      "@typescript-eslint/no-object-literal-type-assertion": 0,
      "@typescript-eslint/no-var-requires": 0,
      "@typescript-eslint/no-unused-vars": [
        "error",
        {
          "argsIgnorePattern": "^_",
          "varsIgnorePattern": "^_"
        }
      ],
      "comma-dangle": 0,
      "multiline-ternary": 0,
      "no-undef": 0,
      "no-unused-vars": 0,
      "no-use-before-define": 0,
      "no-global-assign": 0,
      "quotes": 0,
      "react-native/no-raw-text": 0,
      "react/no-unescaped-entities": 0,
      "react/prop-types": 0,
      "space-before-function-paren": 0
    }
  }
}
ChristopherGabba commented 1 year ago

I am using a dev client on a handheld device, not a simulator.

I do not own an android phone to try, but my brother does. I will try to test it out at the next opportunity I see him.

ChristopherGabba commented 1 year ago

Update. When I expand the error stack trace, I see it appears to be coming from: node_modules/xstate/lib/waitFor.js

'use strict';

Object.defineProperty(exports, '__esModule', { value: true });

var _tslib = require('./_virtual/_tslib.js');

var defaultWaitForOptions = {
  timeout: 10000 // 10 seconds

};
/**^M
 * Subscribes to an actor ref and waits for its emitted value to satisfy^M
 * a predicate, and then resolves with that value.^M
 * Will throw if the desired state is not reached after a timeout^M
 * (defaults to 10 seconds).^M
 *^M
 * @example^M
 * ```js^M
 * const state = await waitFor(someService, state => {^M
 *   return state.hasTag('loaded');^M
 * });^M
 *^M
 * state.hasTag('loaded'); // true^M
 * ```^M
 *^M
 * @param actorRef The actor ref to subscribe to^M
 * @param predicate Determines if a value matches the condition to wait for^M
 * @param options^M
 * @returns A promise that eventually resolves to the emitted value^M
 * that matches the condition^M
 */

function waitFor(actorRef, predicate, options) {
  var resolvedOptions = _tslib.__assign(_tslib.__assign({}, defaultWaitForOptions), options);

  return new Promise(function (res, rej) {
    var done = false;

    if (process.env.NODE_ENV !== 'production' && resolvedOptions.timeout < 0) {
      console.error('`timeout` passed to `waitFor` is negative and it will reject its internal promise immediately.');
    }
var handle = resolvedOptions.timeout === Infinity ? undefined : setTimeout(function () {
      sub.unsubscribe();
      rej(new Error("Timeout of ".concat(resolvedOptions.timeout, " ms exceeded")));
    }, resolvedOptions.timeout);

    var dispose = function () {
      clearTimeout(handle);
      done = true;
      sub === null || sub === void 0 ? void 0 : sub.unsubscribe();
    };

    var sub = actorRef.subscribe({
      next: function (emitted) {
        if (predicate(emitted)) {
          dispose();
          res(emitted);
        }
      },
      error: function (err) {
        dispose();
        rej(err);
      },
      complete: function () {
        dispose();
        rej(new Error("Actor terminated without satisfying predicate"));
      }
    });

    if (done) {
      sub.unsubscribe();
    }
  });
}

exports.waitFor = waitFor;
nadetastic commented 1 year ago

@ChristopherGabba thanks for sharing the above. From the looks of it, this may potentially be originating from the @aws-amplify/ui-react-native package (which has a dependency to xstate) and not the aws-amplify JavaScript package as this doesn't have this dependency.

Can you clarify how you are using the ui-react-native package?

ChristopherGabba commented 1 year ago

@nadetastic Interesting. I am only using it here to wrap my app:

import { Authenticator } from "@aws-amplify/ui-react-native"

function App(props: AppProps) {
/ * more code for app function */ 
return (
    <Authenticator.Provider>
      <SafeAreaProvider initialMetrics={initialWindowMetrics}>
        <ErrorBoundary catchErrors={Config.catchErrors}>
          <GestureHandlerRootView style={$rootView}>
            <AppNavigator
              linking={linking}
              initialState={initialNavigationState}
              onStateChange={onNavigationStateChange}
            />
          </GestureHandlerRootView>
        </ErrorBoundary>
      </SafeAreaProvider>
    </Authenticator.Provider>
)
}

Do I really need to wrap this with the Authentication provider if I'm doing the Auth custom from the separate aws amplify library?

import { Auth, Hub } from "aws-amplify"

I guess I probably don't...

nadetastic commented 1 year ago

@ChristopherGabba if you are using the useAuthenticator hook, then yes, you would need to have the Provider in you app to wrap the hook. Otherwise, you may not need to use the Provider as it is specific to the ui library. Have you been able to get unblocked on this?

nadetastic commented 11 months ago

Hi @ChristopherGabba I'm going to mark this issue as closed now, but do let me know if you have any questions.

kanikagarg7 commented 7 months ago

Hi @nadetastic We are getting below issue after using Auth.confirmsignup(). This confirmSignup is getting successful after 5 sec getting this error

waitFor.js:44 Uncaught TypeError: Cannot read properties of undefined (reading 'unsubscribe') at waitFor.js:44:1