diff --git a/RevenueCat.xcodeproj/project.pbxproj b/RevenueCat.xcodeproj/project.pbxproj index 68a223ebdf..2f800d0329 100644 --- a/RevenueCat.xcodeproj/project.pbxproj +++ b/RevenueCat.xcodeproj/project.pbxproj @@ -268,6 +268,8 @@ 4FA696BD2A0020A000D228B1 /* MainThreadMonitor.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4FA696BC2A0020A000D228B1 /* MainThreadMonitor.swift */; }; 4FB3FE132A38CB1F004789C6 /* SignatureVerificationIntegrationTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4FB3FE122A38CB1F004789C6 /* SignatureVerificationIntegrationTests.swift */; }; 4FBBC5682A61E42F0077281F /* NonEmptyStringDecodable.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4FBBC5672A61E42F0077281F /* NonEmptyStringDecodable.swift */; }; + 4FBBD4E42A620539001CBA21 /* PaywallColorTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4FBBD4E32A620539001CBA21 /* PaywallColorTests.swift */; }; + 4FBBD4E62A620573001CBA21 /* PaywallColor.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4FBBD4E52A620573001CBA21 /* PaywallColor.swift */; }; 4FC083292A4A35FB00A97089 /* Integer+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4FC083282A4A35FB00A97089 /* Integer+Extensions.swift */; }; 4FC0832B2A4A361700A97089 /* IntegerExtensionsTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4FC0832A2A4A361700A97089 /* IntegerExtensionsTests.swift */; }; 4FCBA84F2A15391B004134BD /* SnapshotTesting+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 576C8A9127D27DDD0058FA6E /* SnapshotTesting+Extensions.swift */; }; @@ -981,6 +983,8 @@ 4FA696BC2A0020A000D228B1 /* MainThreadMonitor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MainThreadMonitor.swift; sourceTree = ""; }; 4FB3FE122A38CB1F004789C6 /* SignatureVerificationIntegrationTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SignatureVerificationIntegrationTests.swift; sourceTree = ""; }; 4FBBC5672A61E42F0077281F /* NonEmptyStringDecodable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NonEmptyStringDecodable.swift; sourceTree = ""; }; + 4FBBD4E32A620539001CBA21 /* PaywallColorTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PaywallColorTests.swift; sourceTree = ""; }; + 4FBBD4E52A620573001CBA21 /* PaywallColor.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PaywallColor.swift; sourceTree = ""; }; 4FC083282A4A35FB00A97089 /* Integer+Extensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Integer+Extensions.swift"; sourceTree = ""; }; 4FC0832A2A4A361700A97089 /* IntegerExtensionsTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = IntegerExtensionsTests.swift; sourceTree = ""; }; 4FCBA8522A1539D0004134BD /* __Snapshots__ */ = {isa = PBXFileReference; lastKnownFileType = folder; path = __Snapshots__; sourceTree = ""; }; @@ -1639,6 +1643,7 @@ 2DDF41DD24F6F4F9005BC22D /* Mocks */, 35D832FE262FAD6900E60AC5 /* Networking */, 57488B8929CB75520000EE7E /* OfflineEntitlements */, + 4FBBD4E22A620516001CBA21 /* Paywalls */, 354235D624C11160008C84EE /* Purchasing */, 5759B403296DF68D002472D5 /* ReceiptParserTests */, 2DDE559824C8B5D100DCB087 /* Resources */, @@ -2211,6 +2216,7 @@ 4F87610D2A5C9E330006FA14 /* Paywalls */ = { isa = PBXGroup; children = ( + 4FBBD4E52A620573001CBA21 /* PaywallColor.swift */, 4F87610E2A5C9E490006FA14 /* PaywallData.swift */, 4F87612B2A5CAB980006FA14 /* PaywallTemplate.swift */, ); @@ -2225,6 +2231,15 @@ path = Helpers; sourceTree = ""; }; + 4FBBD4E22A620516001CBA21 /* Paywalls */ = { + isa = PBXGroup; + children = ( + 4F05876E2A5DE03F00E9A834 /* PaywallDataTests.swift */, + 4FBBD4E32A620539001CBA21 /* PaywallColorTests.swift */, + ); + path = Paywalls; + sourceTree = ""; + }; 4FCEEA5F2A379CD6002C2112 /* Support */ = { isa = PBXGroup; children = ( @@ -2360,7 +2375,6 @@ 574A2F3E282D75E300150D40 /* OfferingsDecodingTests.swift */, 574A2F4E282D7B9E00150D40 /* PostOfferDecodingTests.swift */, 5766C61F282DA3D50067D886 /* GetIntroEligibilityDecodingTests.swift */, - 4F05876E2A5DE03F00E9A834 /* PaywallDataTests.swift */, 57045B3729C514A8001A5417 /* ProductEntitlementMappingDecodingTests.swift */, ); path = Responses; @@ -3409,6 +3423,7 @@ 9A65DFDE258AD60A00DE00B0 /* LogIntent.swift in Sources */, B37815492857F1E7000A7B93 /* BackendConfiguration.swift in Sources */, 57045B3C29C51AF7001A5417 /* ProductEntitlementMappingCallback.swift in Sources */, + 4FBBD4E62A620573001CBA21 /* PaywallColor.swift in Sources */, B35042C626CDD3B100905B95 /* PurchasesDelegate.swift in Sources */, 0313FD41268A506400168386 /* DateProvider.swift in Sources */, 57FDAABA284937A0009A48F1 /* SandboxEnvironmentDetector.swift in Sources */, @@ -3490,6 +3505,7 @@ 57E415EB2846962500EA5460 /* PurchasesSyncPurchasesTests.swift in Sources */, 5766AAC5283E843300FA6091 /* PurchasesConfiguringTests.swift in Sources */, 351B514D26D44A8600BD2BD7 /* MockHTTPClient.swift in Sources */, + 4FBBD4E42A620539001CBA21 /* PaywallColorTests.swift in Sources */, 5733B1AA27FFBCF900EC2045 /* BaseErrorTests.swift in Sources */, 578FB10E27ADDA8000F70709 /* AvailabilityChecks.swift in Sources */, 57E415EF284697A300EA5460 /* PurchasesDeferredPurchasesTests.swift in Sources */, diff --git a/RevenueCatUI/Helpers/ColorInformation+MultiScheme.swift b/RevenueCatUI/Helpers/ColorInformation+MultiScheme.swift new file mode 100644 index 0000000000..86d21a55d7 --- /dev/null +++ b/RevenueCatUI/Helpers/ColorInformation+MultiScheme.swift @@ -0,0 +1,46 @@ +// +// ColorInformation+MultiScheme.swift +// +// +// Created by Nacho Soto on 7/14/23. +// + +import Foundation +import RevenueCat + +#if canImport(SwiftUI) && canImport(UIKit) + +extension PaywallData.Configuration.ColorInformation { + + /// - Returns: `PaywallData.Configuration.Colors` combining `light` and `dark` if they're available + @available(iOS 14.0, macOS 11.0, tvOS 14.0, watchOS 7.0, *) + var multiScheme: PaywallData.Configuration.Colors { + let light = self.light + guard let dark = self.dark else { + // With no dark information, simply use `light`. + return light + } + + return .init( + background: .init(light: light.background, dark: dark.background), + foreground: .init(light: light.foreground, dark: dark.foreground), + callToActionBackground: .init(light: light.callToActionBackground, dark: dark.callToActionBackground), + callToActionForeground: .init(light: light.callToActionForeground, dark: dark.callToActionForeground) + ) + } + +} + +#else + +extension PaywallData.Configuration.ColorInformation { + + /// - Returns: `light` colors for platforms that don't support dark mode. + @available(iOS 14.0, macOS 11.0, tvOS 14.0, watchOS 7.0, *) + var multiScheme: PaywallData.Configuration.Colors { + return self.light + } + +} + +#endif diff --git a/RevenueCatUI/TestData.swift b/RevenueCatUI/TestData.swift index fed713cdd0..e9371ae09b 100644 --- a/RevenueCatUI/TestData.swift +++ b/RevenueCatUI/TestData.swift @@ -66,7 +66,8 @@ internal enum TestData { template: .example1, config: .init( packages: [.monthly], - headerImageName: Self.paywallHeaderImageName + headerImageName: Self.paywallHeaderImageName, + colors: .init(light: Self.lightColors) ), localization: Self.localization, assetBaseURL: Self.paywallAssetBaseURL @@ -75,7 +76,8 @@ internal enum TestData { template: .example1, config: .init( packages: [.annual], - headerImageName: Self.paywallHeaderImageName + headerImageName: Self.paywallHeaderImageName, + colors: .init(light: Self.lightColors) ), localization: Self.localization, assetBaseURL: Self.paywallAssetBaseURL @@ -97,6 +99,13 @@ internal enum TestData { availablePackages: Self.packages ) + static let lightColors: PaywallData.Configuration.Colors = .init( + background: "#000000", + foreground: "#FF0000", + callToActionBackground: "#FF0AB1", + callToActionForeground: "#FF0000" + ) + static let customerInfo: CustomerInfo = { let json = """ { @@ -191,4 +200,15 @@ extension PurchaseHandler { } } +extension PaywallColor: ExpressibleByStringLiteral { + + /// Creates a `PaywallColor` with a string literal + /// - Warning: This will crash at runtime if the string is invalid. Only for debugging purposes. + public init(stringLiteral value: StringLiteralType) { + // swiftlint:disable:next force_try + try! self.init(stringRepresentation: value) + } + +} + #endif diff --git a/Sources/Paywalls/PaywallColor.swift b/Sources/Paywalls/PaywallColor.swift new file mode 100644 index 0000000000..63229d73d5 --- /dev/null +++ b/Sources/Paywalls/PaywallColor.swift @@ -0,0 +1,241 @@ +// +// PaywallColor.swift +// +// +// Created by Nacho Soto on 7/14/23. +// + +import Foundation + +#if canImport(SwiftUI) +import SwiftUI +#endif + +#if canImport(UIKit) +import UIKit +#endif + +// swiftlint:disable redundant_string_enum_value + +/// Represents a color to be used by `RevenueCatUI` +public struct PaywallColor { + + /// The possible color schemes, corresponding to the light and dark appearances. + @frozen + public enum ColorScheme: String { + + /// The color scheme that corresponds to a light appearance. + case light = "light" + /// The color scheme that corresponds to a dark appearance. + case dark = "dark" + + } + + /// The original Hex representation for this color. + public var stringRepresentation: String + + // `Color` is not `Sendable` in Xcode 13. + #if canImport(SwiftUI) && swift(>=5.7) + /// The underlying SwiftUI `Color`. + @available(iOS 13.0, tvOS 13.0, macOS 10.15, watchOS 6.2, *) + public var underlyingColor: Color { + // swiftlint:disable:next force_cast + return self._underlyingColor as! Color + } + #endif + + // Only available from iOS 13 + fileprivate var _underlyingColor: (any Sendable)? + +} + +// MARK: - Public constructors + +extension PaywallColor { + + #if canImport(SwiftUI) && swift(>=5.7) + + /// Creates a color from a Hex string: `#RRGGBB` or `#RRGGBBAA`. + public init(stringRepresentation: String) throws { + if #available(iOS 13.0, tvOS 13.0, macOS 10.15, watchOS 6.2, *) { + self.init(stringRepresentation: stringRepresentation, color: try Self.parseColor(stringRepresentation)) + } else { + // In older devices, `_underlyingColor` will be `nil`, but it also won't be + // accessible through `underlyingColor`. + self.init(stringRepresentation: stringRepresentation, underlyingColor: nil) + } + } + + #if canImport(UIKit) && !os(watchOS) + + /// Creates a dynamic color for 2 ``ColorScheme``s. + @available(iOS 14.0, macOS 11.0, tvOS 14.0, watchOS 7.0, *) + public init(light: PaywallColor, dark: PaywallColor) { + self.init(stringRepresentation: light.stringRepresentation, + color: .init(light: light.underlyingColor, dark: dark.underlyingColor)) + } + + #endif + + #else + + /// Creates a color from a Hex string: `#RRGGBB` or `#RRGGBBAA`. + public init(stringRepresentation: String) throws { + self.init(stringRepresentation: stringRepresentation, underlyingColor: nil) + } + + #endif + +} + +// MARK: - Private constructors + +private extension PaywallColor { + + #if canImport(SwiftUI) && swift(>=5.7) + + @available(iOS 13.0, tvOS 13.0, macOS 10.15, watchOS 6.2, *) + init(stringRepresentation: String, color: Color) { + self.init(stringRepresentation: stringRepresentation, underlyingColor: color) + } + + #endif + + @available(iOS 13.0, tvOS 13.0, macOS 10.15, watchOS 6.2, *) + static func parseColor(_ input: String) throws -> Color { + let red, green, blue, alpha: CGFloat + + guard input.hasPrefix("#") else { + throw Error.invalidStringFormat(input) + } + + let start = input.index(input.startIndex, offsetBy: 1) + let hexColor = String(input[start...]) + + guard hexColor.count == 6 || hexColor.count == 8 else { + throw Error.invalidStringFormat(input) + } + + let scanner = Scanner(string: hexColor) + var hexNumber: UInt64 = 0 + + if scanner.scanHexInt64(&hexNumber) { + // If Alpha channel is missing, it's a fully opaque color. + if hexNumber <= 0xffffff { + hexNumber <<= 8 + hexNumber |= 0xff + } + + red = CGFloat((hexNumber & 0xff000000) >> 24) / 256 + green = CGFloat((hexNumber & 0x00ff0000) >> 16) / 256 + blue = CGFloat((hexNumber & 0x0000ff00) >> 8) / 256 + alpha = CGFloat(hexNumber & 0x000000ff) / 256 + + return .init(red: red, green: green, blue: blue, opacity: alpha) + } else { + throw Error.invalidColor(input) + } + } + + /// "Designated" initializer + private init(stringRepresentation: String, underlyingColor: (any Sendable)?) { + self.stringRepresentation = stringRepresentation + self._underlyingColor = underlyingColor + } + +} + +// MARK: - Errors + +private extension PaywallColor { + + enum Error: Swift.Error { + + case invalidStringFormat(String) + case invalidColor(String) + + } + +} + +// MARK: - Extensions + +#if canImport(UIKit) && !os(watchOS) +private extension UIColor { + + @available(iOS 13.0, tvOS 13.0, macOS 10.15, watchOS 6.2, *) + convenience init(light: UIColor, dark: UIColor) { + self.init { trait in + switch trait.userInterfaceStyle { + case .dark: + return dark + case .light, .unspecified: + fallthrough + @unknown default: + return light + } + } + } + +} +#endif + +#if canImport(SwiftUI) && canImport(UIKit) && !os(watchOS) +@available(iOS 13.0, tvOS 13.0, macOS 10.15, watchOS 6.2, *) +private extension Color { + + init(light: UIColor, dark: UIColor) { + self.init(UIColor(light: light, dark: dark)) + } + +} + +@available(iOS 14.0, macOS 11.0, tvOS 14.0, watchOS 7.0, *) +private extension Color { + + init(light: Color, dark: Color) { + self.init(light: UIColor(light), dark: UIColor(dark)) + } + +} +#endif + +// MARK: - Conformances + +// swiftlint:disable missing_docs + +extension PaywallColor.ColorScheme: Equatable {} +extension PaywallColor.ColorScheme: Sendable {} +extension PaywallColor.ColorScheme: Codable {} + +extension PaywallColor: CustomDebugStringConvertible { + + public var debugDescription: String { + return "\(type(of: self)): \(self.stringRepresentation)" + } + +} + +extension PaywallColor: Equatable { + + public static func == (lhs: PaywallColor, rhs: PaywallColor) -> Bool { + return lhs.stringRepresentation == rhs.stringRepresentation + } + +} + +extension PaywallColor: Sendable {} +extension PaywallColor: Codable { + + public init(from decoder: Decoder) throws { + try self.init(stringRepresentation: decoder.singleValueContainer().decode(String.self)) + } + + public func encode(to encoder: Encoder) throws { + var container = encoder.singleValueContainer() + try container .encode(self.stringRepresentation) + } + +} + +// swiftlint:enable missing_docs diff --git a/Sources/Paywalls/PaywallData.swift b/Sources/Paywalls/PaywallData.swift index db605a8a97..cc149ab48e 100644 --- a/Sources/Paywalls/PaywallData.swift +++ b/Sources/Paywalls/PaywallData.swift @@ -140,14 +140,67 @@ extension PaywallData { /// The name for the header image asset. public var headerImageName: String + /// The set of colors used + public var colors: ColorInformation + // swiftlint:disable:next missing_docs - public init(packages: [PackageType], headerImageName: String) { + public init(packages: [PackageType], headerImageName: String, colors: ColorInformation) { self.packages = packages self.headerImageName = headerImageName + self.colors = colors + } + + } + +} + +extension PaywallData.Configuration { + + /// The set of colors for all ``PaywallColor/ColorScheme``s. + public struct ColorInformation { + + /// Set of colors for ``PaywallColor/ColorScheme/light``. + public var light: Colors + /// Set of colors for ``PaywallColor/ColorScheme/dark``. + public var dark: Colors? + + // swiftlint:disable:next missing_docs + public init( + light: PaywallData.Configuration.Colors, + dark: PaywallData.Configuration.Colors? = nil + ) { + self.light = light + self.dark = dark } } + /// The list of colors for a given appearance (light / dark). + public struct Colors { + + /// Color for the background of the paywall. + public var background: PaywallColor + /// Color for foreground elements. + public var foreground: PaywallColor + /// Background color of the main call to action button. + public var callToActionBackground: PaywallColor + /// Foreground color of the main call to action button. + public var callToActionForeground: PaywallColor + + // swiftlint:disable:next missing_docs + public init( + background: PaywallColor, + foreground: PaywallColor, + callToActionBackground: PaywallColor, + callToActionForeground: PaywallColor + ) { + self.background = background + self.foreground = foreground + self.callToActionBackground = callToActionBackground + self.callToActionForeground = callToActionForeground + } + } + } // MARK: - Extensions @@ -214,11 +267,15 @@ extension PaywallData.LocalizedConfiguration: Codable { } +extension PaywallData.Configuration.ColorInformation: Codable {} +extension PaywallData.Configuration.Colors: Codable {} + extension PaywallData.Configuration: Codable { private enum CodingKeys: String, CodingKey { case packages case headerImageName = "headerImage" + case colors } } @@ -239,12 +296,16 @@ extension PaywallData: Codable { // MARK: - Equatable extension PaywallData.LocalizedConfiguration: Equatable {} +extension PaywallData.Configuration.ColorInformation: Equatable {} +extension PaywallData.Configuration.Colors: Equatable {} extension PaywallData.Configuration: Equatable {} extension PaywallData: Equatable {} // MARK: - Sendable extension PaywallData.LocalizedConfiguration: Sendable {} +extension PaywallData.Configuration.ColorInformation: Sendable {} +extension PaywallData.Configuration.Colors: Sendable {} extension PaywallData.Configuration: Sendable {} #if swift(>=5.7) diff --git a/Tests/APITesters/SwiftAPITester/SwiftAPITester/PaywallAPI.swift b/Tests/APITesters/SwiftAPITester/SwiftAPITester/PaywallAPI.swift index a912afec5c..084f9189ba 100644 --- a/Tests/APITesters/SwiftAPITester/SwiftAPITester/PaywallAPI.swift +++ b/Tests/APITesters/SwiftAPITester/SwiftAPITester/PaywallAPI.swift @@ -8,6 +8,10 @@ import Foundation import RevenueCat +#if canImport(SwiftUI) +import SwiftUI +#endif + func checkPaywallData(_ data: PaywallData) { let template: PaywallTemplate = data.template let config: PaywallData.Configuration = data.config @@ -22,8 +26,9 @@ func checkPaywallData(_ data: PaywallData) { assetBaseURL: assetBaseURL) } -func checkPaywallConfiguration(_ config: PaywallData.Configuration) { - let _: PaywallData.Configuration = .init(packages: [.monthly, .annual], headerImageName: "") +func checkPaywallConfiguration(_ config: PaywallData.Configuration, + _ colors: PaywallData.Configuration.ColorInformation) { + let _: PaywallData.Configuration = .init(packages: [.monthly, .annual], headerImageName: "", colors: colors) let _: [PackageType] = config.packages let _: String = config.headerImageName } @@ -46,6 +51,48 @@ func checkPaywallLocalizedConfig(_ config: PaywallData.LocalizedConfiguration) { ) } +func checkPaywallColors(_ config: PaywallData.Configuration.Colors) { + let background: PaywallColor = config.background + let foreground: PaywallColor = config.foreground + let callToActionBackground: PaywallColor = config.callToActionBackground + let callToActionForeground: PaywallColor = config.callToActionForeground + + _ = PaywallData.Configuration.Colors( + background: background, + foreground: foreground, + callToActionBackground: callToActionBackground, + callToActionForeground: callToActionForeground + ) +} + +func checkPaywallColorInformation(_ config: PaywallData.Configuration.ColorInformation) { + let light: PaywallData.Configuration.Colors = config.light + let dark: PaywallData.Configuration.Colors? = config.dark + + _ = PaywallData.Configuration.ColorInformation( + light: light, + dark: dark + ) +} + +func checkPaywallColor(_ color: PaywallColor) throws { + _ = try PaywallColor(stringRepresentation: "") + + #if canImport(UIKit) && !os(watchOS) && swift(>=5.7) + if #available(iOS 14.0, macOS 11.0, tvOS 14.0, watchOS 7.0, *) { + _ = PaywallColor(light: color, dark: color) + } + #endif + + let _: String = color.debugDescription + let _: String = color.stringRepresentation + #if canImport(SwiftUI) && swift(>=5.7) + if #available(iOS 13.0, tvOS 13.0, macOS 10.15, watchOS 6.2, *) { + let _: Color = color.underlyingColor + } + #endif +} + func checkPaywallTemplate(_ template: PaywallTemplate) { switch template { case .example1: diff --git a/Tests/UnitTests/Networking/Responses/Fixtures/Offerings.json b/Tests/UnitTests/Networking/Responses/Fixtures/Offerings.json index 07fef12625..05366f6ff1 100644 --- a/Tests/UnitTests/Networking/Responses/Fixtures/Offerings.json +++ b/Tests/UnitTests/Networking/Responses/Fixtures/Offerings.json @@ -61,7 +61,16 @@ "default_locale": "en_US", "config": { "packages": ["$rc_monthly", "$rc_annual"], - "header_image": "asset_name" + "header_image": "asset_name", + "colors": { + "light": { + "background": "#FF00AA", + "foreground": "#FF00AA22", + "call_to_action_background": "#FF00AACC", + "call_to_action_foreground": "#FF00AA" + }, + "dark": null + } }, "asset_base_url": "https://rc-paywalls.s3.amazonaws.com" } diff --git a/Tests/UnitTests/Networking/Responses/Fixtures/PaywallData-Sample1.json b/Tests/UnitTests/Networking/Responses/Fixtures/PaywallData-Sample1.json index e362c7854f..3b79df35ca 100644 --- a/Tests/UnitTests/Networking/Responses/Fixtures/PaywallData-Sample1.json +++ b/Tests/UnitTests/Networking/Responses/Fixtures/PaywallData-Sample1.json @@ -20,7 +20,21 @@ "default_locale": "en_US", "config": { "packages": ["$rc_monthly", "$rc_annual"], - "header_image": "asset_name.png" + "header_image": "asset_name.png", + "colors": { + "light": { + "background": "#FF00AA", + "foreground": "#FF00AA22", + "call_to_action_background": "#FF00AACC", + "call_to_action_foreground": "#FF00AA" + }, + "dark": { + "background": "#FF0000", + "foreground": "#1100FFAA", + "call_to_action_background": "#112233AA", + "call_to_action_foreground": "#AABBCC" + } + } }, "asset_base_url": "https://rc-paywalls.s3.amazonaws.com" } diff --git a/Tests/UnitTests/Networking/Responses/Fixtures/PaywallData-missing_current_and_default_locale.json b/Tests/UnitTests/Networking/Responses/Fixtures/PaywallData-missing_current_and_default_locale.json index e1c3f1b869..a94257672c 100644 --- a/Tests/UnitTests/Networking/Responses/Fixtures/PaywallData-missing_current_and_default_locale.json +++ b/Tests/UnitTests/Networking/Responses/Fixtures/PaywallData-missing_current_and_default_locale.json @@ -13,7 +13,16 @@ "default_locale": "es_ES", "config": { "packages": ["$rc_monthly", "$rc_annual"], - "header_image": "asset_name" + "header_image": "asset_name", + "colors": { + "light": { + "background": "#FFFFFF", + "foreground": "#FFFFFF", + "call_to_action_background": "#FFFFFF", + "call_to_action_foreground": "#FFFFFF" + }, + "dark": null + } }, "asset_base_url": "https://rc-paywalls.s3.amazonaws.com" } diff --git a/Tests/UnitTests/Networking/Responses/Fixtures/PaywallData-missing_current_locale.json b/Tests/UnitTests/Networking/Responses/Fixtures/PaywallData-missing_current_locale.json index 8a2b766c72..893ab70547 100644 --- a/Tests/UnitTests/Networking/Responses/Fixtures/PaywallData-missing_current_locale.json +++ b/Tests/UnitTests/Networking/Responses/Fixtures/PaywallData-missing_current_locale.json @@ -13,7 +13,16 @@ "default_locale": "es_ES", "config": { "packages": ["$rc_monthly", "$rc_annual"], - "header_image": "asset_name" + "header_image": "asset_name", + "colors": { + "light": { + "background": "#FFFFFF", + "foreground": "#FFFFFF", + "call_to_action_background": "#FFFFFF", + "call_to_action_foreground": "#FFFFFF" + }, + "dark": null + } }, "asset_base_url": "https://rc-paywalls.s3.amazonaws.com" } diff --git a/Tests/UnitTests/Paywalls/PaywallColorTests.swift b/Tests/UnitTests/Paywalls/PaywallColorTests.swift new file mode 100644 index 0000000000..ecae8b865e --- /dev/null +++ b/Tests/UnitTests/Paywalls/PaywallColorTests.swift @@ -0,0 +1,133 @@ +// +// Copyright RevenueCat Inc. All Rights Reserved. +// +// Licensed under the MIT License (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://opensource.org/licenses/MIT +// +// PaywallColorTests.swift +// +// Created by Nacho Soto on 7/14/23. + +#if canImport(UIKit) && canImport(SwiftUI) && swift(>=5.7) + +import Nimble +@testable import RevenueCat +import SwiftUI +import UIKit + +@available(iOS 14.0, macOS 11.0, tvOS 14.0, watchOS 7.0, *) +final class PaywallColorTests: TestCase { + + override func setUpWithError() throws { + try super.setUpWithError() + + try AvailabilityChecks.iOS14APIAvailableOrSkipTest() + } + + func testCreateWithEmptyStringThrows() { + expect(try PaywallColor(stringRepresentation: "")).to(throwError()) + } + + func testCreateWithInvalidStringsThrows() { + expect(try PaywallColor(stringRepresentation: "#")).to(throwError()) + expect(try PaywallColor(stringRepresentation: "AAAAAA")).to(throwError()) + expect(try PaywallColor(stringRepresentation: "#FFFF")).to(throwError()) + expect(try PaywallColor(stringRepresentation: "FFFFFFFF")).to(throwError()) + } + + func testCreateWithRGB() throws { + try PaywallColor(stringRepresentation: "#FF0000") + .verifyComponents(255, 0, 0, 255) + + try PaywallColor(stringRepresentation: "#AABBCC") + .verifyComponents(170, 187, 204, 255) + } + + func testCreateWithRGBA() throws { + try PaywallColor(stringRepresentation: "#FF0000FF") + .verifyComponents(255, 0, 0, 255) + try PaywallColor(stringRepresentation: "#AABBCC22") + .verifyComponents(170, 187, 204, 34) + } + + func testCodable() throws { + try PaywallColor(stringRepresentation: "#FF0000").verifyCodable() + try PaywallColor(stringRepresentation: "#FF0000FF").verifyCodable() + try PaywallColor(stringRepresentation: "#AABBCC22").verifyCodable() + } + + func testDecodingParsesColor() throws { + try JSONDecoder.default.decode(PaywallColor.self, jsonData: "\"#AABBCC22\"".asData) + .verifyComponents(170, 187, 204, 34) + } + + func testDecodingInvalidColorThrows() throws { + expect(try JSONDecoder.default.decode(PaywallColor.self, jsonData: "\"ABBCC22\"".asData)).to(throwError()) + } + +} + +// MARK: - + +@available(iOS 14.0, macOS 11.0, tvOS 14.0, watchOS 7.0, *) +private extension PaywallColor { + + func verifyCodable( + file: StaticString = #file, + line: UInt = #line + ) throws { + expect( + file: file, + line: line, + try self.encodeAndDecode() + ) == self + } + + func verifyComponents( + _ red: Int, + _ green: Int, + _ blue: Int, + _ alpha: Int, + file: StaticString = #file, + line: UInt = #line + ) { + let components = self.underlyingColor.rgba + + expect( + file: file, + line: line, + components + ) == (red, green, blue, alpha) + } + +} + +@available(iOS 14.0, macOS 11.0, tvOS 14.0, watchOS 7.0, *) +private extension Color { + + var rgba: (red: Int, green: Int, blue: Int, alpha: Int) { + let color = UIColor(self) + + var red: CGFloat = 0 + var green: CGFloat = 0 + var blue: CGFloat = 0 + var alpha: CGFloat = 0 + assert(color.getRed(&red, green: &green, blue: &blue, alpha: &alpha)) + + return (red.rounded, green.rounded, blue.rounded, alpha.rounded) + } + +} + +private extension CGFloat { + + var rounded: Int { + return Int((self * 256).rounded()) + } + +} + +#endif diff --git a/Tests/UnitTests/Networking/Responses/PaywallDataTests.swift b/Tests/UnitTests/Paywalls/PaywallDataTests.swift similarity index 80% rename from Tests/UnitTests/Networking/Responses/PaywallDataTests.swift rename to Tests/UnitTests/Paywalls/PaywallDataTests.swift index 1b99f07e90..c65e750fad 100644 --- a/Tests/UnitTests/Networking/Responses/PaywallDataTests.swift +++ b/Tests/UnitTests/Paywalls/PaywallDataTests.swift @@ -35,6 +35,16 @@ class PaywallDataTests: BaseHTTPResponseTest { expect(paywall.config.packages) == [.monthly, .annual] expect(paywall.config.headerImageName) == "asset_name.png" + expect(paywall.config.colors.light.background.stringRepresentation) == "#FF00AA" + expect(paywall.config.colors.light.foreground.stringRepresentation) == "#FF00AA22" + expect(paywall.config.colors.light.callToActionBackground.stringRepresentation) == "#FF00AACC" + expect(paywall.config.colors.light.callToActionForeground.stringRepresentation) == "#FF00AA" + + expect(paywall.config.colors.dark?.background.stringRepresentation) == "#FF0000" + expect(paywall.config.colors.dark?.foreground.stringRepresentation) == "#1100FFAA" + expect(paywall.config.colors.dark?.callToActionBackground.stringRepresentation) == "#112233AA" + expect(paywall.config.colors.dark?.callToActionForeground.stringRepresentation) == "#AABBCC" + expect(paywall.headerImageURL) == URL(string: "https://rc-paywalls.s3.amazonaws.com/asset_name.png")! let enConfig = try XCTUnwrap(paywall.config(for: Locale(identifier: "en_US")))