Rebuild the IAP part
This commit is contained in:
@@ -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
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -6,8 +6,14 @@
|
|||||||
objectVersion = 77;
|
objectVersion = 77;
|
||||||
objects = {
|
objects = {
|
||||||
|
|
||||||
|
/* Begin PBXBuildFile section */
|
||||||
|
26A7DB402F8A8FD80065C26D /* Tips.storekit in Resources */ = {isa = PBXBuildFile; fileRef = 26A7DB3F2F8A8FD80065C26D /* Tips.storekit */; };
|
||||||
|
/* End PBXBuildFile section */
|
||||||
|
|
||||||
/* Begin PBXFileReference section */
|
/* Begin PBXFileReference section */
|
||||||
261299D62F6C686D00EC1C97 /* bookstax.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = bookstax.app; sourceTree = BUILT_PRODUCTS_DIR; };
|
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 */
|
/* End PBXFileReference section */
|
||||||
|
|
||||||
/* Begin PBXFileSystemSynchronizedRootGroup section */
|
/* Begin PBXFileSystemSynchronizedRootGroup section */
|
||||||
@@ -32,8 +38,10 @@
|
|||||||
261299CD2F6C686D00EC1C97 = {
|
261299CD2F6C686D00EC1C97 = {
|
||||||
isa = PBXGroup;
|
isa = PBXGroup;
|
||||||
children = (
|
children = (
|
||||||
|
26A7DB412F8A930C0065C26D /* Tips.storekit */,
|
||||||
261299D82F6C686D00EC1C97 /* bookstax */,
|
261299D82F6C686D00EC1C97 /* bookstax */,
|
||||||
261299D72F6C686D00EC1C97 /* Products */,
|
261299D72F6C686D00EC1C97 /* Products */,
|
||||||
|
26A7DB3F2F8A8FD80065C26D /* Tips.storekit */,
|
||||||
);
|
);
|
||||||
sourceTree = "<group>";
|
sourceTree = "<group>";
|
||||||
};
|
};
|
||||||
@@ -111,6 +119,7 @@
|
|||||||
isa = PBXResourcesBuildPhase;
|
isa = PBXResourcesBuildPhase;
|
||||||
buildActionMask = 2147483647;
|
buildActionMask = 2147483647;
|
||||||
files = (
|
files = (
|
||||||
|
26A7DB402F8A8FD80065C26D /* Tips.storekit in Resources */,
|
||||||
);
|
);
|
||||||
runOnlyForDeploymentPostprocessing = 0;
|
runOnlyForDeploymentPostprocessing = 0;
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -51,6 +51,9 @@
|
|||||||
ReferencedContainer = "container:bookstax.xcodeproj">
|
ReferencedContainer = "container:bookstax.xcodeproj">
|
||||||
</BuildableReference>
|
</BuildableReference>
|
||||||
</BuildableProductRunnable>
|
</BuildableProductRunnable>
|
||||||
|
<StoreKitConfigurationFileReference
|
||||||
|
identifier = "../../Tips.storekit">
|
||||||
|
</StoreKitConfigurationFileReference>
|
||||||
</LaunchAction>
|
</LaunchAction>
|
||||||
<ProfileAction
|
<ProfileAction
|
||||||
buildConfiguration = "Release"
|
buildConfiguration = "Release"
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import SwiftUI
|
import SwiftUI
|
||||||
import SafariServices
|
import SafariServices
|
||||||
|
import StoreKit
|
||||||
|
|
||||||
struct SettingsView: View {
|
struct SettingsView: View {
|
||||||
@AppStorage("onboardingComplete") private var onboardingComplete = false
|
@AppStorage("onboardingComplete") private var onboardingComplete = false
|
||||||
@@ -180,6 +181,9 @@ struct SettingsView: View {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Donate section
|
||||||
|
DonationSection()
|
||||||
|
|
||||||
// About section
|
// About section
|
||||||
Section(L("settings.about")) {
|
Section(L("settings.about")) {
|
||||||
LabeledContent(L("settings.about.version"), value: "\(appVersion) (\(buildNumber))")
|
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
|
// MARK: - Safari View
|
||||||
|
|
||||||
struct SafariView: UIViewControllerRepresentable {
|
struct SafariView: UIViewControllerRepresentable {
|
||||||
|
|||||||
@@ -262,6 +262,13 @@
|
|||||||
"onboarding.server.name.label" = "Servername";
|
"onboarding.server.name.label" = "Servername";
|
||||||
"onboarding.server.name.placeholder" = "z.B. Firmen-Wiki";
|
"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
|
// MARK: - Common
|
||||||
"common.ok" = "OK";
|
"common.ok" = "OK";
|
||||||
"common.cancel" = "Abbrechen";
|
"common.cancel" = "Abbrechen";
|
||||||
|
|||||||
@@ -262,6 +262,13 @@
|
|||||||
"onboarding.server.name.label" = "Server Name";
|
"onboarding.server.name.label" = "Server Name";
|
||||||
"onboarding.server.name.placeholder" = "e.g. Work Wiki";
|
"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
|
// MARK: - Common
|
||||||
"common.ok" = "OK";
|
"common.ok" = "OK";
|
||||||
"common.cancel" = "Cancel";
|
"common.cancel" = "Cancel";
|
||||||
|
|||||||
@@ -262,6 +262,13 @@
|
|||||||
"onboarding.server.name.label" = "Nombre del servidor";
|
"onboarding.server.name.label" = "Nombre del servidor";
|
||||||
"onboarding.server.name.placeholder" = "p.ej. Wiki de trabajo";
|
"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
|
// MARK: - Common
|
||||||
"common.ok" = "Aceptar";
|
"common.ok" = "Aceptar";
|
||||||
"common.cancel" = "Cancelar";
|
"common.cancel" = "Cancelar";
|
||||||
|
|||||||
Reference in New Issue
Block a user