From dac9d73d2652d5260512001167f1d025f5105ec8 Mon Sep 17 00:00:00 2001 From: Lucas Fischer Date: Sat, 30 May 2026 15:31:45 +0800 Subject: [PATCH 1/6] add AmoreStore target with `Product` and `Price` --- Sources/AmoreStore/AmoreStore.swift | 45 ++++++++++ Sources/AmoreStore/HTTPProductsClient.swift | 46 ++++++++++ Sources/AmoreStore/Models/Price.swift | 47 ++++++++++ Sources/AmoreStore/Models/Product.swift | 60 +++++++++++++ .../AmoreStore/Models/RecurringInterval.swift | 7 ++ Sources/AmoreStore/ProductsClient.swift | 5 ++ Sources/AmoreStore/StoreError.swift | 22 +++++ Tests/AmoreStoreTests/AmoreStoreTests.swift | 85 +++++++++++++++++++ .../AmoreStoreTests/MockProductsClient.swift | 12 +++ 9 files changed, 329 insertions(+) create mode 100644 Sources/AmoreStore/AmoreStore.swift create mode 100644 Sources/AmoreStore/HTTPProductsClient.swift create mode 100644 Sources/AmoreStore/Models/Price.swift create mode 100644 Sources/AmoreStore/Models/Product.swift create mode 100644 Sources/AmoreStore/Models/RecurringInterval.swift create mode 100644 Sources/AmoreStore/ProductsClient.swift create mode 100644 Sources/AmoreStore/StoreError.swift create mode 100644 Tests/AmoreStoreTests/AmoreStoreTests.swift create mode 100644 Tests/AmoreStoreTests/MockProductsClient.swift diff --git a/Sources/AmoreStore/AmoreStore.swift b/Sources/AmoreStore/AmoreStore.swift new file mode 100644 index 0000000..b067798 --- /dev/null +++ b/Sources/AmoreStore/AmoreStore.swift @@ -0,0 +1,45 @@ +import Foundation + +extension URL { + /// Default Amore licensing server. Internal to `AmoreStore` so it does not + /// collide with the public `URL.amoreServer` declared in `AmoreLicensing`. + static let amoreServer = URL(string: "https://api.amore.computer")! +} + +/// Lists the products configured for an Amore-licensed app. +/// +/// Fetch products with ``products()``, then read ``Product/checkoutURL`` on a +/// returned product to send a customer to Stripe checkout. Use this to build +/// paywalls, pickers, or any view that displays the products and prices +/// configured in the licensing dashboard. +public struct AmoreStore: Sendable { + private let bundleIdentifier: String + private let productsClient: ProductsClient + + /// Creates a store client for the given bundle identifier. + /// - Parameters: + /// - bundleIdentifier: The app's bundle identifier. Defaults to `Bundle.main.bundleIdentifier`. + /// - baseURL: The licensing server base URL. Defaults to the Amore server when `nil`. + public init(bundleIdentifier: String? = nil, baseURL: URL? = nil) { + self.bundleIdentifier = bundleIdentifier ?? Bundle.main.bundleIdentifier ?? "" + self.productsClient = HTTPProductsClient(baseURL: baseURL ?? .amoreServer) + } + + init(bundleIdentifier: String, productsClient: ProductsClient) { + self.bundleIdentifier = bundleIdentifier + self.productsClient = productsClient + } + + /// Returns the products configured for this app. Purchasable products carry a + /// non-`nil` ``Product/checkoutURL``. + /// - Throws: ``StoreError`` if the request fails. + public func products() async throws(StoreError) -> [Product] { + do { + return try await productsClient.fetchProducts(bundleIdentifier: bundleIdentifier) + } catch let error as StoreError { + throw error + } catch { + throw .network(error.localizedDescription) + } + } +} diff --git a/Sources/AmoreStore/HTTPProductsClient.swift b/Sources/AmoreStore/HTTPProductsClient.swift new file mode 100644 index 0000000..c5dd69f --- /dev/null +++ b/Sources/AmoreStore/HTTPProductsClient.swift @@ -0,0 +1,46 @@ +import Foundation + +struct HTTPProductsClient: ProductsClient { + private let baseURL: URL + + init(baseURL: URL) { + self.baseURL = baseURL + } + + func fetchProducts(bundleIdentifier: String) async throws -> [Product] { + let url = baseURL + .appendingPathComponent("v1/public/apps") + .appendingPathComponent(bundleIdentifier) + .appendingPathComponent("products") + + var request = URLRequest(url: url) + request.httpMethod = "GET" + request.setValue("application/json", forHTTPHeaderField: "Accept") + + let data: Data + let response: URLResponse + do { + (data, response) = try await URLSession.shared.data(for: request) + } catch { + throw StoreError.network(error.localizedDescription) + } + + guard let http = response as? HTTPURLResponse else { + throw StoreError.network("Invalid response from server") + } + switch http.statusCode { + case 200...299: + do { + return try JSONDecoder().decode([Product].self, from: data) + } catch { + throw StoreError.network("Could not decode product list: \(error.localizedDescription)") + } + case 404: + throw StoreError.appNotFound + case 429: + throw StoreError.rateLimited + default: + throw StoreError.serverError(statusCode: http.statusCode) + } + } +} diff --git a/Sources/AmoreStore/Models/Price.swift b/Sources/AmoreStore/Models/Price.swift new file mode 100644 index 0000000..4edbbd7 --- /dev/null +++ b/Sources/AmoreStore/Models/Price.swift @@ -0,0 +1,47 @@ +import Foundation + +/// Pricing information for a product as configured in Stripe. +public struct Price: Hashable, Codable, Sendable { + /// Amount in the smallest unit of `currency` (e.g. cents for USD, yen for JPY). + public var unitAmount: Int + /// ISO 4217 currency code (e.g. `"USD"`). + public var currency: String + /// Billing interval for recurring prices, or `nil` for one-time purchases. + public var recurringInterval: RecurringInterval? + + public init(unitAmount: Int, currency: String, recurringInterval: RecurringInterval?) { + self.unitAmount = unitAmount + self.currency = currency + self.recurringInterval = recurringInterval + } +} + +extension Price { + + /// The decimal representation of the cost of the product in ``currency``. + public var decimalAmount: Decimal { + Decimal(unitAmount) / pow(Decimal(10), Self.fractionDigits(for: currency)) + } + + /// The localized string representation of the product price, suitable for display. + public var displayPrice: String { + decimalAmount.formatted(.currency(code: currency)) + } + + /// Minor-unit exponent for `currency` (2 for USD, 0 for JPY, 3 for BHD), + private static func fractionDigits(for currency: String) -> Int { + let formatter = NumberFormatter() + formatter.numberStyle = .currency + formatter.currencyCode = currency + return formatter.maximumFractionDigits + } + +} + +extension Price: CustomStringConvertible { + + public var description: String { + displayPrice + } + +} diff --git a/Sources/AmoreStore/Models/Product.swift b/Sources/AmoreStore/Models/Product.swift new file mode 100644 index 0000000..4b776dd --- /dev/null +++ b/Sources/AmoreStore/Models/Product.swift @@ -0,0 +1,60 @@ +import Foundation + +/// A product offered by an Amore-licensed application. +public struct Product: Identifiable, Hashable, Codable, Sendable { + /// Server identifier for this product. + public var id: UUID + /// Display name configured by the app owner. + public var name: String + /// License duration in seconds, or `nil` for non-expiring licenses. + public var durationInSeconds: Int? + /// Maximum number of devices that can activate a single license for this product. + public var deviceLimit: Int + /// Pricing information, or `nil` when no price is configured. + public var price: Price? + + /// The checkout URL for this product + /// + /// Open it to send the customer to checkout, for example: + /// ```swift + /// NSWorkspace.shared.open(product.checkoutURL) + /// ``` + public var checkoutURL: URL + + public init( + id: UUID, + name: String, + durationInSeconds: Int?, + deviceLimit: Int, + price: Price?, + checkoutURL: URL + ) { + self.id = id + self.name = name + self.durationInSeconds = durationInSeconds + self.deviceLimit = deviceLimit + self.price = price + self.checkoutURL = checkoutURL + } +} + +public extension Product { + + /// The localized string representation of the product price, suitable for display. + var displayPrice: String? { + price?.displayPrice + } + +} + +extension Product: CustomStringConvertible { + + public var description: String { + if let displayPrice { + "\(name) (\(id)): \(displayPrice)" + } else { + "\(name) (\(id))" + } + } + +} diff --git a/Sources/AmoreStore/Models/RecurringInterval.swift b/Sources/AmoreStore/Models/RecurringInterval.swift new file mode 100644 index 0000000..9680750 --- /dev/null +++ b/Sources/AmoreStore/Models/RecurringInterval.swift @@ -0,0 +1,7 @@ +/// Billing intervals supported by Stripe recurring prices. +public enum RecurringInterval: String, Hashable, Codable, Sendable { + case day + case week + case month + case year +} diff --git a/Sources/AmoreStore/ProductsClient.swift b/Sources/AmoreStore/ProductsClient.swift new file mode 100644 index 0000000..4926339 --- /dev/null +++ b/Sources/AmoreStore/ProductsClient.swift @@ -0,0 +1,5 @@ +import Foundation + +protocol ProductsClient: Sendable { + func fetchProducts(bundleIdentifier: String) async throws -> [Product] +} diff --git a/Sources/AmoreStore/StoreError.swift b/Sources/AmoreStore/StoreError.swift new file mode 100644 index 0000000..15e4adf --- /dev/null +++ b/Sources/AmoreStore/StoreError.swift @@ -0,0 +1,22 @@ +import Foundation + +/// Errors thrown by ``AmoreStore/products()``. +public enum StoreError: LocalizedError, Equatable, Sendable { + /// The network request failed (offline, timeout, or an unexpected transport error). + case network(String) + /// The server rejected the request because too many were made in a short window. + case rateLimited + /// No app matches the configured bundle identifier. + case appNotFound + /// The server returned an unexpected status code. + case serverError(statusCode: Int) + + public var errorDescription: String? { + switch self { + case .network(let message): message + case .rateLimited: "Too many requests. Please try again later." + case .appNotFound: "App not found." + case .serverError(let statusCode): "The server returned an unexpected response (\(statusCode))." + } + } +} diff --git a/Tests/AmoreStoreTests/AmoreStoreTests.swift b/Tests/AmoreStoreTests/AmoreStoreTests.swift new file mode 100644 index 0000000..0d712d8 --- /dev/null +++ b/Tests/AmoreStoreTests/AmoreStoreTests.swift @@ -0,0 +1,85 @@ +import Foundation +import Testing + +@testable import AmoreStore + +@Suite struct AmoreStoreTests { + private let bundleId = "com.test.amorekit" + + private func makeStore(client: MockProductsClient = MockProductsClient()) -> (AmoreStore, MockProductsClient) { + (AmoreStore(bundleIdentifier: bundleId, productsClient: client), client) + } + + @Test func returnsParsedProducts() async throws { + let (store, mock) = makeStore() + let expected = Product( + id: UUID(), + name: "Pro", + durationInSeconds: nil, + deviceLimit: 3, + price: Price(unitAmount: 999, currency: "usd", recurringInterval: .month), + checkoutURL: URL(string: "https://api.amore.computer/v1/checkout/\(UUID())")! + ) + mock.onFetch = { id in + #expect(id == "com.test.amorekit") + return [expected] + } + + let result = try await store.products() + + #expect(result == [expected]) + #expect(result.first?.checkoutURL == expected.checkoutURL) + } + + @Test func mapsClientErrorOnProducts() async throws { + let (store, mock) = makeStore() + mock.onFetch = { _ in throw StoreError.appNotFound } + + await #expect(throws: StoreError.appNotFound) { + try await store.products() + } + } + + @Test func wrapsUnknownErrorOnProducts() async throws { + let (store, mock) = makeStore() + mock.onFetch = { _ in throw URLError(.notConnectedToInternet) } + + await #expect(throws: StoreError.self) { + try await store.products() + } + } + + @Test func decodesServerJSON() throws { + let json = """ + [ + { + "id": "11111111-1111-1111-1111-111111111111", + "name": "Pro", + "durationInSeconds": null, + "deviceLimit": 3, + "price": { "unitAmount": 999, "currency": "usd", "recurringInterval": "month" }, + "checkoutURL": "https://api.amore.computer/v1/checkout/11111111-1111-1111-1111-111111111111" + }, + { + "id": "22222222-2222-2222-2222-222222222222", + "name": "Lite", + "durationInSeconds": 2592000, + "deviceLimit": 1, + "price": null, + "checkoutURL": "https://api.amore.computer/v1/checkout/22222222-2222-2222-2222-222222222222" + } + ] + """ + let decoded = try JSONDecoder().decode([Product].self, from: Data(json.utf8)) + + #expect(decoded.count == 2) + #expect(decoded[0].name == "Pro") + #expect(decoded[0].price?.unitAmount == 999) + #expect(decoded[0].price?.recurringInterval == .month) + #expect(decoded[0].checkoutURL == URL(string: "https://api.amore.computer/v1/checkout/11111111-1111-1111-1111-111111111111")) + #expect(decoded[1].name == "Lite") + #expect(decoded[1].price == nil) + #expect(decoded[1].durationInSeconds == 2_592_000) + #expect(decoded[1].checkoutURL == URL(string: "https://api.amore.computer/v1/checkout/22222222-2222-2222-2222-222222222222")) + } +} diff --git a/Tests/AmoreStoreTests/MockProductsClient.swift b/Tests/AmoreStoreTests/MockProductsClient.swift new file mode 100644 index 0000000..1ce461a --- /dev/null +++ b/Tests/AmoreStoreTests/MockProductsClient.swift @@ -0,0 +1,12 @@ +import Foundation + +@testable import AmoreStore + +final class MockProductsClient: ProductsClient, @unchecked Sendable { + var onFetch: ((String) async throws -> [Product])? + + func fetchProducts(bundleIdentifier: String) async throws -> [Product] { + guard let handler = onFetch else { return [] } + return try await handler(bundleIdentifier) + } +} From 0575f858b9016b5a6be3d069ab3be722b88a7b50 Mon Sep 17 00:00:00 2001 From: Lucas Fischer Date: Sat, 30 May 2026 15:36:34 +0800 Subject: [PATCH 2/6] add docs to AmoreStore --- .../Documentation.docc/Getting Started.md | 61 +++++++++++++++++++ .../AmoreStore/Documentation.docc/Index.md | 26 ++++++++ .../Documentation.docc/images/heart.svg | 6 ++ .../Documentation.docc/theme-settings.json | 35 +++++++++++ 4 files changed, 128 insertions(+) create mode 100644 Sources/AmoreStore/Documentation.docc/Getting Started.md create mode 100644 Sources/AmoreStore/Documentation.docc/Index.md create mode 100644 Sources/AmoreStore/Documentation.docc/images/heart.svg create mode 100644 Sources/AmoreStore/Documentation.docc/theme-settings.json diff --git a/Sources/AmoreStore/Documentation.docc/Getting Started.md b/Sources/AmoreStore/Documentation.docc/Getting Started.md new file mode 100644 index 0000000..a1355d5 --- /dev/null +++ b/Sources/AmoreStore/Documentation.docc/Getting Started.md @@ -0,0 +1,61 @@ +# Getting Started + +This article describes how to get started with AmoreStore. + +## Installation + +In Xcode, go to **File → Add Package Dependencies…** and enter: + +``` +https://github.com/AmoreComputer/AmoreKit +``` + +Or add it to your `Package.swift`: + +```swift +.package(url: "https://github.com/AmoreComputer/AmoreKit", from: "0.1") +``` + +## AmoreStore + +To get started with AmoreStore, create an instance of ``AmoreStore``. By default it uses your app's `Bundle.main.bundleIdentifier`. + +```swift +let store = AmoreStore() +``` + +## Fetching Products + +Call ``AmoreStore/products()`` to fetch the products configured for your app. + +```swift +let products = try await store.products() +``` + +> Note: ``AmoreStore/products()`` throws ``StoreError`` with detailed information about what went wrong. + +## Displaying Prices + +Each ``Product`` carries an optional ``Product/price`` with a localized, display-ready string. + +```swift +ForEach(products) { product in + HStack { + Text(product.name) + Spacer() + if let displayPrice = product.displayPrice { + Text(displayPrice) + } + } +} +``` + +Use ``Price/recurringInterval`` to tell one-time purchases from subscriptions. + +## Checkout + +Every purchasable ``Product`` carries a ``Product/checkoutURL``. Open it to send the customer to Stripe checkout. + +```swift +NSWorkspace.shared.open(product.checkoutURL) +``` diff --git a/Sources/AmoreStore/Documentation.docc/Index.md b/Sources/AmoreStore/Documentation.docc/Index.md new file mode 100644 index 0000000..7ef59c9 --- /dev/null +++ b/Sources/AmoreStore/Documentation.docc/Index.md @@ -0,0 +1,26 @@ +# ``AmoreStore`` + +A macOS SDK for listing an app's products and sending customers to checkout. + +## Overview + +AmoreStore is the store SDK for [Amore](https://amore.computer). + +AmoreStore fetches the products configured for your app in the licensing dashboard and exposes their prices and checkout URLs. Use it to build paywalls, pickers, or any view that displays what a customer can buy. + +## Topics + +### Articles + +- + +### Essentials + +- ``AmoreStore`` +- ``Product`` +- ``Price`` +- ``RecurringInterval`` + +### Errors + +- ``StoreError`` diff --git a/Sources/AmoreStore/Documentation.docc/images/heart.svg b/Sources/AmoreStore/Documentation.docc/images/heart.svg new file mode 100644 index 0000000..4a7d72e --- /dev/null +++ b/Sources/AmoreStore/Documentation.docc/images/heart.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/Sources/AmoreStore/Documentation.docc/theme-settings.json b/Sources/AmoreStore/Documentation.docc/theme-settings.json new file mode 100644 index 0000000..c437a68 --- /dev/null +++ b/Sources/AmoreStore/Documentation.docc/theme-settings.json @@ -0,0 +1,35 @@ +{ + "theme": { + "aside": { + "border-radius": "6px", + "border-style": "solid", + "border-width": "2px" + }, + "border-radius": "0", + "button": { + "border-radius": "16px", + "border-width": "1px", + "border-style": "solid" + }, + "code": { + "border-radius": "16px", + "border-width": "1px", + "border-style": "solid" + }, + "color": { + "amore": "#f09000", + "documentation-intro-fill": { "dark": "#000", "light": "#fff" }, + "documentation-intro-accent": "var(--color-jwtkit)", + "logo-base": { "dark": "#fff", "light": "#000" }, + "logo-shape": { "dark": "#000", "light": "#fff" }, + "link": { "dark": "var(--color-amore)", "light": "var(--color-amore)" }, + "fill": { "dark": "#222", "light": "#eee" }, + "button-background": "var(--color-amore)", + "fill-blue": "var(--color-amore)", + "figure-blue": "var(--color-amore)" + }, + "icons": { + "technology": "/images/AmoreStore/heart.svg" + } + } +} From 0a77c7b4c678ca860517006b0489a98a0f9cee89 Mon Sep 17 00:00:00 2001 From: Lucas Fischer Date: Sat, 30 May 2026 15:56:53 +0800 Subject: [PATCH 3/6] tweak AmoreStore.init --- Sources/AmoreStore/AmoreStore.swift | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/Sources/AmoreStore/AmoreStore.swift b/Sources/AmoreStore/AmoreStore.swift index b067798..2b9c1ce 100644 --- a/Sources/AmoreStore/AmoreStore.swift +++ b/Sources/AmoreStore/AmoreStore.swift @@ -16,13 +16,19 @@ public struct AmoreStore: Sendable { private let bundleIdentifier: String private let productsClient: ProductsClient - /// Creates a store client for the given bundle identifier. + /// Creates a store client for the given bundle identifier, targeting the Amore server. + /// - Parameter bundleIdentifier: The app's bundle identifier. Defaults to `Bundle.main.bundleIdentifier`. + public init(bundleIdentifier: String? = nil) { + self.init(bundleIdentifier: bundleIdentifier, baseURL: .amoreServer) + } + + /// Creates a store client for the given bundle identifier and server URL. /// - Parameters: /// - bundleIdentifier: The app's bundle identifier. Defaults to `Bundle.main.bundleIdentifier`. - /// - baseURL: The licensing server base URL. Defaults to the Amore server when `nil`. - public init(bundleIdentifier: String? = nil, baseURL: URL? = nil) { + /// - baseURL: The licensing server base URL. + public init(bundleIdentifier: String? = nil, baseURL: URL) { self.bundleIdentifier = bundleIdentifier ?? Bundle.main.bundleIdentifier ?? "" - self.productsClient = HTTPProductsClient(baseURL: baseURL ?? .amoreServer) + self.productsClient = HTTPProductsClient(baseURL: baseURL) } init(bundleIdentifier: String, productsClient: ProductsClient) { From d71cbd44fa53969a727cc6b4742579eb3d7de645 Mon Sep 17 00:00:00 2001 From: Lucas Fischer Date: Sat, 30 May 2026 15:57:06 +0800 Subject: [PATCH 4/6] add missing tests for price formatting --- Tests/AmoreStoreTests/PriceTests.swift | 57 ++++++++++++++++++++++++++ 1 file changed, 57 insertions(+) create mode 100644 Tests/AmoreStoreTests/PriceTests.swift diff --git a/Tests/AmoreStoreTests/PriceTests.swift b/Tests/AmoreStoreTests/PriceTests.swift new file mode 100644 index 0000000..58deed5 --- /dev/null +++ b/Tests/AmoreStoreTests/PriceTests.swift @@ -0,0 +1,57 @@ +import Foundation +import Testing + +@testable import AmoreStore + +@Suite struct PriceTests { + + @Test func decimalAmountForTwoDecimalCurrency() { + let price = Price(unitAmount: 999, currency: "USD", recurringInterval: .month) + #expect(price.decimalAmount == Decimal(string: "9.99")) + } + + @Test func decimalAmountForZeroDecimalCurrency() { + // JPY has no minor unit: 1000 yen is 1000, not 10.00. + let price = Price(unitAmount: 1000, currency: "JPY", recurringInterval: nil) + #expect(price.decimalAmount == Decimal(1000)) + } + + @Test func decimalAmountForThreeDecimalCurrency() { + // BHD has three minor-unit digits: 1234 fils is 1.234 dinar. + let price = Price(unitAmount: 1234, currency: "BHD", recurringInterval: nil) + #expect(price.decimalAmount == Decimal(string: "1.234")) + } + + @Test func displayPriceUsesCurrencyFormatting() { + let price = Price(unitAmount: 999, currency: "USD", recurringInterval: .month) + // Locale-independent checks: the formatted amount and a currency marker are present. + #expect(price.displayPrice.contains("9")) + #expect(price.displayPrice.contains("99")) + #expect(!price.displayPrice.isEmpty) + } + + @Test func productDisplayPriceDelegatesToPrice() { + let priced = Product( + id: UUID(), + name: "Pro", + durationInSeconds: nil, + deviceLimit: 3, + price: Price(unitAmount: 999, currency: "USD", recurringInterval: .month), + checkoutURL: URL(string: "https://api.amore.computer/v1/checkout/\(UUID())")! + ) + #expect(priced.displayPrice == priced.price?.displayPrice) + #expect(priced.displayPrice != nil) + } + + @Test func productDisplayPriceIsNilWithoutPrice() { + let free = Product( + id: UUID(), + name: "Lite", + durationInSeconds: 2_592_000, + deviceLimit: 1, + price: nil, + checkoutURL: URL(string: "https://api.amore.computer/v1/checkout/\(UUID())")! + ) + #expect(free.displayPrice == nil) + } +} From 288111acb7cfd6f16942611a250f2227612dda65 Mon Sep 17 00:00:00 2001 From: Lucas Fischer Date: Sat, 30 May 2026 15:57:38 +0800 Subject: [PATCH 5/6] add AmoreStore library to Package.swift --- Package.swift | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/Package.swift b/Package.swift index 82e3bcc..0b84769 100644 --- a/Package.swift +++ b/Package.swift @@ -12,6 +12,10 @@ let package = Package( name: "AmoreLicensing", targets: ["AmoreLicensing"] ), + .library( + name: "AmoreStore", + targets: ["AmoreStore"] + ), ], dependencies: [ .package(url: "https://github.com/vapor/jwt-kit.git", from: "5.0.0"), @@ -24,6 +28,7 @@ let package = Package( .product(name: "JWTKit", package: "jwt-kit"), ], ), + .target(name: "AmoreStore"), .testTarget( name: "AmoreLicensingTests", dependencies: [ @@ -31,6 +36,10 @@ let package = Package( .product(name: "JWTKit", package: "jwt-kit"), ] ), + .testTarget( + name: "AmoreStoreTests", + dependencies: ["AmoreStore"] + ), ], swiftLanguageModes: [.v6] ) From c75a5bfbaed6118284d814f79238e217012ae640 Mon Sep 17 00:00:00 2001 From: Lucas Fischer Date: Sat, 30 May 2026 15:58:08 +0800 Subject: [PATCH 6/6] update `make docs` command to generate all targets --- Makefile | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/Makefile b/Makefile index a4b64b3..6ff6133 100644 --- a/Makefile +++ b/Makefile @@ -6,8 +6,9 @@ docs: --disable-indexing \ --transform-for-static-hosting \ --target AmoreLicensing \ - --output-path ./docs - # --enable-experimental-combined-documentation \ + --target AmoreStore \ + --output-path ./docs \ + --enable-experimental-combined-documentation \ docs-preview: swift package --disable-sandbox preview-documentation --target AmoreLicensing