Skip to content
Open
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
8 changes: 4 additions & 4 deletions Mindbox.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -367,6 +367,7 @@
A153E04129BB0A8B003C34D4 /* InAppConfigurationWithOperations.json in Resources */ = {isa = PBXBuildFile; fileRef = A153E04029BB0A8B003C34D4 /* InAppConfigurationWithOperations.json */; };
A154E304299C189300F8F074 /* MBLoggerCoreDataManagerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = A154E303299C189300F8F074 /* MBLoggerCoreDataManagerTests.swift */; };
A154E32E299E0D8900F8F074 /* SDKLogManagerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = A154E32D299E0D8900F8F074 /* SDKLogManagerTests.swift */; };
974C7734AF47A6A1E1137E0E /* DateFormatTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1EC463C4B1A88855D9A47FAF /* DateFormatTests.swift */; };
A154E330299E0F1600F8F074 /* InAppGeoResponse.swift in Sources */ = {isa = PBXBuildFile; fileRef = A154E32F299E0F1600F8F074 /* InAppGeoResponse.swift */; };
A154E334299E110E00F8F074 /* EventRepositoryMock.swift in Sources */ = {isa = PBXBuildFile; fileRef = A154E333299E110E00F8F074 /* EventRepositoryMock.swift */; };
A15D701629AF810E007131E7 /* SDKLogsRequest.swift in Sources */ = {isa = PBXBuildFile; fileRef = A154E381299E5B7500F8F074 /* SDKLogsRequest.swift */; };
Expand Down Expand Up @@ -463,7 +464,6 @@
D2F7E24C2BADC4CA00B24BB8 /* MockSessionManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = D2F7E24B2BADC4CA00B24BB8 /* MockSessionManager.swift */; };
DEC482157E5249DBBFAEFC9A /* FeatureTogglesModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9778038796A8426ABDED1E97 /* FeatureTogglesModel.swift */; };
EA395B77BB16CEFE6DC91D1D /* TransparentViewSyncOperationResponseTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4B7DAFAB687945FA908DB1AC /* TransparentViewSyncOperationResponseTests.swift */; };
F26DFF81C3FF57C3DE68DEDC /* Date+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 326423031CA9C6BF0E62BEFD /* Date+Extensions.swift */; };
F30005442CFF3F7D004BE915 /* ABTestStubs.swift in Sources */ = {isa = PBXBuildFile; fileRef = F30005432CFF3F7D004BE915 /* ABTestStubs.swift */; };
F306291A2BD27D7500EF6609 /* InappFrequencyTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = F30629192BD27D7500EF6609 /* InappFrequencyTests.swift */; };
F30654BB2F1A83520058808C /* MindboxWebViewFacade.swift in Sources */ = {isa = PBXBuildFile; fileRef = F30654BA2F1A83520058808C /* MindboxWebViewFacade.swift */; };
Expand Down Expand Up @@ -765,7 +765,6 @@
31ED2DF025C4456600301FAD /* TestConfig_Invalid_1.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; path = TestConfig_Invalid_1.plist; sourceTree = "<group>"; };
31ED2DF125C4456600301FAD /* TestConfig_Invalid_3.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; path = TestConfig_Invalid_3.plist; sourceTree = "<group>"; };
31ED2DF925C4459400301FAD /* TestConfig_Invalid_4.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; path = TestConfig_Invalid_4.plist; sourceTree = "<group>"; };
326423031CA9C6BF0E62BEFD /* Date+Extensions.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = "Date+Extensions.swift"; sourceTree = "<group>"; };
33072F2D2664C24F001F1AB2 /* AreaResponse.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AreaResponse.swift; sourceTree = "<group>"; };
33072F2F2664C2E4001F1AB2 /* SubscriptionResponse.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SubscriptionResponse.swift; sourceTree = "<group>"; };
33072F312664C357001F1AB2 /* ProductListResponse.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProductListResponse.swift; sourceTree = "<group>"; };
Expand Down Expand Up @@ -1098,6 +1097,7 @@
A153E04029BB0A8B003C34D4 /* InAppConfigurationWithOperations.json */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.json; path = InAppConfigurationWithOperations.json; sourceTree = "<group>"; };
A154E303299C189300F8F074 /* MBLoggerCoreDataManagerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MBLoggerCoreDataManagerTests.swift; sourceTree = "<group>"; };
A154E32D299E0D8900F8F074 /* SDKLogManagerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SDKLogManagerTests.swift; sourceTree = "<group>"; };
1EC463C4B1A88855D9A47FAF /* DateFormatTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DateFormatTests.swift; sourceTree = "<group>"; };
A154E32F299E0F1600F8F074 /* InAppGeoResponse.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = InAppGeoResponse.swift; sourceTree = "<group>"; };
A154E333299E110E00F8F074 /* EventRepositoryMock.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EventRepositoryMock.swift; sourceTree = "<group>"; };
A154E33A299E5B6D00F8F074 /* LogLevel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = LogLevel.swift; sourceTree = "<group>"; };
Expand Down Expand Up @@ -2811,6 +2811,7 @@
A154E32D299E0D8900F8F074 /* SDKLogManagerTests.swift */,
47A4FA772E73741700569870 /* LoggerDatabaseLoaderTests.swift */,
47A4FA752E735C5200569870 /* LogStoreTrimmerTests.swift */,
1EC463C4B1A88855D9A47FAF /* DateFormatTests.swift */,
);
path = MindboxLogger;
sourceTree = "<group>";
Expand Down Expand Up @@ -3573,7 +3574,6 @@
BB4D7CC62BDEC51D008E3AB8 /* Notification+Extensions.swift */,
BB65630F2BE3BA430090C473 /* UIApplication+Extensions.swift */,
F31DB4072F56A50E00DCEB85 /* NSError+Extensions.swift */,
326423031CA9C6BF0E62BEFD /* Date+Extensions.swift */,
F3482F292A65DCFC002A41EC /* String+Extensions.swift */,
);
path = Extensions;
Expand Down Expand Up @@ -4657,7 +4657,6 @@
B3A625502689F8B600B6A3B7 /* BenefitResponse.swift in Sources */,
84C65E6425D4FBBB008996FA /* MobileApplicationInfoUpdated.swift in Sources */,
84DEE8AD25CC036A00C98CC7 /* MBDatabaseRepository.swift in Sources */,
F26DFF81C3FF57C3DE68DEDC /* Date+Extensions.swift in Sources */,
6182078DDFC681D168546DAD /* HapticService.swift in Sources */,
A1B2C3D4E5F6A7B8C9D0E1F2 /* MotionService.swift in Sources */,
6182078DDFC681D168546DAE /* HapticRequest.swift in Sources */,
Expand Down Expand Up @@ -4698,6 +4697,7 @@
F367301D2B7B8B6A00DD0039 /* NotificationFormatTests.swift in Sources */,
F35E0C4F2DF0535E00E8A768 /* InAppTrackingServiceTests.swift in Sources */,
A154E32E299E0D8900F8F074 /* SDKLogManagerTests.swift in Sources */,
974C7734AF47A6A1E1137E0E /* DateFormatTests.swift in Sources */,
9B52570728D1AF880029B1BC /* InAppPresentationManagerMock.swift in Sources */,
D2F7E2482BADB9EF00B24BB8 /* UserVisitManagerTests.swift in Sources */,
BBAAC17C2BB2FC9100E1E25E /* MockEvent.swift in Sources */,
Expand Down
39 changes: 0 additions & 39 deletions Mindbox/Extensions/Date+Extensions.swift

