Rebuild the IAP part

This commit is contained in:
2026-04-11 16:36:24 +02:00
parent 5590100990
commit 67de78837f
7 changed files with 182 additions and 0 deletions
+61
View File
@@ -0,0 +1,61 @@
{
"identifier" : "B00KST4X-T1PS",
"nonConsumableProducts" : [],
"nonRenewingSubscriptionProducts" : [],
"products" : [
{
"displayPrice" : "0.99",
"familySharable" : false,
"internalID" : "D1A2B3C4-0001-0001-0001-000000000001",
"localizations" : [
{
"description" : "Support the developer with a small tip",
"displayName" : "Page",
"locale" : "en_US"
}
],
"productID" : "donatepage",
"referenceName" : "Page",
"type" : "consumable"
},
{
"displayPrice" : "4.99",
"familySharable" : false,
"internalID" : "D1A2B3C4-0002-0002-0002-000000000002",
"localizations" : [
{
"description" : "Support the developer with a generous tip",
"displayName" : "Book",
"locale" : "en_US"
}
],
"productID" : "donatebook",
"referenceName" : "Book",
"type" : "consumable"
},
{
"displayPrice" : "49.99",
"familySharable" : false,
"internalID" : "D1A2B3C4-0003-0003-0003-000000000003",
"localizations" : [
{
"description" : "Support the developer with a very generous tip",
"displayName" : "Encyclopedia",
"locale" : "en_US"
}
],
"productID" : "donateencyclopaedia",
"referenceName" : "Encyclopedia",
"type" : "consumable"
}
],
"settings" : {
"_enableUserPurchaseTracking" : true
},
"subscriptionGroups" : [],
"version" : {
"major" : 3,
"minor" : 0,
"patch" : 0
}
}
+9
View File
@@ -6,8 +6,14 @@
objectVersion = 77;
objects = {
/* Begin PBXBuildFile section */
26A7DB402F8A8FD80065C26D /* Tips.storekit in Resources */ = {isa = PBXBuildFile; fileRef = 26A7DB3F2F8A8FD80065C26D /* Tips.storekit */; };
/* End PBXBuildFile section */
/* Begin PBXFileReference section */
261299D62F6C686D00EC1C97 /* bookstax.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = bookstax.app; sourceTree = BUILT_PRODUCTS_DIR; };
26A7DB3F2F8A8FD80065C26D /* Tips.storekit */ = {isa = PBXFileReference; lastKnownFileType = text; path = Tips.storekit; sourceTree = "<group>"; };
26A7DB412F8A930C0065C26D /* Tips.storekit */ = {isa = PBXFileReference; lastKnownFileType = text; path = Tips.storekit; sourceTree = "<group>"; };
/* End PBXFileReference section */
/* Begin PBXFileSystemSynchronizedRootGroup section */
@@ -32,8 +38,10 @@
261299CD2F6C686D00EC1C97 = {
isa = PBXGroup;
children = (
26A7DB412F8A930C0065C26D /* Tips.storekit */,
261299D82F6C686D00EC1C97 /* bookstax */,
261299D72F6C686D00EC1C97 /* Products */,
26A7DB3F2F8A8FD80065C26D /* Tips.storekit */,
);
sourceTree = "<group>";
};
@@ -111,6 +119,7 @@
isa = PBXResourcesBuildPhase;
buildActionMask = 2147483647;
files = (
26A7DB402F8A8FD80065C26D /* Tips.storekit in Resources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
@@ -51,6 +51,9 @@
ReferencedContainer = "container:bookstax.xcodeproj">
</BuildableReference>
</BuildableProductRunnable>
<StoreKitConfigurationFileReference
identifier = "../../Tips.storekit">
</StoreKitConfigurationFileReference>
</LaunchAction>
<ProfileAction
buildConfiguration = "Release"
@@ -1,5 +1,6 @@
import SwiftUI
import SafariServices
import StoreKit
struct SettingsView: View {
@AppStorage("onboardingComplete") private var onboardingComplete = false
@@ -180,6 +181,9 @@ struct SettingsView: View {
}
}
// Donate section
DonationSection()
// About section
Section(L("settings.about")) {
LabeledContent(L("settings.about.version"), value: "\(appVersion) (\(buildNumber))")
@@ -275,6 +279,90 @@ struct SettingsView: View {
}
}
// MARK: - Donation Section
@MainActor
private struct DonationSection: View {
@Environment(\.purchase) private var purchase
private struct Tier: Identifiable {
let id: String
let titleKey: String
let icon: String
let fallbackPrice: String
}
private let tiers: [Tier] = [
Tier(id: "donatepage", titleKey: "settings.donate.page", icon: "doc.text.fill", fallbackPrice: "€0.99"),
Tier(id: "donatebook", titleKey: "settings.donate.book", icon: "book.closed.fill", fallbackPrice: "€4.99"),
Tier(id: "donateencyclopaedia", titleKey: "settings.donate.encyclopedia", icon: "books.vertical.fill", fallbackPrice: "€49.99"),
]
@State private var products: [String: Product] = [:]
@State private var purchasingID: String? = nil
@State private var thankYouID: String? = nil
var body: some View {
Section {
ForEach(tiers) { tier in
Button {
guard purchasingID == nil else { return }
Task { await buy(tier.id) }
} label: {
HStack {
Label(L(tier.titleKey), systemImage: tier.icon)
.foregroundStyle(.primary)
Spacer()
Group {
if purchasingID == tier.id {
ProgressView().controlSize(.small)
} else if thankYouID == tier.id {
Image(systemName: "heart.fill").foregroundStyle(.pink)
} else {
Text(products[tier.id]?.displayPrice ?? tier.fallbackPrice)
.foregroundStyle(.tint)
.bold()
}
}
}
}
.disabled(purchasingID != nil && purchasingID != tier.id)
}
} header: {
Text(L("settings.donate"))
} footer: {
Text(L("settings.donate.footer"))
}
.task { await loadProducts() }
}
private func loadProducts() async {
let ids = tiers.map(\.id)
if let loaded = try? await Product.products(for: ids), !loaded.isEmpty {
products = Dictionary(uniqueKeysWithValues: loaded.map { ($0.id, $0) })
}
}
private func buy(_ productID: String) async {
purchasingID = productID
defer { purchasingID = nil }
guard let product = products[productID] else { return }
do {
let result = try await purchase(product)
if case .success(let verification) = result {
if case .verified(let transaction) = verification {
await transaction.finish()
}
thankYouID = productID
try? await Task.sleep(for: .seconds(3))
if thankYouID == productID { thankYouID = nil }
}
} catch {
print("[DonationSection] Purchase error: \(error)")
}
}
}
// MARK: - Safari View
struct SafariView: UIViewControllerRepresentable {
+7
View File
@@ -262,6 +262,13 @@
"onboarding.server.name.label" = "Servername";
"onboarding.server.name.placeholder" = "z.B. Firmen-Wiki";
// MARK: - Donations
"settings.donate" = "BookStax unterstützen";
"settings.donate.page" = "Seite";
"settings.donate.book" = "Buch";
"settings.donate.encyclopedia" = "Enzyklopädie";
"settings.donate.footer" = "Gefällt dir BookStax? Deine Unterstützung hilft, die App kostenlos und in aktiver Entwicklung zu halten. Danke!";
// MARK: - Common
"common.ok" = "OK";
"common.cancel" = "Abbrechen";
+7
View File
@@ -262,6 +262,13 @@
"onboarding.server.name.label" = "Server Name";
"onboarding.server.name.placeholder" = "e.g. Work Wiki";
// MARK: - Donations
"settings.donate" = "Support BookStax";
"settings.donate.page" = "Page";
"settings.donate.book" = "Book";
"settings.donate.encyclopedia" = "Encyclopedia";
"settings.donate.footer" = "Enjoying BookStax? Your support helps keep the app free and in active development. Thank you!";
// MARK: - Common
"common.ok" = "OK";
"common.cancel" = "Cancel";
+7
View File
@@ -262,6 +262,13 @@
"onboarding.server.name.label" = "Nombre del servidor";
"onboarding.server.name.placeholder" = "p.ej. Wiki de trabajo";
// MARK: - Donations
"settings.donate" = "Apoya BookStax";
"settings.donate.page" = "Página";
"settings.donate.book" = "Libro";
"settings.donate.encyclopedia" = "Enciclopedia";
"settings.donate.footer" = "¿Disfrutas BookStax? Tu apoyo ayuda a mantener la app gratuita y en desarrollo activo. ¡Gracias!";
// MARK: - Common
"common.ok" = "Aceptar";
"common.cancel" = "Cancelar";