Skip to content

Replace Device struct with the OpenAPI generated model#4161

Merged
laevandus merged 8 commits into
developfrom
open-api-device-additional-data
Jul 10, 2026
Merged

Replace Device struct with the OpenAPI generated model#4161
laevandus merged 8 commits into
developfrom
open-api-device-additional-data

Conversation

@laevandus

@laevandus laevandus commented Jul 7, 2026

Copy link
Copy Markdown
Contributor

🔗 Issue Links

Resolves: IOS-1871

🎯 Goal

Expose the full set of device data the backend returns through the public API instead of only id and createdAt.

📝 Summary

  • The OpenAPI-generated DeviceResponse model replaces the hand-written Device type as the public device model in the public API layer (renamed to Device).
  • The public Device now exposes additional data in the public interface: disabled, disabledReason, hardwareId, pushProvider, pushProviderName, userId, and voip — in addition to the existing id and createdAt.
  • The new fields are persisted locally and surfaced through CurrentChatUser.devices / currentDevice and ConnectedUser.loadDevices().

🛠 Implementation

  • Scripts/openapi_generate.sh renames the generated DeviceResponse to Device and makes it public, following the same mechanism used for AppSettings.
  • Device.createdAt is now a non-optional Date (the backend always returns it); the remaining new fields follow the backend's optionality.
  • DeviceDTO and the Core Data model gained the new attributes (additive, optional/defaulted → lightweight migration).
  • The generated Device now flows through both the connect payload and the listDevices endpoint, so the legacy internal DevicePayload/DeviceListPayload were removed.

🎨 Showcase

N/A — no UI changes.

🧪 Manual Testing Notes

Launch the demo app and login (device gets added). No JSON parsing issues in the console.

Test function, can be copy-pasted into demo app and called at DemoAppCoordinator+DemoApp.swift line 81

func testDeviceEndpoints() {
        // IOS-1871 verification: exercises the device write + read paths and logs the new public fields.
        let client = StreamChatWrapper.shared.client!
        let controller = client.currentUserController()

        func logDevices(_ context: String) {
            let devices = controller.currentUser?.devices ?? []
            print("[IOS-1871] \(context): \(devices.count) device(s)")
            for device in devices {
                print(
                    """
                    [IOS-1871] Device \
                    id=\(device.id), \
                    createdAt=\(device.createdAt), \
                    userId=\(device.userId), \
                    pushProvider=\(device.pushProvider), \
                    pushProviderName=\(device.pushProviderName ?? "nil"), \
                    hardwareId=\(device.hardwareId ?? "nil"), \
                    disabled=\(device.disabled.map { "\($0)" } ?? "nil"), \
                    disabledReason=\(device.disabledReason ?? "nil"), \
                    voip=\(device.voip.map { "\($0)" } ?? "nil")
                    """
                )
            }
        }

        // Write path: addDevice → CurrentUserUpdater.addDevice → saveCurrentDevice + createDevice endpoint.
        controller.addDevice(.apn(token: Data(repeating: 1, count: 32))) { error in
            if let error = error {
                print("[IOS-1871] addDevice failed: \(error)")
            }
            // Read path: synchronizeDevices → fetchDevices → listDevices → saveCurrentUserDevices([Device]) → asModel.
            controller.synchronizeDevices { error in
                if let error = error {
                    print("[IOS-1871] synchronizeDevices failed: \(error)")
                    return
                }
                logDevices("After sync")
            }
        }
    }

☑️ Contributor Checklist

  • I have signed the Stream CLA (required)
  • This change should be manually QAed
  • Changelog is updated with client-facing changes
  • Changelog is updated with new localization keys
  • New code is covered by unit tests
  • Documentation has been updated in the docs-content repo

Summary by CodeRabbit

  • New Features

    • Device records now include additional fields for push provider details, hardware ID, user ID, VoIP status, and disablement (with disablement reason).
    • Device information is now handled end-to-end using the unified device model (instead of a separate payload type).
  • Bug Fixes

    • Improved saving/loading for current user devices so all supported device attributes persist correctly.
    • Updated the generated device representation so the device creation timestamp is handled more defensively.

