feat: add connect client - #4245
Conversation
Codecov Report❌ Patch coverage is 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
Flags with carried forward coverage won't be shown. Click here to find out more. ☔ View full report in Codecov by Harness. 🚀 New features to boost your workflow:
|
|
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. |
| public let awsRegion: String? | ||
| public let amazonPinpointAppId: String? | ||
| public let channels: [AmazonPinpointChannelType]? |
There was a problem hiding this comment.
Why do we need this change?
There was a problem hiding this comment.
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.
| <key>NSPrivacyTracking</key> | ||
| <false/> | ||
| <key>NSPrivacyCollectedDataTypes</key> | ||
| <array/> |
There was a problem hiding this comment.
We actually want to make sure that this is accurate as this is collecting/identifying user and its info.
| /// - 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 { |
There was a problem hiding this comment.
Since we are explicitly looking for a json extension file.. can we throw an error if the user passes in the extension?
| let endpoint = customerProfiles["endpoint"] as? String | ||
| else { | ||
| throw ConnectError.configuration( | ||
| "Missing notifications.amazon_connect_customer_profiles in \(resource).json", |
There was a problem hiding this comment.
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.
|
|
||
| import Foundation | ||
|
|
||
| private let deviceIdKey = "com.amplifyframework.device_id" |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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", |
There was a problem hiding this comment.
?? "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.
| let sessionConfig = URLSessionConfiguration.default | ||
| sessionConfig.urlCache = nil | ||
| sessionConfig.requestCachePolicy = .reloadIgnoringLocalCacheData | ||
| self.urlSession = URLSession(configuration: sessionConfig) | ||
| self.logger = AmplifyLogging.logger(for: AmplifyConnectClient.self) |
There was a problem hiding this comment.
Isn't there SDK support for making the request directly and we are building everything by ourselves?
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.
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 PinpointidentifyUser/UpdateEndpointflow, 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
/identify-user) for authenticated users, SigV4 (/identify-user-guest) for guest/unauthenticated usersAWSCredentialsProvider+AuthTokenProvider— matches existing client patterns; decoupled from Amplify Auth pluginAWSSigV4SignerfromAWSSDKHTTPAuthfor the guest pathCodablemodels — clean serialization viaJSONEncoderamplify_outputs.json— readsnotifications.amazon_connect_customer_profileskey viaConnectClientConfiguration(from:bundle:)URLSessionconfigured without cache since every request is a mutationlib/amplify-swift#<version> md/amplify-connectfor service-side attributionPublic API
AmplifyConnectClientidentifyUser(userId:userProfile:options:)ConnectClientConfigurationamplify_outputs.jsonUserProfileIdentifyUserOptionsguestIdentityId)ConnectErrorAmplifyError-conforming error enumFoundation additions
AuthTokenAmplifyFoundation/Sources/Credentials/AuthToken.swiftAuthTokenProviderAmplifyFoundation/Sources/Credentials/AuthTokenProvider.swiftFiles
AmplifyClients/AmplifyConnectClient/Sources/Tests/UnitTests/Tests/ConnectClientHostApp/Package.swift— new target + productunit_test_connect.yml) + added tounit_test.ymlTest plan
swift build --target AmplifyConnectClientcompiles cleanlyswift test --filter AmplifyConnectClientTests— 7 tests)identifyUsercreates profiles in Connect Customer Profiles)General Checklist
Given When Theninline code documentation and are named accordinglytestThing_condition_expectation()By submitting this pull request, I confirm that my contribution is made under the terms of the Apache 2.0 license.