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.39k stars 2.11k forks source link

React Native – "identifyUser" function does not work on launch #13504

Closed julian-dotcom closed 1 week ago

julian-dotcom commented 2 weeks ago

Before opening, please confirm:

JavaScript Framework

React Native

Amplify APIs

Analytics, Push Notifications

Amplify Version

v6

Amplify Categories

notifications

Backend

None

Environment information

``` # Put output below this line System: OS: macOS 13.6.1 CPU: (8) arm64 Apple M1 Memory: 48.81 MB / 16.00 GB Shell: 5.9 - /bin/zsh Binaries: Node: 20.13.1 - ~/.nvm/versions/node/v20.13.1/bin/node Yarn: 1.22.17 - /usr/local/bin/yarn npm: 10.8.0 - ~/.nvm/versions/node/v20.13.1/bin/npm pnpm: 8.15.6 - /opt/homebrew/bin/pnpm Watchman: 2024.04.01.00 - /opt/homebrew/bin/watchman Browsers: Chrome: 125.0.6422.176 Safari: 16.6 npmPackages: @apollo/client: ^3.7.17 => 3.10.4 @apollo/client/cache: undefined () @apollo/client/core: undefined () @apollo/client/dev: undefined () @apollo/client/errors: undefined () @apollo/client/link/batch: undefined () @apollo/client/link/batch-http: undefined () @apollo/client/link/context: undefined () @apollo/client/link/core: undefined () @apollo/client/link/error: undefined () @apollo/client/link/http: undefined () @apollo/client/link/persisted-queries: undefined () @apollo/client/link/remove-typename: undefined () @apollo/client/link/retry: undefined () @apollo/client/link/schema: undefined () @apollo/client/link/subscriptions: undefined () @apollo/client/link/utils: undefined () @apollo/client/link/ws: undefined () @apollo/client/react: undefined () @apollo/client/react/components: undefined () @apollo/client/react/context: undefined () @apollo/client/react/hoc: undefined () @apollo/client/react/hooks: undefined () @apollo/client/react/internal: undefined () @apollo/client/react/parser: undefined () @apollo/client/react/ssr: undefined () @apollo/client/testing: undefined () @apollo/client/testing/core: undefined () @apollo/client/testing/experimental: undefined () @apollo/client/utilities: undefined () @apollo/client/utilities/globals: undefined () @apollo/client/utilities/subscriptions/relay: undefined () @apollo/client/utilities/subscriptions/urql: undefined () @aws-amplify/react-native: ^1.1.1 => 1.1.1 @aws-amplify/rtn-push-notification: ^1.2.29 => 1.2.29 @babel/core: ^7.20.0 => 7.24.7 @babel/preset-env: ^7.20.0 => 7.24.7 @babel/runtime: ^7.20.0 => 7.24.7 @commitlint/cli: ^12.1.4 => 12.1.4 @commitlint/config-conventional: ^12.1.4 => 12.1.4 @commitlint/cz-commitlint: ^12.1.4 => 12.1.4 @datadog/mobile-react-native: ^2.3.2 => 2.3.5 @datadog/mobile-react-navigation: ^2.3.2 => 2.3.5 @eva-design/eva: 2.0.0 => 2.0.0 @graphql-codegen/add: ^4.0.0 => 4.0.1 @graphql-codegen/cli: ^3.0.0 => 3.3.1 @graphql-codegen/typescript: ^3.0.0 => 3.0.4 @graphql-codegen/typescript-operations: ^3.0.0 => 3.0.4 @graphql-codegen/typescript-react-apollo: ^3.3.7 => 3.3.7 @intercom/intercom-react-native: ^7.1.1 => 7.1.2 @onfido/react-native-sdk: ^12.1.0 => 12.1.0 @react-native-async-storage/async-storage: ^1.23.1 => 1.23.1 @react-native-clipboard/clipboard: ^1.10.0 => 1.14.1 @react-native-community/hooks: ^2.8.0 => 2.8.1 @react-native-community/push-notification-ios: ^1.10.1 => 1.11.0 @react-native-community/slider: ^4.4.2 => 4.5.2 @react-native/babel-preset: 0.73.21 => 0.73.21 (0.74.84) @react-native/eslint-config: 0.73.2 => 0.73.2 @react-native/metro-config: 0.73.5 => 0.73.5 @react-native/typescript-config: ^0.75.0-main => 0.75.0-nightly-20240612-fd618819c @react-navigation/bottom-tabs: ^6.5.20 => 6.5.20 @react-navigation/drawer: ^6.6.15 => 6.6.15 @react-navigation/elements: ^1.3.30 => 1.3.30 @react-navigation/native: ^6.1.17 => 6.1.17 @react-navigation/stack: ^6.3.29 => 6.3.29 @types/bech32: ^1.1.4 => 1.1.4 @types/bip21: ^2.0.0 => 2.0.3 @types/d3-scale: ^4.0.1 => 4.0.8 @types/d3-shape: ^3.0.2 => 3.1.6 @types/jest: ^27.4.1 => 27.5.2 @types/lodash: ^4.14.170 => 4.17.5 @types/react: ^18.2.6 => 18.3.3 @types/react-native-push-notification: ^7.3.2 => 7.3.3 @types/react-native-snap-carousel: ^3.8.3 => 3.8.11 @types/react-native-video: ^5.0.10 => 5.0.20 @types/react-test-renderer: ^18.0.0 => 18.3.0 @types/semver: ^7.3.10 => 7.5.8 @ui-kitten/components: 4.4.1 => 4.4.1 @ui-kitten/eva-icons: ^4.4.1 => 4.4.1 HelloWorld: 0.0.1 apollo-link-timeout: ^4.0.0 => 4.0.0 apollo3-cache-persist: ^0.14.1 => 0.14.1 aws-amplify: 6.3.4 => 6.3.4 aws-amplify/adapter-core: undefined () aws-amplify/analytics: undefined () aws-amplify/analytics/kinesis: undefined () aws-amplify/analytics/kinesis-firehose: undefined () aws-amplify/analytics/personalize: undefined () aws-amplify/analytics/pinpoint: undefined () aws-amplify/api: undefined () aws-amplify/api/server: undefined () aws-amplify/auth: undefined () aws-amplify/auth/cognito: undefined () aws-amplify/auth/cognito/server: undefined () aws-amplify/auth/enable-oauth-listener: undefined () aws-amplify/auth/server: undefined () aws-amplify/data: undefined () aws-amplify/data/server: undefined () aws-amplify/datastore: undefined () aws-amplify/in-app-messaging: undefined () aws-amplify/in-app-messaging/pinpoint: undefined () aws-amplify/push-notifications: undefined () aws-amplify/push-notifications/pinpoint: undefined () aws-amplify/storage: undefined () aws-amplify/storage/s3: undefined () aws-amplify/storage/s3/server: undefined () aws-amplify/storage/server: undefined () aws-amplify/utils: undefined () aws-appsync-auth-link: ^3.0.4 => 3.0.7 axios: ^0.24.0 => 0.24.0 (1.7.2) babel-jest: ^29.6.3 => 29.7.0 babel-plugin-module-resolver: ^5.0.0 => 5.0.2 babel-plugin-transform-remove-console: ^6.9.4 => 6.9.4 bech32: ^2.0.0 => 2.0.0 bignumber.js: ^9.0.2 => 9.1.2 bip21: ^2.0.3 => 2.0.3 bitcoin-address-validation: ^2.1.0 => 2.2.3 bitcoin-units: ^0.3.0 => 0.3.0 commitizen: ^4.2.4 => 4.3.0 country-iso-2-to-3: ^1.1.0 => 1.1.0 d3-scale: ^4.0.2 => 4.0.2 (2.2.2) d3-shape: ^3.0.1 => 3.2.0 date-fns: ^2.28.0 => 2.30.0 deepl-node: ^1.7.1 => 1.13.0 deprecated-react-native-prop-types: ^4.0.0 => 4.2.3 (2.3.0, 5.0.0) eslint: ^8.19.0 => 8.57.0 eslint-plugin-ft-flow: ^2.0.3 => 2.0.3 example: 0.0.1 expo: ^50.0.14 => 50.0.19 expo-barcode-scanner: 12.9.3 => 12.9.3 expo-blur: 12.9.2 => 12.9.2 expo-camera: ^14.1.3 => 14.1.3 expo-file-system: ^16.0.9 => 16.0.9 expo-local-authentication: 13.8.0 => 13.8.0 expo-sharing: 11.10.0 => 11.10.0 fast-text-encoding: ^1.0.6 => 1.0.6 fior-tracking: ^0.1.1 => 0.1.3 global: ^4.4.0 => 4.4.0 graphql: ^16.6.0 => 16.8.1 (15.8.0) graphql-tag: ^2.12.4 => 2.12.6 husky: ^6.0.0 => 6.0.0 i18next: ^20.3.0 => 20.6.1 jest: ^29.6.3 => 29.7.0 libphonenumber-js: ^1.10.54 => 1.11.3 libphonenumber-js/build: undefined () libphonenumber-js/core: undefined () libphonenumber-js/max: undefined () libphonenumber-js/max/metadata: undefined () libphonenumber-js/min: undefined () libphonenumber-js/min/metadata: undefined () libphonenumber-js/mobile: undefined () libphonenumber-js/mobile/examples: undefined () libphonenumber-js/mobile/metadata: undefined () light-bolt11-decoder: ^3.0.0 => 3.1.1 lodash: ^4.17.21 => 4.17.21 lottie-react-native: ^6.7.2 => 6.7.2 metro-minify-terser: ^0.78.1 => 0.78.1 (0.80.9) metro-react-native-babel-preset: ^0.77.0 => 0.77.0 metro-react-native-babel-transformer: ^0.77.0 => 0.77.0 patch-package: ^6.4.7 => 6.5.1 postinstall-postinstall: ^2.1.0 => 2.1.0 prettier: 2.8.8 => 2.8.8 prompt-sync: ^4.2.0 => 4.2.0 proxy-polyfill: ^0.3.2 => 0.3.2 query-string: ^7.1.1 => 7.1.3 react: 18.2.0 => 18.2.0 react-content-loader: ^6.0.3 => 6.2.1 react-content-loader/native: undefined () react-hook-form: ^7.43.1 => 7.51.5 react-i18next: ^11.10.0 => 11.18.6 react-native: 0.73.8 => 0.73.8 (0.74.2) react-native-actions-sheet: ^0.8.29 => 0.8.29 react-native-bootsplash: 4.7.5 => 4.7.5 react-native-branch: ^6.2.1 => 6.2.2 react-native-circular-progress: ^1.3.7 => 1.4.0 react-native-cli-bump-version: ^1.5.0 => 1.5.0 react-native-code-push: ^8.1.0 => 8.2.2 react-native-collapsible: ^1.6.0 => 1.6.1 react-native-config: ^1.5.1 => 1.5.1 react-native-confirmation-code-field: ^7.1.0 => 7.4.0 react-native-device-country: ^1.0.3 => 1.0.4 react-native-device-info: ^10.13.2 => 10.14.0 react-native-event-listeners: ^1.0.7 => 1.0.7 react-native-flags: ^1.0.0 => 1.0.0 react-native-gesture-handler: 2.16.0 => 2.16.0 react-native-get-random-values: ^1.11.0 => 1.11.0 react-native-google-places-autocomplete: ^2.4.1 => 2.5.6 react-native-haptic-feedback: ^1.14.0 => 1.14.0 react-native-in-app-review: ^4.3.3 => 4.3.3 react-native-inappbrowser-reborn: ^3.7.0 => 3.7.0 react-native-iphone-x-helper: ^1.3.1 => 1.3.1 react-native-keychain: ^8.1.0 => 8.2.0 react-native-modal: ^13.0.0 => 13.0.1 react-native-push-notification: ^8.1.1 => 8.1.1 react-native-reanimated: ^3.9.0 => 3.12.0 react-native-safe-area-context: ^4.7.1 => 4.10.4 react-native-screens: 3.30.1 => 3.30.1 react-native-snap-carousel: ^3.9.1 => 3.9.1 react-native-startup-time: ^2.0.0 => 2.1.0 react-native-svg: 13.9.0 => 13.9.0 react-native-svg-transformer: ^1.1.0 => 1.4.0 react-native-toast-message: ^2.2.0 => 2.2.0 react-native-video: ^5.2.1 => 5.2.1 react-native-wagmi-charts: 2.3.2 => 2.3.2 react-navigation-header-buttons: ^7.0.1 => 7.0.2 react-query: ^3.33.4 => 3.39.3 react-test-renderer: 18.2.0 => 18.2.0 recoil: ^0.7.4 => 0.7.7 semver: ^7.3.7 => 7.6.2 (6.3.1, 7.3.5, 5.7.2, 7.5.3, 7.3.2) typescript: 5.0.4 => 5.0.4 url: ^0.11.3 => 0.11.3 url-pattern: ^1.0.3 => 1.0.3 npmGlobalPackages: corepack: 0.28.0 npm: 10.8.0 ```

