Skip to content

feat: add connect client - #4245

Merged
thisisabhash merged 14 commits into
mainfrom
feat/connect-client
Jul 30, 2026
Merged

feat: add connect client#4245
thisisabhash merged 14 commits into
mainfrom
feat/connect-client

Conversation

@thisisabhash

@thisisabhash thisisabhash commented Jul 10, 2026

Copy link
Copy Markdown
Member

Issue #

None

Description

Summary

Adds AmplifyConnectClient — a standalone client for managing user profiles with Amazon Connect Customer Profiles via a Lambda-backed HTTP API. This replaces the deprecated Pinpoint identifyUser/UpdateEndpoint flow, enabling push notification targeting via Connect Journeys.

This is the Swift counterpart to the JS implementation (amplify-js#14864) and uses the backend provisioned by @aws-amplify/backend-notifications (amplify-backend#3265).

Key design points

  • Dual auth paths — Bearer token (/identify-user) for authenticated users, SigV4 (/identify-user-guest) for guest/unauthenticated users
  • AWSCredentialsProvider + AuthTokenProvider — matches existing client patterns; decoupled from Amplify Auth plugin
  • SigV4 signing via AWSSigV4Signer from AWSSDKHTTPAuth for the guest path
  • Codable models — clean serialization via JSONEncoder
  • Configuration from amplify_outputs.json — reads notifications.amazon_connect_customer_profiles key via ConnectClientConfiguration(from:bundle:)
  • No cachingURLSession configured without cache since every request is a mutation
  • User-Agentlib/amplify-swift#<version> md/amplify-connect for service-side attribution

Public API

Type Role
AmplifyConnectClient Main client — identifyUser(userId:userProfile:options:)
ConnectClientConfiguration Region + endpoint, loadable from amplify_outputs.json
UserProfile User attributes (email, name, plan, location, customProperties)
IdentifyUserOptions Device info, channel type, merge-on-sign-in (guestIdentityId)
ConnectError AmplifyError-conforming error enum

Foundation additions

Type Location
AuthToken AmplifyFoundation/Sources/Credentials/AuthToken.swift
AuthTokenProvider AmplifyFoundation/Sources/Credentials/AuthTokenProvider.swift

Files

  • 4 source files in AmplifyClients/AmplifyConnectClient/Sources/
  • 7 unit tests in Tests/UnitTests/
  • Integration test host app with 6 tests in Tests/ConnectClientHostApp/
  • Design doc, README for backend setup
  • Package.swift — new target + product
  • GitHub Actions workflow (unit_test_connect.yml) + added to unit_test.yml
  • Xcode scheme for unit tests

Test plan

  • swift build --target AmplifyConnectClient compiles cleanly
  • Unit tests pass (swift test --filter AmplifyConnectClientTests — 7 tests)
  • Integration tests pass against deployed sandbox (identifyUser creates profiles in Connect Customer Profiles)
  • Verified profiles in AWS Console (Amazon Connect → Customer Profiles)
  • Guest (SigV4) path integration test
  • Test on device with real APNs token

General Checklist

  • Added new tests to cover change, if needed
  • Build succeeds with all target using Swift Package Manager
  • All unit tests pass
  • All integration tests pass
  • Security oriented best practices and standards are followed (e.g. using input sanitization, principle of least privilege, etc)
  • Documentation update for the change if required
  • PR title conforms to conventional commit style
  • New or updated tests include Given When Then inline code documentation and are named accordingly testThing_condition_expectation()
  • If breaking change, documentation/changelog update with migration instructions

By submitting this pull request, I confirm that my contribution is made under the terms of the Apache 2.0 license.

@codecov

codecov Bot commented Jul 10, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 43.31210% with 178 lines in your changes missing coverage. Please review.
✅ Project coverage is 66.61%. Comparing base (9ae2c07) to head (843f66a).

Files with missing lines Patch % Lines
...fyConnectClient/Sources/AmplifyConnectClient.swift 17.90% 133 Missing ⚠️
...t/Sources/Support/ConnectClientConfiguration.swift 45.83% 39 Missing ⚠️
...AWSPinpointPushNotificationsPlugin+Configure.swift 50.00% 4 Missing ⚠️
...fyConnectClient/Sources/Support/ConnectError.swift 92.30% 2 Missing ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##             main    #4245      +/-   ##
==========================================
- Coverage   66.76%   66.61%   -0.16%     
==========================================
  Files        1151     1157       +6     
  Lines       43874    44185     +311     
==========================================
+ Hits        29294    29434     +140     
- Misses      14580    14751     +171     
Flag Coverage Δ
API_plugin_unit_test 68.33% <ø> (ø)
AWSPluginsCore 68.07% <ø> (ø)
Amplify 47.47% <100.00%> (ø)
Amplify_Foundation_Bridge_unit_test 62.28% <ø> (ø)
Amplify_Foundation_unit_test 67.64% <ø> (ø)
Analytics_plugin_unit_test 83.43% <ø> (ø)
Auth_plugin_unit_test 72.40% <ø> (+0.04%) ⬆️
Connect_unit_test 42.95% <42.95%> (?)
DataStore_plugin_unit_test 81.83% <ø> (+0.01%) ⬆️
Firehose_plugin_unit_test 53.15% <ø> (ø)
Geo_plugin_unit_test 73.39% <ø> (ø)
Kinesis_plugin_unit_test 52.17% <ø> (ø)
Logging_plugin_unit_test 64.86% <ø> (ø)
Predictions_plugin_unit_test 34.09% <ø> (ø)
PushNotifications_plugin_unit_test 84.58% <50.00%> (-1.08%) ⬇️
RecordCache_unit_test 76.40% <ø> (ø)
Storage_plugin_unit_test 78.67% <ø> (ø)
unit_tests 66.61% <43.31%> (-0.16%) ⬇️

Flags with carried forward coverage won't be shown. Click here to find out more.

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

@ekjotmultani

Copy link
Copy Markdown
Member

Flagging a gap against the Flutter and Android counterparts: those clients auto fill deviceId with a stable per install UUID persisted in plain storage under com.amplifyframework.device_id (shared with the event enrichment client), and only fall back to a caller supplied value as an override. Here deviceId is caller supplied only, so device registrations from Swift would create a new profile object per app run unless every caller manages persistence themselves. Suggest adding a UserDefaults backed read or create under the shared key to match the other platforms.

Comment on lines +236 to +238
public let awsRegion: String?
public let amazonPinpointAppId: String?
public let channels: [AmazonPinpointChannelType]?

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.

Why do we need this change?

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.

The fields below describe an Amazon Pinpoint backend and are optional because
notifications may be present without Pinpoint being configured — for example
when only Amazon Connect is provisioned, the backend emits notifications
containing just an amazon_connect block (there's no pinpoint block). Declaring these as non-optional made Amplify.configure() fail to decode such a configuration outright,
preventing an app from using any category. Consumers must therefore check
for the values they need; see AWSPinpointPushNotificationsPlugin+Configure.

Comment on lines +5 to +8
<key>NSPrivacyTracking</key>
<false/>
<key>NSPrivacyCollectedDataTypes</key>
<array/>

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.

We actually want to make sure that this is accurate as this is collecting/identifying user and its info.

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.

updated

/// - bundle: The bundle containing the resource. Defaults to `.main`.
/// - Throws: ``ConnectError/configuration(_:_:_:)`` if the file or keys are missing.
public init(from resource: String = "amplify_outputs", bundle: Bundle = .main) throws {
guard let url = bundle.url(forResource: resource, withExtension: "json") else {

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.

Since we are explicitly looking for a json extension file.. can we throw an error if the user passes in the extension?

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.

updated

let endpoint = customerProfiles["endpoint"] as? String
else {
throw ConnectError.configuration(
"Missing notifications.amazon_connect_customer_profiles in \(resource).json",

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.

The error message could be a bit misleading, as this could also throw when other things apart from amazon_connect_customer_profiles is missing. SO it would be nice if we could just break it into individual guards or be more versbose that one of the 4 things could be missing or json is not valid.

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.

updated


import Foundation

private let deviceIdKey = "com.amplifyframework.device_id"

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.

private let deviceIdKey = "com.amplify.client.connect..device_id"

And we want to make sure what happens if a customer has created multiple clients for the same app or region?

We should callout our support based on the implementation.

Once you have a decision, update the doc comments accordingly.

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.

the device id key is intentional. reference: #4245 (comment)

updated doc comments

channelType: base.channelType,
optOut: base.optOut,
deviceId: base.deviceId ?? DeviceIdProvider.resolve(),
platform: base.platform ?? "iOS",

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.

?? "iOS" this seems to be wrong, no? We had a way to figure out the platform in our V2 somewhere..

Something similar for device id too.

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.

updated

Comment on lines +62 to +66
let sessionConfig = URLSessionConfiguration.default
sessionConfig.urlCache = nil
sessionConfig.requestCachePolicy = .reloadIgnoringLocalCacheData
self.urlSession = URLSession(configuration: sessionConfig)
self.logger = AmplifyLogging.logger(for: AmplifyConnectClient.self)

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.

Isn't there SDK support for making the request directly and we are building everything by ourselves?

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.

updated

harsh62
harsh62 previously approved these changes Jul 28, 2026
thisisabhash and others added 14 commits July 28, 2026 10:23
Public surface is now identifyUser(userProfile:), registerDevice(token:),
and removeDevice(). All routes are SigV4-signed (Bearer token path removed;
the backend derives the caller identity from the request signature, so
userId is no longer sent). IdentifyUserOptions and OptOut are removed.
UserProfile now carries email, name, phone, customAttributes, and
location (city, country, postalCode, region). registerDevice resolves
deviceId, platform, appVersion, and channelType internally, with platform
derived from the target OS. Removes the now-unused AuthToken and
AuthTokenProvider protocols from AmplifyFoundation and updates unit and
integration tests to the new contract.
…anifest

AmplifyConnectClient persists a device identifier in UserDefaults, so the
privacy manifest now declares NSPrivacyAccessedAPICategoryUserDefaults
with reason CA92.1, and the target is added to the required-reason API
list in the root README.
The client can transmit email address, name, phone number, coarse
location (city, country, postal code, region), and a device identifier
(persistent device id and push token). Each is declared as linked to
identity, not used for tracking, collected for app functionality.
Adds a README note that apps should narrow their app-level privacy
declarations to the optional UserProfile fields they actually send.
Endpoint hardening: configuration loading now rejects malformed or
non-https endpoint URLs, and the client re-validates before every
request so manually constructed configurations are covered. The SigV4
signing target (host, port, protocol) is derived from the endpoint URL
instead of hardcoding port 443, so the signature is always computed
over the same authority the request is sent to.

Input bounds: user profile fields (email, name, phone, location
fields), customAttributes keys and values, and the device token are
validated against the backend's 255-character attribute limit before
any network call, throwing the new ConnectError.validation case that
names the offending field.

Also removes the device identifier from log messages.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants