Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -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 () => {
Expand Down Expand Up @@ -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.

<InlineFilter filters={['react-native']}>

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:
Expand All @@ -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.

</InlineFilter>

<InlineFilter filters={['android', 'flutter', 'swift']}>

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.

</InlineFilter>

<InlineFilter filters={['android']}>

```kotlin
// After sign-in completes:
val token = FirebaseMessaging.getInstance().token.await()
client.registerDevice(token)
```

</InlineFilter>

<InlineFilter filters={['flutter']}>

```dart
// After sign-in completes:
final token = await FirebaseMessaging.instance.getToken();
if (token != null) {
await client.registerDevice(token: token);
}
```

</InlineFilter>

<InlineFilter filters={['swift']}>

```swift
// After sign-in completes, with the APNs token you received
// in didRegisterForRemoteNotificationsWithDeviceToken:
try await client.registerDevice(token: token)
```

</InlineFilter>

<InlineFilter filters={['android', 'flutter', 'swift']}>

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.

</InlineFilter>

## 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:

<InlineFilter filters={['react-native']}>

```ts
import { signOut } from 'aws-amplify/auth';
import { removeDevice } from 'aws-amplify/push-notifications/customer-profiles';
Expand All @@ -72,23 +122,46 @@ 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/).
</InlineFilter>

## Profile information for guests
<InlineFilter filters={['android']}>

`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:
</InlineFilter>

```ts
import { identifyUser } from 'aws-amplify/push-notifications/customer-profiles';

await identifyUser({
userProfile: {
email: 'jane@example.com',
name: 'Jane Doe'
}
});
<InlineFilter filters={['flutter']}>

```dart
await client.removeDevice();
await Amplify.Auth.signOut();
```

</InlineFilter>

<InlineFilter filters={['swift']}>

```swift
func handleSignOut() async {
do {
try await client.removeDevice()
} catch {
print("Failed to remove device before sign out: \(error)")
}

_ = await Amplify.Auth.signOut()
}
```

See [Identify a user](/[platform]/frontend/push-notifications/customer-profiles/identify-user/).
</InlineFilter>

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/).

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good restructuring here, moving the ordering callout to wrap all four platform variants instead of duplicating it per platform, and generalizing "after signOut completes" to "after sign-out completes" so the prose doesn't leak a specific API name into shared copy.


## 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/).
Original file line number Diff line number Diff line change
Expand Up @@ -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 () => {
Expand All @@ -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.

<InlineFilter filters={['react-native']}>

```ts
import { identifyUser } from 'aws-amplify/push-notifications/customer-profiles';

Expand All @@ -44,6 +46,87 @@ await identifyUser({
});
```

</InlineFilter>

<InlineFilter filters={['android']}>

```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"
)
)
)
```

</InlineFilter>

<InlineFilter filters={['flutter']}>

```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',
},
),
);
```

</InlineFilter>

<InlineFilter filters={['swift']}>

```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"
)
)
)
```

</InlineFilter>

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
Expand All @@ -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<string, string>` | Additional key-value pairs to store on the profile. |
| `customAttributes` | `map` | Additional string key-value pairs to store on the profile. |

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[nitpick] The old copy explicitly said "customAttributes values must be strings", this drops that constraint when the type became map instead of Record<string, string>. If all four SDKs still require string values here, worth restoring that note so it doesn't read like arbitrary values are now allowed.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Restored the note.


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. `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.

<Callout info>

Expand All @@ -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/).

<InlineFilter filters={['react-native']}>

<Callout info>

`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.

</Callout>

</InlineFilter>

## 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

<InlineFilter filters={['react-native']}>

`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
Expand All @@ -102,3 +191,64 @@ try {
console.error('Failed to identify user', error);
}
```

</InlineFilter>

<InlineFilter filters={['android']}>

`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 */ }
}
}
```

</InlineFilter>

<InlineFilter filters={['flutter']}>

`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');
}
```

</InlineFilter>

<InlineFilter filters={['swift']}>

`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)")
}
}
```

</InlineFilter>
Loading
Loading