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

Removing Endpoint From User #13572

Open julian-dotcom opened 1 month ago

julian-dotcom commented 1 month ago

Before opening, please confirm:

JavaScript Framework

React Native

Amplify APIs

Push Notifications

Amplify Version

v6

Amplify Categories

No response

Backend

None

Environment information

``` System: OS: macOS 13.6.1 CPU: (8) arm64 Apple M1 Memory: 111.48 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: 126.0.6478.127 Safari: 16.6 npmPackages: @apollo/client: ^3.7.17 => 3.10.8 @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.6 @datadog/mobile-react-navigation: ^2.3.2 => 2.3.6 @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.3 => 7.1.3 @onfido/react-native-sdk: ^12.1.0 => 12.2.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.85) @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-rc.3 @react-navigation/bottom-tabs: ^6.5.20 => 6.6.0 @react-navigation/drawer: ^6.6.15 => 6.7.0 @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.4.0 @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.6 @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.9.0 (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.4 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.52.1 react-i18next: ^11.10.0 => 11.18.6 react-native: 0.73.8 => 0.73.8 (0.74.3) 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.2 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.13.0 react-native-safe-area-context: ^4.7.1 => 4.10.7 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

How do we remove an endpoint from a user in push notifications?

Currently, to deregister a user and prevent him from receiving notification, I call this function, where optOut: 'ALL' ensures that no more notifications are sent:

      await PushNotification.identifyUser({
        userId: userAttributes?.sub || '',
        userProfile: {
               (...)
        },
        options: {
          address: deviceToken?.token,
          optOut: 'ALL',
        },
      });

The issue with this approach is that while it works, you can only have a maximum of 15 endpoints per user. And if a user signs in & out multiple times, the endpoints get cluttered, and you reach a limit very soon.

What's the best approach to remove an endpoint from a user?

Seems like I'm not the only one facing this issue:

Expected behavior

Have function that simply allows you to remove/delete an endpoint on sign out.

Reproduction steps

Documentation.

Code Snippet

// Put your code below this line.

Log output

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

aws-exports.js

No response

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

cshfang commented 1 month ago

Hi @julian-dotcom, thank you for opening this issue. We are definitely aware there are some sharp edges to using these APIs and I'll try to explain what's happening under the hood a bit.

How do we remove an endpoint from a user in push notifications?

An endpoint can be thought of as the combination of a device + notification channel. E.g., all of these would be separate endpoints:

Under the hood, what Amplify is doing on each device when you use a specific channel's APIs is creating a unique endpoint for that device + channel combination. So if you want remove an endpoint from a user, you are actually looking to associate their device + channel combination to... some other user id (e.g. '')

The issue with this approach is that while it works, you can only have a maximum of 15 endpoints per user. And if a user signs in & out multiple times, the endpoints get cluttered, and you reach a limit very soon.

As mentioned above, an endpoint should be tied to a specific device + channel combination - so as your user signs and an out (and you're calling identifyUser each time) - what should be happening is that the same endpoint is being overwritten rather than new ones being created. E.g.

  1. User signs in for the first time and uses push notifications - New endpoint ABC is created
  2. User signs out (and you call identifyUser) - Endpoint ABC is updated but not recreated
  3. User signs in again (and you call identifyUser) - Endpoint ABC is updated but not recreated

If your user is signing in on 15 different devices or reinstalling your app 15 different times - then 15 separate endpoints for your user would be created. However, this should still not be an issue because the Pinpoint service is supposed to automatically mark the least active push channel type endpoint as "inactive" - thus not counting against the 15 maximum.

Given the above, could you help us understand better how you are running into the maximum endpoints issue? I am providing some background above but I would still need to understand your particular circumstances to narrow down the source of the issue.

julian-dotcom commented 1 month ago

Wow, thank you for this explanation. I'll check in a bit.

julian-dotcom commented 1 month ago

Unfortunately, this is not working as expected.

When a user logs in, this is how I identify the endpoint:

        await PushNotification.identifyUser({
          userId: userAttributes.sub,
          userProfile: {
            customProperties: {
              locale: [userAttributes.locale],
            },
          },
          options: { address: deviceToken.token, optOut: 'NONE' },
        });

When a user logs out, this is how I attempt to deregister the endpoint:

      await PushNotification.identifyUser({
        userId: '',
        userProfile: {},
        options: {
          address: deviceToken?.token,
          optOut: 'ALL',
        },
      });

Unfortunately, this doesn't work as expected, where the following things go wrong:

  1. The userId is never wiped from the endpoint. The only thing that successfully changes is optOut from NONE to ALL
  2. When I log the user back in with the same device token, a new endpoint is created.
  3. Because a new endpoint is created every time I sign in (even with an identical token), I reach the limit of 15.

Thanks and looking forward to the response.

Samaritan1011001 commented 1 month ago

Hi @julian-dotcom, what I suspect might be happening is the wrong combination of userId, address and optOut values. Can you try the below code?

When a user logs in, identify the endpoint:

await PushNotification.identifyUser({
  userId: userAttributes.sub,
  userProfile: {
    customProperties: {
      locale: [userAttributes.locale],
    },
  },
  options: { 
      address: deviceToken.token, // make sure there is an deviceToken present here.
      optOut: 'NONE' 
  },
});

When a user logs out, attempt to deregister the endpoint:

await PushNotification.identifyUser({
  userId: userAttributes.sub,
  userProfile: {},
  options: {
    address: '',
    optOut: 'ALL',
  },
});

Basing it on this Pinpoint documentation.

julian-dotcom commented 1 month ago

@Samaritan1011001, thank you so much for the suggestion. I tried exactly as you suggested, but the following happens after logging out:

Using your above methodology, by logging in and out 15 times, I hit the threshold and can no longer create endpoints.

This really sucks, how come there is no clear way to remove an endpoint from a user?

Samaritan1011001 commented 1 month ago

@julian-dotcom sorry that did not work. Can you try this slightly different code. In my testing it removed the endpoint from the segment that had a criteria set to the customProperty value. I am assuming it means Pinpoint disassociated the endpoint from that user.

After log in ,

await identifyUser({
  userId: 'user1',
  userProfile: {customProperties: {testProperty: ['test']}},
});

After log out,

await identifyUser({
  userId: 'user1',
  userProfile: {customProperties: {testProperty: ['test']}},
  options: {address: undefined, optOut: 'ALL'},
});
julian-dotcom commented 1 month ago

@Samaritan1011001, thanks again for your time and energy.

Unfortunately, the suggested above also will not work.

  1. For the login identification you suggested, you're missing the device token / address. This has to be submitted, or else the endpoint is not specific to the device.

  2. When I try the above method for logging out, the endpoint unfortunately persists.

I also tried it like this, where I do pass the device token, but to no avail.

log in:

        await PushNotification.identifyUser({
          userId: userAttributes.sub,
          userProfile: { customProperties: { testProperty: ['test'] } },
          options: { address: deviceToken.token, optOut: 'NONE' },
        });
      }

log out:

      await PushNotification.identifyUser({
        userId: sub,
        userProfile: { customProperties: { testProperty: ['test'] } },
        options: {
          address: undefined,
          optOut: 'ALL',
        },
      });

Thanks a lot! Any ideas?

julian-dotcom commented 1 month ago

Just for my understanding, why would your above suggested method work?

In one of your comments above, you said that the way to remove an endpoint from a user is to associate the device with another user id:

So if you want remove an endpoint from a user, you are actually looking to associate their device + channel combination to... some other user id (e.g. '')

But what you suggested in your most recent comment is to use the userId in both calls.

cwomack commented 6 days ago

@julian-dotcom, this seems to be a limitation of the Pinpoint service to be able to remove a user from an endpoint. We're reaching out to the Pinpoint team to confirm if this is the case, and will provide an update as soon as we can.

cwomack commented 1 day ago

@julian-dotcom, we appreciate your patience on a response.

Unfortunately, we do not currently have an API that allows for the direct removal of a user from an endpoint. The way Pinpoint's system is designed, once a user is attached to an endpoint via the updateEndpoint operation, that endpoint will always have a user associated with it. The only workaround we can suggest is to change the userId field for the endpoint to a different ID or an empty string. This would effectively “remove” the user, but the endpoint would still retain a user association. We understand this may not be an ideal solution, and we apologize for the inconvenience. The Pinpoint team is continuously working to improve their APIs and functionality, and we appreciate you bringing this issue to our attention. Please let us know if you have any other questions or concerns.