diff --git a/Package.swift b/Package.swift index 906d33d..0d94515 100644 --- a/Package.swift +++ b/Package.swift @@ -1,4 +1,4 @@ -// swift-tools-version:5.9 +// swift-tools-version:6.0 // // This source file is part of the Stanford Spezi open-source project @@ -12,13 +12,6 @@ import class Foundation.ProcessInfo import PackageDescription -#if swift(<6) -let swiftConcurrency: SwiftSetting = .enableExperimentalFeature("StrictConcurrency") -#else -let swiftConcurrency: SwiftSetting = .enableUpcomingFeature("StrictConcurrency") -#endif - - let package = Package( name: "SpeziOnboarding", defaultLocalization: "en", @@ -47,7 +40,6 @@ let package = Package( .product(name: "TPPDF", package: "TPPDF") ], swiftSettings: [ - swiftConcurrency, .enableUpcomingFeature("ExistentialAny") ], plugins: [] + swiftLintPlugin() @@ -61,7 +53,6 @@ let package = Package( .process("Resources/") ], swiftSettings: [ - swiftConcurrency, .enableUpcomingFeature("ExistentialAny") ], plugins: [] + swiftLintPlugin() diff --git a/Sources/SpeziOnboarding/ConsentConstraint.swift b/Sources/SpeziOnboarding/ConsentConstraint.swift index 5fc7b9f..b2f9b3b 100644 --- a/Sources/SpeziOnboarding/ConsentConstraint.swift +++ b/Sources/SpeziOnboarding/ConsentConstraint.swift @@ -17,5 +17,5 @@ public protocol ConsentConstraint: Standard { /// /// - Parameters: /// - consent: The exported consent form represented as `ConsentDocumentExport` that should be added. - func store(consent: ConsentDocumentExport) async throws + func store(consent: consuming sending ConsentDocumentExport) async throws } diff --git a/Sources/SpeziOnboarding/ConsentView/ConsentDocument+ExportConfiguration.swift b/Sources/SpeziOnboarding/ConsentView/ConsentDocument+ExportConfiguration.swift index 8237d3e..51a9732 100644 --- a/Sources/SpeziOnboarding/ConsentView/ConsentDocument+ExportConfiguration.swift +++ b/Sources/SpeziOnboarding/ConsentView/ConsentDocument+ExportConfiguration.swift @@ -69,23 +69,23 @@ extension ConsentDocument { /// - headerTitleFont: The font used for the header title. /// - headerExportTimeStampFont: The font used for the header timestamp. public init( - signatureNameFont: UIFont, - signaturePrefixFont: UIFont, - documentContentFont: UIFont, - headerTitleFont: UIFont, - headerExportTimeStampFont: UIFont - ) { - self.signatureNameFont = signatureNameFont - self.signaturePrefixFont = signaturePrefixFont - self.documentContentFont = documentContentFont - self.headerTitleFont = headerTitleFont - self.headerExportTimeStampFont = headerExportTimeStampFont - } + signatureNameFont: UIFont, + signaturePrefixFont: UIFont, + documentContentFont: UIFont, + headerTitleFont: UIFont, + headerExportTimeStampFont: UIFont + ) { + self.signatureNameFont = signatureNameFont + self.signaturePrefixFont = signaturePrefixFont + self.documentContentFont = documentContentFont + self.headerTitleFont = headerTitleFont + self.headerExportTimeStampFont = headerExportTimeStampFont + } } #else /// The ``FontSettings`` store configuration of the fonts used to render the exported /// consent document, i.e., fonts for the content, title and signature. - public struct FontSettings { + public struct FontSettings: @unchecked Sendable { /// The font of the name rendered below the signature line. public let signatureNameFont: NSFont /// The font of the prefix of the signature ("X" in most cases). @@ -107,18 +107,18 @@ extension ConsentDocument { /// - headerTitleFont: The font used for the header title. /// - headerExportTimeStampFont: The font used for the header timestamp. public init( - signatureNameFont: NSFont, - signaturePrefixFont: NSFont, - documentContentFont: NSFont, - headerTitleFont: NSFont, - headerExportTimeStampFont: NSFont - ) { - self.signatureNameFont = signatureNameFont - self.signaturePrefixFont = signaturePrefixFont - self.documentContentFont = documentContentFont - self.headerTitleFont = headerTitleFont - self.headerExportTimeStampFont = headerExportTimeStampFont - } + signatureNameFont: NSFont, + signaturePrefixFont: NSFont, + documentContentFont: NSFont, + headerTitleFont: NSFont, + headerExportTimeStampFont: NSFont + ) { + self.signatureNameFont = signatureNameFont + self.signaturePrefixFont = signaturePrefixFont + self.documentContentFont = documentContentFont + self.headerTitleFont = headerTitleFont + self.headerExportTimeStampFont = headerExportTimeStampFont + } } #endif @@ -127,7 +127,7 @@ extension ConsentDocument { let paperSize: PaperSize let includingTimestamp: Bool let fontSettings: FontSettings - + /// Creates an `ExportConfiguration` specifying the properties of the exported consent form. /// - Parameters: diff --git a/Sources/SpeziOnboarding/ConsentView/ConsentDocument.swift b/Sources/SpeziOnboarding/ConsentView/ConsentDocument.swift index 0e8fc66..2148080 100644 --- a/Sources/SpeziOnboarding/ConsentView/ConsentDocument.swift +++ b/Sources/SpeziOnboarding/ConsentView/ConsentDocument.swift @@ -47,7 +47,7 @@ public struct ConsentDocument: View { private let familyNamePlaceholder: LocalizedStringResource let documentExport: ConsentDocumentExport - + @Environment(\.colorScheme) var colorScheme @State var name = PersonNameComponents() #if !os(macOS) @@ -66,7 +66,7 @@ public struct ConsentDocument: View { #if !os(macOS) nameInputView #else - // Need to wrap the `NameFieldRow` from SpeziViews into a SwiftUI `Form, otherwise the Label is omitted + // Need to wrap the `NameFieldRow` from SpeziViews into a SwiftUI `Form`, otherwise the Label is omitted Form { nameInputView } diff --git a/Sources/SpeziOnboarding/ConsentView/ConsentDocumentExport+Export.swift b/Sources/SpeziOnboarding/ConsentView/ConsentDocumentExport+Export.swift index bd069d0..54b335a 100644 --- a/Sources/SpeziOnboarding/ConsentView/ConsentDocumentExport+Export.swift +++ b/Sources/SpeziOnboarding/ConsentView/ConsentDocumentExport+Export.swift @@ -11,6 +11,7 @@ import PencilKit import SwiftUI import TPPDF + /// Extension of `ConsentDocumentExport` enabling the export of the signed consent page. extension ConsentDocumentExport { /// Generates a `PDFAttributedText` containing the timestamp of the time at which the PDF was exported. diff --git a/Sources/SpeziOnboarding/ConsentView/ConsentDocumentExport.swift b/Sources/SpeziOnboarding/ConsentView/ConsentDocumentExport.swift index 462c354..f5bc8ea 100644 --- a/Sources/SpeziOnboarding/ConsentView/ConsentDocumentExport.swift +++ b/Sources/SpeziOnboarding/ConsentView/ConsentDocumentExport.swift @@ -10,12 +10,13 @@ import PencilKit import SwiftUI + /// A type representing an exported `ConsentDocument`. It holds the exported `PDFDocument` and the corresponding document identifier String. -@Observable -public final class ConsentDocumentExport: Equatable, Sendable { +public final class ConsentDocumentExport: Equatable { /// Provides default values for fields related to the `ConsentDocumentExport`. public enum Defaults { /// Default value for a document identifier. + /// /// This identifier will be used as default value if no identifier is provided. public static let documentIdentifier = "ConsentDocument" } @@ -25,9 +26,10 @@ public final class ConsentDocumentExport: Equatable, Sendable { var cachedPDF: PDFDocument? /// An unique identifier for the exported `ConsentDocument`. + /// /// Corresponds to the identifier which was passed when creating the `ConsentDocument` using an `OnboardingConsentView`. public let documentIdentifier: String - + /// The name of the person which signed the document. public var name = PersonNameComponents() #if !os(macOS) @@ -40,48 +42,48 @@ public final class ConsentDocumentExport: Equatable, Sendable { public var signature = String() #endif - /// The `PDFDocument` exported from a `ConsentDocument`. - /// This property is asynchronous and accesing it potentially triggers the export of the PDF from the underlying `ConsentDocument`, - /// if the `ConsentDocument` has not been previously exported or the `PDFDocument` was not cached. - /// For now, we always require a PDF to be cached to create a ConsentDocumentExport. In the future, we might change this to lazy-PDF loading. - @MainActor public var pdf: PDFDocument { - get async { - if let pdf = cachedPDF { - return pdf - } - - guard let pdf = try? await export() else { - return .init() - } - - cachedPDF = pdf - return pdf - } - } - - + /// Creates a `ConsentDocumentExport`, which holds an exported PDF and the corresponding document identifier string. /// - Parameters: /// - markdown: The markdown text for the document, which is shown to the user. - /// - documentIdentfier: A unique String identifying the exported `ConsentDocument`. /// - exportConfiguration: The `ExportConfiguration` holding the properties of the document. + /// - documentIdentifier: A unique String identifying the exported `ConsentDocument`. /// - cachedPDF: A `PDFDocument` exported from a `ConsentDocument`. init( markdown: @escaping () async -> Data, exportConfiguration: ConsentDocument.ExportConfiguration, documentIdentifier: String, - cachedPDF: PDFDocument? = nil + cachedPDF: sending PDFDocument? = nil ) { self.asyncMarkdown = markdown self.exportConfiguration = exportConfiguration self.documentIdentifier = documentIdentifier self.cachedPDF = cachedPDF } - + + public static func == (lhs: ConsentDocumentExport, rhs: ConsentDocumentExport) -> Bool { lhs.documentIdentifier == rhs.documentIdentifier && lhs.name == rhs.name && lhs.signature == rhs.signature && lhs.cachedPDF == rhs.cachedPDF } + + + /// Consume the exported `PDFDocument` from a `ConsentDocument`. + /// + /// This method consumes the `ConsentDocumentExport/cachedPDF` by retrieving the exported `PDFDocument`. + /// + /// - Note: For now, we always require a PDF to be cached to create a `ConsentDocumentExport`. In the future, we might change this to lazy-PDF loading. + public consuming func consumePDF() -> sending PDFDocument { + // Accessing `cachedPDF` via `take()` ensures single consumption of the `PDFDocument` by transferring ownership + // from the enclosing class and leaving `nil` behind after the access. Though `ConsentDocumentExport` is a reference + // type, this manual ownership model guarantees the PDF is only used once, enabling safe cross-concurrency transfer. + // The explicit `sending` return type reinforces transfer semantics, while `take()` enforces single-access at runtime. + // This pattern provides compiler-verifiable safety for the `PDFDocument` transfer despite the class's reference semantics. + // + // See similar discussion: https://forums.swift.org/t/swift-6-consume-optional-noncopyable-property-and-transfer-sending-it-out/72414/3 + nonisolated(unsafe) let cachedPDF = cachedPDF.take() ?? .init() + return cachedPDF + } } diff --git a/Sources/SpeziOnboarding/ConsentView/ExportConfiguration+Defaults.swift b/Sources/SpeziOnboarding/ConsentView/ExportConfiguration+Defaults.swift index bac4249..b084a1b 100644 --- a/Sources/SpeziOnboarding/ConsentView/ExportConfiguration+Defaults.swift +++ b/Sources/SpeziOnboarding/ConsentView/ExportConfiguration+Defaults.swift @@ -9,55 +9,56 @@ import Foundation import SwiftUI + extension ConsentDocument.ExportConfiguration { /// Provides default values for fields related to the `ConsentDocumentExportConfiguration`. public enum Defaults { - #if !os(macOS) - /// Default export font settings with fixed font sizes, ensuring a consistent appearance across platforms. - /// - /// This configuration uses `systemFont` and `boldSystemFont` with absolute font sizes to achieve uniform font sizes - /// on different operating systems such as macOS, iOS, and visionOS. - public static let defaultExportFontSettings = FontSettings( - signatureNameFont: UIFont.systemFont(ofSize: 10), - signaturePrefixFont: UIFont.boldSystemFont(ofSize: 12), - documentContentFont: UIFont.systemFont(ofSize: 12), - headerTitleFont: UIFont.boldSystemFont(ofSize: 28), - headerExportTimeStampFont: UIFont.systemFont(ofSize: 8) - ) - - /// Default font based on system standards. In contrast to defaultExportFontSettings, - /// the font sizes might change according to the system settings, potentially leading to varying exported PDF documents - /// on devices with different system settings (e.g., larger default font size). - public static let defaultSystemDefaultFontSettings = FontSettings( - signatureNameFont: UIFont.preferredFont(forTextStyle: .subheadline), - signaturePrefixFont: UIFont.preferredFont(forTextStyle: .title2), - documentContentFont: UIFont.preferredFont(forTextStyle: .body), - headerTitleFont: UIFont.boldSystemFont(ofSize: UIFont.preferredFont(forTextStyle: .largeTitle).pointSize), - headerExportTimeStampFont: UIFont.preferredFont(forTextStyle: .caption1) - ) - #else - /// Default export font settings with fixed font sizes, ensuring a consistent appearance across platforms. - /// - /// This configuration uses `systemFont` and `boldSystemFont` with absolute font sizes to achieve uniform font sizes - /// on different operating systems such as macOS, iOS, and visionOS. - public static let defaultExportFontSettings = FontSettings( - signatureNameFont: NSFont.systemFont(ofSize: 10), - signaturePrefixFont: NSFont.boldSystemFont(ofSize: 12), - documentContentFont: NSFont.systemFont(ofSize: 12), - headerTitleFont: NSFont.boldSystemFont(ofSize: 28), - headerExportTimeStampFont: NSFont.systemFont(ofSize: 8) - ) - - /// Default font based on system standards. In contrast to defaultExportFontSettings, - /// the font sizes might change according to the system settings, potentially leading to varying exported PDF documents - /// on devices with different system settings (e.g., larger default font size). - public static let defaultSystemDefaultFontSettings = FontSettings( - signatureNameFont: NSFont.preferredFont(forTextStyle: .subheadline), - signaturePrefixFont: NSFont.preferredFont(forTextStyle: .title2), - documentContentFont: NSFont.preferredFont(forTextStyle: .body), - headerTitleFont: NSFont.boldSystemFont(ofSize: NSFont.preferredFont(forTextStyle: .largeTitle).pointSize), - headerExportTimeStampFont: NSFont.preferredFont(forTextStyle: .caption1) - ) - #endif - } + #if !os(macOS) + /// Default export font settings with fixed font sizes, ensuring a consistent appearance across platforms. + /// + /// This configuration uses `systemFont` and `boldSystemFont` with absolute font sizes to achieve uniform font sizes + /// on different operating systems such as macOS, iOS, and visionOS. + public static let defaultExportFontSettings = FontSettings( + signatureNameFont: UIFont.systemFont(ofSize: 10), + signaturePrefixFont: UIFont.boldSystemFont(ofSize: 12), + documentContentFont: UIFont.systemFont(ofSize: 12), + headerTitleFont: UIFont.boldSystemFont(ofSize: 28), + headerExportTimeStampFont: UIFont.systemFont(ofSize: 8) + ) + + /// Default font based on system standards. In contrast to defaultExportFontSettings, + /// the font sizes might change according to the system settings, potentially leading to varying exported PDF documents + /// on devices with different system settings (e.g., larger default font size). + public static let defaultSystemDefaultFontSettings = FontSettings( + signatureNameFont: UIFont.preferredFont(forTextStyle: .subheadline), + signaturePrefixFont: UIFont.preferredFont(forTextStyle: .title2), + documentContentFont: UIFont.preferredFont(forTextStyle: .body), + headerTitleFont: UIFont.boldSystemFont(ofSize: UIFont.preferredFont(forTextStyle: .largeTitle).pointSize), + headerExportTimeStampFont: UIFont.preferredFont(forTextStyle: .caption1) + ) + #else + /// Default export font settings with fixed font sizes, ensuring a consistent appearance across platforms. + /// + /// This configuration uses `systemFont` and `boldSystemFont` with absolute font sizes to achieve uniform font sizes + /// on different operating systems such as macOS, iOS, and visionOS. + public static let defaultExportFontSettings = FontSettings( + signatureNameFont: NSFont.systemFont(ofSize: 10), + signaturePrefixFont: NSFont.boldSystemFont(ofSize: 12), + documentContentFont: NSFont.systemFont(ofSize: 12), + headerTitleFont: NSFont.boldSystemFont(ofSize: 28), + headerExportTimeStampFont: NSFont.systemFont(ofSize: 8) + ) + + /// Default font based on system standards. In contrast to defaultExportFontSettings, + /// the font sizes might change according to the system settings, potentially leading to varying exported PDF documents + /// on devices with different system settings (e.g., larger default font size). + public static let defaultSystemDefaultFontSettings = FontSettings( + signatureNameFont: NSFont.preferredFont(forTextStyle: .subheadline), + signaturePrefixFont: NSFont.preferredFont(forTextStyle: .title2), + documentContentFont: NSFont.preferredFont(forTextStyle: .body), + headerTitleFont: NSFont.boldSystemFont(ofSize: NSFont.preferredFont(forTextStyle: .largeTitle).pointSize), + headerExportTimeStampFont: NSFont.preferredFont(forTextStyle: .caption1) + ) + #endif + } } diff --git a/Sources/SpeziOnboarding/ConsentView/OnboardingConsentView+ShareSheet.swift b/Sources/SpeziOnboarding/ConsentView/OnboardingConsentView+ShareSheet.swift index e84f1e7..1fecf3e 100644 --- a/Sources/SpeziOnboarding/ConsentView/OnboardingConsentView+ShareSheet.swift +++ b/Sources/SpeziOnboarding/ConsentView/OnboardingConsentView+ShareSheet.swift @@ -46,6 +46,7 @@ extension OnboardingConsentView { let sharedItem: PDFDocument + @MainActor func show() { // Note: Need to write down the PDF to storage as in-memory PDFs are not recognized properly let temporaryPath = FileManager.default.temporaryDirectory.appendingPathComponent( diff --git a/Sources/SpeziOnboarding/OnboardingConstraint.swift b/Sources/SpeziOnboarding/OnboardingConstraint.swift index 3d14dec..346634c 100644 --- a/Sources/SpeziOnboarding/OnboardingConstraint.swift +++ b/Sources/SpeziOnboarding/OnboardingConstraint.swift @@ -33,5 +33,5 @@ public protocol OnboardingConstraint: Standard { Please use `ConsentConstraint.store(consent: PDFDocument, identifier: String)` instead. """ ) - func store(consent: PDFDocument) async + func store(consent: sending PDFDocument) async } diff --git a/Sources/SpeziOnboarding/OnboardingDataSource.swift b/Sources/SpeziOnboarding/OnboardingDataSource.swift index e78e330..9cc9fb4 100644 --- a/Sources/SpeziOnboarding/OnboardingDataSource.swift +++ b/Sources/SpeziOnboarding/OnboardingDataSource.swift @@ -11,6 +11,11 @@ import Spezi import SwiftUI +private protocol DeprecationSuppression { + func storeInLegacyConstraint(for standard: any Standard, _ consent: sending ConsentDocumentExport) async +} + + /// Configuration for the Spezi Onboarding module. /// /// Make sure that your standard in your Spezi Application conforms to the ``OnboardingConstraint`` @@ -33,13 +38,14 @@ import SwiftUI /// } /// } /// ``` -public class OnboardingDataSource: Module, EnvironmentAccessible { +public final class OnboardingDataSource: Module, EnvironmentAccessible, @unchecked Sendable { @StandardActor var standard: any Standard public init() { } + @available(*, deprecated, message: "Propagate deprecation warning") public func configure() { guard standard is any OnboardingConstraint || standard is any ConsentConstraint else { fatalError("A \(type(of: standard).self) must conform to `ConsentConstraint` to process signed consent documents.") @@ -60,7 +66,7 @@ public class OnboardingDataSource: Module, EnvironmentAccessible { by using `ConsentConstraint.store(_ consent: ConsentDocumentExport)` instead. """ ) - public func store(_ consent: PDFDocument, identifier: String = ConsentDocumentExport.Defaults.documentIdentifier) async throws { + public func store(_ consent: sending PDFDocument, identifier: String = ConsentDocumentExport.Defaults.documentIdentifier) async throws { // Normally, the ConsentDocumentExport stores all data relevant to generate the PDFDocument, such as the data and ExportConfiguration. // Since we can not determine the original data and the ExportConfiguration at this point, we simply use some placeholder data // to generate the ConsentDocumentExport. @@ -78,13 +84,26 @@ public class OnboardingDataSource: Module, EnvironmentAccessible { /// Adds a new exported consent form represented as `PDFDocument` to the ``OnboardingDataSource``. /// - /// - Parameter consent: The exported consent form represented as `ConsentDocumentExport` that should be added. - public func store(_ consent: ConsentDocumentExport) async throws { + /// - Parameters: + /// - consent: The exported consent form represented as `ConsentDocumentExport` that should be added. + /// - identifier: The document identifier for the exported consent document. + public func store(_ consent: sending ConsentDocumentExport, identifier: String = ConsentDocumentExport.Defaults.documentIdentifier) async throws { if let consentConstraint = standard as? any ConsentConstraint { try await consentConstraint.store(consent: consent) - } else if let onboardingConstraint = standard as? any OnboardingConstraint { - let pdf = try await consent.pdf - await onboardingConstraint.store(consent: pdf) + } else { + // By down-casting to the protocol we avoid "seeing" the deprecation warning, allowing us to hide it from the compiler. + // We need to call the deprecated symbols for backwards-compatibility. + await (self as any DeprecationSuppression).storeInLegacyConstraint(for: standard, consent) + } + } +} + + +extension OnboardingDataSource: DeprecationSuppression { + @available(*, deprecated, message: "Suppress deprecation warning.") + func storeInLegacyConstraint(for standard: any Standard, _ consent: sending ConsentDocumentExport) async { + if let onboardingConstraint = standard as? any OnboardingConstraint { + await onboardingConstraint.store(consent: consent.consumePDF()) } else { fatalError("A \(type(of: standard).self) must conform to `ConsentConstraint` to process signed consent documents.") } diff --git a/Tests/UITests/TestApp/ExampleStandard.swift b/Tests/UITests/TestApp/ExampleStandard.swift index 5102dc8..b41c4d2 100644 --- a/Tests/UITests/TestApp/ExampleStandard.swift +++ b/Tests/UITests/TestApp/ExampleStandard.swift @@ -12,34 +12,36 @@ import SpeziOnboarding import SwiftUI -/// An example Standard used for the configuration. +/// An example `Standard` used for the configuration. actor ExampleStandard: Standard, EnvironmentAccessible { - @Published @MainActor var firstConsentData: PDFDocument = .init() - @Published @MainActor var secondConsentData: PDFDocument = .init() + @MainActor var firstConsentData: PDFDocument = .init() + @MainActor var secondConsentData: PDFDocument = .init() } extension ExampleStandard: ConsentConstraint { // Example of an async function using MainActor and Task - func store(consent: ConsentDocumentExport) async throws { - // Extract data outside of the MainActor.run block - let documentIdentifier = await consent.documentIdentifier - let pdf = try await consent.pdf - + func store(consent: consuming sending ConsentDocumentExport) async throws { + let documentIdentifier = consent.documentIdentifier + let pdf = consent.consumePDF() + // Perform operations on the main actor - try await MainActor.run { - if documentIdentifier == DocumentIdentifiers.first { - self.firstConsentData = pdf - } else if documentIdentifier == DocumentIdentifiers.second { - self.secondConsentData = pdf - } else { - throw ConsentStoreError.invalidIdentifier("Invalid Identifier \(documentIdentifier)") - } + try await self.store(document: pdf, for: documentIdentifier) + + try? await Task.sleep(for: .seconds(0.5)) // Simulates storage delay + } + + @MainActor + func store(document pdf: sending PDFDocument, for documentIdentifier: String) throws { + if documentIdentifier == DocumentIdentifiers.first { + self.firstConsentData = pdf + } else if documentIdentifier == DocumentIdentifiers.second { + self.secondConsentData = pdf + } else { + throw ConsentStoreError.invalidIdentifier("Invalid Identifier \(documentIdentifier)") } - - try? await Task.sleep(for: .seconds(0.5)) } - + func resetDocument(identifier: String) async throws { await MainActor.run { if identifier == DocumentIdentifiers.first { @@ -49,12 +51,13 @@ extension ExampleStandard: ConsentConstraint { } } } - + + @MainActor func loadConsentDocument(identifier: String) async throws -> PDFDocument? { if identifier == DocumentIdentifiers.first { - return await self.firstConsentData + return self.firstConsentData } else if identifier == DocumentIdentifiers.second { - return await self.secondConsentData + return self.secondConsentData } // In case an invalid identifier is provided, return nil.