diff --git a/Examples/MistDemo/Sources/MistDemoKit/Resources/index.html b/Examples/MistDemo/Sources/MistDemoKit/Resources/index.html index 38e87299..14f41def 100644 --- a/Examples/MistDemo/Sources/MistDemoKit/Resources/index.html +++ b/Examples/MistDemo/Sources/MistDemoKit/Resources/index.html @@ -262,6 +262,9 @@

Discover users/discover (POST) (CloudKit JS
+
+ +
diff --git a/Examples/MistDemo/Sources/MistDemoKit/Resources/js/users.js b/Examples/MistDemo/Sources/MistDemoKit/Resources/js/users.js index 47e518cc..313cf7dd 100644 --- a/Examples/MistDemo/Sources/MistDemoKit/Resources/js/users.js +++ b/Examples/MistDemo/Sources/MistDemoKit/Resources/js/users.js @@ -1,7 +1,7 @@ // users/caller · users/discover panel handlers. The deprecated // users/lookup/email and users/lookup/id primitives are not exposed — -// users/discover is Apple's supported replacement and handles both email -// and record-name lookups (phone-number support tracked in #398). +// users/discover is Apple's supported replacement and handles email, +// phone-number, and record-name lookups. const usersCallerStatus = document.getElementById('users-caller-status'); const usersCallerRaw = document.getElementById('users-caller-raw'); @@ -24,9 +24,10 @@ document.getElementById('users-caller-btn').addEventListener('click', async () = document.getElementById('users-discover-btn').addEventListener('click', async () => { const emails = csv(document.getElementById('users-discover-emails').value); + const phoneNumbers = csv(document.getElementById('users-discover-phone-numbers').value); const userRecordNames = csv(document.getElementById('users-discover-record-names').value); - if (emails.length === 0 && userRecordNames.length === 0) { - setStatus(usersDiscoverStatus, 'Provide at least one email or record name.', 'error'); + if (emails.length === 0 && phoneNumbers.length === 0 && userRecordNames.length === 0) { + setStatus(usersDiscoverStatus, 'Provide at least one email, phone number, or record name.', 'error'); return; } await runPanelOperation({ @@ -35,7 +36,7 @@ document.getElementById('users-discover-btn').addEventListener('click', async () label: 'Discover users', fn: async () => { if (currentMode === 'mistkit') { - return await postJSON('/api/users/discover', { emails, userRecordNames }); + return await postJSON('/api/users/discover', { emails, phoneNumbers, userRecordNames }); } // CloudKit JS exposes per-item primitives — loop and aggregate // to match the REST endpoint's batch shape. @@ -48,6 +49,14 @@ document.getElementById('users-discover-btn').addEventListener('click', async () results.push({ email, error: error.message }); } } + for (const phoneNumber of phoneNumbers) { + try { + const identity = await ckJsContainer().discoverUserIdentityWithPhoneNumber(phoneNumber); + results.push({ phoneNumber, identity }); + } catch (error) { + results.push({ phoneNumber, error: error.message }); + } + } for (const recordName of userRecordNames) { try { const identity = await ckJsContainer().discoverUserIdentityWithUserRecordName(recordName); diff --git a/Examples/MistDemo/Sources/MistDemoKit/Server/CloudKitService+WebBackend+Users.swift b/Examples/MistDemo/Sources/MistDemoKit/Server/CloudKitService+WebBackend+Users.swift index 9704fd2b..35137849 100644 --- a/Examples/MistDemo/Sources/MistDemoKit/Server/CloudKitService+WebBackend+Users.swift +++ b/Examples/MistDemo/Sources/MistDemoKit/Server/CloudKitService+WebBackend+Users.swift @@ -41,10 +41,12 @@ extension CloudKitService { internal func webDiscoverUsers( emails: [String], + phoneNumbers: [String], userRecordNames: [String] ) async throws -> [UserIdentity] { let lookupInfos = emails.map { UserIdentityLookupInfo(emailAddress: $0) } + + phoneNumbers.map { UserIdentityLookupInfo(phoneNumber: $0) } + userRecordNames.map { UserIdentityLookupInfo(userRecordName: $0) } return try await discoverUserIdentities(lookupInfos: lookupInfos) } diff --git a/Examples/MistDemo/Sources/MistDemoKit/Server/WebBackend.swift b/Examples/MistDemo/Sources/MistDemoKit/Server/WebBackend.swift index 2669072b..b4fa1f9c 100644 --- a/Examples/MistDemo/Sources/MistDemoKit/Server/WebBackend.swift +++ b/Examples/MistDemo/Sources/MistDemoKit/Server/WebBackend.swift @@ -103,6 +103,7 @@ internal protocol WebBackend: Sendable { func webDiscoverUsers( emails: [String], + phoneNumbers: [String], userRecordNames: [String] ) async throws -> [UserIdentity] diff --git a/Examples/MistDemo/Sources/MistDemoKit/Server/WebRequests+Users.swift b/Examples/MistDemo/Sources/MistDemoKit/Server/WebRequests+Users.swift index 46fd42d4..3c0889c4 100644 --- a/Examples/MistDemo/Sources/MistDemoKit/Server/WebRequests+Users.swift +++ b/Examples/MistDemo/Sources/MistDemoKit/Server/WebRequests+Users.swift @@ -33,22 +33,28 @@ internal import Foundation // wrapper (`discoverUserIdentities`) operates on the public database with // web-auth credentials regardless of the request's selected database. extension WebRequests { - /// `POST /api/users/discover` — discover user identities by email address - /// and/or user record name. Either list may be omitted; an absent key - /// decodes to an empty array. + /// `POST /api/users/discover` — discover user identities by email address, + /// phone number, and/or user record name. Any list may be omitted; an + /// absent key decodes to an empty array. internal struct DiscoverUsers: Decodable { private enum CodingKeys: String, CodingKey { case emails + case phoneNumbers case userRecordNames } internal let emails: [String] + internal let phoneNumbers: [String] internal let userRecordNames: [String] internal init(from decoder: any Decoder) throws { let container = try decoder.container(keyedBy: CodingKeys.self) self.emails = try container.decodeIfPresent([String].self, forKey: .emails) ?? [] + self.phoneNumbers = + try container.decodeIfPresent( + [String].self, forKey: .phoneNumbers + ) ?? [] self.userRecordNames = try container.decodeIfPresent( [String].self, forKey: .userRecordNames diff --git a/Examples/MistDemo/Sources/MistDemoKit/Server/WebServer+Users.swift b/Examples/MistDemo/Sources/MistDemoKit/Server/WebServer+Users.swift index 977c1d7b..c12a220a 100644 --- a/Examples/MistDemo/Sources/MistDemoKit/Server/WebServer+Users.swift +++ b/Examples/MistDemo/Sources/MistDemoKit/Server/WebServer+Users.swift @@ -37,7 +37,8 @@ /// operate on the public database with web-auth credentials, so neither /// carries a `database` selector. The deprecated `lookup/email` and /// `lookup/id` primitives are intentionally not exposed — `discover` is - /// Apple's supported replacement and handles email + record-name lookups. + /// Apple's supported replacement and handles email, phone-number, and + /// record-name lookups. internal func addUsersEndpoints( api: RouterGroup ) { @@ -66,7 +67,7 @@ } /// `POST /api/users/discover` — discover user identities by email - /// address and/or user record name. + /// address, phone number, and/or user record name. private func addUsersDiscoverEndpoint( api: RouterGroup ) { @@ -83,6 +84,7 @@ let backend = try backendFactory.make(token) let users = try await backend.webDiscoverUsers( emails: body.emails, + phoneNumbers: body.phoneNumbers, userRecordNames: body.userRecordNames ) return try WebJSON.encoder().encode( diff --git a/Examples/MistDemo/Tests/MistDemoTests/Server/MockBackend+Calls.swift b/Examples/MistDemo/Tests/MistDemoTests/Server/MockBackend+Calls.swift index 299c1ad3..777b7f85 100644 --- a/Examples/MistDemo/Tests/MistDemoTests/Server/MockBackend+Calls.swift +++ b/Examples/MistDemo/Tests/MistDemoTests/Server/MockBackend+Calls.swift @@ -107,6 +107,7 @@ /// Captured arguments from the most recent `webDiscoverUsers` call. internal struct DiscoverUsersCall: Sendable { internal let emails: [String] + internal let phoneNumbers: [String] internal let userRecordNames: [String] } diff --git a/Examples/MistDemo/Tests/MistDemoTests/Server/MockBackend+UserOperations.swift b/Examples/MistDemo/Tests/MistDemoTests/Server/MockBackend+UserOperations.swift index 8b9861ef..772d2049 100644 --- a/Examples/MistDemo/Tests/MistDemoTests/Server/MockBackend+UserOperations.swift +++ b/Examples/MistDemo/Tests/MistDemoTests/Server/MockBackend+UserOperations.swift @@ -46,16 +46,23 @@ internal func webDiscoverUsers( emails: [String], + phoneNumbers: [String], userRecordNames: [String] ) async throws -> [UserIdentity] { lastDiscoverUsers = DiscoverUsersCall( emails: emails, + phoneNumbers: phoneNumbers, userRecordNames: userRecordNames ) try consumePendingError() return emails.map { email in UserIdentity(lookupInfo: UserIdentityLookupInfo(emailAddress: email)) } + + phoneNumbers.map { phoneNumber in + UserIdentity( + lookupInfo: UserIdentityLookupInfo(phoneNumber: phoneNumber) + ) + } + userRecordNames.map { name in UserIdentity(userRecordName: .recordName(name)) } diff --git a/Examples/MistDemo/Tests/MistDemoTests/Server/WebServerTests+Users.swift b/Examples/MistDemo/Tests/MistDemoTests/Server/WebServerTests+Users.swift index 86b84c38..b13c265a 100644 --- a/Examples/MistDemo/Tests/MistDemoTests/Server/WebServerTests+Users.swift +++ b/Examples/MistDemo/Tests/MistDemoTests/Server/WebServerTests+Users.swift @@ -91,13 +91,17 @@ } @Test( - "POST /api/users/discover forwards emails and record names to the backend" + """ + POST /api/users/discover forwards emails, phone numbers, \ + and record names to the backend + """ ) internal func usersDiscoverForwards() async throws { let fixture = Self.makeFixture(authenticated: true) let app = Application(router: try fixture.server.makeRouter()) let jsonBody = """ {"emails":["a@example.com","b@example.com"],\ + "phoneNumbers":["+15555550123"],\ "userRecordNames":["_user-1"]} """ @@ -113,12 +117,13 @@ UsersPayload.self, from: Data(response.body.readableBytesView) ) - #expect(payload.users.count == 3) + #expect(payload.users.count == 4) } } let captured = await fixture.backend.lastDiscoverUsers #expect(captured?.emails == ["a@example.com", "b@example.com"]) + #expect(captured?.phoneNumbers == ["+15555550123"]) #expect(captured?.userRecordNames == ["_user-1"]) }