This file was deleted.

Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@

import UIKit
import WebKit
import MindboxLogger

private enum PayloadKey {
static let sdkVersion = "sdkVersion"
Expand Down Expand Up @@ -238,7 +239,7 @@ extension MindboxWebViewFacade {
]

if let firstInitDate = persistenceStorage.firstInitializationDateTime {
params[PayloadKey.firstInitializationDateTime] = firstInitDate.iso8601
params[PayloadKey.firstInitializationDateTime] = firstInitDate.toString(withFormat: .utc)
}

return params
Expand Down
3 changes: 2 additions & 1 deletion Mindbox/Model/Common/MBDate.swift
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,8 @@ public final class DateTime: MBDate {
}

override func decodeWithFormat(_ rawString: String) -> Date? {
return Date.fromISO8601(rawString)
return rawString.toDate(withFormat: .utc)
?? rawString.toDate(withFormat: .utcWithMillis)
}
}

Expand Down
6 changes: 5 additions & 1 deletion MindboxLogger/Shared/Extensions/Date+Extension.swift
Original file line number Diff line number Diff line change
Expand Up @@ -15,18 +15,21 @@ public extension Date {

func toFullString() -> String {
let dateFormatter = DateFormatter()
dateFormatter.locale = Locale(identifier: "en_US_POSIX")
dateFormatter.dateFormat = "yyyy-MM-dd'T'HH:mm:ssZ"
return dateFormatter.string(from: self as Date)
}

static var dateFormatter: DateFormatter {
let formatter = DateFormatter()
formatter.dateFormat = "hh:mm:ss.SSSS"
formatter.locale = Locale(identifier: "en_US_POSIX")
formatter.dateFormat = "HH:mm:ss.SSSS"
return formatter
}

func toString(withFormat format: DateFormat) -> String {
let dateFormatter = DateFormatter()
dateFormatter.locale = Locale(identifier: "en_US_POSIX")
dateFormatter.dateFormat = format.value
dateFormatter.timeZone = TimeZone(identifier: "UTC")
return dateFormatter.string(from: self)
Expand All @@ -36,6 +39,7 @@ public extension Date {
public extension TimeInterval {
private static let readableDateTimeFormatter: DateFormatter = {
let formatter = DateFormatter()
formatter.locale = Locale(identifier: "en_US_POSIX")
formatter.dateFormat = "yyyy-MM-dd HH:mm:ss.SSS"
return formatter
}()
Expand Down
2 changes: 2 additions & 0 deletions MindboxLogger/Shared/Extensions/String+Extensions.swift
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import Foundation
public enum DateFormat: String {
case api = "yyyy-MM-dd'T'HH:mm:ss"
case utc = "yyyy-MM-dd'T'HH:mm:ss'Z'"
case utcWithMillis = "yyyy-MM-dd'T'HH:mm:ss.SSSXXXXX"

var value: String {
return self.rawValue
Expand All @@ -19,6 +20,7 @@ public enum DateFormat: String {
public extension String {
func toDate(withFormat format: DateFormat) -> Date? {
let dateFormatterGet = DateFormatter()
dateFormatterGet.locale = Locale(identifier: "en_US_POSIX")
dateFormatterGet.dateFormat = format.value
dateFormatterGet.timeZone = TimeZone(identifier: "UTC")

Expand Down
2 changes: 2 additions & 0 deletions MindboxTests/Extensions/Tag+Extensions.swift
Original file line number Diff line number Diff line change
Expand Up @@ -27,4 +27,6 @@ extension Tag {
@Tag static var trackVisit: Self
@Tag static var operationsRouting: Self
@Tag static var mbConfiguration: Self

@Tag static var dateFormatting: Self
}
20 changes: 11 additions & 9 deletions MindboxTests/InApp/Tests/InappShowFailureManagerTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
import XCTest
import UIKit
@testable import Mindbox
@testable import MindboxLogger

final class InappShowFailureManagerTests: XCTestCase {
private var databaseRepository: InappShowFailureDatabaseRepositoryMock!
Expand Down Expand Up @@ -65,7 +66,16 @@ final class InappShowFailureManagerTests: XCTestCase {
let event = try XCTUnwrap(databaseRepository.createdEvents.first)
let failure = try XCTUnwrap(decodeFailures(from: event)?.first)
XCTAssertFalse(failure.dateTimeUtc.isEmpty)
XCTAssertNotNil(makeUTCFormatter().date(from: failure.dateTimeUtc))
XCTAssertNotNil(failure.dateTimeUtc.toDate(withFormat: .utc))

let dateTimeUtc = failure.dateTimeUtc
XCTAssertEqual(dateTimeUtc.count, 20)
XCTAssertTrue(dateTimeUtc.hasSuffix("Z"))
XCTAssertFalse(dateTimeUtc.contains("AM"))
XCTAssertFalse(dateTimeUtc.contains("PM"))
XCTAssertFalse(dateTimeUtc.contains(" "))
let pattern = #"^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}Z$"#
XCTAssertNotNil(dateTimeUtc.range(of: pattern, options: .regularExpression))
}

func testAddFailure_duplicateInappId_isIgnored() throws {
Expand Down Expand Up @@ -359,14 +369,6 @@ private extension InappShowFailureManagerTests {
BodyDecoder<InAppShowFailuresBody>(decodable: event.body)?.body.failures
}

func makeUTCFormatter() -> DateFormatter {
let formatter = DateFormatter()
formatter.locale = Locale(identifier: "en_US_POSIX")
formatter.timeZone = TimeZone(secondsFromGMT: 0)
formatter.dateFormat = "yyyy-MM-dd'T'HH:mm:ss'Z'"
return formatter
}

func applyFeatureToggle(shouldSendInAppShowError: Bool) {
let settingsJSON = """
{
Expand Down
64 changes: 64 additions & 0 deletions MindboxTests/MindboxLogger/DateFormatTests.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
//
// DateFormatTests.swift
// MindboxTests
//
// Copyright © 2026 Mindbox. All rights reserved.
//

import Foundation
import Testing
@testable import MindboxLogger

@Suite("DateFormat ISO-8601 primitive", .tags(.dateFormatting))
struct DateFormatTests {

private let fixedDate = Date(timeIntervalSince1970: 1_747_017_155) // 2025-05-12T02:32:35Z

@Test("Date.toString(withFormat: .utc) produces literal yyyy-MM-dd'T'HH:mm:ss'Z'")
func utcFormatLiteral() {
#expect(fixedDate.toString(withFormat: .utc) == "2025-05-12T02:32:35Z")
}

@Test("Date.toString(withFormat: .api) produces literal yyyy-MM-dd'T'HH:mm:ss")
func apiFormatLiteral() {
#expect(fixedDate.toString(withFormat: .api) == "2025-05-12T02:32:35")
}

@Test("String.toDate(withFormat: .utc) parses canonical UTC literal")
func utcParseRoundTrip() {
let parsed = "2025-05-12T02:32:35Z".toDate(withFormat: .utc)
#expect(parsed == fixedDate)
}

@Test("String.toDate(withFormat: .utcWithMillis) parses millisecond-precision payloads")
func millisParse() {
let parsed = "2025-05-12T02:32:35.123Z".toDate(withFormat: .utcWithMillis)
#expect(parsed != nil)
let expected = Date(timeIntervalSince1970: 1_747_017_155.123)
if let parsed {
#expect(abs(parsed.timeIntervalSince(expected)) < 0.001)
}
}

@Test("Formatter does not silently switch to 12h pattern under 12-hour preference")
func twelveHourPreferenceDoesNotLeakIntoOutput() {
// QA1480: when the user's region preference is 12h, DateFormatter rewrites
// HH:mm:ss into h:mm:ss a unless locale is en_US_POSIX. We assert the
// literal still matches the contract — no AM/PM, no whitespace, leading zero.
let serialized = fixedDate.toString(withFormat: .utc)
#expect(!serialized.contains("AM"))
#expect(!serialized.contains("PM"))
#expect(!serialized.contains(" "))
#expect(serialized.hasSuffix("Z"))
#expect(serialized.count == 20)
}

@Test("UTC and millis round-trip via DateTime decoder fallback")
func roundTripFallbackChain() {
let withMillis = "2025-05-12T02:32:35.000Z"
let plain = "2025-05-12T02:32:35Z"
#expect(plain.toDate(withFormat: .utc) != nil)
#expect(withMillis.toDate(withFormat: .utc) == nil)
#expect(withMillis.toDate(withFormat: .utcWithMillis) != nil)
}
}