Describe the bug

identifyUser from aws-amplify/push-notifications never successfully saves the endpoint with the address on first launch.

It never registers the endpoint, until I close the app and restart it with the user already authenticated. On second launch, it succesfully submits the endpoint on Android, but never does on iOS.

Expected behavior

I sign in and upon sign in, the identifyUser registers the endpoint on first call, allowing me to receive notifications.

Reproduction steps

unexpected behavior arises when I call identifyUser

Code Snippet

My index.js

Amplify.configure(AWS_CONFIG);
initializePushNotifications();

How I register the device token:

import * as PushNotification from 'aws-amplify/push-notifications';
(...)

  // in my custom hook function, called after user is authenticated
  const registerDeviceToken = useCallback(async () => {
    try {
      const deviceToken = await getSavedDeviceTokenState();

      if (!deviceToken?.token) {
        return;
      }

      const userAttributes = await authService.getUserAttributes();

      if (userAttributes?.sub && userAttributes?.locale && deviceToken.token) {
        await PushNotification.identifyUser({
          userId: userAttributes.sub,
          userProfile: {
            customProperties: {
              locale: [userAttributes.locale],
            },
          },
          options: { address: deviceToken.token, optOut: 'NONE' },
        });
      }
    } catch (error) {
      console.log('Registering device token request error', error);
    }
  }, []);

