From 55dbe9cddb1c123c0a4f1b7dee1c3f0c31451a89 Mon Sep 17 00:00:00 2001 From: Ekjot <43255916+ekjotmultani@users.noreply.github.com> Date: Fri, 31 Jul 2026 15:18:13 -0700 Subject: [PATCH 1/3] docs: expand Customer Profiles push notifications to Android, Flutter, and Swift The standalone Connect clients are released in amplify-android (aws-connect), amplify-flutter (amplify_connect_client), and amplify-swift (AmplifyConnectClient). Expand the five client pages that apply to them from platforms: ['react-native'] to the four-platform set, with per-platform install, client creation, and identifyUser / registerDevice / removeDevice snippets and error handling. The four React Native push-handling pages (permissions, device token, notification interaction, badge count) stay React Native only: the standalone clients register tokens with the backend but do not handle notification display. Verified with jest (305 passing), cspell (clean), and a full production build: all four platforms prerender the customer-profiles routes, and every internal link target builds on the three new platforms. --- .../guest-and-authenticated-users/index.mdx | 96 ++++++-- .../customer-profiles/identify-user/index.mdx | 156 ++++++++++++- .../customer-profiles/index.mdx | 211 +++++++++++++++++- .../register-device/index.mdx | 180 ++++++++++++++- .../customer-profiles/remove-device/index.mdx | 113 +++++++++- 5 files changed, 725 insertions(+), 31 deletions(-) diff --git a/src/pages/[platform]/frontend/push-notifications/customer-profiles/guest-and-authenticated-users/index.mdx b/src/pages/[platform]/frontend/push-notifications/customer-profiles/guest-and-authenticated-users/index.mdx index 804e682654b..0280e48d051 100644 --- a/src/pages/[platform]/frontend/push-notifications/customer-profiles/guest-and-authenticated-users/index.mdx +++ b/src/pages/[platform]/frontend/push-notifications/customer-profiles/guest-and-authenticated-users/index.mdx @@ -4,7 +4,7 @@ export const meta = { title: 'Guest and authenticated users', description: 'How Amazon Connect Customer Profiles notifications work for guest and signed-in users, and how a device moves to the authenticated identity on sign-in.', - platforms: ['react-native'] + platforms: ['android', 'flutter', 'react-native', 'swift'] }; export const getStaticPaths = async () => { @@ -43,6 +43,8 @@ A guest identity is scoped to the installation, not to a person. Treat a guest p A guest user's push token and a signed-in user's push token are the same value, because the native platform issues the token to the installation rather than to a user. Without any handling, a device registered as a guest would stay attached to the guest identity after the user signed in, and journeys targeting the authenticated profile would not reach it. + + The library handles this for you. After `initializePushNotifications` has been called, it listens for the sign-in event and re-registers the device, which moves the existing registration from the guest identity to the authenticated one. Because registration is an idempotent operation keyed on a device identifier that is stable for the installation, this updates the existing record rather than creating a second one. No application code is required for this to happen: @@ -60,10 +62,58 @@ Re-registration on sign-in is a best-effort operation. A failure is logged and d If no push token has been issued yet when a user signs in, there is nothing to move. The first registration then happens against the authenticated identity once the native platform issues the token. + + + + +Call `registerDevice` again after a user signs in to move the registration. The call is signed with the authenticated credentials, and because registration is an idempotent operation keyed on a device identifier that is stable for the installation, it moves the existing record to the authenticated identity rather than creating a second one. + + + + + +```kotlin +// After sign-in completes: +val token = FirebaseMessaging.getInstance().token.await() +client.registerDevice(token) +``` + + + + + +```dart +// After sign-in completes: +final token = await FirebaseMessaging.instance.getToken(); +if (token != null) { + await client.registerDevice(token: token); +} +``` + + + + + +```swift +// After sign-in completes, with the APNs token you received +// in didRegisterForRemoteNotificationsWithDeviceToken: +try await client.registerDevice(token: token) +``` + + + + + +If no push token has been issued yet when a user signs in, there is nothing to move. The first registration then happens against the authenticated identity once the platform issues the token. + + + ## Sign a user out Signing a user out does not de-register their device. Call `removeDevice` while the user is still signed in, because de-registration is signed with their credentials: + + ```ts import { signOut } from 'aws-amplify/auth'; import { removeDevice } from 'aws-amplify/push-notifications/customer-profiles'; @@ -72,23 +122,39 @@ await removeDevice(); await signOut(); ``` -After `signOut` completes, subsequent calls are signed as a new guest identity. See [Remove a device](/[platform]/frontend/push-notifications/customer-profiles/remove-device/). + -## Profile information for guests + -`identifyUser` works for guest users, so you can store profile details before a user creates an account. Those details are written to the guest profile. +```kotlin +client.removeDevice() +Amplify.Auth.signOut { /* handle sign-out result */ } +``` -A guest profile and an authenticated profile are separate profiles, because they have different `principalId` values. Profile information is not copied between them when a user signs in, so call `identifyUser` after sign-in to populate the authenticated profile with the details you hold: + -```ts -import { identifyUser } from 'aws-amplify/push-notifications/customer-profiles'; - -await identifyUser({ - userProfile: { - email: 'jane@example.com', - name: 'Jane Doe' - } -}); + + +```dart +await client.removeDevice(); +await Amplify.Auth.signOut(); +``` + + + + + +```swift +try await client.removeDevice() +_ = await Amplify.Auth.signOut() ``` -See [Identify a user](/[platform]/frontend/push-notifications/customer-profiles/identify-user/). + + +After sign-out completes, subsequent calls are signed as a new guest identity. See [Remove a device](/[platform]/frontend/push-notifications/customer-profiles/remove-device/). + +## Profile information for guests + +`identifyUser` works for guest users, so you can store profile details before a user creates an account. Those details are written to the guest profile. + +A guest profile and an authenticated profile are separate profiles, because they have different `principalId` values. Profile information is not copied between them when a user signs in, so call `identifyUser` after sign-in to populate the authenticated profile with the details you hold. See [Identify a user](/[platform]/frontend/push-notifications/customer-profiles/identify-user/). diff --git a/src/pages/[platform]/frontend/push-notifications/customer-profiles/identify-user/index.mdx b/src/pages/[platform]/frontend/push-notifications/customer-profiles/identify-user/index.mdx index b0398030c18..bd859497d40 100644 --- a/src/pages/[platform]/frontend/push-notifications/customer-profiles/identify-user/index.mdx +++ b/src/pages/[platform]/frontend/push-notifications/customer-profiles/identify-user/index.mdx @@ -4,7 +4,7 @@ export const meta = { title: 'Identify a user', description: 'Send profile information for the current user to Amazon Connect Customer Profiles with the identifyUser API.', - platforms: ['react-native'] + platforms: ['android', 'flutter', 'react-native', 'swift'] }; export const getStaticPaths = async () => { @@ -22,6 +22,8 @@ export function getStaticProps(context) { Use `identifyUser` to send profile information for the current user to Amazon Connect Customer Profiles. The values you send populate the Customer Profile that your Amazon Connect journeys target and personalize messages with. + + ```ts import { identifyUser } from 'aws-amplify/push-notifications/customer-profiles'; @@ -44,6 +46,87 @@ await identifyUser({ }); ``` + + + + +```kotlin +import com.amplifyframework.connect.UserProfile +import com.amplifyframework.connect.UserProfileLocation + +val result = client.identifyUser( + UserProfile( + email = "jane@example.com", + name = "Jane Doe", + phone = "+15551234567", + location = UserProfileLocation( + city = "Seattle", + country = "US", + postalCode = "98101", + region = "WA" + ), + customAttributes = mapOf( + "plan" to "premium", + "favoriteCategory" to "outdoors" + ) + ) +) +``` + + + + + +```dart +import 'package:amplify_connect_client/amplify_connect_client.dart'; + +await client.identifyUser( + userProfile: const UserProfile( + email: 'jane@example.com', + name: 'Jane Doe', + phone: '+15551234567', + location: Location( + city: 'Seattle', + country: 'US', + postalCode: '98101', + region: 'WA', + ), + customAttributes: { + 'plan': 'premium', + 'favoriteCategory': 'outdoors', + }, + ), +); +``` + + + + + +```swift +import AmplifyConnectClient + +try await client.identifyUser( + userProfile: UserProfile( + email: "jane@example.com", + name: "Jane Doe", + phone: "+15551234567", + customAttributes: [ + "plan": "premium", + "favoriteCategory": "outdoors" + ], + location: UserProfileLocation( + city: "Seattle", + country: "US", + postalCode: "98101", + region: "WA" + ) + ) +) +``` + + + Every field is optional, so send only the values your application has. Each call replaces the fields you provide on the profile. ## User profile fields @@ -54,9 +137,9 @@ Every field is optional, so send only the values your application has. Each call | `name` | `string` | The user's name. | | `phone` | `string` | The user's phone number. | | `location` | `object` | The user's location. Accepts `city`, `country`, `postalCode`, and `region`, each a `string`. | -| `customAttributes` | `Record` | Additional key-value pairs to store on the profile. | +| `customAttributes` | `map` | Additional string key-value pairs to store on the profile. | -Values are validated before the request is sent. Every string, along with each `customAttributes` key and value, must be 255 characters or fewer, and `customAttributes` values must be strings. A profile that violates these bounds throws a validation error. +Values are validated before the request is sent. Every string, along with each `customAttributes` key and value, must be 255 characters or fewer. A profile that violates these bounds produces a validation error. @@ -79,18 +162,24 @@ Call `identifyUser` when the profile information you hold changes, for example: `identifyUser` sends profile information only and performs no device work. To manage push devices, use [`registerDevice`](/[platform]/frontend/push-notifications/customer-profiles/register-device/) and [`removeDevice`](/[platform]/frontend/push-notifications/customer-profiles/remove-device/). + + `identifyUser` does not require `initializePushNotifications`. Its only prerequisites are a configured endpoint and identity pool credentials, so you can call it before push notifications are initialized. + + ## Personalize messages with profile values Message templates reference profile values with the `Attributes` namespace, so a `customAttributes` entry named `favoriteCategory` is available to a template as `{{Attributes.favoriteCategory}}`. See [Author message templates](/[platform]/build-a-backend/add-aws-services/notifications/author-message-templates/). ## Handle errors + + `identifyUser` returns a promise that rejects when validation fails or the endpoint returns an error, so handle failures where a rejected promise would otherwise go unobserved: ```ts @@ -102,3 +191,64 @@ try { console.error('Failed to identify user', error); } ``` + + + + + +`identifyUser` returns a `Result` with an `AmplifyConnectException` failure type, so no call throws. Inspect the result to handle failures: + +```kotlin +import com.amplifyframework.connect.ConnectValidationException +import com.amplifyframework.foundation.result.Result + +when (val result = client.identifyUser(UserProfile(email = "jane@example.com"))) { + is Result.Success -> { /* profile sent */ } + is Result.Failure -> when (result.error) { + is ConnectValidationException -> { /* a value exceeded the bounds */ } + else -> { /* network, credentials, or service error */ } + } +} +``` + + + + + +`identifyUser` throws a `ConnectClientException` subtype when validation fails or the endpoint returns an error: + +```dart +try { + await client.identifyUser( + userProfile: const UserProfile(email: 'jane@example.com'), + ); +} on ConnectValidationException { + // A value exceeded the bounds. +} on ConnectClientException catch (e) { + // Network, credentials, or service error. + safePrint('Failed to identify user: $e'); +} +``` + + + + + +`identifyUser` throws a `ConnectError` when validation fails or the endpoint returns an error: + +```swift +do { + try await client.identifyUser( + userProfile: UserProfile(email: "jane@example.com") + ) +} catch let error as ConnectError { + switch error { + case .validation(let description, _, _): + print("Validation error: \(description)") + default: + print("Failed to identify user: \(error)") + } +} +``` + + diff --git a/src/pages/[platform]/frontend/push-notifications/customer-profiles/index.mdx b/src/pages/[platform]/frontend/push-notifications/customer-profiles/index.mdx index eb6eeca7eb9..ac23b3f74e0 100644 --- a/src/pages/[platform]/frontend/push-notifications/customer-profiles/index.mdx +++ b/src/pages/[platform]/frontend/push-notifications/customer-profiles/index.mdx @@ -6,7 +6,7 @@ export const meta = { description: 'Send profile information and register push devices with Amazon Connect Customer Profiles from your application.', route: '/[platform]/frontend/push-notifications/customer-profiles', - platforms: ['react-native'] + platforms: ['android', 'flutter', 'react-native', 'swift'] }; export const getStaticPaths = async () => { @@ -26,8 +26,30 @@ export function getStaticProps(context) { The Amazon Connect Customer Profiles APIs let your application send profile information for the current user and manage that person's registered push devices. Devices are stored separately from the Customer Profile, in a DynamoDB device store, associated by `principalId`, rather than embedded inside the profile. Requests are signed with [Signature Version 4](https://docs.aws.amazon.com/IAM/latest/UserGuide/reference_sigv.html) using Amazon Cognito identity pool credentials, and your backend derives the profile identity from the signed request. + + These APIs are exported from the `aws-amplify/push-notifications/customer-profiles` sub-path. + + + + +These APIs are provided by `AmplifyConnectClient`, a standalone client in the `aws-connect` library. It exposes three methods: `identifyUser`, `registerDevice`, and `removeDevice`. + + + + + +These APIs are provided by `AmplifyConnectClientFlutter`, a standalone client in the `amplify_connect_client` package. It exposes three methods: `identifyUser`, `registerDevice`, and `removeDevice`. + + + + + +These APIs are provided by `AmplifyConnectClient`, a standalone client distributed with the Amplify Swift libraries. It exposes three methods: `identifyUser`, `registerDevice`, and `removeDevice`. + + + This page assumes you have already deployed a notifications resource. See [Set up notifications](/[platform]/build-a-backend/add-aws-services/notifications/set-up-notifications/). @@ -36,6 +58,8 @@ This page assumes you have already deployed a notifications resource. See [Set u ## Install the library + + Push notifications require the native module in addition to `aws-amplify`: ```bash title="Terminal" showLineNumbers={false} @@ -47,6 +71,47 @@ Because push notifications interact with the native platform, you also need to l - On iOS, run `npx pod-install`, add the push notification capability to your target in Xcode, and forward the remote notification callbacks from your `AppDelegate` to `AmplifyPushNotification`. - On Android, no additional integration steps are required beyond installing the packages above. + + + + +Add the dependency to your module's `build.gradle.kts`: + +```kotlin +dependencies { + implementation("com.amplifyframework:aws-connect:LATEST_VERSION") +} +``` + +The client is annotated as an experimental Amplify API, so opt in at the use site: + +```kotlin +@OptIn(ExperimentalAmplifyApi::class) +``` + + + + + +Add the dependency to your `pubspec.yaml`: + +```yaml +dependencies: + amplify_connect_client: ^2.14.0 +``` + +The client resolves AWS credentials through Amplify Auth, so your application also needs `amplify_flutter` and `amplify_auth_cognito` configured. See [Set up Auth](/[platform]/build-a-backend/auth/set-up-auth/). + + + + + +Add `AmplifyConnectClient` to your project using Swift Package Manager. In Xcode, go to **File > Add Package Dependencies** and enter the repository URL for the Amplify Swift SDK, then select the `AmplifyConnectClient` product. + + + + + ## Configure Amplify Pass `amplify_outputs.json` to `Amplify.configure()`. The endpoint and Region that your backend deployed are read from the `notifications` section of that file. @@ -72,9 +137,151 @@ AppRegistry.registerComponent(appName, () => App); Once initialized, the library registers the device automatically the first time the native platform issues a push token, so most applications do not call `registerDevice` themselves. See [Register a device](/[platform]/frontend/push-notifications/customer-profiles/register-device/). + + + + +## Create the client + +The endpoint and Region that your backend deployed are read from the `notifications.amazon_connect` section of `amplify_outputs.json`. Create the client with a Cognito-backed credentials provider: + +```kotlin +import com.amplifyframework.auth.CognitoCredentialsProvider +import com.amplifyframework.connect.AmplifyConnectClient +import com.amplifyframework.connect.ChannelType +import com.amplifyframework.connect.ConnectClientConfiguration +import com.amplifyframework.core.configuration.AmplifyOutputs +import com.amplifyframework.core.configuration.AmplifyOutputsData +import com.amplifyframework.foundation.credentials.toAwsCredentialsProvider + +val configuration = ConnectClientConfiguration.fromAmplifyOutputs( + AmplifyOutputsData.deserialize(context, AmplifyOutputs(R.raw.amplify_outputs)) +) + +val client = AmplifyConnectClient( + context = applicationContext, + configuration = configuration, + credentialsProvider = CognitoCredentialsProvider().toAwsCredentialsProvider(), + platform = "Android", + appVersion = "1.0.0", + channelType = ChannelType.GCM +) +``` + +`platform` and `appVersion` are optional strings stored on the device registration. `channelType` selects the push channel for this device and defaults to `ChannelType.GCM`; `APNS` and `APNS_SANDBOX` are also available. + +You can also construct the configuration manually: + +```kotlin +val configuration = ConnectClientConfiguration( + endpoint = "https://.execute-api..amazonaws.com", + region = "us-east-1" +) +``` + +The endpoint must be an `https://` URL. + + + + + +## Create the client + +The endpoint and Region that your backend deployed are read from the `notifications.amazon_connect` section of `amplify_outputs.json`. Create the client from the decoded outputs after configuring Amplify Auth: + +```dart +import 'dart:convert'; + +import 'package:amplify_auth_cognito/amplify_auth_cognito.dart'; +import 'package:amplify_connect_client/amplify_connect_client.dart'; +import 'package:amplify_flutter/amplify_flutter.dart' hide UserProfile; +import 'package:flutter/services.dart' show rootBundle; + +final raw = await rootBundle.loadString('amplify_outputs.json'); +final outputs = jsonDecode(raw) as Map; + +await Amplify.addPlugin(AmplifyAuthCognito()); +await Amplify.configure(raw); + +final client = AmplifyConnectClientFlutter.createFromAmplifyOutputs( + amplifyOutputs: outputs, +); +``` + +By default the client resolves credentials from Amplify Auth and persists the device identifier in shared preferences. The push channel type is resolved from the platform and build mode; pass an explicit `channelType` when you need to override it, for example a release-mode build that delivers through the APNs sandbox. + + + + + +## Create the client + +The endpoint and Region that your backend deployed are read from the `notifications.amazon_connect` section of `amplify_outputs.json` in your main bundle. Create the client with a credentials provider that resolves credentials from Amplify Auth: + +```swift +import Amplify +import AmplifyConnectClient +import AmplifyFoundation +import AWSCognitoAuthPlugin +import AWSPluginsCore + +let configuration = try ConnectClientConfiguration() + +let client = AmplifyConnectClient( + configuration: configuration, + credentialsProvider: CognitoConnectCredentialsProvider() +) +``` + +The credentials provider bridges the Amplify Auth session to the `AWSCredentialsProvider` protocol from the Amplify Swift foundation packages: + +```swift +struct CognitoConnectCredentialsProvider: AmplifyFoundation.AWSCredentialsProvider { + func resolve() async throws -> AmplifyFoundation.AWSCredentials { + let session = try await Amplify.Auth.fetchAuthSession() + guard let provider = session as? AuthAWSCredentialsProvider else { + throw ConnectError.credentials( + "Auth session does not vend AWS credentials.", + "Configure Amplify Auth with a Cognito identity pool." + ) + } + let credentials = try provider.getAWSCredentials().get() + + if let temporary = credentials as? AWSPluginsCore.AWSTemporaryCredentials { + return TemporaryCredentials( + accessKeyId: temporary.accessKeyId, + secretAccessKey: temporary.secretAccessKey, + sessionToken: temporary.sessionToken, + expiration: temporary.expiration + ) + } + return StaticCredentials( + accessKeyId: credentials.accessKeyId, + secretAccessKey: credentials.secretAccessKey + ) + } +} + +struct StaticCredentials: AmplifyFoundation.AWSCredentials { + let accessKeyId: String + let secretAccessKey: String +} + +struct TemporaryCredentials: AmplifyFoundation.AWSTemporaryCredentials { + let accessKeyId: String + let secretAccessKey: String + let sessionToken: String + let expiration: Date +} +``` + +You can also construct the configuration manually with `ConnectClientConfiguration(region:endpoint:)`. The endpoint must be an `https://` URL. + + + -Call `removeDevice` before signing a user out. De-registration is signed with the current user's credentials, so it cannot succeed after `signOut` has completed. See [Remove a device](/[platform]/frontend/push-notifications/customer-profiles/remove-device/). +Call `removeDevice` before signing a user out. De-registration is signed with the current user's credentials, so it cannot succeed after sign-out has completed. See [Remove a device](/[platform]/frontend/push-notifications/customer-profiles/remove-device/). diff --git a/src/pages/[platform]/frontend/push-notifications/customer-profiles/register-device/index.mdx b/src/pages/[platform]/frontend/push-notifications/customer-profiles/register-device/index.mdx index 24a00328e23..b5a6d0c80a2 100644 --- a/src/pages/[platform]/frontend/push-notifications/customer-profiles/register-device/index.mdx +++ b/src/pages/[platform]/frontend/push-notifications/customer-profiles/register-device/index.mdx @@ -4,7 +4,7 @@ export const meta = { title: 'Register a device', description: 'Register a push device with Amazon Connect Customer Profiles so that Amazon Connect journeys can deliver notifications to it.', - platforms: ['react-native'] + platforms: ['android', 'flutter', 'react-native', 'swift'] }; export const getStaticPaths = async () => { @@ -22,6 +22,8 @@ export function getStaticProps(context) { A device must be registered before an Amazon Connect journey can deliver a notification to it. Registering stores the device's push token against the current user's profile identity, so your backend knows where to send messages. + + ## Registration happens automatically In most applications you do not call `registerDevice` yourself. After you call `initializePushNotifications`, the library listens for the push token that the native platform issues and registers the device the first time a token arrives. @@ -72,7 +74,83 @@ const listener = onTokenReceived(async (token) => { }); ``` -The `token` is the only value you provide. The library resolves the remaining device fields for you: + + + + +## Register with the FCM registration token + +Obtain the token from Firebase Cloud Messaging and pass it to `registerDevice`: + +```kotlin +import com.google.firebase.messaging.FirebaseMessaging +import kotlinx.coroutines.tasks.await + +val token = FirebaseMessaging.getInstance().token.await() +val result = client.registerDevice(token) +``` + +Register again whenever FCM rotates the token, by calling `registerDevice` from your `FirebaseMessagingService.onNewToken` callback. Requesting notification permission and receiving messages remain your application's responsibility through Firebase; the client only registers the token with your backend. + + + + + +## Register with the platform push token + +Obtain the token from your push provider, for example Firebase Cloud Messaging, and pass it to `registerDevice`: + +```dart +import 'package:firebase_messaging/firebase_messaging.dart'; + +final token = await FirebaseMessaging.instance.getToken(); +if (token != null) { + await client.registerDevice(token: token); +} + +// Register again whenever the token rotates. +FirebaseMessaging.instance.onTokenRefresh.listen((token) async { + await client.registerDevice(token: token); +}); +``` + +Requesting notification permission and receiving messages remain your application's responsibility through your push provider; the client only registers the token with your backend. + + + +Device registration requires a push channel, so `registerDevice` throws `ConnectUnsupportedOperationException` on web and desktop platforms. `identifyUser` works on every platform. + + + + + + + +## Register with the APNs device token + +Obtain the token from your application delegate's remote notification callback and pass it to `registerDevice`: + +```swift +func application( + _ application: UIApplication, + didRegisterForRemoteNotificationsWithDeviceToken deviceToken: Data +) { + let token = deviceToken.map { String(format: "%02x", $0) }.joined() + Task { + try await client.registerDevice(token: token) + } +} +``` + +Requesting notification permission with `UNUserNotificationCenter` and calling `registerForRemoteNotifications()` remain your application's responsibility; the client only registers the token with your backend. + + + +## The token is the only value you provide + +The library resolves the remaining device fields for you: + + | Field | Source | | --- | --- | @@ -80,6 +158,41 @@ The `token` is the only value you provide. The library resolves the remaining de | `platform` | Derived from the operating system. | | `channelType` | Derived from the operating system, which selects the APNs or FCM channel. | + + + + +| Field | Source | +| --- | --- | +| `deviceId` | A stable identifier the library generates and persists per installation, shared with other Amplify libraries. | +| `platform` | The `platform` string passed when creating the client. | +| `appVersion` | The `appVersion` string passed when creating the client. | +| `channelType` | The `channelType` passed when creating the client. Defaults to `ChannelType.GCM`. | + + + + + +| Field | Source | +| --- | --- | +| `deviceId` | A stable identifier the library generates and persists per installation, shared with other Amplify libraries. | +| `platform` | Derived from the operating system. | +| `appVersion` | The `appVersion` string passed when creating the client, if provided. | +| `channelType` | Derived from the operating system and build mode: FCM on Android, and APNs on iOS, where debug builds use the APNs sandbox. Pass `channelType` when creating the client to override. | + + + + + +| Field | Source | +| --- | --- | +| `deviceId` | A stable identifier the library generates and persists per installation, shared with other Amplify libraries. | +| `platform` | The current operating system. | +| `appVersion` | The host application's `CFBundleShortVersionString`, if present. | +| `channelType` | `APNS_SANDBOX` for debug builds, `APNS` otherwise. | + + + Registration is an idempotent operation keyed on `deviceId`, so calling `registerDevice` again with a new token updates the existing record rather than creating a second one. ## The device belongs to the current identity @@ -90,6 +203,8 @@ Because the identity comes from Amazon Cognito identity pool credentials, regist ## When to call registerDevice + + Call `registerDevice` in these situations: - After a user opts in to notifications, when you are managing registration manually rather than relying on automatic registration. @@ -97,8 +212,24 @@ Call `registerDevice` in these situations: You do not need to call `registerDevice` on every application start, or after a user signs in. The registration persists, and sign-in re-registration is handled for you. + + + + +Call `registerDevice` in these situations: + +- After the user grants notification permission and the platform issues a push token. +- Whenever the platform rotates the token. +- After a user signs in, to move the device registration from the guest identity to the authenticated one. See [Guest and authenticated users](/[platform]/frontend/push-notifications/customer-profiles/guest-and-authenticated-users/). + +You do not need to call `registerDevice` on every application start. The registration persists until it is removed or the token changes. + + + ## Handle errors + + `registerDevice` rejects in the following cases: - **Push notifications are not initialized.** Call `initializePushNotifications` before registering a device. @@ -115,6 +246,51 @@ try { } ``` + + + + +`registerDevice` returns a `Result` that fails with a `ConnectValidationException` when the token is blank, or with a network, credentials, or service exception when the request cannot complete: + +```kotlin +import com.amplifyframework.foundation.result.Result + +when (val result = client.registerDevice(token)) { + is Result.Success -> { /* device registered */ } + is Result.Failure -> { /* inspect result.error */ } +} +``` + + + + + +`registerDevice` throws a `ConnectClientException` subtype when the token is invalid, the platform has no push channel, or the request cannot complete: + +```dart +try { + await client.registerDevice(token: token); +} on ConnectClientException catch (e) { + safePrint('Failed to register device: $e'); +} +``` + + + + + +`registerDevice` throws a `ConnectError` when the token is invalid or the request cannot complete: + +```swift +do { + try await client.registerDevice(token: token) +} catch { + print("Failed to register device: \(error)") +} +``` + + + ## Next steps - [Remove a device](/[platform]/frontend/push-notifications/customer-profiles/remove-device/): de-register a device before signing a user out. diff --git a/src/pages/[platform]/frontend/push-notifications/customer-profiles/remove-device/index.mdx b/src/pages/[platform]/frontend/push-notifications/customer-profiles/remove-device/index.mdx index e389dd7e2d9..de7a0f7192c 100644 --- a/src/pages/[platform]/frontend/push-notifications/customer-profiles/remove-device/index.mdx +++ b/src/pages/[platform]/frontend/push-notifications/customer-profiles/remove-device/index.mdx @@ -4,7 +4,7 @@ export const meta = { title: 'Remove a device', description: 'De-register a push device from Amazon Connect Customer Profiles, and why removal must happen before signing a user out.', - platforms: ['react-native'] + platforms: ['android', 'flutter', 'react-native', 'swift'] }; export const getStaticPaths = async () => { @@ -22,26 +22,46 @@ export function getStaticProps(context) { Use `removeDevice` to de-register the current device so that Amazon Connect journeys stop delivering notifications to it. + + ```ts import { removeDevice } from 'aws-amplify/push-notifications/customer-profiles'; await removeDevice(); ``` -`removeDevice` takes no arguments. The library resolves the device identifier for you, and your backend permits removal only of a device that the calling identity owns. + - + -**Call `removeDevice` before `signOut`.** De-registration is signed with the current user's credentials, and your backend removes a device only when the calling identity owns it. After `signOut` completes those credentials are gone and the caller signs as a new guest identity, so a removal attempted at that point cannot de-register the signed-in user's device. Always await `removeDevice` first: +```kotlin +val result = client.removeDevice() +``` -```ts -import { signOut } from 'aws-amplify/auth'; -import { removeDevice } from 'aws-amplify/push-notifications/customer-profiles'; + -await removeDevice(); -await signOut(); + + +```dart +await client.removeDevice(); ``` + + + + +```swift +try await client.removeDevice() +``` + + + +`removeDevice` takes no arguments. The library resolves the device identifier for you, and your backend permits removal only of a device that the calling identity owns. + + + +**Call `removeDevice` before signing out.** De-registration is signed with the current user's credentials, and your backend removes a device only when the calling identity owns it. After sign-out completes those credentials are gone and the caller signs as a new guest identity, so a removal attempted at that point cannot de-register the signed-in user's device. + Reversing the order leaves the device registered against the signed-out user's profile, and a later journey can deliver their notifications to a device they no longer use. @@ -50,6 +70,8 @@ Reversing the order leaves the device registered against the signed-out user's p Handle a failed removal deliberately rather than letting it block sign-out. The following pattern signs the user out even when de-registration fails, which avoids trapping a user in a signed-in state because of a network error: + + ```ts import { signOut } from 'aws-amplify/auth'; import { removeDevice } from 'aws-amplify/push-notifications/customer-profiles'; @@ -65,6 +87,57 @@ const handleSignOut = async () => { }; ``` + + + + +```kotlin +import com.amplifyframework.foundation.result.Result + +suspend fun handleSignOut() { + val result = client.removeDevice() + if (result is Result.Failure) { + Log.w("SignOut", "Failed to remove device before sign out", result.error) + } + + Amplify.Auth.signOut { /* handle sign-out result */ } +} +``` + + + + + +```dart +Future handleSignOut() async { + try { + await client.removeDevice(); + } on ConnectClientException catch (e) { + safePrint('Failed to remove device before sign out: $e'); + } + + await Amplify.Auth.signOut(); +} +``` + + + + + +```swift +func handleSignOut() async { + do { + try await client.removeDevice() + } catch { + print("Failed to remove device before sign out: \(error)") + } + + _ = await Amplify.Auth.signOut() +} +``` + + + ## When to call removeDevice Call `removeDevice` in these situations: @@ -81,7 +154,29 @@ The device identifier persists after removal, so a user who opts back in can be ## Handle errors + + `removeDevice` rejects in the following cases: - **Push notifications are not initialized.** Call `initializePushNotifications` before removing a device. - **The request failed.** The endpoint returned a non-success status, or the request could not complete. + + + + + +`removeDevice` returns a `Result` that fails with a network, credentials, or service exception when the request cannot complete. When no device has ever been registered on this installation there is nothing to remove, so the call succeeds without contacting the endpoint. + + + + + +`removeDevice` throws a `ConnectClientException` subtype when the request cannot complete. When no device has ever been registered on this installation there is nothing to remove, so the call returns without contacting the endpoint. + + + + + +`removeDevice` throws a `ConnectError` when the request cannot complete, and throws `ConnectError.validation` when no device has ever been registered on this installation, because there is no registration to remove. + + From 208fd5d589ebd213a414ce320b1eb4a263ff7747 Mon Sep 17 00:00:00 2001 From: Ekjot <43255916+ekjotmultani@users.noreply.github.com> Date: Fri, 31 Jul 2026 15:39:54 -0700 Subject: [PATCH 2/3] docs: strip the notifications key before Amplify.configure in the Flutter setup amplify_core's NotificationsOutputs parser still requires the legacy Pinpoint fields (aws_region, amazon_pinpoint_app_id), so passing the full outputs file containing notifications.amazon_connect to Amplify.configure throws a parse error. Remove the key before configuring, matching the released example app; the Connect client reads it from the decoded map instead. --- .../push-notifications/customer-profiles/index.mdx | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/src/pages/[platform]/frontend/push-notifications/customer-profiles/index.mdx b/src/pages/[platform]/frontend/push-notifications/customer-profiles/index.mdx index ac23b3f74e0..886719b5374 100644 --- a/src/pages/[platform]/frontend/push-notifications/customer-profiles/index.mdx +++ b/src/pages/[platform]/frontend/push-notifications/customer-profiles/index.mdx @@ -200,8 +200,13 @@ import 'package:flutter/services.dart' show rootBundle; final raw = await rootBundle.loadString('amplify_outputs.json'); final outputs = jsonDecode(raw) as Map; +// Amplify.configure parses only its own sections, so remove the +// notifications key before configuring. The Connect client reads it +// from the decoded map instead. +final authOutputs = Map.from(outputs)..remove('notifications'); + await Amplify.addPlugin(AmplifyAuthCognito()); -await Amplify.configure(raw); +await Amplify.configure(jsonEncode(authOutputs)); final client = AmplifyConnectClientFlutter.createFromAmplifyOutputs( amplifyOutputs: outputs, From 29b4c5ff6697b4001d9e258ba90ccc1c8b327157 Mon Sep 17 00:00:00 2001 From: Ekjot <43255916+ekjotmultani@users.noreply.github.com> Date: Mon, 3 Aug 2026 11:34:09 -0700 Subject: [PATCH 3/3] docs: address review feedback on Customer Profiles mobile expansion - identify-user: restore the note that customAttributes values must be strings, which was dropped when the field table generalized the type for the mobile platforms. - guest-and-authenticated-users: guard removeDevice in the Swift sign-out snippet so a failed removal cannot skip signOut, matching the pattern the remove-device page documents. --- .../guest-and-authenticated-users/index.mdx | 11 +++++++++-- .../customer-profiles/identify-user/index.mdx | 2 +- 2 files changed, 10 insertions(+), 3 deletions(-) diff --git a/src/pages/[platform]/frontend/push-notifications/customer-profiles/guest-and-authenticated-users/index.mdx b/src/pages/[platform]/frontend/push-notifications/customer-profiles/guest-and-authenticated-users/index.mdx index 0280e48d051..5953ce8a0f7 100644 --- a/src/pages/[platform]/frontend/push-notifications/customer-profiles/guest-and-authenticated-users/index.mdx +++ b/src/pages/[platform]/frontend/push-notifications/customer-profiles/guest-and-authenticated-users/index.mdx @@ -145,8 +145,15 @@ await Amplify.Auth.signOut(); ```swift -try await client.removeDevice() -_ = await Amplify.Auth.signOut() +func handleSignOut() async { + do { + try await client.removeDevice() + } catch { + print("Failed to remove device before sign out: \(error)") + } + + _ = await Amplify.Auth.signOut() +} ``` diff --git a/src/pages/[platform]/frontend/push-notifications/customer-profiles/identify-user/index.mdx b/src/pages/[platform]/frontend/push-notifications/customer-profiles/identify-user/index.mdx index bd859497d40..244e8937d79 100644 --- a/src/pages/[platform]/frontend/push-notifications/customer-profiles/identify-user/index.mdx +++ b/src/pages/[platform]/frontend/push-notifications/customer-profiles/identify-user/index.mdx @@ -139,7 +139,7 @@ Every field is optional, so send only the values your application has. Each call | `location` | `object` | The user's location. Accepts `city`, `country`, `postalCode`, and `region`, each a `string`. | | `customAttributes` | `map` | Additional string key-value pairs to store on the profile. | -Values are validated before the request is sent. Every string, along with each `customAttributes` key and value, must be 255 characters or fewer. A profile that violates these bounds produces a validation error. +Values are validated before the request is sent. `customAttributes` values must be strings, and every string, along with each `customAttributes` key, must be 255 characters or fewer. A profile that violates these bounds produces a validation error.