From 8b8448924a9ce845de9b9001e434f3dae0484af0 Mon Sep 17 00:00:00 2001 From: Go Takagi <15936908+shimastripe@users.noreply.github.com> Date: Sun, 29 Sep 2024 15:29:48 +0900 Subject: [PATCH 1/2] Drop Mac OS under 14.3 --- .../InAppPurchaseViewer.xcodeproj/project.pbxproj | 4 ++-- Package.swift | 2 +- Sources/IAPView/Binding+.swift | 0 Sources/IAPView/IAPCellDataSource.swift | 0 Sources/IAPView/TransactionHistoryInspectorView.swift | 0 5 files changed, 3 insertions(+), 3 deletions(-) create mode 100644 Sources/IAPView/Binding+.swift create mode 100644 Sources/IAPView/IAPCellDataSource.swift create mode 100644 Sources/IAPView/TransactionHistoryInspectorView.swift diff --git a/App/InAppPurchaseViewer/InAppPurchaseViewer.xcodeproj/project.pbxproj b/App/InAppPurchaseViewer/InAppPurchaseViewer.xcodeproj/project.pbxproj index 2d8c567..599505f 100644 --- a/App/InAppPurchaseViewer/InAppPurchaseViewer.xcodeproj/project.pbxproj +++ b/App/InAppPurchaseViewer/InAppPurchaseViewer.xcodeproj/project.pbxproj @@ -311,7 +311,7 @@ INFOPLIST_FILE = InAppPurchaseViewer/Info.plist; LD_RUNPATH_SEARCH_PATHS = "@executable_path/Frameworks"; "LD_RUNPATH_SEARCH_PATHS[sdk=macosx*]" = "@executable_path/../Frameworks"; - MACOSX_DEPLOYMENT_TARGET = 14.0; + MACOSX_DEPLOYMENT_TARGET = 14.4; MARKETING_VERSION = 1.5.0; PRODUCT_NAME = "$(TARGET_NAME)"; SDKROOT = auto; @@ -335,7 +335,7 @@ INFOPLIST_FILE = InAppPurchaseViewer/Info.plist; LD_RUNPATH_SEARCH_PATHS = "@executable_path/Frameworks"; "LD_RUNPATH_SEARCH_PATHS[sdk=macosx*]" = "@executable_path/../Frameworks"; - MACOSX_DEPLOYMENT_TARGET = 14.0; + MACOSX_DEPLOYMENT_TARGET = 14.4; MARKETING_VERSION = 1.5.0; PRODUCT_NAME = "$(TARGET_NAME)"; SDKROOT = auto; diff --git a/Package.swift b/Package.swift index fef451d..be7cf9e 100644 --- a/Package.swift +++ b/Package.swift @@ -6,7 +6,7 @@ import PackageDescription let package = Package( name: "InAppPurchaseViewer", defaultLocalization: "ja", - platforms: [.macOS(.v14)], + platforms: [.macOS("14.4")], products: [ .library(name: "IAPClient", targets: ["IAPClient"]), .library(name: "IAPCore", targets: ["IAPCore"]), diff --git a/Sources/IAPView/Binding+.swift b/Sources/IAPView/Binding+.swift new file mode 100644 index 0000000..e69de29 diff --git a/Sources/IAPView/IAPCellDataSource.swift b/Sources/IAPView/IAPCellDataSource.swift new file mode 100644 index 0000000..e69de29 diff --git a/Sources/IAPView/TransactionHistoryInspectorView.swift b/Sources/IAPView/TransactionHistoryInspectorView.swift new file mode 100644 index 0000000..e69de29 From e34e4eee48604e7f01f19fd90a7b7984c9c491df Mon Sep 17 00:00:00 2001 From: Go Takagi <15936908+shimastripe@users.noreply.github.com> Date: Sun, 29 Sep 2024 15:30:58 +0900 Subject: [PATCH 2/2] Support dynamic columns in transaction history table --- .../Model/JWSTransactionDecodedPayload+.swift | 2 + Sources/IAPView/Binding+.swift | 34 +++ Sources/IAPView/IAPCellDataSource.swift | 179 ++++++++++++++ .../IAPView/Resources/Localizable.xcstrings | 16 ++ .../TransactionHistoryInspectorView.swift | 38 +++ .../IAPView/TransactionHistoryTableView.swift | 230 ++++++++---------- Sources/IAPView/TransactionHistoryView.swift | 14 +- 7 files changed, 390 insertions(+), 123 deletions(-) diff --git a/Sources/IAPInterface/Model/JWSTransactionDecodedPayload+.swift b/Sources/IAPInterface/Model/JWSTransactionDecodedPayload+.swift index 39bd19a..e04bb26 100644 --- a/Sources/IAPInterface/Model/JWSTransactionDecodedPayload+.swift +++ b/Sources/IAPInterface/Model/JWSTransactionDecodedPayload+.swift @@ -7,6 +7,8 @@ import AppStoreServerLibrary +public typealias JWSTransactionDecodedPayload = AppStoreServerLibrary.JWSTransactionDecodedPayload + extension JWSTransactionDecodedPayload: @retroactive Identifiable { public var id: String? { transactionId diff --git a/Sources/IAPView/Binding+.swift b/Sources/IAPView/Binding+.swift index e69de29..c2c1df1 100644 --- a/Sources/IAPView/Binding+.swift +++ b/Sources/IAPView/Binding+.swift @@ -0,0 +1,34 @@ +// +// Binding+.swift +// InAppPurchaseViewer +// +// Created by shimastripe on 2024/09/29. +// + +import SwiftUI + +extension Binding +where + Value: MutableCollection, Value: RangeReplaceableCollection, Value: Sendable, + Value.Element: Identifiable +{ + func filter(_ isIncluded: @Sendable @escaping (Value.Element) -> Bool) -> Binding< + [Value.Element] + > { + Binding<[Value.Element]>( + get: { + wrappedValue.filter(isIncluded) + }, + set: { newValue in + newValue.forEach { newItem in + guard let i = wrappedValue.firstIndex(where: { $0.id == newItem.id }) + else { + self.wrappedValue.append(newItem) + return + } + self.wrappedValue[i] = newItem + } + } + ) + } +} diff --git a/Sources/IAPView/IAPCellDataSource.swift b/Sources/IAPView/IAPCellDataSource.swift index e69de29..a64ffbb 100644 --- a/Sources/IAPView/IAPCellDataSource.swift +++ b/Sources/IAPView/IAPCellDataSource.swift @@ -0,0 +1,179 @@ +// +// IAPCellDataSource.swift +// InAppPurchaseViewer +// +// Created by shimastripe on 2024/09/29. +// + +import Foundation +import IAPInterface + +struct IAPCellDataSource: Sendable, Identifiable { + let keyPath: PartialKeyPath & Sendable + let name: String + let idealWidth: CGFloat + var isOn: Bool + + var id: PartialKeyPath & Sendable { keyPath } +} + +extension IAPCellDataSource { + static let defaultTransactionInfo: [IAPCellDataSource] = [ + .init( + keyPath: \JWSTransactionDecodedPayload.purchaseDate, + name: "purchaseDate", + idealWidth: 120, + isOn: true + ), + .init( + keyPath: \JWSTransactionDecodedPayload.transactionReason, + name: "transactionReason", + idealWidth: 160, + isOn: true + ), + .init( + keyPath: \JWSTransactionDecodedPayload.price, + name: "price", + idealWidth: 60, + isOn: true + ), + .init( + keyPath: \JWSTransactionDecodedPayload.currency, + name: "currency", + idealWidth: 60, + isOn: true + ), + .init( + keyPath: \JWSTransactionDecodedPayload.originalTransactionId, + name: "originalTransactionId", + idealWidth: 140, + isOn: true + ), + .init( + keyPath: \JWSTransactionDecodedPayload.transactionId, + name: "transactionId", + idealWidth: 140, + isOn: true + ), + .init( + keyPath: \JWSTransactionDecodedPayload.originalPurchaseDate, + name: "originalPurchaseDate", + idealWidth: 120, + isOn: true + ), + .init( + keyPath: \JWSTransactionDecodedPayload.expiresDate, + name: "expiresDate", + idealWidth: 120, + isOn: true + ), + .init( + keyPath: \JWSTransactionDecodedPayload.offerIdentifier, + name: "offerIdentifier", + idealWidth: 120, + isOn: true + ), + .init( + keyPath: \JWSTransactionDecodedPayload.offerType, + name: "offerType", + idealWidth: 120, + isOn: true + ), + .init( + keyPath: \JWSTransactionDecodedPayload.offerDiscountType, + name: "offerDiscountType", + idealWidth: 120, + isOn: true + ), + .init( + keyPath: \JWSTransactionDecodedPayload.appAccountToken, + name: "appAccountToken", + idealWidth: 120, + isOn: true + ), + .init( + keyPath: \JWSTransactionDecodedPayload.bundleId, + name: "bundleId", + idealWidth: 140, + isOn: true + ), + .init( + keyPath: \JWSTransactionDecodedPayload.productId, + name: "productId", + idealWidth: 140, + isOn: true + ), + .init( + keyPath: \JWSTransactionDecodedPayload.subscriptionGroupIdentifier, + name: "subscriptionGroupIdentifier", + idealWidth: 160, + isOn: true + ), + .init( + keyPath: \JWSTransactionDecodedPayload.quantity, + name: "quantity", + idealWidth: 60, + isOn: true + ), + .init( + keyPath: \JWSTransactionDecodedPayload.type, + name: "type", + idealWidth: 180, + isOn: true + ), + .init( + keyPath: \JWSTransactionDecodedPayload.inAppOwnershipType, + name: "inAppOwnershipType", + idealWidth: 160, + isOn: true + ), + .init( + keyPath: \JWSTransactionDecodedPayload.environment, + name: "environment", + idealWidth: 80, + isOn: true + ), + .init( + keyPath: \JWSTransactionDecodedPayload.storefront, + name: "storefront", + idealWidth: 60, + isOn: true + ), + .init( + keyPath: \JWSTransactionDecodedPayload.storefrontId, + name: "storefrontId", + idealWidth: 80, + isOn: true + ), + .init( + keyPath: \JWSTransactionDecodedPayload.webOrderLineItemId, + name: "webOrderLineItemId", + idealWidth: 140, + isOn: true + ), + .init( + keyPath: \JWSTransactionDecodedPayload.revocationReason, + name: "revocationReason", + idealWidth: 120, + isOn: true + ), + .init( + keyPath: \JWSTransactionDecodedPayload.revocationDate, + name: "revocationDate", + idealWidth: 120, + isOn: true + ), + .init( + keyPath: \JWSTransactionDecodedPayload.isUpgraded, + name: "isUpgraded", + idealWidth: 120, + isOn: true + ), + .init( + keyPath: \JWSTransactionDecodedPayload.signedDate, + name: "signedDate", + idealWidth: 120, + isOn: true + ), + ] +} diff --git a/Sources/IAPView/Resources/Localizable.xcstrings b/Sources/IAPView/Resources/Localizable.xcstrings index aa827f9..1e94e97 100644 --- a/Sources/IAPView/Resources/Localizable.xcstrings +++ b/Sources/IAPView/Resources/Localizable.xcstrings @@ -128,6 +128,16 @@ }, "Download AppleRootCA-G3.cer" : { + }, + "Edit" : { + "localizations" : { + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "編集" + } + } + } }, "eligibleWinBackOfferIds" : { @@ -363,12 +373,18 @@ }, "TransactionID" : { + }, + "transactionInfo" : { + }, "transactionReason" : { }, "type" : { + }, + "Unsupported Format" : { + }, "webOrderLineItemId" : { diff --git a/Sources/IAPView/TransactionHistoryInspectorView.swift b/Sources/IAPView/TransactionHistoryInspectorView.swift index e69de29..95c4bb0 100644 --- a/Sources/IAPView/TransactionHistoryInspectorView.swift +++ b/Sources/IAPView/TransactionHistoryInspectorView.swift @@ -0,0 +1,38 @@ +// +// TransactionHistoryInspectorView.swift +// InAppPurchaseViewer +// +// Created by shimastripe on 2024/09/29. +// + +import SwiftUI + +struct TransactionHistoryInspectorView: View { + + @Binding var currentColumns: [IAPCellDataSource] + + var body: some View { + List { + Section(header: Text("transactionInfo")) { + ForEach($currentColumns) { $item in + LabeledContent { + Image(systemName: "line.3.horizontal") + } label: { + HStack { + Toggle("", isOn: $item.isOn) + Text(item.name) + } + } + .opacity(item.isOn ? 1 : 0.5) + } + .onDelete(perform: { indexSet in + currentColumns.remove(atOffsets: indexSet) + }) + .onMove(perform: { indices, newOffset in + currentColumns.move(fromOffsets: indices, toOffset: newOffset) + }) + } + } + .listStyle(.sidebar) + } +} diff --git a/Sources/IAPView/TransactionHistoryTableView.swift b/Sources/IAPView/TransactionHistoryTableView.swift index 81c3f32..9224304 100644 --- a/Sources/IAPView/TransactionHistoryTableView.swift +++ b/Sources/IAPView/TransactionHistoryTableView.swift @@ -8,8 +8,6 @@ import IAPInterface import SwiftUI -import struct AppStoreServerLibrary.JWSTransactionDecodedPayload - struct TransactionHistoryTableView: View { private let columnCounts = 26 @@ -18,137 +16,125 @@ struct TransactionHistoryTableView: View { let model: TransactionHistory - @TableColumnBuilder - var mainColumns: some TableColumnContent { - TableColumn("purchaseDate") { - CellText($0.purchaseDate?.formatted()) - } - .width(ideal: 120) - TableColumn("transactionReason") { item in - Label { - CellText(item.transactionReason?.rawValue) - } icon: { - let eventIcon = item.transactionReason?.eventIcon ?? "questionmark" - let eventColor = item.transactionReason?.eventColor ?? .black - Image(systemName: eventIcon).foregroundStyle(eventColor).frame(width: iconSize) - } - } - .width(ideal: 160) - TableColumn("price") { - CellText($0.price?.description) - } - .width(ideal: 60) - TableColumn("currency") { - CellText($0.currency) - } - .width(ideal: 60) - TableColumn("originalTransactionID") { - CellText($0.originalTransactionId) - } - .width(ideal: 140) - TableColumn("transactionID") { - CellText($0.transactionId) - } - .width(ideal: 140) - TableColumn("originalPurchaseDate") { - CellText($0.originalPurchaseDate?.formatted()) - } - .width(ideal: 120) - TableColumn("expiresDate") { - CellText($0.expiresDate?.formatted()) - } - .width(ideal: 120) - } + @Binding var currentColumns: [IAPCellDataSource] @TableColumnBuilder - var transactionColumns: some TableColumnContent { - TableColumn("offerIdentifier") { - CellText($0.offerIdentifier) - } - .width(ideal: 120) - TableColumn("offerType") { - CellText($0.offerType?.description) - } - .width(ideal: 120) - TableColumn("offerDiscountType") { - CellText($0.offerDiscountType?.rawValue) - } - .width(ideal: 120) - TableColumn("appAccountToken") { - CellText($0.appAccountToken?.uuidString) - } - .width(ideal: 120) - TableColumn("bundleId") { - CellText($0.bundleId) - } - .width(ideal: 140) - TableColumn("productId") { - CellText($0.productId) - } - .width(ideal: 140) - TableColumn("subscriptionGroupIdentifier") { - CellText($0.subscriptionGroupIdentifier) - } - .width(ideal: 160) - TableColumn("quantity") { - CellText($0.quantity?.description) - } - .width(ideal: 60) - } + func column(dataSource: IAPCellDataSource) -> some TableColumnContent< + JWSTransactionDecodedPayload, Never + > { + TableColumn(dataSource.name) { item in + let stringKeyPaths = [ + \JWSTransactionDecodedPayload.originalTransactionId, + \JWSTransactionDecodedPayload.transactionId, + \JWSTransactionDecodedPayload.webOrderLineItemId, + \JWSTransactionDecodedPayload.bundleId, + \JWSTransactionDecodedPayload.productId, + \JWSTransactionDecodedPayload.subscriptionGroupIdentifier, + \JWSTransactionDecodedPayload.offerIdentifier, + \JWSTransactionDecodedPayload.storefront, + \JWSTransactionDecodedPayload.storefrontId, + \JWSTransactionDecodedPayload.currency, + ] + let dateKeyPaths = [ + \JWSTransactionDecodedPayload.purchaseDate, + \JWSTransactionDecodedPayload.originalPurchaseDate, + \JWSTransactionDecodedPayload.expiresDate, + \JWSTransactionDecodedPayload.signedDate, + \JWSTransactionDecodedPayload.revocationDate, + ] + let int64KeyPaths = [ + \JWSTransactionDecodedPayload.quantity + ] + let int32KeyPaths = [ + \JWSTransactionDecodedPayload.price + ] + let boolKeyPaths = [ + \JWSTransactionDecodedPayload.isUpgraded + ] + let productTypeKeyPaths = [ + \JWSTransactionDecodedPayload.type + ] + let uuidKeyPaths = [ + \JWSTransactionDecodedPayload.appAccountToken + ] + let inAppOwnershipTypeKeyPaths = [ + \JWSTransactionDecodedPayload.inAppOwnershipType + ] + let revocationReasonKeyPaths = [ + \JWSTransactionDecodedPayload.revocationReason + ] + let offerTypeKeyPaths = [ + \JWSTransactionDecodedPayload.offerType + ] + let environmentKeyPaths = [ + \JWSTransactionDecodedPayload.environment + ] + let transactionReasonKeyPaths = [ + \JWSTransactionDecodedPayload.transactionReason + ] + let offerDiscountTypeKeyPaths = [ + \JWSTransactionDecodedPayload.offerDiscountType + ] - @TableColumnBuilder - var transactionColumns2: some TableColumnContent { - TableColumn("type") { - CellText($0.type?.rawValue) - } - .width(ideal: 180) - TableColumn("inAppOwnershipType") { - CellText($0.inAppOwnershipType?.rawValue) - } - .width(ideal: 160) - TableColumn("environment") { - CellText($0.environment?.rawValue) - } - .width(ideal: 80) - TableColumn("storefront") { - CellText($0.storefront) - } - .width(ideal: 60) - TableColumn("storefrontId") { - CellText($0.storefrontId) - } - .width(ideal: 80) - TableColumn("webOrderLineItemId") { - CellText($0.webOrderLineItemId) - } - .width(ideal: 140) - TableColumn("revocationReason") { - CellText($0.revocationReason?.description) - } - .width(ideal: 120) - TableColumn("revocationDate") { - CellText($0.revocationDate?.formatted()) - } - .width(ideal: 120) - TableColumn("isUpgraded") { - CellText($0.isUpgraded?.description) - } - .width(ideal: 120) - TableColumn("transaction signedDate") { - CellText($0.signedDate?.formatted()) + if let keyPath = stringKeyPaths.first(where: { $0 == dataSource.keyPath }) { + CellText(item[keyPath: keyPath]) + } else if let keyPath = dateKeyPaths.first(where: { $0 == dataSource.keyPath }) { + CellText(item[keyPath: keyPath]?.formatted()) + } else if let keyPath = int64KeyPaths.first(where: { $0 == dataSource.keyPath }) { + CellText(item[keyPath: keyPath]?.description) + } else if let keyPath = int32KeyPaths.first(where: { $0 == dataSource.keyPath }) { + CellText(item[keyPath: keyPath]?.description) + } else if let keyPath = boolKeyPaths.first(where: { $0 == dataSource.keyPath }) { + CellText(item[keyPath: keyPath]?.description) + } else if let keyPath = productTypeKeyPaths.first(where: { $0 == dataSource.keyPath }) { + CellText(item[keyPath: keyPath]?.rawValue) + } else if let keyPath = uuidKeyPaths.first(where: { $0 == dataSource.keyPath }) { + CellText(item[keyPath: keyPath]?.uuidString) + } else if let keyPath = inAppOwnershipTypeKeyPaths.first(where: { + $0 == dataSource.keyPath + }) { + CellText(item[keyPath: keyPath]?.rawValue) + } else if let keyPath = revocationReasonKeyPaths.first(where: { + $0 == dataSource.keyPath + }) { + CellText(item[keyPath: keyPath]?.description) + } else if let keyPath = offerTypeKeyPaths.first(where: { $0 == dataSource.keyPath }) { + CellText(item[keyPath: keyPath]?.description) + } else if let keyPath = environmentKeyPaths.first(where: { $0 == dataSource.keyPath }) { + CellText(item[keyPath: keyPath]?.rawValue) + } else if let keyPath = transactionReasonKeyPaths.first(where: { + $0 == dataSource.keyPath + }) { + Label { + CellText(item[keyPath: keyPath]?.rawValue) + } icon: { + let eventIcon = item[keyPath: keyPath]?.eventIcon ?? "questionmark" + let eventColor = item[keyPath: keyPath]?.eventColor ?? .black + Image(systemName: eventIcon).foregroundStyle(eventColor).frame(width: iconSize) + } + } else if let keyPath = offerDiscountTypeKeyPaths.first(where: { + $0 == dataSource.keyPath + }) { + CellText(item[keyPath: keyPath]?.rawValue) + } else { + Text("Unsupported Format") + } } - .width(ideal: 120) + .width(ideal: dataSource.idealWidth) } var body: some View { - Text("(\(model.items.count) transactions x \(columnCounts) columns)").frame( + Text( + "(\(model.items.count) transactions x \(currentColumns.count) columns)" + ) + .frame( maxWidth: .infinity, alignment: .leading ).padding(.horizontal) Table(of: JWSTransactionDecodedPayload.self) { - mainColumns - transactionColumns - transactionColumns2 + TableColumnForEach(currentColumns) { currentColumn in + column(dataSource: currentColumn) + } } rows: { ForEach(model.items, content: TableRow.init) } diff --git a/Sources/IAPView/TransactionHistoryView.swift b/Sources/IAPView/TransactionHistoryView.swift index 641a052..8a610de 100644 --- a/Sources/IAPView/TransactionHistoryView.swift +++ b/Sources/IAPView/TransactionHistoryView.swift @@ -13,6 +13,8 @@ struct TransactionHistoryView: View { @Environment(IAPModel.self) private var model + @State private var isPresentedInspector = false + @State var currentColumns: [IAPCellDataSource] = IAPCellDataSource.defaultTransactionInfo private var state: LoadingViewState { model.fetchTransactionHistoryState @@ -36,6 +38,10 @@ struct TransactionHistoryView: View { } .padding() Image(systemName: "flowchart").font(.largeTitle).scenePadding() + Button("Edit") { + isPresentedInspector.toggle() + } + .padding(.trailing) } .padding() Divider().padding(.horizontal) @@ -51,7 +57,10 @@ struct TransactionHistoryView: View { } } } else if let transaction = state.value { - TransactionHistoryTableView(model: transaction) + TransactionHistoryTableView( + model: transaction, + currentColumns: $currentColumns.filter(\.isOn) + ) if let hasMore = transaction.hasMore, hasMore, transaction.revision != nil @@ -113,6 +122,9 @@ struct TransactionHistoryView: View { } .frame(maxHeight: .infinity) } + .inspector(isPresented: $isPresentedInspector) { + TransactionHistoryInspectorView(currentColumns: $currentColumns) + } .toolbar { ToolbarItem { HStack(alignment: .firstTextBaseline, spacing: 8) {