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
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
import Foundation
import ShopifyCheckoutKit

class FileLogger: Logger {
private var fileHandle: FileHandle?
final class FileLogger: Logger {
private let fileHandle: FileHandle?

var logFileUrl: URL
let logFileUrl: URL

public init(_ filename: String) {
let paths = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask)
Expand All @@ -18,6 +18,7 @@ class FileLogger: Logger {
do {
fileHandle = try FileHandle(forWritingTo: logFileUrl)
} catch let error as NSError {
fileHandle = nil
print("Couldn't open the log file. Error: \(error)")
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,13 +8,16 @@ public enum ShopifyAcceleratedCheckouts {

/// The logging level for Accelerated Checkouts operations
/// Default: .error - which will emit "error" and "fault" logs
public static var logLevel: LogLevel = .error {
didSet {
logger.logLevel = logLevel
public static var logLevel: LogLevel {
get {
logger.logLevel
}
set {
logger.logLevel = newValue
}
}

/// Shared logger for ShopifyAcceleratedCheckouts
/// To modify the logLevel
internal static var logger = OSLogger(prefix: name, logLevel: logLevel)
internal static let logger = OSLogger(prefix: name, logLevel: .error)
}
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import UIKit

public struct Platform: Equatable {
public struct Platform: Equatable, Sendable {
public let identifier: String
public let version: String?

Expand All @@ -11,7 +11,7 @@ public struct Platform: Equatable {
}
}

public struct Configuration {
public struct Configuration: Sendable {
/// Determines the color scheme used when checkout is presented.
///
/// By default, the color scheme is determined based on the current
Expand Down Expand Up @@ -48,7 +48,7 @@ public struct Configuration {
}

extension Configuration {
public enum ColorScheme: String, CaseIterable {
public enum ColorScheme: String, CaseIterable, Sendable {
/// Uses a light, idiomatic color scheme.
case light
/// Uses a dark, idiomatic color scheme.
Expand All @@ -61,7 +61,7 @@ extension Configuration {
}

extension Configuration {
public struct Confetti {
public struct Confetti: Sendable {
public var enabled: Bool = false

public var particles = [UIImage]()
Expand Down
100 changes: 100 additions & 0 deletions platforms/swift/Sources/ShopifyCheckoutKit/LockedValue.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
import Foundation
import os.lock

private protocol LockedValueStorage<Value>: Sendable {
associatedtype Value: Sendable

func get() -> Value
func set(_ newValue: Value)
func update(_ block: (inout Value) -> Void)
}

@available(iOS 16.0, *)
private final class OSAllocatedUnfairLockedValueStorage<Value: Sendable>: LockedValueStorage {
/// When the package minimum deployment target is iOS 18, consider
/// replacing this storage with Synchronization.Mutex<Value>.
private let lock: OSAllocatedUnfairLock<Value>

init(_ value: Value) {
lock = OSAllocatedUnfairLock(initialState: value)
}

func get() -> Value {
lock.withLock { $0 }
}

func set(_ newValue: Value) {
lock.withLock {
$0 = newValue
}
}

func update(_ block: (inout Value) -> Void) {
lock.withLockUnchecked {
block(&$0)
}
}
}

/// iOS 15 fallback for lock-backed Sendable storage.
///
/// SAFETY:
/// - `value` is only read and written while holding `lock`.
/// - `Value` is constrained to `Sendable`, so returned values can cross concurrency domains.
/// - This exists because `OSAllocatedUnfairLock` is only available on iOS 16+.
///
/// Delete this fallback when the package minimum deployment target is iOS 16.
@available(iOS, deprecated: 16.0, message: "Use OSAllocatedUnfairLockedValueStorage on iOS 16+.")
private final class NSLockedValueStorage<Value: Sendable>: LockedValueStorage, @unchecked Sendable {
private let lock = NSLock()
private var value: Value

init(_ value: Value) {
self.value = value
}

func get() -> Value {
lock.withLock { value }
}

func set(_ newValue: Value) {
lock.withLock {
value = newValue
}
}

func update(_ block: (inout Value) -> Void) {
lock.withLock {
block(&value)
}
}
}

/// Lock-backed storage for mutable values that must remain synchronously readable and writable.
///
/// Use this for module-internal backing storage when a public or static mutable API needs to
/// preserve synchronous mutation, but the stored value would otherwise be non-isolated shared
/// mutable state under Swift 6 concurrency checking.
final class LockedValue<Value: Sendable>: Sendable {
private let storage: any LockedValueStorage<Value>

init(_ value: Value) {
if #available(iOS 16.0, *) {
storage = OSAllocatedUnfairLockedValueStorage(value)
} else {
storage = NSLockedValueStorage(value)
}
}

func get() -> Value {
storage.get()
}

func set(_ newValue: Value) {
storage.set(newValue)
}

func update(_ block: (inout Value) -> Void) {
storage.update(block)
}
}
63 changes: 48 additions & 15 deletions platforms/swift/Sources/ShopifyCheckoutKit/Logger.swift
Original file line number Diff line number Diff line change
Expand Up @@ -3,28 +3,55 @@ import os.log

private let subsystem = "com.shopify.checkoutkit"

public enum LogLevel: String, CaseIterable {
public enum LogLevel: String, CaseIterable, Sendable {
case all
case debug
case error
case none
}

public class OSLogger {
public final class OSLogger: Sendable {
private let logger = OSLog(subsystem: subsystem, category: OSLog.Category.pointsOfInterest)
private var prefix: String
package var logLevel: LogLevel
private let prefix: String
private let lockedLogLevel: LockedValue<LogLevel>
private let sendToOSLogHandler: (@Sendable (String, OSLogType) -> Void)?

public static var shared = OSLogger()
package var logLevel: LogLevel {
get {
lockedLogLevel.get()
}
set {
lockedLogLevel.set(newValue)
}
}

private static let lockedSharedLogger = LockedValue(OSLogger())

public static var shared: OSLogger {
get {
lockedSharedLogger.get()
}
set {
lockedSharedLogger.set(newValue)
}
}

public convenience init() {
self.init(prefix: "ShopifyCheckoutKit", logLevel: ShopifyCheckoutKit.configuration.logLevel)
}

public init() {
prefix = "ShopifyCheckoutKit"
logLevel = ShopifyCheckoutKit.configuration.logLevel
public convenience init(prefix: String, logLevel: LogLevel) {
self.init(prefix: prefix, logLevel: logLevel, sendToOSLogHandler: nil)
}

public init(prefix: String, logLevel: LogLevel) {
init(
prefix: String,
logLevel: LogLevel,
sendToOSLogHandler: (@Sendable (String, OSLogType) -> Void)?
) {
self.prefix = prefix
self.logLevel = logLevel
lockedLogLevel = LockedValue(logLevel)
self.sendToOSLogHandler = sendToOSLogHandler
}

public func debug(_ message: String) {
Expand Down Expand Up @@ -54,24 +81,30 @@ public class OSLogger {
/// Capturing `os_log` output is not possible
/// This indirection lets us capture messages in `LoggerTests.swift`
internal func sendToOSLog(_ message: String, type: OSLogType) {
os_log("%@", log: logger, type: type, message)
if let sendToOSLogHandler {
sendToOSLogHandler(message, type)
} else {
os_log("%@", log: logger, type: type, message)
}
}

private func shouldEmit(_ choice: LogLevel) -> Bool {
if logLevel == .none {
let currentLogLevel = logLevel

if currentLogLevel == .none {
return false
}

return logLevel == .all || logLevel == choice
return currentLogLevel == .all || currentLogLevel == choice
}
}

public protocol Logger {
public protocol Logger: Sendable {
func log(_ message: String)
func clearLogs()
}

public class NoOpLogger: Logger {
public final class NoOpLogger: Logger {
public func log(_: String) {}

public func clearLogs() {}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,16 +6,26 @@ import UIKit
/// The version of the `ShopifyCheckoutKit` library.
public let version = "4.0.0-alpha.1"

private let lockedCheckoutKitConfiguration = LockedValue(Configuration())

/// The configuration options for the `ShopifyCheckoutKit` library.
public var configuration = Configuration() {
didSet {
OSLogger.shared.logLevel = configuration.logLevel
public var configuration: Configuration {
get { lockedCheckoutKitConfiguration.get() }
set {
lockedCheckoutKitConfiguration.set(newValue)
applyConfigurationChange()
}
}

/// A convienence function for configuring the `ShopifyCheckoutKit` library.
public func configure(_ block: (inout Configuration) -> Void) {
block(&configuration)
lockedCheckoutKitConfiguration.update(block)
applyConfigurationChange()
}

private func applyConfigurationChange() {
let configuration = lockedCheckoutKitConfiguration.get()
OSLogger.shared.logLevel = configuration.logLevel
}

@MainActor
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,15 @@ import XCTest
class ConfigurationTests: XCTestCase {
override func setUp() {
super.setUp()
// Reset configuration to defaults
resetConfigurationState()
}

override func tearDown() {
resetConfigurationState()
super.tearDown()
}

private func resetConfigurationState() {
ShopifyCheckoutKit.configuration = Configuration()
}

Expand All @@ -27,4 +35,27 @@ class ConfigurationTests: XCTestCase {
ShopifyCheckoutKit.configuration.closeButtonTintColor = nil
XCTAssertNil(ShopifyCheckoutKit.configuration.closeButtonTintColor)
}

func testColorSchemeCanBeSetDirectly() {
ShopifyCheckoutKit.configuration.colorScheme = .light

XCTAssertEqual(ShopifyCheckoutKit.configuration.colorScheme, .light)
}

func testConfigureCanBatchConfigurationChanges() {
ShopifyCheckoutKit.configure {
$0.colorScheme = .dark
$0.closeButtonTintColor = .blue
}

XCTAssertEqual(ShopifyCheckoutKit.configuration.colorScheme, .dark)
XCTAssertEqual(ShopifyCheckoutKit.configuration.closeButtonTintColor, .blue)
}

func testDirectConfigurationMutationUpdatesLogger() {
ShopifyCheckoutKit.configuration.logLevel = .all

XCTAssertEqual(ShopifyCheckoutKit.configuration.logLevel, .all)
XCTAssertEqual(OSLogger.shared.logLevel, .all)
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
@testable import ShopifyCheckoutKit
import XCTest

final class LockedValueTests: XCTestCase {
func testGetReturnsInitialValue() {
let value = LockedValue("initial")

XCTAssertEqual(value.get(), "initial")
}

func testSetReplacesStoredValue() {
let value = LockedValue("initial")

value.set("updated")

XCTAssertEqual(value.get(), "updated")
}

func testUpdateMutatesStoredValue() {
let value = LockedValue(["initial"])

value.update { storedValue in
storedValue.append("updated")
}

XCTAssertEqual(value.get(), ["initial", "updated"])
}

func testUpdateSerializesConcurrentMutations() {
let value = LockedValue(0)
let iterations = 1000

DispatchQueue.concurrentPerform(iterations: iterations) { _ in
value.update { storedValue in
storedValue += 1
}
}

XCTAssertEqual(value.get(), iterations)
}
}
Loading
Loading