identifyUser call the function in this file /node_modules/@aws-amplify/notifications/src/pushNotifications/providers/pinpoint/apis/identifyUser.native.ts where unfortunately, await getEndpointId(appId, 'Analytics') returns undefined

Log output

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

aws-exports.js

import Config from 'react-native-config';
import { ResourcesConfig } from 'aws-amplify';

export const AWS_CONFIG: ResourcesConfig = {
  API: {
    GraphQL: {
      endpoint: Config.AWS_APPSYNC_URL || '',
      region: Config.AWS_REGION || '',
      defaultAuthMode: 'userPool',
    },
  },
  Analytics: {
    Pinpoint: {
      appId: Config.AWS_PINPOINT_APP_ID || '',
      region: Config.AWS_REGION || '',
      bufferSize: 1000,
      flushInterval: 5000,
      flushSize: 100,
      resendLimit: 5,
    },
  },
  Auth: {
    Cognito: {
      identityPoolId: Config.AWS_IDENTITY_POOL_ID || '',
      userPoolClientId: Config.AWS_COGNITO_POOL_CLIENT_ID || '',
      userPoolId: Config.AWS_COGNITO_POOL_ID || '',
    },
  },
  Notifications: {
    PushNotification: {
      Pinpoint: {
        appId: Config.AWS_PINPOINT_APP_ID || '',
        region: Config.AWS_REGION || '',
      },
    },
  },
};

