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
21 changes: 20 additions & 1 deletion Sources/Shared/Settings/SettingsStore.swift
Original file line number Diff line number Diff line change
Expand Up @@ -24,8 +24,14 @@ public class SettingsStore {
}
}

static let integrationDeviceIDKey = "integrationDeviceID"

/// The identifier the server knows this installation by. It is taken from the platform's vendor
/// identifier the first time it is needed and kept from then on, so a build that derives it
/// differently (the Mac moving from Catalyst to the native app) keeps the registration it has
/// rather than registering a second device.
public var integrationDeviceID: String {
let baseString = Current.device.identifierForVendor() ?? deviceID
let baseString = persistedIntegrationDeviceID ?? deviceID

switch Current.appConfiguration {
case .debug:
Expand All @@ -44,6 +50,19 @@ public class SettingsStore {
}
}

/// The vendor identifier as first seen, stored alongside the other registration data in the app group.
/// Nothing is stored while the platform has no identifier to give, so a later read can still pick it up.
private var persistedIntegrationDeviceID: String? {
if let stored = prefs.string(forKey: Self.integrationDeviceIDKey) {
return stored
}
guard let current = Current.device.identifierForVendor() else {
return nil
}
prefs.set(current, forKey: Self.integrationDeviceIDKey)
return current
}

private var seenWhatsNewReleaseIDs: Set<String> {
Set(prefs.stringArray(forKey: seenWhatsNewReleaseIDsKey) ?? [])
}
Expand Down
45 changes: 45 additions & 0 deletions Tests/App/Settings/SettingsStoreIntegrationDeviceID.test.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
@testable import Shared
import Testing

/// The identifier the server knows the installation by is taken from the vendor identifier once and kept.
// Serialized: the tests swap `Current.device.identifierForVendor` and the stored value, which concurrent
// tests would race on.
@Suite(.serialized)
struct SettingsStoreIntegrationDeviceIDTests {
private func withVendorIdentifier(
_ identifier: @escaping () -> String?,
_ body: () throws -> Void
) rethrows {
let previousIdentifier = Current.device.identifierForVendor
let previousStored = Current.settingsStore.prefs.string(forKey: SettingsStore.integrationDeviceIDKey)
defer {
Current.device.identifierForVendor = previousIdentifier
Current.settingsStore.prefs.set(previousStored, forKey: SettingsStore.integrationDeviceIDKey)
}
Current.settingsStore.prefs.removeObject(forKey: SettingsStore.integrationDeviceIDKey)
Current.device.identifierForVendor = identifier
try body()
}

@Test func theFirstVendorIdentifierIsKeptWhenItLaterChanges() {
withVendorIdentifier({ "first-vendor-id" }) {
let first = Current.settingsStore.integrationDeviceID
#expect(first.hasSuffix("first-vendor-id"))

Current.device.identifierForVendor = { "second-vendor-id" }

#expect(Current.settingsStore.integrationDeviceID == first)
}
}

@Test func nothingIsKeptWhileThereIsNoVendorIdentifier() {
withVendorIdentifier({ nil }) {
#expect(Current.settingsStore.integrationDeviceID.hasSuffix(Current.settingsStore.deviceID))
#expect(Current.settingsStore.prefs.string(forKey: SettingsStore.integrationDeviceIDKey) == nil)

Current.device.identifierForVendor = { "late-vendor-id" }

#expect(Current.settingsStore.integrationDeviceID.hasSuffix("late-vendor-id"))
}
}
}
4 changes: 4 additions & 0 deletions Tests/Shared/Watch/WatchDeviceRegistrar.test.swift
Original file line number Diff line number Diff line change
Expand Up @@ -288,13 +288,17 @@ struct WatchDeviceRegistrarTests {
let systemName = Current.device.systemName
let systemVersion = Current.device.systemVersion
let identifierForVendor = Current.device.identifierForVendor
// The integration device ID is kept from its first read, so the stored one is set aside for the test.
let storedIntegrationDeviceID = Current.settingsStore.prefs.string(forKey: SettingsStore.integrationDeviceIDKey)
defer {
Current.device.deviceName = deviceName
Current.device.systemModel = systemModel
Current.device.systemName = systemName
Current.device.systemVersion = systemVersion
Current.device.identifierForVendor = identifierForVendor
Current.settingsStore.prefs.set(storedIntegrationDeviceID, forKey: SettingsStore.integrationDeviceIDKey)
}
Current.settingsStore.prefs.removeObject(forKey: SettingsStore.integrationDeviceIDKey)
Current.device.deviceName = { "Apple Watch" }
Current.device.systemModel = { "Watch7,1" }
Current.device.systemName = { "watchOS" }
Expand Down
Loading