@laevandus laevandus requested a review from a team as a code owner July 7, 2026 12:12
@laevandus laevandus added the 🤞 Ready For QA A PR that is Ready for QA label Jul 7, 2026
@coderabbitai

coderabbitai Bot commented Jul 7, 2026

Copy link
Copy Markdown

Review Change Stack

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 6a175aae-0e3b-4b7b-a34d-e8b652c16846

📥 Commits

Reviewing files that changed from the base of the PR and between 0256f81 and 89b2c29.

⛔ Files ignored due to path filters (1)
  • Sources/StreamChat/Generated/OpenAPI/models/Device.swift is excluded by !**/generated/**
📒 Files selected for processing (2)
  • Scripts/openapi_generate.sh
  • Sources/StreamChat/Models/Device+Extensions.swift
💤 Files with no reviewable changes (1)
  • Sources/StreamChat/Models/Device+Extensions.swift

📝 Walkthrough

Walkthrough

Device is now used in payload decoding, persistence, worker flow, and tests. The generator, Core Data schema, DTOs, and fixtures were updated to carry the expanded device fields, and the changelog was amended.

Changes

Device model migration

Layer / File(s) Summary
Generated Device export
Scripts/openapi_generate.sh
Adds optionalize_property for DeviceResponse.createdAt, renames generated DeviceResponse to Device, and publicizes the Device model.
Device model helpers
Sources/StreamChat/Models/Device+Extensions.swift, CHANGELOG.md
New extension file adds DeviceId, Data.deviceId, and Identifiable to the generated Device; changelog documents new fields.
Payload and persistence switch to Device
Sources/StreamChat/APIClient/Endpoints/Payloads/CurrentUserPayloads.swift, Sources/StreamChat/Database/DatabaseSession.swift, Sources/StreamChat/Database/DTOs/CurrentUserDTO.swift, Sources/StreamChat/Database/DTOs/DeviceDTO.swift, Sources/StreamChat/Database/StreamChatModel.xcdatamodeld/...
CurrentUserPayload.devices, DatabaseSession, and CurrentUserDTO.saveCurrentUserDevices switch from [DevicePayload] to [Device]; DeviceDTO gains new managed properties and asModel() reconstructs the full Device.
Fetch devices passes Device through
Sources/StreamChat/Workers/CurrentUserUpdater.swift
fetchDevices forwards response.devices directly into saveCurrentUserDevices instead of mapping to DevicePayload first.
Fixtures, mocks, and tests update
TestTools/StreamChatTestTools/..., Tests/StreamChatTests/..., StreamChat.xcodeproj/project.pbxproj
Dummy data, mocks, and tests switch to Device fixtures, obsolete DevicePayload dummy/tests are removed, and a new test verifies all persisted Device fields.

Estimated code review effort: 3 (Moderate) | ~25 minutes

Possibly related PRs

Suggested labels: ✅ Feature

Suggested reviewers: nuno-vieira, martinmitrevski

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately reflects the main change: replacing the hand-written Device struct with the generated OpenAPI-backed model exposed as Device.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch open-api-device-additional-data

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@Stream-SDK-Bot

Copy link
Copy Markdown
Collaborator

SDK Performance

target metric benchmark branch performance status
MessageList Hitches total duration 10 ms 6.68 ms 33.2% 🔼 🟢
Duration 2.6 s 2.53 s 2.69% 🔼 🟢
Hitch time ratio 4 ms per s 2.64 ms per s 34.0% 🔼 🟢
Frame rate 75 fps 79.42 fps 5.89% 🔼 🟢
Number of hitches 1 0.8 20.0% 🔼 🟢

@github-actions

github-actions Bot commented Jul 7, 2026

Copy link
Copy Markdown
1 Warning
⚠️ The changes should be manually QAed before the Pull Request will be merged

Generated by 🚫 Danger

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🧹 Nitpick comments (1)
Tests/StreamChatTests/Database/DTOs/DeviceDTO_Tests.swift (1)

45-75: 🎯 Functional Correctness | 🔵 Trivial | 💤 Low value

Good round-trip coverage; consider adding a false/nil-boolean variant.

Solid test verifying all persisted Device fields. The boolean fields (disabled, voip) are only exercised with true; given DeviceDTO wraps them via NSNumber(value:), an explicit false case would guard against future regressions where false could be mistaken for "unset".

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@Tests/StreamChatTests/Database/DTOs/DeviceDTO_Tests.swift` around lines 45 -
75, Add a round-trip test for the DeviceDTO boolean fields using a
false/nil-style case, because the current DeviceDTO_Tests coverage only verifies
true values for disabled and voip. Update or add a test alongside
test_deviceAllFields_areStoredAndLoadedFromDB to persist a Device with disabled
and voip set to false, then load it back through currentUser?.asModel() and
assert the values remain false; this ensures DeviceDTO’s NSNumber(value:)
mapping does not accidentally treat false as unset.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Nitpick comments:
In `@Tests/StreamChatTests/Database/DTOs/DeviceDTO_Tests.swift`:
- Around line 45-75: Add a round-trip test for the DeviceDTO boolean fields
using a false/nil-style case, because the current DeviceDTO_Tests coverage only
verifies true values for disabled and voip. Update or add a test alongside
test_deviceAllFields_areStoredAndLoadedFromDB to persist a Device with disabled
and voip set to false, then load it back through currentUser?.asModel() and
assert the values remain false; this ensures DeviceDTO’s NSNumber(value:)
mapping does not accidentally treat false as unset.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 05abb598-346b-45ae-9d5d-bffcf7d7f51b

📥 Commits

Reviewing files that changed from the base of the PR and between e1929ab and 92562a5.

⛔ Files ignored due to path filters (2)
  • Sources/StreamChat/Generated/OpenAPI/models/Device.swift is excluded by !**/generated/**
  • Sources/StreamChat/Generated/OpenAPI/models/ListDevicesResponse.swift is excluded by !**/generated/**
📒 Files selected for processing (23)
  • CHANGELOG.md
  • Scripts/openapi_generate.sh
  • Sources/StreamChat/APIClient/Endpoints/Payloads/CurrentUserPayloads.swift
  • Sources/StreamChat/APIClient/Endpoints/Payloads/DevicePayloads.swift
  • Sources/StreamChat/Database/DTOs/CurrentUserDTO.swift
  • Sources/StreamChat/Database/DTOs/DeviceDTO.swift
  • Sources/StreamChat/Database/DatabaseSession.swift
  • Sources/StreamChat/Database/StreamChatModel.xcdatamodeld/StreamChatModel.xcdatamodel/contents
  • Sources/StreamChat/Models/Device+Extensions.swift
  • Sources/StreamChat/Models/Device.swift
  • Sources/StreamChat/Workers/CurrentUserUpdater.swift
  • StreamChat.xcodeproj/project.pbxproj
  • TestTools/StreamChatTestTools/Fixtures/JSONs/Devices.json
  • TestTools/StreamChatTestTools/Mocks/StreamChat/Database/DatabaseSession_Mock.swift
  • TestTools/StreamChatTestTools/TestData/DummyData/CurrentUserPayload.swift
  • TestTools/StreamChatTestTools/TestData/DummyData/Device+Dummy.swift
  • TestTools/StreamChatTestTools/TestData/DummyData/DevicePayloads.swift
  • Tests/StreamChatTests/APIClient/Endpoints/Payloads/DevicePayloads_Tests.swift
  • Tests/StreamChatTests/Database/DTOs/CurrentUserDTO_Tests.swift
  • Tests/StreamChatTests/Database/DTOs/DeviceDTO_Tests.swift
  • Tests/StreamChatTests/Database/DTOs/UserDTO_Tests.swift
  • Tests/StreamChatTests/StateLayer/ConnectedUser_Tests.swift
  • Tests/StreamChatTests/Workers/CurrentUserUpdater_Tests.swift
💤 Files with no reviewable changes (6)
  • Tests/StreamChatTests/APIClient/Endpoints/Payloads/DevicePayloads_Tests.swift
  • TestTools/StreamChatTestTools/Fixtures/JSONs/Devices.json
  • Sources/StreamChat/APIClient/Endpoints/Payloads/DevicePayloads.swift
  • Sources/StreamChat/Models/Device.swift
  • TestTools/StreamChatTestTools/TestData/DummyData/DevicePayloads.swift
  • StreamChat.xcodeproj/project.pbxproj

class DeviceDTO: NSManagedObject {
@NSManaged var id: String
@NSManaged var createdAt: DBDate?
@NSManaged var disabled: NSNumber?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Represents Bool? in the generated model. Not much to do here if we wanna switch to OpenAPI model directly without any additional wrapper.

Comment on lines +17 to +29
extension Device: Equatable {
public static func == (lhs: Device, rhs: Device) -> Bool {
lhs.id == rhs.id
&& lhs.createdAt == rhs.createdAt
&& lhs.disabled == rhs.disabled
&& lhs.disabledReason == rhs.disabledReason
&& lhs.hardwareId == rhs.hardwareId
&& lhs.pushProvider == rhs.pushProvider
&& lhs.pushProviderName == rhs.pushProviderName
&& lhs.userId == rhs.userId
&& lhs.voip == rhs.voip
}
}

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

OpenAPI generator change is coming which allows me to generate this. Next PR can clean it up.

public final class Device: Sendable, Codable, JSONEncodable {
/// Date/time of creation
let createdAt: Date
public let createdAt: Date

@laevandus laevandus Jul 7, 2026

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Note! createdAt used be an optional value in the public API layer. It is actually always returned. Can we accept making it non-optional (breaking API change because it was optional before)? If not, DeviceResponse (this PR currently renames it directly to Device) can't be directly exposed and Device type needs to wrap it.

/// An object representing a device which can receive push notifications.
public struct Device: Codable, Equatable, Identifiable, Sendable {
    private enum CodingKeys: String, CodingKey {
        case id
        case createdAt = "created_at"
    }

    /// The device identifier.
    public let id: DeviceId
    /// The date when the device for created.
    public let createdAt: Date?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

I think I add a script to make it optional and v6 can make this small breaking change.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Added post-processing step to make it back to optional in v5. In v6 we can properly fix it.

Comment thread Sources/StreamChat/Generated/OpenAPI/models/Device.swift
"$file"
}
publicize_model AppSettings
publicize_model Device

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

OpenAPI models is not used as public model for Device type

Comment on lines +159 to +168
# Relax selected generated stored properties back to optional. Some models are
# exposed as public API where a property was historically optional (e.g.
# Device.createdAt was Date? before the OpenAPI migration).
optionalize_property() {
local file="$OUTPUT_DIR_CHAT/models/$1.swift"
sed -i '' -E \
-e "s/^( let $2: [^?]+)$/\1?/" \
"$file"
}
optionalize_property DeviceResponse createdAt

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

In v6 this can be removed. Because Device is now public model, but our current model has createdAt as optional value, I need to change the generated model to match the current public model for avoiding API breaking changes in v5. Of course, in v6 we delete this and make a small breaking change.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🧹 Nitpick comments (1)
Scripts/openapi_generate.sh (1)

159-169: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Add a verification check after the sed substitution.

If the regex doesn't match (e.g., the generator output format changes or the property is already optional), sed silently does nothing and createdAt stays non-optional. While the downstream device.createdAt?.bridgeDate usage would catch this at compile time, a quick post-check would surface the problem immediately during codegen rather than later in the build.

♻️ Suggested verification guard
 optionalize_property() {
   local file="$OUTPUT_DIR_CHAT/models/$1.swift"
+  local before
+  before="$(grep -c "    let $2: " "$file" || true)"
   sed -i '' -E \
     -e "s/^(    let $2: [^?]+)$/\1?/" \
     "$file"
+  local after
+  after="$(grep -c "    let $2: " "$file" || true)"
+  if [ "$before" -eq "$after" ]; then
+    echo "warning: optionalize_property: no substitution made for $2 in $1.swift" >&2
+  fi
 }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@Scripts/openapi_generate.sh` around lines 159 - 169, The optionalize_property
helper in openapi_generate.sh should verify that the sed replacement actually
changed the target property. After running the substitution for
DeviceResponse.createdAt, add a post-check in optionalize_property that confirms
the generated Swift file now contains an optional declaration for the requested
symbol; if not, fail codegen immediately with a clear message. This keeps the
fix localized to optionalize_property and guards against generator format
changes or already-optional properties.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Nitpick comments:
In `@Scripts/openapi_generate.sh`:
- Around line 159-169: The optionalize_property helper in openapi_generate.sh
should verify that the sed replacement actually changed the target property.
After running the substitution for DeviceResponse.createdAt, add a post-check in
optionalize_property that confirms the generated Swift file now contains an
optional declaration for the requested symbol; if not, fail codegen immediately
with a clear message. This keeps the fix localized to optionalize_property and
guards against generator format changes or already-optional properties.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: d8b5b52a-2a9f-4512-8a59-a738ae87d61b

📥 Commits

Reviewing files that changed from the base of the PR and between 2297a68 and ad46c6e.

⛔ Files ignored due to path filters (1)
  • Sources/StreamChat/Generated/OpenAPI/models/Device.swift is excluded by !**/generated/**
📒 Files selected for processing (2)
  • Scripts/openapi_generate.sh
  • Sources/StreamChat/Database/DTOs/CurrentUserDTO.swift
🚧 Files skipped from review as they are similar to previous changes (1)
  • Sources/StreamChat/Database/DTOs/CurrentUserDTO.swift

@laevandus laevandus enabled auto-merge (squash) July 9, 2026 13:09
@laevandus laevandus disabled auto-merge July 9, 2026 13:09

@martinmitrevski martinmitrevski left a comment

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.

lgtm ✅

@github-actions

github-actions Bot commented Jul 9, 2026

Copy link
Copy Markdown

Public Interface

+ public final class Device: Sendable, Codable, JSONEncodable  
+ 
+   case createdAt = "created_at"
+   case disabled
+   case disabledReason = "disabled_reason"
+   case hardwareId = "hardware_id"
+   case id
+   case pushProvider = "push_provider"
+   case pushProviderName = "push_provider_name"
+   case userId = "user_id"
+   case voip
+   
+ 
+   public let createdAt: Date?
+   public let disabled: Bool?
+   public let disabledReason: String?
+   public let hardwareId: String?
+   public let id: String
+   public let pushProvider: String
+   public let pushProviderName: String?
+   public let userId: String
+   public let voip: Bool?

+ extension Device: Hashable  
+ 
+   public static func ==(lhs: Device,rhs: Device)-> Bool
+   public func hash(into hasher: inout Hasher)

- public struct Device: Codable, Equatable, Identifiable, Sendable  
- 
-   case id
-   case createdAt = "created_at"
-   
- 
-   public let id: DeviceId
-   public let createdAt: Date?

@laevandus

Copy link
Copy Markdown
Contributor Author

Manually verified it (sample script and running demo app and inspecting device queries). Will merge it.

@laevandus laevandus removed the 🤞 Ready For QA A PR that is Ready for QA label Jul 10, 2026
@laevandus laevandus merged commit 1a35173 into develop Jul 10, 2026
8 of 9 checks passed
@laevandus laevandus deleted the open-api-device-additional-data branch July 10, 2026 07:49
@Stream-SDK-Bot

Copy link
Copy Markdown
Collaborator

SDK Size

title develop branch diff status
StreamChat 7.53 MB 7.53 MB -1 KB 🚀
StreamChatUI 4.27 MB 4.27 MB 0 KB 🟢
StreamChatCommonUI 0.84 MB 0.84 MB 0 KB 🟢

@Stream-SDK-Bot

Copy link
Copy Markdown
Collaborator

StreamChat XCSize

Object Diff (bytes)
DeviceResponse.o -7773
DevicePayloads.o -6653
Device.o +3768
PushPreferencePayloads.o +1376
Device+Extensions.o +1187
Show 13 more objects
Object Diff (bytes)
DeviceDTO.o +728
CurrentUserDTO.o +725
CurrentUserUpdater.o -600
ChatClient.o -308
AnyAttachmentPayload.o -260
AttachmentDownloader.o -176
CurrentUser.o -150
AttachmentTypes.o -92
Token.o +88
MutedChannelPayload.o +84
LocationPayloads.o +80
UnknownChannelEvent.o -68
ListDevicesResponse.o -50

@sonarqubecloud

Copy link
Copy Markdown

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