Manual configuration

No response

Additional configuration

No response

Mobile Device

No response

Mobile Operating System

No response

Mobile Browser

No response

Mobile Browser Version

No response

Additional information and screenshots

No response

julian-dotcom commented 2 weeks ago

As an update, if I comment out / remove this if statement if (!(await getEndpointId(appId, 'PushNotification'))) { the endpoint creation works as expected.

node_modules/@aws-amplify/notifications/src/pushNotifications/providers/pinpoint/apis/identifyUser.native.ts

export const identifyUser: IdentifyUser = async ({
    userId,
    userProfile,
    options,
}) => {
    assertIsInitialized();
    const { credentials, identityId } = await resolveCredentials();
    const { appId, region } = resolveConfig();
    const { address, optOut, userAttributes } = options ?? {};

    if (!(await getEndpointId(appId, 'PushNotification'))) {
        // if there is no cached endpoint id, wait for successful endpoint creation before continuing
        await getInflightDeviceRegistration();
    }

    await updateEndpoint({
        address,
        channelType: getChannelType(),
        optOut,
        appId,
        category: 'PushNotification',
        credentials,
        identityId,
        region,
        userAttributes,
        userId,
        userProfile,
        userAgentValue: getPushNotificationUserAgentString(
            PushNotificationAction.IdentifyUser,
        ),
    });
};

So it seems like the getInflightDeviceRegistration function fails, but it never throws an error or informs the developer what goes wrong.

julian-dotcom commented 1 week ago

As an update, I tried logging from the resolveInflightDeviceRegistration and rejectInflightDeviceRegistration but they are never called by the getInflightDeviceRegistration in node_modules/@aws-amplify/notifications/src/pushNotifications/providers/pinpoint/utils/inflightDeviceRegistration.ts

import { PushNotificationError } from '../../../errors';
import {
    InflightDeviceRegistration,
    InflightDeviceRegistrationResolver,
} from '../types';

const inflightDeviceRegistrationResolver: InflightDeviceRegistrationResolver =
    {};

let inflightDeviceRegistration: InflightDeviceRegistration = new Promise<void>(
    (resolve, reject) => {
        inflightDeviceRegistrationResolver.resolve = resolve;
        inflightDeviceRegistrationResolver.reject = reject;
    },
);

export const getInflightDeviceRegistration = () => inflightDeviceRegistration;

export const resolveInflightDeviceRegistration = () => {
    inflightDeviceRegistrationResolver.resolve?.();
    // release promise from memory
    inflightDeviceRegistration = undefined;
};

export const rejectInflightDeviceRegistration = (underlyingError: unknown) => {
    inflightDeviceRegistrationResolver.reject?.(
        new PushNotificationError({
            name: 'DeviceRegistrationFailed',
            message: 'Failed to register device for push notifications.',
            underlyingError,
        }),
    );
    // release promise from memory
    inflightDeviceRegistration = undefined;
};
julian-dotcom commented 1 week ago

I investigated some more and figured what goes wrong. To me, it looks like a bug.

When calling initializePushNotifications() in my index.js, the function calls addNativeListeners(), which calls addTokenEventListener(), which calls registerDevice().

The problem is that the register device function fails until a user is authenticated. This is fine and makes sense.

However, the issue is that the exception is not passed on to initializePushNotifications(). This function continues and calls initialize(), setting initialized to true.

This incorrect behavior because the device was never successfully registered, but the code now thinks it's initialized.

Hence, whenever calling initializePushNotifications() after, the code just exits the function early and nothing happens.

In my estimation, this should not happen. We should set initialized to true, ONLY if the device was successfully registered.

export const initializePushNotifications = (): void => {
    if (isInitialized()) {
        logger.info('Push notifications have already been enabled');
        console.log('Push notifications already initialized')
        return;
    }
    addNativeListeners();
    addAnalyticsListeners();
    initialize();
};

Source: node_modules/@aws-amplify/notifications/src/pushNotifications/providers/pinpoint/apis/initializePushNotifications.native.ts

julian-dotcom commented 1 week ago

It seems like the ticket is related to my other open issue. We should continue the conversation in this thread for simplicity.

haverchuck commented 1 week ago

@julian-dotcom per your suggestions above, closing this in favor of your other issue.

julian-dotcom commented 1 week ago

Sorry for the confusion/misunderstanding, but I think we should continue in this thread right here, and close the other one.