From 05ee7c9bb8c03844cb782a33a641997f9beb34f4 Mon Sep 17 00:00:00 2001 From: Balazs Perlaki-Horvath Date: Sat, 21 Sep 2024 16:22:21 +0200 Subject: [PATCH 01/54] Add merchantID for ApplePay --- project.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/project.yml b/project.yml index 4b9819562..84b64e5b7 100644 --- a/project.yml +++ b/project.yml @@ -59,6 +59,7 @@ targetTemplates: com.apple.security.files.user-selected.read-write: true com.apple.security.network.client: true com.apple.security.print: true + com.apple.developer.in-app-payments: [merchant.org.kiwix] dependencies: - framework: CoreKiwix.xcframework embed: false From 3fef622b03dacd6e7f72b39712be391a92e23b80 Mon Sep 17 00:00:00 2001 From: Balazs Perlaki-Horvath Date: Sat, 21 Sep 2024 18:48:33 +0200 Subject: [PATCH 02/54] Import PassKit --- project.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/project.yml b/project.yml index 84b64e5b7..7b2618db9 100644 --- a/project.yml +++ b/project.yml @@ -69,6 +69,7 @@ targetTemplates: - sdk: WebKit.framework - sdk: NotificationCenter.framework - sdk: QuickLook.framework + - sdk: PassKit.framework - sdk: SystemConfiguration.framework - package: Defaults sources: From ce2a80cafa998a8c9b084983d98c64a2745eacef Mon Sep 17 00:00:00 2001 From: Balazs Perlaki-Horvath Date: Sat, 21 Sep 2024 19:45:29 +0200 Subject: [PATCH 03/54] Initial payment setup --- App/App_macOS.swift | 17 ++++++++++++ Model/Payment.swift | 67 +++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 84 insertions(+) create mode 100644 Model/Payment.swift diff --git a/App/App_macOS.swift b/App/App_macOS.swift index d695b261e..727b8ad9c 100644 --- a/App/App_macOS.swift +++ b/App/App_macOS.swift @@ -18,6 +18,7 @@ import UserNotifications import Combine import Defaults import CoreKiwix +import PassKit #if os(macOS) final class AppDelegate: NSObject, NSApplicationDelegate { @@ -109,6 +110,7 @@ struct RootView: View { private let tabCloses = NotificationCenter.default.publisher(for: NSWindow.willCloseNotification) /// Close other tabs then the ones received private let keepOnlyTabs = NotificationCenter.default.publisher(for: .keepOnlyTabs) + private let payment = Payment() var body: some View { NavigationSplitView { @@ -127,6 +129,21 @@ struct RootView: View { } } } + .safeAreaInset(edge: .bottom) { + if PKPaymentAuthorizationController.canMakePayments( + usingNetworks: Payment.supportedNetworks, + capabilities: Payment.capabilities + ) { + PayWithApplePayButton( + .donate, + request: payment.donationRequest(), + onPaymentAuthorizationChange: payment.onPaymentAuthPhase(phase:), + onMerchantSessionRequested: payment.onMerchantSessionUpdate + ) + .frame(width: 200, height: 30, alignment: .center) + .padding() + } + } .frame(minWidth: 150) } detail: { switch navigation.currentItem { diff --git a/Model/Payment.swift b/Model/Payment.swift new file mode 100644 index 000000000..d7d722eac --- /dev/null +++ b/Model/Payment.swift @@ -0,0 +1,67 @@ +// This file is part of Kiwix for iOS & macOS. +// +// Kiwix is free software; you can redistribute it and/or modify it +// under the terms of the GNU General Public License as published by +// the Free Software Foundation; either version 3 of the License, or +// any later version. +// +// Kiwix is distributed in the hope that it will be useful, but +// WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +// General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with Kiwix; If not, see https://www.gnu.org/licenses/. + +import Foundation +import PassKit +import SwiftUI + +struct Payment { + + static let merchantId = "merchant.org.kiwix" + static let supportedNetworks: [PKPaymentNetwork] = [.masterCard, .visa, .discover, .amex, .chinaUnionPay, .electron, .girocard] + static let capabilities: PKMerchantCapability = [.threeDSecure, .credit, .debit, .emv] + + func donationRequest() -> PKPaymentRequest { + let request = PKPaymentRequest() + request.merchantIdentifier = Self.merchantId + request.merchantCapabilities = Self.capabilities + request.countryCode = "CH" + request.currencyCode = "USD" + request.supportedNetworks = Self.supportedNetworks + request.paymentSummaryItems = [ + PKPaymentSummaryItem(label: "Kiwix", amount: 15, type: .final) + ] + return request + } + + func onPaymentAuthPhase(phase: PayWithApplePayButtonPaymentAuthorizationPhase) { + debugPrint("onPaymentAuthPhase: \(phase)") + switch phase { + case .willAuthorize: + break + case .didAuthorize(let payment, let resultHandler): +// server.process(with: payment) { serverResult in +// guard case .success = serverResult else { +// // handle error +// resultHandler(PKPaymentAuthorizationResult(status: .failure, errors: Error())) +// return +// } +// // handle success +// let result = PKPaymentAuthorizationResult(status: .success, errors: nil) +// resultHandler(result) +// } + break + case .didFinish: + break + @unknown default: + break + } + } + + @available(macOS 13.0, *) + func onMerchantSessionUpdate() async -> PKPaymentRequestMerchantSessionUpdate { + .init(status: .success, merchantSession: nil) + } +} From 695b7e47873cd28fed8dec70221bf7d5718d1fd2 Mon Sep 17 00:00:00 2001 From: Balazs Perlaki-Horvath Date: Sat, 21 Sep 2024 20:01:53 +0200 Subject: [PATCH 04/54] Add Donate with Apple Pay button to iOS --- Views/Settings/Settings.swift | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/Views/Settings/Settings.swift b/Views/Settings/Settings.swift index f2940a377..6eb9dc2b0 100644 --- a/Views/Settings/Settings.swift +++ b/Views/Settings/Settings.swift @@ -126,6 +126,8 @@ struct SettingSection: View { #elseif os(iOS) +import PassKit + struct Settings: View { @Default(.backupDocumentDirectory) private var backupDocumentDirectory @Default(.downloadUsingCellular) private var downloadUsingCellular @@ -135,6 +137,8 @@ struct Settings: View { @Default(.webViewPageZoom) private var webViewPageZoom @EnvironmentObject private var library: LibraryViewModel + private let payment = Payment() + enum Route { case languageSelector, about } @@ -244,6 +248,23 @@ struct Settings: View { var miscellaneous: some View { Section("settings.miscellaneous.title".localized) { + if PKPaymentAuthorizationController.canMakePayments( + usingNetworks: Payment.supportedNetworks, + capabilities: Payment.capabilities + ) { + HStack { + Spacer() + PayWithApplePayButton( + .donate, + request: payment.donationRequest(), + onPaymentAuthorizationChange: payment.onPaymentAuthPhase(phase:), + onMerchantSessionRequested: payment.onMerchantSessionUpdate + ) + .frame(width: 200, height: 38) + .padding(2) + Spacer() + } + } Button("settings.miscellaneous.button.feedback".localized) { UIApplication.shared.open(URL(string: "mailto:feedback@kiwix.org")!) } From 9fcee6bfa7fdb085c6912b596551c87a8a3f472b Mon Sep 17 00:00:00 2001 From: Balazs Perlaki-Horvath Date: Sat, 2 Nov 2024 09:28:01 +0100 Subject: [PATCH 05/54] Add payment files --- Model/Payment.swift | 14 ++++ Views/Payment/CustomAmount.swift | 94 +++++++++++++++++++++++++++ Views/Payment/ListOfAmounts.swift | 95 +++++++++++++++++++++++++++ Views/Payment/PaymentForm.swift | 103 ++++++++++++++++++++++++++++++ 4 files changed, 306 insertions(+) create mode 100644 Views/Payment/CustomAmount.swift create mode 100644 Views/Payment/ListOfAmounts.swift create mode 100644 Views/Payment/PaymentForm.swift diff --git a/Model/Payment.swift b/Model/Payment.swift index d7d722eac..88c0df3e9 100644 --- a/Model/Payment.swift +++ b/Model/Payment.swift @@ -22,6 +22,20 @@ struct Payment { static let merchantId = "merchant.org.kiwix" static let supportedNetworks: [PKPaymentNetwork] = [.masterCard, .visa, .discover, .amex, .chinaUnionPay, .electron, .girocard] static let capabilities: PKMerchantCapability = [.threeDSecure, .credit, .debit, .emv] + static let currencyCodes = ["USD", "EUR", "CHF"] + static let defaultCurrencyCode = "USD" + + static let oneTimes: [AmountOption] = [ + .init(value: 10), + .init(value: 34, isAverage: true), + .init(value: 50) + ] + + static let monthlies: [AmountOption] = [ + .init(value: 5), + .init(value: 8, isAverage: true), + .init(value: 10) + ] func donationRequest() -> PKPaymentRequest { let request = PKPaymentRequest() diff --git a/Views/Payment/CustomAmount.swift b/Views/Payment/CustomAmount.swift new file mode 100644 index 000000000..f55c49264 --- /dev/null +++ b/Views/Payment/CustomAmount.swift @@ -0,0 +1,94 @@ +// This file is part of Kiwix for iOS & macOS. +// +// Kiwix is free software; you can redistribute it and/or modify it +// under the terms of the GNU General Public License as published by +// the Free Software Foundation; either version 3 of the License, or +// any later version. +// +// Kiwix is distributed in the hope that it will be useful, but +// WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +// General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with Kiwix; If not, see https://www.gnu.org/licenses/. + +import SwiftUI +import Combine + +struct CustomAmount: View { + private let selected: PassthroughSubject + private let isMonthly: Bool + @State private var customAmount: Double? + @State private var customCurrency: String = Payment.defaultCurrencyCode + @FocusState private var focusedField: FocusedField? + private var currencies = Payment.currencyCodes + + public init(selected: PassthroughSubject, isMonthly: Bool) { + self.selected = selected + self.isMonthly = isMonthly + } + + var body: some View { + VStack { + Spacer() + List { + HStack { + TextField("Custom amount", + value: $customAmount, + format: .number.precision(.fractionLength(2))) + .focused($focusedField, equals: .customAmount) +#if os(iOS) + .padding(6) + .keyboardType(.decimalPad) +#else + .textFieldStyle(.plain) + .fontWeight(.bold) + .font(Font.headline) + .padding(4) + .border(Color.accentColor.opacity(0.618), width: 2) +#endif + Picker("", selection: $customCurrency) { + ForEach(currencies, id: \.self) { + Text(Locale.current.localizedString(forCurrencyCode: $0) ?? $0) + } + } + } + }.frame(maxHeight: 100) + Spacer() + HStack { + Spacer() + Button { + if let customAmount { + selected.send( + SelectedAmount( + value: customAmount, + currency: customCurrency, + isMonthly: isMonthly + ) + ) + } + } label: { + Text("Confirm") + } + .buttonStyle(BorderedProminentButtonStyle()) + .padding() + .disabled( customAmount == nil || (customAmount ?? 0) <= 0) + } + Spacer() + } + .task { @MainActor in + focusedField = .customAmount + } + } + +} + +private enum FocusedField: String { + case customAmount +} + +#Preview { + CustomAmount(selected: PassthroughSubject(), isMonthly: true) +} + diff --git a/Views/Payment/ListOfAmounts.swift b/Views/Payment/ListOfAmounts.swift new file mode 100644 index 000000000..a784cd111 --- /dev/null +++ b/Views/Payment/ListOfAmounts.swift @@ -0,0 +1,95 @@ +// This file is part of Kiwix for iOS & macOS. +// +// Kiwix is free software; you can redistribute it and/or modify it +// under the terms of the GNU General Public License as published by +// the Free Software Foundation; either version 3 of the License, or +// any later version. +// +// Kiwix is distributed in the hope that it will be useful, but +// WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +// General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with Kiwix; If not, see https://www.gnu.org/licenses/. + +import SwiftUI +import Combine + +struct ListOfAmounts: View { + let amountSelected: PassthroughSubject + + @Binding public var isMonthly: Bool + @State private var listState: ListState = .list + #if os(macOS) + @EnvironmentObject var formReset: FormReset + #endif + + init(amountSelected: PassthroughSubject, isMonthly: Binding) { + self.amountSelected = amountSelected + _isMonthly = isMonthly + } + + var body: some View { + if case .customAmount = listState { + CustomAmount(selected: amountSelected, isMonthly: isMonthly) + #if os(macOS) + .onReceive(formReset.objectWillChange) { _ in + reset() + } + #endif + } else { + listing() + // doesn't need reset, since this is the default state + } + } + + private func reset() { + listState = .list + } + + private func listing() -> some View { + let items = isMonthly ? Payment.monthlies : Payment.oneTimes + let averageText: String = isMonthly ? "Average monthly donation" : "Last year's average" + let defaultCurrency: String = "USD" + return List { + ForEach(items) { amount in + Button(action: { + amountSelected.send(SelectedAmount(value: amount.value, currency: defaultCurrency, isMonthly: isMonthly)) + }, label: { + VStack(alignment: .leading, spacing: 4) { + Text(amount.value, format: .currency(code: defaultCurrency)) + .frame(alignment: .leading) + if amount.isAverage { + Text(averageText) + .foregroundColor(.secondary) + .font(.caption2) + } + } + }) + .padding(6) + } + Button(action: { + listState = .customAmount + }, label: { + Text("Custom amount") + }) + .padding(6) + } + #if os(macOS) + .buttonStyle(LinkButtonStyle()) + #endif + } +} + +private enum ListState { + case list + case customAmount +} + +#Preview { + ListOfAmounts( + amountSelected: PassthroughSubject(), + isMonthly: Binding.constant(true) + ) +} diff --git a/Views/Payment/PaymentForm.swift b/Views/Payment/PaymentForm.swift new file mode 100644 index 000000000..26da9384a --- /dev/null +++ b/Views/Payment/PaymentForm.swift @@ -0,0 +1,103 @@ +// This file is part of Kiwix for iOS & macOS. +// +// Kiwix is free software; you can redistribute it and/or modify it +// under the terms of the GNU General Public License as published by +// the Free Software Foundation; either version 3 of the License, or +// any later version. +// +// Kiwix is distributed in the hope that it will be useful, but +// WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +// General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with Kiwix; If not, see https://www.gnu.org/licenses/. + +import SwiftUI +import Combine + +struct PaymentForm: View { + let amountSelected: PassthroughSubject + @State var isMonthly: Bool = false + @Environment(\.dismiss) var dismiss + #if os(macOS) + @EnvironmentObject var formReset: FormReset + #endif + + init(amountSelected: PassthroughSubject) { + self.amountSelected = amountSelected + } + + private func reset() { + isMonthly = false + } + + var body: some View { + #if os(iOS) + HStack { + Button("Cancel", action: { + dismiss() + }) + .padding() + .buttonStyle(BorderlessButtonStyle()) + Spacer() + } + + let pickerTitle = "Donate" + #else + let pickerTitle = "" + #endif + + VStack { + Picker(pickerTitle, selection: $isMonthly) { + Label("One time", systemImage: "heart.circle").tag(false) + Label("Monthly", systemImage: "arrow.clockwise.heart").tag(true) + }.pickerStyle(.segmented) + .padding([.leading, .trailing, .bottom]) + + ListOfAmounts(amountSelected: amountSelected, isMonthly: $isMonthly) + } + #if os(macOS) + .padding() + .navigationTitle("Donate") + .onReceive(formReset.objectWillChange) { _ in + reset() + } + #else + .onReceive(amountSelected) { amount in + if amount != nil { + dismiss() + } + } + #endif + } +} + +#Preview { + PaymentForm(amountSelected: PassthroughSubject()) +} + +struct SelectedAmount { + let value: Double + let currency: String + let isMonthly: Bool +} + +struct AmountOption: Identifiable { + // stabelise the scroll, if we have the same amount + // for both one-time and monthly and we switch in-between them + let id = UUID() + let value: Double + let isAverage: Bool + + init(value: Double, isAverage: Bool = false) { + self.value = value + self.isAverage = isAverage + } +} + +final class FormReset: ObservableObject { + func reset() { + objectWillChange.send() + } +} From d3b1bf676e59de8010740d116449181ea87a2b9b Mon Sep 17 00:00:00 2001 From: Balazs Perlaki-Horvath Date: Sat, 2 Nov 2024 11:15:58 +0100 Subject: [PATCH 06/54] macOS donation window --- App/App_macOS.swift | 49 +++++++++++++++++++++----- Views/Buttons/SupportKiwixButton.swift | 36 +++++++++++++++++++ 2 files changed, 76 insertions(+), 9 deletions(-) create mode 100644 Views/Buttons/SupportKiwixButton.swift diff --git a/App/App_macOS.swift b/App/App_macOS.swift index 727b8ad9c..f5fd1bbd5 100644 --- a/App/App_macOS.swift +++ b/App/App_macOS.swift @@ -32,6 +32,8 @@ struct Kiwix: App { @NSApplicationDelegateAdaptor(AppDelegate.self) var appDelegate @StateObject private var libraryRefreshViewModel = LibraryViewModel() private let notificationCenterDelegate = NotificationCenterDelegate() + private var amountSelected = PassthroughSubject() + @StateObject var formReset = FormReset() init() { UNUserNotificationCenter.current().delegate = notificationCenterDelegate @@ -80,6 +82,31 @@ struct Kiwix: App { } .frame(width: 550, height: 400) } + Window("Donate", id: "donation") { + PaymentForm(amountSelected: amountSelected) + .frame(width: 320, height: 320) + .onReceive(NotificationCenter.default.publisher(for: NSWindow.willCloseNotification)) { notification in + if let window = notification.object as? NSWindow, + window.identifier?.rawValue == "donation" { + debugPrint("closing donation") + formReset.reset() + } + } + .environmentObject(formReset) + .onReceive(amountSelected) { amount in + debugPrint("amountSelected: \(amount)") + // after upgrading to macOS 14, use: + // @Environment(\.dismissWindow) var dismissWindow + // and call: + // dismissWindow(id: "donation") + NSApplication.shared.windows.first { window in + window.identifier?.rawValue == "donation" + }?.close() + } + } + .windowResizability(.contentSize) + .windowStyle(.titleBar) + .commandsRemoved() } private class NotificationCenterDelegate: NSObject, UNUserNotificationCenterDelegate { @@ -99,6 +126,7 @@ struct Kiwix: App { } struct RootView: View { + @Environment(\.openWindow) var openWindow @Environment(\.controlActiveState) var controlActiveState @StateObject private var navigation = NavigationViewModel() @StateObject private var windowTracker = WindowTracker() @@ -134,17 +162,20 @@ struct RootView: View { usingNetworks: Payment.supportedNetworks, capabilities: Payment.capabilities ) { - PayWithApplePayButton( - .donate, - request: payment.donationRequest(), - onPaymentAuthorizationChange: payment.onPaymentAuthPhase(phase:), - onMerchantSessionRequested: payment.onMerchantSessionUpdate - ) - .frame(width: 200, height: 30, alignment: .center) - .padding() + SupportKiwixButton { + openWindow(id: "donation") + } +// PayWithApplePayButton( +// .donate, +// request: payment.donationRequest(), +// onPaymentAuthorizationChange: payment.onPaymentAuthPhase(phase:), +// onMerchantSessionRequested: payment.onMerchantSessionUpdate +// ) +// .frame(width: 200, height: 30, alignment: .center) +// .padding() } } - .frame(minWidth: 150) + .frame(minWidth: 160) } detail: { switch navigation.currentItem { case .loading: diff --git a/Views/Buttons/SupportKiwixButton.swift b/Views/Buttons/SupportKiwixButton.swift new file mode 100644 index 000000000..f4ee96fd4 --- /dev/null +++ b/Views/Buttons/SupportKiwixButton.swift @@ -0,0 +1,36 @@ +// This file is part of Kiwix for iOS & macOS. +// +// Kiwix is free software; you can redistribute it and/or modify it +// under the terms of the GNU General Public License as published by +// the Free Software Foundation; either version 3 of the License, or +// any later version. +// +// Kiwix is distributed in the hope that it will be useful, but +// WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +// General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with Kiwix; If not, see https://www.gnu.org/licenses/. + +import Foundation +import SwiftUI + +struct SupportKiwixButton: View { + + let openDonation: () -> Void + + var body: some View { + Button { + openDonation() + } label: { + HStack { + Image(systemName: "heart.fill") + .foregroundStyle(.red) + Text("Support Kiwix") + }.padding(6) + } + .buttonStyle(BorderlessButtonStyle()) + .padding() + } +} From 244c896c1e0c7462bbf8a69a15fbb71cdc392a7c Mon Sep 17 00:00:00 2001 From: Balazs Perlaki-Horvath Date: Sat, 2 Nov 2024 15:56:13 +0100 Subject: [PATCH 07/54] Integrating PaymentForm --- Model/Payment.swift | 16 ++++-- Views/Buttons/SupportKiwixButton.swift | 7 ++- Views/Settings/Settings.swift | 74 +++++++++++++++++--------- 3 files changed, 67 insertions(+), 30 deletions(-) diff --git a/Model/Payment.swift b/Model/Payment.swift index 88c0df3e9..167f01514 100644 --- a/Model/Payment.swift +++ b/Model/Payment.swift @@ -37,15 +37,25 @@ struct Payment { .init(value: 10) ] - func donationRequest() -> PKPaymentRequest { + func donationRequest(for selectedAmount: SelectedAmount) -> PKPaymentRequest { let request = PKPaymentRequest() request.merchantIdentifier = Self.merchantId request.merchantCapabilities = Self.capabilities request.countryCode = "CH" - request.currencyCode = "USD" + request.currencyCode = selectedAmount.currency request.supportedNetworks = Self.supportedNetworks + let recurring: PKRecurringPaymentRequest? = if selectedAmount.isMonthly { + PKRecurringPaymentRequest(paymentDescription: "Support Kiwix", + regularBilling: .init(label: "Monthly support for Kiwix", + amount: NSDecimalNumber(value: selectedAmount.value), + type: .final), + managementURL: URL(string: "https://www.kiwix.org")!) + } else { + nil + } + request.recurringPaymentRequest = recurring request.paymentSummaryItems = [ - PKPaymentSummaryItem(label: "Kiwix", amount: 15, type: .final) + PKPaymentSummaryItem(label: "Kiwix", amount: NSDecimalNumber(value: selectedAmount.value), type: .final) ] return request } diff --git a/Views/Buttons/SupportKiwixButton.swift b/Views/Buttons/SupportKiwixButton.swift index f4ee96fd4..303334c81 100644 --- a/Views/Buttons/SupportKiwixButton.swift +++ b/Views/Buttons/SupportKiwixButton.swift @@ -28,9 +28,14 @@ struct SupportKiwixButton: View { Image(systemName: "heart.fill") .foregroundStyle(.red) Text("Support Kiwix") - }.padding(6) + } + #if os(macOS) + .padding(6) + #endif } .buttonStyle(BorderlessButtonStyle()) + #if os(macOS) .padding() + #endif } } diff --git a/Views/Settings/Settings.swift b/Views/Settings/Settings.swift index 6eb9dc2b0..700937552 100644 --- a/Views/Settings/Settings.swift +++ b/Views/Settings/Settings.swift @@ -127,8 +127,14 @@ struct SettingSection: View { #elseif os(iOS) import PassKit +import Combine struct Settings: View { + private var amountSelected = PassthroughSubject() + @State private var showDonationPopUp: Bool = false + func openDonation() { + showDonationPopUp = true + } @Default(.backupDocumentDirectory) private var backupDocumentDirectory @Default(.downloadUsingCellular) private var downloadUsingCellular @Default(.externalLinkLoadingPolicy) private var externalLinkLoadingPolicy @@ -144,23 +150,35 @@ struct Settings: View { } var body: some View { - if FeatureFlags.hasLibrary { - List { - readingSettings - librarySettings - catalogSettings - backupSettings - miscellaneous + Group { + if FeatureFlags.hasLibrary { + List { + readingSettings + librarySettings + catalogSettings + backupSettings + miscellaneous + } + .modifier(ToolbarRoleBrowser()) + .navigationTitle("settings.navigation.title".localized) + } else { + List { + readingSettings + miscellaneous + } + .modifier(ToolbarRoleBrowser()) + .navigationTitle("settings.navigation.title".localized) } - .modifier(ToolbarRoleBrowser()) - .navigationTitle("settings.navigation.title".localized) - } else { - List { - readingSettings - miscellaneous + } + .sheet(isPresented: $showDonationPopUp) { + NavigationStack { + PaymentForm(amountSelected: amountSelected) } - .modifier(ToolbarRoleBrowser()) - .navigationTitle("settings.navigation.title".localized) + .presentationDetents([.fraction(0.6128)]) + } + .onReceive(amountSelected) { amount in + // TODO: close the window / view and trigger apple pay + debugPrint("selected: \(String(describing: amount))") } } @@ -252,18 +270,22 @@ struct Settings: View { usingNetworks: Payment.supportedNetworks, capabilities: Payment.capabilities ) { - HStack { - Spacer() - PayWithApplePayButton( - .donate, - request: payment.donationRequest(), - onPaymentAuthorizationChange: payment.onPaymentAuthPhase(phase:), - onMerchantSessionRequested: payment.onMerchantSessionUpdate - ) - .frame(width: 200, height: 38) - .padding(2) - Spacer() + SupportKiwixButton { + openDonation() } + +// HStack { +// Spacer() +// PayWithApplePayButton( +// .donate, +// request: payment.donationRequest(), +// onPaymentAuthorizationChange: payment.onPaymentAuthPhase(phase:), +// onMerchantSessionRequested: payment.onMerchantSessionUpdate +// ) +// .frame(width: 200, height: 38) +// .padding(2) +// Spacer() +// } } Button("settings.miscellaneous.button.feedback".localized) { UIApplication.shared.open(URL(string: "mailto:feedback@kiwix.org")!) From 9c5c66faabbd90ddf1407e5acf620205c957e51c Mon Sep 17 00:00:00 2001 From: Balazs Perlaki-Horvath Date: Sat, 2 Nov 2024 22:50:35 +0100 Subject: [PATCH 08/54] Fixup for iOS --- App/App_macOS.swift | 11 ++---- Model/Payment.swift | 14 +++++++- Views/Payment/PaymentForm.swift | 28 +++++++-------- Views/Payment/PaymentSummary.swift | 57 ++++++++++++++++++++++++++++++ Views/Settings/Settings.swift | 38 ++++++++------------ 5 files changed, 100 insertions(+), 48 deletions(-) create mode 100644 Views/Payment/PaymentSummary.swift diff --git a/App/App_macOS.swift b/App/App_macOS.swift index f5fd1bbd5..171401984 100644 --- a/App/App_macOS.swift +++ b/App/App_macOS.swift @@ -158,13 +158,9 @@ struct RootView: View { } } .safeAreaInset(edge: .bottom) { - if PKPaymentAuthorizationController.canMakePayments( - usingNetworks: Payment.supportedNetworks, - capabilities: Payment.capabilities - ) { - SupportKiwixButton { - openWindow(id: "donation") - } + SupportKiwixButton { + openWindow(id: "donation") + } // PayWithApplePayButton( // .donate, // request: payment.donationRequest(), @@ -173,7 +169,6 @@ struct RootView: View { // ) // .frame(width: 200, height: 30, alignment: .center) // .padding() - } } .frame(minWidth: 160) } detail: { diff --git a/Model/Payment.swift b/Model/Payment.swift index 167f01514..1108f0751 100644 --- a/Model/Payment.swift +++ b/Model/Payment.swift @@ -20,7 +20,7 @@ import SwiftUI struct Payment { static let merchantId = "merchant.org.kiwix" - static let supportedNetworks: [PKPaymentNetwork] = [.masterCard, .visa, .discover, .amex, .chinaUnionPay, .electron, .girocard] + static let supportedNetworks: [PKPaymentNetwork] = [.masterCard, .visa, .discover, .amex, .chinaUnionPay, .electron, .girocard, .mada] static let capabilities: PKMerchantCapability = [.threeDSecure, .credit, .debit, .emv] static let currencyCodes = ["USD", "EUR", "CHF"] static let defaultCurrencyCode = "USD" @@ -37,6 +37,18 @@ struct Payment { .init(value: 10) ] + static func paymentButtonType() -> PayWithApplePayButtonLabel? { + if PKPaymentAuthorizationController.canMakePayments() { + return PayWithApplePayButtonLabel.donate + } + if PKPaymentAuthorizationController.canMakePayments( + usingNetworks: Payment.supportedNetworks, + capabilities: Payment.capabilities) { + return PayWithApplePayButtonLabel.setUp + } + return nil + } + func donationRequest(for selectedAmount: SelectedAmount) -> PKPaymentRequest { let request = PKPaymentRequest() request.merchantIdentifier = Self.merchantId diff --git a/Views/Payment/PaymentForm.swift b/Views/Payment/PaymentForm.swift index 26da9384a..afaa08965 100644 --- a/Views/Payment/PaymentForm.swift +++ b/Views/Payment/PaymentForm.swift @@ -35,21 +35,24 @@ struct PaymentForm: View { var body: some View { #if os(iOS) HStack { - Button("Cancel", action: { + Spacer() + Text("Donate") + .font(.title) + .padding() + Spacer() + } + .overlay(alignment: .topTrailing) { + Button("", systemImage: "x.circle.fill") { dismiss() - }) + } + .font(.title2) + .foregroundStyle(.secondary) .padding() - .buttonStyle(BorderlessButtonStyle()) - Spacer() } - - let pickerTitle = "Donate" - #else - let pickerTitle = "" #endif VStack { - Picker(pickerTitle, selection: $isMonthly) { + Picker("", selection: $isMonthly) { Label("One time", systemImage: "heart.circle").tag(false) Label("Monthly", systemImage: "arrow.clockwise.heart").tag(true) }.pickerStyle(.segmented) @@ -63,13 +66,8 @@ struct PaymentForm: View { .onReceive(formReset.objectWillChange) { _ in reset() } - #else - .onReceive(amountSelected) { amount in - if amount != nil { - dismiss() - } - } #endif + } } diff --git a/Views/Payment/PaymentSummary.swift b/Views/Payment/PaymentSummary.swift new file mode 100644 index 000000000..0d8742c99 --- /dev/null +++ b/Views/Payment/PaymentSummary.swift @@ -0,0 +1,57 @@ +// This file is part of Kiwix for iOS & macOS. +// +// Kiwix is free software; you can redistribute it and/or modify it +// under the terms of the GNU General Public License as published by +// the Free Software Foundation; either version 3 of the License, or +// any later version. +// +// Kiwix is distributed in the hope that it will be useful, but +// WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +// General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with Kiwix; If not, see https://www.gnu.org/licenses/. + +import SwiftUI +import PassKit + +struct PaymentSummary: View { + + let selectedAmount: SelectedAmount + private let payment = Payment() + + var body: some View { + VStack { + Text("Support Kiwix") + .font(.largeTitle) + .padding() + if selectedAmount.isMonthly { + Text("Monthly").font(.title) + .padding() + } else { + Text("One-time").font(.title) + .padding() + } + Text(selectedAmount.value.formatted(.currency(code: selectedAmount.currency))).font(.title).bold() + if let buttonLabel = Payment.paymentButtonType() { + PayWithApplePayButton( + buttonLabel, + request: payment.donationRequest(for: selectedAmount), + onPaymentAuthorizationChange: payment.onPaymentAuthPhase(phase:), + onMerchantSessionRequested: payment.onMerchantSessionUpdate + ) + .frame(width: 186, height: 44) + .padding() + } else { + Text("We are sorry to see, that your device does not support Apple Pay.") + .foregroundStyle(.red) + .font(.callout) + } + } + } +} + +#Preview { + PaymentSummary(selectedAmount: SelectedAmount(value: 34, currency: "CHF", isMonthly: true)) +} diff --git a/Views/Settings/Settings.swift b/Views/Settings/Settings.swift index 700937552..5a7a5c3e3 100644 --- a/Views/Settings/Settings.swift +++ b/Views/Settings/Settings.swift @@ -131,6 +131,7 @@ import Combine struct Settings: View { private var amountSelected = PassthroughSubject() + @State private var selectedAmount: SelectedAmount? @State private var showDonationPopUp: Bool = false func openDonation() { showDonationPopUp = true @@ -170,15 +171,20 @@ struct Settings: View { .navigationTitle("settings.navigation.title".localized) } } - .sheet(isPresented: $showDonationPopUp) { - NavigationStack { - PaymentForm(amountSelected: amountSelected) + .sheet(isPresented: $showDonationPopUp, onDismiss: { + selectedAmount = nil + }) { + Group { + if let selectedAmount { + PaymentSummary(selectedAmount: selectedAmount) + } else { + PaymentForm(amountSelected: amountSelected) + } } .presentationDetents([.fraction(0.6128)]) - } - .onReceive(amountSelected) { amount in - // TODO: close the window / view and trigger apple pay - debugPrint("selected: \(String(describing: amount))") + .onReceive(amountSelected) { value in + selectedAmount = value + } } } @@ -266,26 +272,10 @@ struct Settings: View { var miscellaneous: some View { Section("settings.miscellaneous.title".localized) { - if PKPaymentAuthorizationController.canMakePayments( - usingNetworks: Payment.supportedNetworks, - capabilities: Payment.capabilities - ) { + if Payment.paymentButtonType() != nil { SupportKiwixButton { openDonation() } - -// HStack { -// Spacer() -// PayWithApplePayButton( -// .donate, -// request: payment.donationRequest(), -// onPaymentAuthorizationChange: payment.onPaymentAuthPhase(phase:), -// onMerchantSessionRequested: payment.onMerchantSessionUpdate -// ) -// .frame(width: 200, height: 38) -// .padding(2) -// Spacer() -// } } Button("settings.miscellaneous.button.feedback".localized) { UIApplication.shared.open(URL(string: "mailto:feedback@kiwix.org")!) From d76de07636e66bf6db86ae71c209950a6f94a3f6 Mon Sep 17 00:00:00 2001 From: Balazs Perlaki-Horvath Date: Sun, 3 Nov 2024 00:20:02 +0100 Subject: [PATCH 09/54] Handle window close on transaction complete --- App/App_macOS.swift | 53 +++++++++++++++++++----------- Model/Payment.swift | 10 ++++-- Views/Payment/PaymentSummary.swift | 11 +++++-- 3 files changed, 48 insertions(+), 26 deletions(-) diff --git a/App/App_macOS.swift b/App/App_macOS.swift index 171401984..c0d050266 100644 --- a/App/App_macOS.swift +++ b/App/App_macOS.swift @@ -33,6 +33,7 @@ struct Kiwix: App { @StateObject private var libraryRefreshViewModel = LibraryViewModel() private let notificationCenterDelegate = NotificationCenterDelegate() private var amountSelected = PassthroughSubject() + @State private var selectedAmount: SelectedAmount? @StateObject var formReset = FormReset() init() { @@ -83,30 +84,43 @@ struct Kiwix: App { .frame(width: 550, height: 400) } Window("Donate", id: "donation") { - PaymentForm(amountSelected: amountSelected) - .frame(width: 320, height: 320) - .onReceive(NotificationCenter.default.publisher(for: NSWindow.willCloseNotification)) { notification in - if let window = notification.object as? NSWindow, - window.identifier?.rawValue == "donation" { - debugPrint("closing donation") - formReset.reset() + Group { + if let selectedAmount { + PaymentSummary(selectedAmount: selectedAmount) { + // after upgrading to macOS 14, use: + // @Environment(\.dismissWindow) var dismissWindow + // and call: + // dismissWindow(id: "donation") + NSApplication.shared.windows.first { window in + window.identifier?.rawValue == "donation" + }?.close() } + } else { + PaymentForm(amountSelected: amountSelected) + .frame(width: 320, height: 320) } - .environmentObject(formReset) - .onReceive(amountSelected) { amount in - debugPrint("amountSelected: \(amount)") - // after upgrading to macOS 14, use: - // @Environment(\.dismissWindow) var dismissWindow - // and call: - // dismissWindow(id: "donation") - NSApplication.shared.windows.first { window in - window.identifier?.rawValue == "donation" - }?.close() + } + .onReceive(amountSelected) { amount in + selectedAmount = amount + } + .onReceive(NotificationCenter.default.publisher(for: NSWindow.willCloseNotification)) { notification in + if let window = notification.object as? NSWindow, + window.identifier?.rawValue == "donation" { + debugPrint("closing donation") + formReset.reset() + selectedAmount = nil } + } + .environmentObject(formReset) +// .onReceive(amountSelected) { amount in +// debugPrint("amountSelected: \(amount)") +// +// } } - .windowResizability(.contentSize) + .windowResizability(.contentMinSize) .windowStyle(.titleBar) .commandsRemoved() + .defaultSize(width: 320, height: 400) } private class NotificationCenterDelegate: NSObject, UNUserNotificationCenterDelegate { @@ -138,7 +152,6 @@ struct RootView: View { private let tabCloses = NotificationCenter.default.publisher(for: NSWindow.willCloseNotification) /// Close other tabs then the ones received private let keepOnlyTabs = NotificationCenter.default.publisher(for: .keepOnlyTabs) - private let payment = Payment() var body: some View { NavigationSplitView { @@ -157,6 +170,7 @@ struct RootView: View { } } } + .frame(minWidth: 160) .safeAreaInset(edge: .bottom) { SupportKiwixButton { openWindow(id: "donation") @@ -170,7 +184,6 @@ struct RootView: View { // .frame(width: 200, height: 30, alignment: .center) // .padding() } - .frame(minWidth: 160) } detail: { switch navigation.currentItem { case .loading: diff --git a/Model/Payment.swift b/Model/Payment.swift index 1108f0751..f7cc062ad 100644 --- a/Model/Payment.swift +++ b/Model/Payment.swift @@ -19,6 +19,8 @@ import SwiftUI struct Payment { + let onComplete: () -> Void + static let merchantId = "merchant.org.kiwix" static let supportedNetworks: [PKPaymentNetwork] = [.masterCard, .visa, .discover, .amex, .chinaUnionPay, .electron, .girocard, .mada] static let capabilities: PKMerchantCapability = [.threeDSecure, .credit, .debit, .emv] @@ -78,6 +80,7 @@ struct Payment { case .willAuthorize: break case .didAuthorize(let payment, let resultHandler): + debugPrint("payment success: \(payment)") // server.process(with: payment) { serverResult in // guard case .success = serverResult else { // // handle error @@ -85,12 +88,13 @@ struct Payment { // return // } // // handle success -// let result = PKPaymentAuthorizationResult(status: .success, errors: nil) -// resultHandler(result) + let result = PKPaymentAuthorizationResult(status: .success, errors: nil) + resultHandler(result) + onComplete() // } break case .didFinish: - break + onComplete() @unknown default: break } diff --git a/Views/Payment/PaymentSummary.swift b/Views/Payment/PaymentSummary.swift index 0d8742c99..0ff3df6f7 100644 --- a/Views/Payment/PaymentSummary.swift +++ b/Views/Payment/PaymentSummary.swift @@ -19,7 +19,12 @@ import PassKit struct PaymentSummary: View { let selectedAmount: SelectedAmount - private let payment = Payment() + private let payment: Payment + + init(selectedAmount: SelectedAmount, onComplete: @escaping () -> Void) { + self.selectedAmount = selectedAmount + payment = Payment(onComplete: onComplete) + } var body: some View { VStack { @@ -44,7 +49,7 @@ struct PaymentSummary: View { .frame(width: 186, height: 44) .padding() } else { - Text("We are sorry to see, that your device does not support Apple Pay.") + Text("We are sorry, your device does not support Apple Pay.") .foregroundStyle(.red) .font(.callout) } @@ -53,5 +58,5 @@ struct PaymentSummary: View { } #Preview { - PaymentSummary(selectedAmount: SelectedAmount(value: 34, currency: "CHF", isMonthly: true)) + PaymentSummary(selectedAmount: SelectedAmount(value: 34, currency: "CHF", isMonthly: true), onComplete: {}) } From 64bc86022a1179fd06cda5a681168e95a5179c91 Mon Sep 17 00:00:00 2001 From: Balazs Perlaki-Horvath Date: Sun, 3 Nov 2024 11:01:12 +0100 Subject: [PATCH 10/54] Close popup on success/fail --- Model/Payment.swift | 8 +++++--- Views/Payment/CustomAmount.swift | 3 +-- Views/Payment/ListOfAmounts.swift | 2 +- Views/Payment/PaymentForm.swift | 4 ++-- Views/Payment/PaymentSummary.swift | 11 +++++++++-- Views/Settings/Settings.swift | 8 ++++---- 6 files changed, 22 insertions(+), 14 deletions(-) diff --git a/Model/Payment.swift b/Model/Payment.swift index f7cc062ad..e17603b9e 100644 --- a/Model/Payment.swift +++ b/Model/Payment.swift @@ -16,16 +16,18 @@ import Foundation import PassKit import SwiftUI +import Combine struct Payment { - let onComplete: () -> Void + let completeSubject = PassthroughSubject() static let merchantId = "merchant.org.kiwix" static let supportedNetworks: [PKPaymentNetwork] = [.masterCard, .visa, .discover, .amex, .chinaUnionPay, .electron, .girocard, .mada] static let capabilities: PKMerchantCapability = [.threeDSecure, .credit, .debit, .emv] static let currencyCodes = ["USD", "EUR", "CHF"] static let defaultCurrencyCode = "USD" + static let minimumAmount: Double = 5 static let oneTimes: [AmountOption] = [ .init(value: 10), @@ -90,11 +92,11 @@ struct Payment { // // handle success let result = PKPaymentAuthorizationResult(status: .success, errors: nil) resultHandler(result) - onComplete() + completeSubject.send(true) // } break case .didFinish: - onComplete() + completeSubject.send(false) @unknown default: break } diff --git a/Views/Payment/CustomAmount.swift b/Views/Payment/CustomAmount.swift index f55c49264..1036d786e 100644 --- a/Views/Payment/CustomAmount.swift +++ b/Views/Payment/CustomAmount.swift @@ -73,7 +73,7 @@ struct CustomAmount: View { } .buttonStyle(BorderedProminentButtonStyle()) .padding() - .disabled( customAmount == nil || (customAmount ?? 0) <= 0) + .disabled( customAmount == nil || (customAmount ?? 0) < Payment.minimumAmount) } Spacer() } @@ -91,4 +91,3 @@ private enum FocusedField: String { #Preview { CustomAmount(selected: PassthroughSubject(), isMonthly: true) } - diff --git a/Views/Payment/ListOfAmounts.swift b/Views/Payment/ListOfAmounts.swift index a784cd111..2c03adfa9 100644 --- a/Views/Payment/ListOfAmounts.swift +++ b/Views/Payment/ListOfAmounts.swift @@ -51,7 +51,7 @@ struct ListOfAmounts: View { private func listing() -> some View { let items = isMonthly ? Payment.monthlies : Payment.oneTimes let averageText: String = isMonthly ? "Average monthly donation" : "Last year's average" - let defaultCurrency: String = "USD" + let defaultCurrency: String = Payment.defaultCurrencyCode return List { ForEach(items) { amount in Button(action: { diff --git a/Views/Payment/PaymentForm.swift b/Views/Payment/PaymentForm.swift index afaa08965..ab8b81040 100644 --- a/Views/Payment/PaymentForm.swift +++ b/Views/Payment/PaymentForm.swift @@ -45,8 +45,8 @@ struct PaymentForm: View { Button("", systemImage: "x.circle.fill") { dismiss() } - .font(.title2) - .foregroundStyle(.secondary) + .font(.title) + .foregroundStyle(.tertiary) .padding() } #endif diff --git a/Views/Payment/PaymentSummary.swift b/Views/Payment/PaymentSummary.swift index 0ff3df6f7..1747d59e3 100644 --- a/Views/Payment/PaymentSummary.swift +++ b/Views/Payment/PaymentSummary.swift @@ -15,15 +15,20 @@ import SwiftUI import PassKit +import Combine struct PaymentSummary: View { - let selectedAmount: SelectedAmount + @Environment(\.dismiss) var dismiss + + private let selectedAmount: SelectedAmount private let payment: Payment + private let onComplete: () -> Void init(selectedAmount: SelectedAmount, onComplete: @escaping () -> Void) { self.selectedAmount = selectedAmount - payment = Payment(onComplete: onComplete) + self.onComplete = onComplete + payment = Payment() } var body: some View { @@ -53,6 +58,8 @@ struct PaymentSummary: View { .foregroundStyle(.red) .font(.callout) } + }.onReceive(payment.completeSubject) { value in + dismiss() } } } diff --git a/Views/Settings/Settings.swift b/Views/Settings/Settings.swift index 5a7a5c3e3..06d765c56 100644 --- a/Views/Settings/Settings.swift +++ b/Views/Settings/Settings.swift @@ -144,8 +144,6 @@ struct Settings: View { @Default(.webViewPageZoom) private var webViewPageZoom @EnvironmentObject private var library: LibraryViewModel - private let payment = Payment() - enum Route { case languageSelector, about } @@ -176,12 +174,14 @@ struct Settings: View { }) { Group { if let selectedAmount { - PaymentSummary(selectedAmount: selectedAmount) + PaymentSummary(selectedAmount: selectedAmount) { +// selectedAmount = nil + } } else { PaymentForm(amountSelected: amountSelected) } } - .presentationDetents([.fraction(0.6128)]) + .presentationDetents([.fraction(0.65)]) .onReceive(amountSelected) { value in selectedAmount = value } From 1ce64a1b48c295745169e67803d52aa84ec8fe72 Mon Sep 17 00:00:00 2001 From: Balazs Perlaki-Horvath Date: Sun, 3 Nov 2024 20:56:15 +0100 Subject: [PATCH 11/54] Fixlint, handle sheet to be closed on iOS --- Model/Payment.swift | 34 ++++++++++++++++++------------ Views/Payment/ListOfAmounts.swift | 14 +++++++++--- Views/Payment/PaymentSummary.swift | 3 ++- Views/Settings/Settings.swift | 6 +++--- 4 files changed, 37 insertions(+), 20 deletions(-) diff --git a/Model/Payment.swift b/Model/Payment.swift index e17603b9e..2c883941b 100644 --- a/Model/Payment.swift +++ b/Model/Payment.swift @@ -23,7 +23,16 @@ struct Payment { let completeSubject = PassthroughSubject() static let merchantId = "merchant.org.kiwix" - static let supportedNetworks: [PKPaymentNetwork] = [.masterCard, .visa, .discover, .amex, .chinaUnionPay, .electron, .girocard, .mada] + static let supportedNetworks: [PKPaymentNetwork] = [ + .masterCard, + .visa, + .discover, + .amex, + .chinaUnionPay, + .electron, + .girocard, + .mada + ] static let capabilities: PKMerchantCapability = [.threeDSecure, .credit, .debit, .emv] static let currencyCodes = ["USD", "EUR", "CHF"] static let defaultCurrencyCode = "USD" @@ -36,8 +45,8 @@ struct Payment { ] static let monthlies: [AmountOption] = [ - .init(value: 5), - .init(value: 8, isAverage: true), + .init(value: 5), + .init(value: 8, isAverage: true), .init(value: 10) ] @@ -85,18 +94,17 @@ struct Payment { debugPrint("payment success: \(payment)") // server.process(with: payment) { serverResult in // guard case .success = serverResult else { -// // handle error -// resultHandler(PKPaymentAuthorizationResult(status: .failure, errors: Error())) -// return -// } -// // handle success - let result = PKPaymentAuthorizationResult(status: .success, errors: nil) - resultHandler(result) - completeSubject.send(true) +// // handle error +// resultHandler( +// PKPaymentAuthorizationResult(status: .failure, errors: Error()) +// ) +// return // } - break + // handle success + let result = PKPaymentAuthorizationResult(status: .success, errors: nil) + resultHandler(result) case .didFinish: - completeSubject.send(false) + completeSubject.send(true) @unknown default: break } diff --git a/Views/Payment/ListOfAmounts.swift b/Views/Payment/ListOfAmounts.swift index 2c03adfa9..9baed8155 100644 --- a/Views/Payment/ListOfAmounts.swift +++ b/Views/Payment/ListOfAmounts.swift @@ -54,9 +54,17 @@ struct ListOfAmounts: View { let defaultCurrency: String = Payment.defaultCurrencyCode return List { ForEach(items) { amount in - Button(action: { - amountSelected.send(SelectedAmount(value: amount.value, currency: defaultCurrency, isMonthly: isMonthly)) - }, label: { + Button( + action: { + amountSelected.send( + SelectedAmount( + value: amount.value, + currency: defaultCurrency, + isMonthly: isMonthly + ) + ) + }, + label: { VStack(alignment: .leading, spacing: 4) { Text(amount.value, format: .currency(code: defaultCurrency)) .frame(alignment: .leading) diff --git a/Views/Payment/PaymentSummary.swift b/Views/Payment/PaymentSummary.swift index 1747d59e3..9c9edfc2e 100644 --- a/Views/Payment/PaymentSummary.swift +++ b/Views/Payment/PaymentSummary.swift @@ -58,8 +58,9 @@ struct PaymentSummary: View { .foregroundStyle(.red) .font(.callout) } - }.onReceive(payment.completeSubject) { value in + }.onReceive(payment.completeSubject) { _ in dismiss() + onComplete() } } } diff --git a/Views/Settings/Settings.swift b/Views/Settings/Settings.swift index 06d765c56..392d7fac0 100644 --- a/Views/Settings/Settings.swift +++ b/Views/Settings/Settings.swift @@ -171,11 +171,11 @@ struct Settings: View { } .sheet(isPresented: $showDonationPopUp, onDismiss: { selectedAmount = nil - }) { + }, content: { Group { if let selectedAmount { PaymentSummary(selectedAmount: selectedAmount) { -// selectedAmount = nil + showDonationPopUp = false } } else { PaymentForm(amountSelected: amountSelected) @@ -185,7 +185,7 @@ struct Settings: View { .onReceive(amountSelected) { value in selectedAmount = value } - } + }) } var readingSettings: some View { From 1e268392fbd49c1580cd4884079559c745d2d2bd Mon Sep 17 00:00:00 2001 From: Balazs Perlaki-Horvath Date: Sun, 3 Nov 2024 21:41:57 +0100 Subject: [PATCH 12/54] Localize strings --- App/App_macOS.swift | 15 +-------------- Model/Payment.swift | 25 ++++++++++--------------- Support/en.lproj/Localizable.strings | 15 +++++++++++++++ Views/Buttons/SupportKiwixButton.swift | 2 +- Views/Payment/CustomAmount.swift | 4 ++-- Views/Payment/ListOfAmounts.swift | 10 +++++++--- Views/Payment/PaymentForm.swift | 8 ++++---- Views/Payment/PaymentSummary.swift | 8 ++++---- 8 files changed, 44 insertions(+), 43 deletions(-) diff --git a/App/App_macOS.swift b/App/App_macOS.swift index c0d050266..17701a876 100644 --- a/App/App_macOS.swift +++ b/App/App_macOS.swift @@ -83,7 +83,7 @@ struct Kiwix: App { } .frame(width: 550, height: 400) } - Window("Donate", id: "donation") { + Window("payment.donate.title".localized, id: "donation") { Group { if let selectedAmount { PaymentSummary(selectedAmount: selectedAmount) { @@ -106,16 +106,11 @@ struct Kiwix: App { .onReceive(NotificationCenter.default.publisher(for: NSWindow.willCloseNotification)) { notification in if let window = notification.object as? NSWindow, window.identifier?.rawValue == "donation" { - debugPrint("closing donation") formReset.reset() selectedAmount = nil } } .environmentObject(formReset) -// .onReceive(amountSelected) { amount in -// debugPrint("amountSelected: \(amount)") -// -// } } .windowResizability(.contentMinSize) .windowStyle(.titleBar) @@ -175,14 +170,6 @@ struct RootView: View { SupportKiwixButton { openWindow(id: "donation") } -// PayWithApplePayButton( -// .donate, -// request: payment.donationRequest(), -// onPaymentAuthorizationChange: payment.onPaymentAuthPhase(phase:), -// onMerchantSessionRequested: payment.onMerchantSessionUpdate -// ) -// .frame(width: 200, height: 30, alignment: .center) -// .padding() } } detail: { switch navigation.currentItem { diff --git a/Model/Payment.swift b/Model/Payment.swift index 2c883941b..c02ce02b1 100644 --- a/Model/Payment.swift +++ b/Model/Payment.swift @@ -22,7 +22,9 @@ struct Payment { let completeSubject = PassthroughSubject() + // TODO: handle custom apps static let merchantId = "merchant.org.kiwix" + static let paymentSubscriptionManagingURL = "https://www.kiwix.org" static let supportedNetworks: [PKPaymentNetwork] = [ .masterCard, .visa, @@ -70,37 +72,30 @@ struct Payment { request.currencyCode = selectedAmount.currency request.supportedNetworks = Self.supportedNetworks let recurring: PKRecurringPaymentRequest? = if selectedAmount.isMonthly { - PKRecurringPaymentRequest(paymentDescription: "Support Kiwix", - regularBilling: .init(label: "Monthly support for Kiwix", + PKRecurringPaymentRequest(paymentDescription: "payment.description.label".localized, + regularBilling: .init(label: "payment.monthly_support.label".localized, amount: NSDecimalNumber(value: selectedAmount.value), type: .final), - managementURL: URL(string: "https://www.kiwix.org")!) + managementURL: URL(string: Self.paymentSubscriptionManagingURL)!) } else { nil } request.recurringPaymentRequest = recurring request.paymentSummaryItems = [ - PKPaymentSummaryItem(label: "Kiwix", amount: NSDecimalNumber(value: selectedAmount.value), type: .final) + PKPaymentSummaryItem( + label: "payment.summary.title".localized, + amount: NSDecimalNumber(value: selectedAmount.value), + type: .final + ) ] return request } func onPaymentAuthPhase(phase: PayWithApplePayButtonPaymentAuthorizationPhase) { - debugPrint("onPaymentAuthPhase: \(phase)") switch phase { case .willAuthorize: break case .didAuthorize(let payment, let resultHandler): - debugPrint("payment success: \(payment)") -// server.process(with: payment) { serverResult in -// guard case .success = serverResult else { -// // handle error -// resultHandler( -// PKPaymentAuthorizationResult(status: .failure, errors: Error()) -// ) -// return -// } - // handle success let result = PKPaymentAuthorizationResult(status: .success, errors: nil) resultHandler(result) case .didFinish: diff --git a/Support/en.lproj/Localizable.strings b/Support/en.lproj/Localizable.strings index 07f563aad..5e9582138 100644 --- a/Support/en.lproj/Localizable.strings +++ b/Support/en.lproj/Localizable.strings @@ -277,3 +277,18 @@ "enum.navigation_item.settings" = "Settings"; "enum.search_result_snippet_mode.disabled" = "Disabled"; "enum.search_result_snippet_mode.matches" = "Matches"; + +"payment.donate.title" = "Donate"; +"payment.description.label" = "Support Kiwix"; +"payment.summary_page.title" = "Support Kiwix"; +"payment.support_button.label" = "Support Kiwix"; +"payment.monthly_support.label" = "Monthly support for Kiwix"; +"payment.summary.title" = "Kiwix"; +"payment.textfield.custom_amount.label" = "Custom amount"; +"payment.confirm.button.title" = "Confirm"; +"payment.selection.average_monthly_donation.subtitle" = "Average monthly donation"; +"payment.selection.last_year_average.subtitle" = "Last year's average"; +"payment.selection.custom_amount" = "Custom amount"; +"payment.selection.option.one_time" = "One time"; +"payment.selection.option.monthly" = "Monthly"; +"payment.support_fallback_message" = "We are sorry, your device does not support Apple Pay."; diff --git a/Views/Buttons/SupportKiwixButton.swift b/Views/Buttons/SupportKiwixButton.swift index 303334c81..8e454b244 100644 --- a/Views/Buttons/SupportKiwixButton.swift +++ b/Views/Buttons/SupportKiwixButton.swift @@ -27,7 +27,7 @@ struct SupportKiwixButton: View { HStack { Image(systemName: "heart.fill") .foregroundStyle(.red) - Text("Support Kiwix") + Text("payment.support_button.label".localized) } #if os(macOS) .padding(6) diff --git a/Views/Payment/CustomAmount.swift b/Views/Payment/CustomAmount.swift index 1036d786e..1124f6d67 100644 --- a/Views/Payment/CustomAmount.swift +++ b/Views/Payment/CustomAmount.swift @@ -34,7 +34,7 @@ struct CustomAmount: View { Spacer() List { HStack { - TextField("Custom amount", + TextField("payment.textfield.custom_amount.label".localized, value: $customAmount, format: .number.precision(.fractionLength(2))) .focused($focusedField, equals: .customAmount) @@ -69,7 +69,7 @@ struct CustomAmount: View { ) } } label: { - Text("Confirm") + Text("payment.confirm.button.title") } .buttonStyle(BorderedProminentButtonStyle()) .padding() diff --git a/Views/Payment/ListOfAmounts.swift b/Views/Payment/ListOfAmounts.swift index 9baed8155..486a761fe 100644 --- a/Views/Payment/ListOfAmounts.swift +++ b/Views/Payment/ListOfAmounts.swift @@ -40,7 +40,7 @@ struct ListOfAmounts: View { #endif } else { listing() - // doesn't need reset, since this is the default state + // doesn't need reset, since this is the default state } } @@ -50,7 +50,11 @@ struct ListOfAmounts: View { private func listing() -> some View { let items = isMonthly ? Payment.monthlies : Payment.oneTimes - let averageText: String = isMonthly ? "Average monthly donation" : "Last year's average" + let averageText: String = if isMonthly { + "payment.selection.average_monthly_donation.subtitle".localized + } else { + "payment.selection.last_year_average.subtitle".localized + } let defaultCurrency: String = Payment.defaultCurrencyCode return List { ForEach(items) { amount in @@ -80,7 +84,7 @@ struct ListOfAmounts: View { Button(action: { listState = .customAmount }, label: { - Text("Custom amount") + Text("payment.selection.custom_amount".localized) }) .padding(6) } diff --git a/Views/Payment/PaymentForm.swift b/Views/Payment/PaymentForm.swift index ab8b81040..f800ac97c 100644 --- a/Views/Payment/PaymentForm.swift +++ b/Views/Payment/PaymentForm.swift @@ -36,7 +36,7 @@ struct PaymentForm: View { #if os(iOS) HStack { Spacer() - Text("Donate") + Text("payment.donate.title".localized) .font(.title) .padding() Spacer() @@ -53,8 +53,8 @@ struct PaymentForm: View { VStack { Picker("", selection: $isMonthly) { - Label("One time", systemImage: "heart.circle").tag(false) - Label("Monthly", systemImage: "arrow.clockwise.heart").tag(true) + Label("payment.selection.option.one_time".localized, systemImage: "heart.circle").tag(false) + Label("payment.selection.option.monthly".localized, systemImage: "arrow.clockwise.heart").tag(true) }.pickerStyle(.segmented) .padding([.leading, .trailing, .bottom]) @@ -62,7 +62,7 @@ struct PaymentForm: View { } #if os(macOS) .padding() - .navigationTitle("Donate") + .navigationTitle("payment.donate.title".localized) .onReceive(formReset.objectWillChange) { _ in reset() } diff --git a/Views/Payment/PaymentSummary.swift b/Views/Payment/PaymentSummary.swift index 9c9edfc2e..48d2ad4fb 100644 --- a/Views/Payment/PaymentSummary.swift +++ b/Views/Payment/PaymentSummary.swift @@ -33,14 +33,14 @@ struct PaymentSummary: View { var body: some View { VStack { - Text("Support Kiwix") + Text("payment.summary_page.title".localized) .font(.largeTitle) .padding() if selectedAmount.isMonthly { - Text("Monthly").font(.title) + Text("payment.selection.option.monthly".localized).font(.title) .padding() } else { - Text("One-time").font(.title) + Text("payment.selection.option.one_time".localized).font(.title) .padding() } Text(selectedAmount.value.formatted(.currency(code: selectedAmount.currency))).font(.title).bold() @@ -54,7 +54,7 @@ struct PaymentSummary: View { .frame(width: 186, height: 44) .padding() } else { - Text("We are sorry, your device does not support Apple Pay.") + Text("payment.support_fallback_message".localized) .foregroundStyle(.red) .font(.callout) } From 3a3c605232241bd38dc3c77d6ed6d6070e73dfe7 Mon Sep 17 00:00:00 2001 From: Balazs Perlaki-Horvath Date: Sun, 3 Nov 2024 21:43:33 +0100 Subject: [PATCH 13/54] Clean up --- Model/Payment.swift | 1 - 1 file changed, 1 deletion(-) diff --git a/Model/Payment.swift b/Model/Payment.swift index c02ce02b1..c36e9bf64 100644 --- a/Model/Payment.swift +++ b/Model/Payment.swift @@ -22,7 +22,6 @@ struct Payment { let completeSubject = PassthroughSubject() - // TODO: handle custom apps static let merchantId = "merchant.org.kiwix" static let paymentSubscriptionManagingURL = "https://www.kiwix.org" static let supportedNetworks: [PKPaymentNetwork] = [ From cd0df1a2f0abd35600edb4e98bc8bbc0e5dbbb31 Mon Sep 17 00:00:00 2001 From: Balazs Perlaki-Horvath Date: Mon, 4 Nov 2024 00:22:18 +0100 Subject: [PATCH 14/54] Remove unused value --- Model/Payment.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Model/Payment.swift b/Model/Payment.swift index c36e9bf64..cb45969df 100644 --- a/Model/Payment.swift +++ b/Model/Payment.swift @@ -94,7 +94,7 @@ struct Payment { switch phase { case .willAuthorize: break - case .didAuthorize(let payment, let resultHandler): + case .didAuthorize(_, let resultHandler): let result = PKPaymentAuthorizationResult(status: .success, errors: nil) resultHandler(result) case .didFinish: From 6113433520f5754a1796a66661df624634b09e24 Mon Sep 17 00:00:00 2001 From: Balazs Perlaki-Horvath Date: Tue, 5 Nov 2024 09:46:33 +0100 Subject: [PATCH 15/54] Update merchant id --- Model/Payment.swift | 2 +- project.yml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Model/Payment.swift b/Model/Payment.swift index cb45969df..abf379543 100644 --- a/Model/Payment.swift +++ b/Model/Payment.swift @@ -22,7 +22,7 @@ struct Payment { let completeSubject = PassthroughSubject() - static let merchantId = "merchant.org.kiwix" + static let merchantId = "merchant.org.kiwix.apple" static let paymentSubscriptionManagingURL = "https://www.kiwix.org" static let supportedNetworks: [PKPaymentNetwork] = [ .masterCard, diff --git a/project.yml b/project.yml index 7b2618db9..276fb779e 100644 --- a/project.yml +++ b/project.yml @@ -59,7 +59,7 @@ targetTemplates: com.apple.security.files.user-selected.read-write: true com.apple.security.network.client: true com.apple.security.print: true - com.apple.developer.in-app-payments: [merchant.org.kiwix] + com.apple.developer.in-app-payments: [merchant.org.kiwix.apple] dependencies: - framework: CoreKiwix.xcframework embed: false From 027a29450828acc85bd561291f7e45fcbe6304fc Mon Sep 17 00:00:00 2001 From: Balazs Perlaki-Horvath Date: Thu, 7 Nov 2024 21:56:04 +0100 Subject: [PATCH 16/54] Remove in app payments merchant id --- project.yml | 1 - 1 file changed, 1 deletion(-) diff --git a/project.yml b/project.yml index 276fb779e..548d75aa0 100644 --- a/project.yml +++ b/project.yml @@ -59,7 +59,6 @@ targetTemplates: com.apple.security.files.user-selected.read-write: true com.apple.security.network.client: true com.apple.security.print: true - com.apple.developer.in-app-payments: [merchant.org.kiwix.apple] dependencies: - framework: CoreKiwix.xcframework embed: false From 546266752dbe3c5438619c27f17966d6e0a0da73 Mon Sep 17 00:00:00 2001 From: Balazs Perlaki-Horvath Date: Thu, 7 Nov 2024 22:40:45 +0100 Subject: [PATCH 17/54] Add Stripe --- project.yml | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/project.yml b/project.yml index 548d75aa0..1e6cca62e 100644 --- a/project.yml +++ b/project.yml @@ -47,6 +47,9 @@ packages: Defaults: url: https://github.com/sindresorhus/Defaults majorVersion: 6.0.0 + Stripe: + url: https://github.com/stripe/stripe-ios-spm + majorVersion: 24.0.0 targetTemplates: ApplicationTemplate: @@ -71,6 +74,9 @@ targetTemplates: - sdk: PassKit.framework - sdk: SystemConfiguration.framework - package: Defaults + - package: Stripe + destinationFilters: + - iOS sources: - path: App - path: Model From f53347c439aea2be27f48d689812c510dd055f1a Mon Sep 17 00:00:00 2001 From: Balazs Perlaki-Horvath Date: Thu, 7 Nov 2024 22:45:02 +0100 Subject: [PATCH 18/54] Add empty merchant id --- project.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/project.yml b/project.yml index 1e6cca62e..8ee41b5ab 100644 --- a/project.yml +++ b/project.yml @@ -62,6 +62,7 @@ targetTemplates: com.apple.security.files.user-selected.read-write: true com.apple.security.network.client: true com.apple.security.print: true + com.apple.developer.in-app-payments: [] dependencies: - framework: CoreKiwix.xcframework embed: false From 9fd6b844c374d63da2ee959d3538d8f161c72b59 Mon Sep 17 00:00:00 2001 From: Balazs Perlaki-Horvath Date: Thu, 7 Nov 2024 22:45:51 +0100 Subject: [PATCH 19/54] Remove stripe dependency --- project.yml | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/project.yml b/project.yml index 8ee41b5ab..f1ee07205 100644 --- a/project.yml +++ b/project.yml @@ -47,9 +47,9 @@ packages: Defaults: url: https://github.com/sindresorhus/Defaults majorVersion: 6.0.0 - Stripe: - url: https://github.com/stripe/stripe-ios-spm - majorVersion: 24.0.0 + # Stripe: + # url: https://github.com/stripe/stripe-ios-spm + # majorVersion: 24.0.0 targetTemplates: ApplicationTemplate: @@ -75,9 +75,9 @@ targetTemplates: - sdk: PassKit.framework - sdk: SystemConfiguration.framework - package: Defaults - - package: Stripe - destinationFilters: - - iOS + # - package: Stripe + # destinationFilters: + # - iOS sources: - path: App - path: Model From 9509d7c7d6f3dae5d2e0e5cffda7691c42640f3c Mon Sep 17 00:00:00 2001 From: Balazs Perlaki-Horvath Date: Thu, 7 Nov 2024 22:46:07 +0100 Subject: [PATCH 20/54] Revert "Remove stripe dependency" This reverts commit c7bcf0fabcadbfb5f92b8965c877b468db928c5b. --- project.yml | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/project.yml b/project.yml index f1ee07205..8ee41b5ab 100644 --- a/project.yml +++ b/project.yml @@ -47,9 +47,9 @@ packages: Defaults: url: https://github.com/sindresorhus/Defaults majorVersion: 6.0.0 - # Stripe: - # url: https://github.com/stripe/stripe-ios-spm - # majorVersion: 24.0.0 + Stripe: + url: https://github.com/stripe/stripe-ios-spm + majorVersion: 24.0.0 targetTemplates: ApplicationTemplate: @@ -75,9 +75,9 @@ targetTemplates: - sdk: PassKit.framework - sdk: SystemConfiguration.framework - package: Defaults - # - package: Stripe - # destinationFilters: - # - iOS + - package: Stripe + destinationFilters: + - iOS sources: - path: App - path: Model From 2a1f2a6378a5bbb511d600a7db2f1f22aca3c355 Mon Sep 17 00:00:00 2001 From: Balazs Perlaki-Horvath Date: Thu, 7 Nov 2024 23:00:51 +0100 Subject: [PATCH 21/54] Add merchant id --- project.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/project.yml b/project.yml index 8ee41b5ab..fb192c63a 100644 --- a/project.yml +++ b/project.yml @@ -62,7 +62,7 @@ targetTemplates: com.apple.security.files.user-selected.read-write: true com.apple.security.network.client: true com.apple.security.print: true - com.apple.developer.in-app-payments: [] + com.apple.developer.in-app-payments: [merchant.org.kiwix.apple] dependencies: - framework: CoreKiwix.xcframework embed: false From cd9af5c9ee57b0943156fa9f0bdff26d62f66a59 Mon Sep 17 00:00:00 2001 From: Balazs Perlaki-Horvath Date: Fri, 8 Nov 2024 21:17:04 +0100 Subject: [PATCH 22/54] Add StipeApplePay to iOS --- App/App_iOS.swift | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/App/App_iOS.swift b/App/App_iOS.swift index 8b1b628e6..37967c1e7 100644 --- a/App/App_iOS.swift +++ b/App/App_iOS.swift @@ -17,6 +17,8 @@ import SwiftUI import UserNotifications #if os(iOS) +import StripeApplePay + @main struct Kiwix: App { @Environment(\.scenePhase) private var scenePhase @@ -94,6 +96,13 @@ struct Kiwix: App { } private class AppDelegate: NSObject, UIApplicationDelegate, UNUserNotificationCenterDelegate { + func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey : Any]? = nil) -> Bool { +// StripeAPI.defaultPublishableKey = "pk_test_51AROWSJX9HHJ5bycpEUP9dK39tXufyuWogSUdeweyZEXy3LC7M8yc5d9NlQ96fRCVL0BlAu7Nqt4V7N5xZjJnrkp005fDiTMIr" + StripeAPI.defaultPublishableKey = "pk_test_oglo2v3Wc7ibH2oQe5oUDkhi" + return true + } + + /// Storing background download completion handler sent to application delegate func application(_ application: UIApplication, handleEventsForBackgroundURLSession identifier: String, From ce530aedb7f9bcb9fccf225819c7eab650624ab1 Mon Sep 17 00:00:00 2001 From: Balazs Perlaki-Horvath Date: Fri, 8 Nov 2024 21:38:56 +0100 Subject: [PATCH 23/54] Add merchant id, again --- project.yml | 1 - 1 file changed, 1 deletion(-) diff --git a/project.yml b/project.yml index fb192c63a..1e6cca62e 100644 --- a/project.yml +++ b/project.yml @@ -62,7 +62,6 @@ targetTemplates: com.apple.security.files.user-selected.read-write: true com.apple.security.network.client: true com.apple.security.print: true - com.apple.developer.in-app-payments: [merchant.org.kiwix.apple] dependencies: - framework: CoreKiwix.xcframework embed: false From c28de99fe4ad60c58e25e738aa7023555579e233 Mon Sep 17 00:00:00 2001 From: Balazs Perlaki-Horvath Date: Mon, 11 Nov 2024 14:17:24 +0100 Subject: [PATCH 24/54] Update dependencies --- App/App_iOS.swift | 4 ++-- App/App_macOS.swift | 4 ++++ Model/Payment.swift | 22 +++++++++++++++------- project.yml | 8 +++----- 4 files changed, 24 insertions(+), 14 deletions(-) diff --git a/App/App_iOS.swift b/App/App_iOS.swift index 37967c1e7..c9a309023 100644 --- a/App/App_iOS.swift +++ b/App/App_iOS.swift @@ -97,8 +97,8 @@ struct Kiwix: App { private class AppDelegate: NSObject, UIApplicationDelegate, UNUserNotificationCenterDelegate { func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey : Any]? = nil) -> Bool { -// StripeAPI.defaultPublishableKey = "pk_test_51AROWSJX9HHJ5bycpEUP9dK39tXufyuWogSUdeweyZEXy3LC7M8yc5d9NlQ96fRCVL0BlAu7Nqt4V7N5xZjJnrkp005fDiTMIr" - StripeAPI.defaultPublishableKey = "pk_test_oglo2v3Wc7ibH2oQe5oUDkhi" + // StripeAPI.defaultPublishableKey = "pk_test_51AROWSJX9HHJ5bycpEUP9dK39tXufyuWogSUdeweyZEXy3LC7M8yc5d9NlQ96fRCVL0BlAu7Nqt4V7N5xZjJnrkp005fDiTMIr" + StripeAPI.defaultPublishableKey = Payment.stripePublicKey return true } diff --git a/App/App_macOS.swift b/App/App_macOS.swift index 17701a876..0118891c8 100644 --- a/App/App_macOS.swift +++ b/App/App_macOS.swift @@ -19,12 +19,16 @@ import Combine import Defaults import CoreKiwix import PassKit +import StripeApplePay #if os(macOS) final class AppDelegate: NSObject, NSApplicationDelegate { func applicationShouldTerminateAfterLastWindowClosed(_ sender: NSApplication) -> Bool { true } + func applicationDidFinishLaunching(_ notification: Notification) { + StripeAPI.defaultPublishableKey = Payment.stripePublicKey + } } @main diff --git a/Model/Payment.swift b/Model/Payment.swift index abf379543..de6f46856 100644 --- a/Model/Payment.swift +++ b/Model/Payment.swift @@ -17,22 +17,23 @@ import Foundation import PassKit import SwiftUI import Combine +import StripeApplePay struct Payment { let completeSubject = PassthroughSubject() static let merchantId = "merchant.org.kiwix.apple" + static let stripePublicKey = "pk_test_oglo2v3Wc7ibH2oQe5oUDkhi" static let paymentSubscriptionManagingURL = "https://www.kiwix.org" static let supportedNetworks: [PKPaymentNetwork] = [ - .masterCard, - .visa, - .discover, .amex, - .chinaUnionPay, + .discover, .electron, - .girocard, - .mada + .mada, + .maestro, + .masterCard, + .visa, ] static let capabilities: PKMerchantCapability = [.threeDSecure, .credit, .debit, .emv] static let currencyCodes = ["USD", "EUR", "CHF"] @@ -70,6 +71,7 @@ struct Payment { request.countryCode = "CH" request.currencyCode = selectedAmount.currency request.supportedNetworks = Self.supportedNetworks + request.requiredBillingContactFields = [.postalAddress] let recurring: PKRecurringPaymentRequest? = if selectedAmount.isMonthly { PKRecurringPaymentRequest(paymentDescription: "payment.description.label".localized, regularBilling: .init(label: "payment.monthly_support.label".localized, @@ -94,7 +96,13 @@ struct Payment { switch phase { case .willAuthorize: break - case .didAuthorize(_, let resultHandler): + case .didAuthorize(let payment, let resultHandler): + // call our server to get payment / setup intent and return the client.secret + // async http call... + let stripe = STPApplePaySimple() + stripe.complete(payment: payment, + returnURLPath: nil, // TODO: update the return path for confirmations + usingClientSecretProvider: ) let result = PKPaymentAuthorizationResult(status: .success, errors: nil) resultHandler(result) case .didFinish: diff --git a/project.yml b/project.yml index 1e6cca62e..6af775285 100644 --- a/project.yml +++ b/project.yml @@ -47,8 +47,8 @@ packages: Defaults: url: https://github.com/sindresorhus/Defaults majorVersion: 6.0.0 - Stripe: - url: https://github.com/stripe/stripe-ios-spm + StripeApplePay: + url: https://github.com/CodeLikeW/stripe-apple-pay majorVersion: 24.0.0 targetTemplates: @@ -74,9 +74,7 @@ targetTemplates: - sdk: PassKit.framework - sdk: SystemConfiguration.framework - package: Defaults - - package: Stripe - destinationFilters: - - iOS + - package: StripeApplePay sources: - path: App - path: Model From 9e9dadc72b2d81f2b73ddc3fe030a6da14749d02 Mon Sep 17 00:00:00 2001 From: Balazs Perlaki-Horvath Date: Mon, 11 Nov 2024 14:22:58 +0100 Subject: [PATCH 25/54] Remove dependency --- project.yml | 4 ---- 1 file changed, 4 deletions(-) diff --git a/project.yml b/project.yml index 6af775285..548d75aa0 100644 --- a/project.yml +++ b/project.yml @@ -47,9 +47,6 @@ packages: Defaults: url: https://github.com/sindresorhus/Defaults majorVersion: 6.0.0 - StripeApplePay: - url: https://github.com/CodeLikeW/stripe-apple-pay - majorVersion: 24.0.0 targetTemplates: ApplicationTemplate: @@ -74,7 +71,6 @@ targetTemplates: - sdk: PassKit.framework - sdk: SystemConfiguration.framework - package: Defaults - - package: StripeApplePay sources: - path: App - path: Model From 5b91de84bbba28905529bce9b3fb6a413df75e96 Mon Sep 17 00:00:00 2001 From: Balazs Perlaki-Horvath Date: Mon, 11 Nov 2024 14:24:41 +0100 Subject: [PATCH 26/54] Add dependency --- project.yml | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/project.yml b/project.yml index 548d75aa0..6af775285 100644 --- a/project.yml +++ b/project.yml @@ -47,6 +47,9 @@ packages: Defaults: url: https://github.com/sindresorhus/Defaults majorVersion: 6.0.0 + StripeApplePay: + url: https://github.com/CodeLikeW/stripe-apple-pay + majorVersion: 24.0.0 targetTemplates: ApplicationTemplate: @@ -71,6 +74,7 @@ targetTemplates: - sdk: PassKit.framework - sdk: SystemConfiguration.framework - package: Defaults + - package: StripeApplePay sources: - path: App - path: Model From 24486ac0ff02153d515002a515406a7f8136f85d Mon Sep 17 00:00:00 2001 From: Balazs Perlaki-Horvath Date: Tue, 12 Nov 2024 10:00:06 +0100 Subject: [PATCH 27/54] Externalise public key, test with local server --- App/App_iOS.swift | 9 +--- App/App_macOS.swift | 4 -- Model/Payment.swift | 40 ++++++++++---- Model/StripeKiwix.swift | 86 ++++++++++++++++++++++++++++++ Views/Payment/CustomAmount.swift | 2 +- Views/Payment/PaymentForm.swift | 7 +++ Views/Payment/PaymentSummary.swift | 7 ++- 7 files changed, 132 insertions(+), 23 deletions(-) create mode 100644 Model/StripeKiwix.swift diff --git a/App/App_iOS.swift b/App/App_iOS.swift index c9a309023..d8bc13f18 100644 --- a/App/App_iOS.swift +++ b/App/App_iOS.swift @@ -17,7 +17,6 @@ import SwiftUI import UserNotifications #if os(iOS) -import StripeApplePay @main struct Kiwix: App { @@ -96,13 +95,7 @@ struct Kiwix: App { } private class AppDelegate: NSObject, UIApplicationDelegate, UNUserNotificationCenterDelegate { - func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey : Any]? = nil) -> Bool { - // StripeAPI.defaultPublishableKey = "pk_test_51AROWSJX9HHJ5bycpEUP9dK39tXufyuWogSUdeweyZEXy3LC7M8yc5d9NlQ96fRCVL0BlAu7Nqt4V7N5xZjJnrkp005fDiTMIr" - StripeAPI.defaultPublishableKey = Payment.stripePublicKey - return true - } - - + /// Storing background download completion handler sent to application delegate func application(_ application: UIApplication, handleEventsForBackgroundURLSession identifier: String, diff --git a/App/App_macOS.swift b/App/App_macOS.swift index 0118891c8..17701a876 100644 --- a/App/App_macOS.swift +++ b/App/App_macOS.swift @@ -19,16 +19,12 @@ import Combine import Defaults import CoreKiwix import PassKit -import StripeApplePay #if os(macOS) final class AppDelegate: NSObject, NSApplicationDelegate { func applicationShouldTerminateAfterLastWindowClosed(_ sender: NSApplication) -> Bool { true } - func applicationDidFinishLaunching(_ notification: Notification) { - StripeAPI.defaultPublishableKey = Payment.stripePublicKey - } } @main diff --git a/Model/Payment.swift b/Model/Payment.swift index de6f46856..74867f147 100644 --- a/Model/Payment.swift +++ b/Model/Payment.swift @@ -24,7 +24,6 @@ struct Payment { let completeSubject = PassthroughSubject() static let merchantId = "merchant.org.kiwix.apple" - static let stripePublicKey = "pk_test_oglo2v3Wc7ibH2oQe5oUDkhi" static let paymentSubscriptionManagingURL = "https://www.kiwix.org" static let supportedNetworks: [PKPaymentNetwork] = [ .amex, @@ -36,9 +35,21 @@ struct Payment { .visa, ] static let capabilities: PKMerchantCapability = [.threeDSecure, .credit, .debit, .emv] + + /// NOTE: consider that these currencies support double precision, eg: 5.25 USD. + /// Revisit `SelectedAmount`, and `SelectedPaymentAmount` + /// before adding a zero-decimal currency such as: ¥100 static let currencyCodes = ["USD", "EUR", "CHF"] static let defaultCurrencyCode = "USD" - static let minimumAmount: Double = 5 + private static let minimumAmount: Double = 5 + /// The Sripe `amount` value supports up to eight digits + /// (e.g., a value of 99999999 for a USD charge of $999,999.99). + /// see: https://docs.stripe.com/api/payment_intents/object#payment_intent_object-amount + static let maximumAmount: Int = 99999999 + static func isInValidRange(amount: Double?) -> Bool { + guard let amount else { return false } + return minimumAmount <= amount && amount <= Double(maximumAmount)*100.0 + } static let oneTimes: [AmountOption] = [ .init(value: 10), @@ -92,19 +103,30 @@ struct Payment { return request } - func onPaymentAuthPhase(phase: PayWithApplePayButtonPaymentAuthorizationPhase) { + func onPaymentAuthPhase(selectedAmount: SelectedAmount, phase: PayWithApplePayButtonPaymentAuthorizationPhase) { switch phase { case .willAuthorize: break case .didAuthorize(let payment, let resultHandler): // call our server to get payment / setup intent and return the client.secret // async http call... - let stripe = STPApplePaySimple() - stripe.complete(payment: payment, - returnURLPath: nil, // TODO: update the return path for confirmations - usingClientSecretProvider: ) - let result = PKPaymentAuthorizationResult(status: .success, errors: nil) - resultHandler(result) + Task { [resultHandler] in + + let paymentServer = StripeKiwix(endPoint: URL(string: "http://192.168.100.7:4242")!, + payment: payment) + do { + let publicKey = try await paymentServer.publishableKey() + StripeAPI.defaultPublishableKey = publicKey + } catch (let serverError) { + resultHandler(.init(status: .failure, errors: [serverError])) + return + } + let stripe = StripeApplePaySimple() + let result = await stripe.complete(payment: payment, + returnURLPath: nil, // TODO: update the return path for confirmations + usingClientSecretProvider: { await paymentServer.clientSecretForPayment(selectedAmount: selectedAmount) } ) + resultHandler(result) + } case .didFinish: completeSubject.send(true) @unknown default: diff --git a/Model/StripeKiwix.swift b/Model/StripeKiwix.swift new file mode 100644 index 000000000..bfff6dbff --- /dev/null +++ b/Model/StripeKiwix.swift @@ -0,0 +1,86 @@ +// This file is part of Kiwix for iOS & macOS. +// +// Kiwix is free software; you can redistribute it and/or modify it +// under the terms of the GNU General Public License as published by +// the Free Software Foundation; either version 3 of the License, or +// any later version. +// +// Kiwix is distributed in the hope that it will be useful, but +// WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +// General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with Kiwix; If not, see https://www.gnu.org/licenses/. + +import Foundation +import PassKit + +struct StripeKiwix { + + /// The very maximum amount stripe payment intent can handle + /// see: https://docs.stripe.com/api/payment_intents/object#payment_intent_object-amount + static let maxAmount: Int = 999999999 + + enum StripeError: Error { + case serverError + } + + let endPoint: URL + let payment: PKPayment + + func publishableKey() async throws -> String { + let (data, response) = try await URLSession.shared.data(from: endPoint.appending(path: "config")) + guard let httpResponse = response as? HTTPURLResponse, + (200..<300).contains(httpResponse.statusCode) else { + throw StripeError.serverError + } + let json = try JSONDecoder().decode(PublishableKey.self, from: data) + return json.publishableKey + } + + func clientSecretForPayment(selectedAmount: SelectedAmount) async -> Result { + do { + var request = URLRequest(url: endPoint.appending(path: "create-payment-intent")) + request.httpMethod = "POST" + request.setValue("application/json", forHTTPHeaderField: "Content-Type") + request.httpBody = try JSONEncoder().encode(SelectedPaymentAmount(from: selectedAmount)) + let (data, response) = try await URLSession.shared.data(for: request) + guard let httpResponse = response as? HTTPURLResponse, + (200..<300).contains(httpResponse.statusCode) else { + throw StripeError.serverError + } + let json = try JSONDecoder().decode(ClientSecretKey.self, from: data) + return .success(json.clientSecret) + } catch (let serverError) { + return .failure(serverError) + } + } +} + +/// Response structure for GET {endPoint}/config +/// {"publishableKey":"pk_test_..."} +private struct PublishableKey: Decodable { + let publishableKey: String +} + +/// Response structure for POST {endPoint}/create-payment-intent +/// {"clientSecret":"pi_..."} +private struct ClientSecretKey: Decodable { + let clientSecret: String +} + +private struct SelectedPaymentAmount: Encodable { + let amount: Int + let currency: String + + init(from selectedAmount: SelectedAmount) { + // Amount intended to be collected by this PaymentIntent. + // A positive integer representing how much to charge in the smallest currency unit + // (e.g., 100 cents to charge $1.00 or 100 to charge ¥100, a zero-decimal currency). + // The minimum amount is $0.50 US or equivalent in charge currency. + amount = Int(selectedAmount.value * 100.0) + currency = selectedAmount.currency + assert(Payment.currencyCodes.contains(currency)) + } +} diff --git a/Views/Payment/CustomAmount.swift b/Views/Payment/CustomAmount.swift index 1124f6d67..c2a303295 100644 --- a/Views/Payment/CustomAmount.swift +++ b/Views/Payment/CustomAmount.swift @@ -73,7 +73,7 @@ struct CustomAmount: View { } .buttonStyle(BorderedProminentButtonStyle()) .padding() - .disabled( customAmount == nil || (customAmount ?? 0) < Payment.minimumAmount) + .disabled( !Payment.isInValidRange(amount: customAmount) ) } Spacer() } diff --git a/Views/Payment/PaymentForm.swift b/Views/Payment/PaymentForm.swift index f800ac97c..03d305f3e 100644 --- a/Views/Payment/PaymentForm.swift +++ b/Views/Payment/PaymentForm.swift @@ -79,6 +79,13 @@ struct SelectedAmount { let value: Double let currency: String let isMonthly: Bool + + init(value: Double, currency: String, isMonthly: Bool) { + // make sure we won't go over Stripe's max amount + self.value = min(value, Double(StripeKiwix.maxAmount) * 100.0) + self.currency = currency + self.isMonthly = isMonthly + } } struct AmountOption: Identifiable { diff --git a/Views/Payment/PaymentSummary.swift b/Views/Payment/PaymentSummary.swift index 48d2ad4fb..e4730bf2b 100644 --- a/Views/Payment/PaymentSummary.swift +++ b/Views/Payment/PaymentSummary.swift @@ -48,7 +48,12 @@ struct PaymentSummary: View { PayWithApplePayButton( buttonLabel, request: payment.donationRequest(for: selectedAmount), - onPaymentAuthorizationChange: payment.onPaymentAuthPhase(phase:), + onPaymentAuthorizationChange: { + phase in payment.onPaymentAuthPhase( + selectedAmount: selectedAmount, + phase: phase + ) + }, onMerchantSessionRequested: payment.onMerchantSessionUpdate ) .frame(width: 186, height: 44) From d92962c117e14f64ab330d2c218aae5a18f88eb1 Mon Sep 17 00:00:00 2001 From: Balazs Perlaki-Horvath Date: Tue, 12 Nov 2024 10:53:15 +0100 Subject: [PATCH 28/54] Add merchant id, log payment phase. Working on iPad sym with local server --- Model/Payment.swift | 7 ++++++- project.yml | 1 + 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/Model/Payment.swift b/Model/Payment.swift index 74867f147..b49f90b33 100644 --- a/Model/Payment.swift +++ b/Model/Payment.swift @@ -18,6 +18,7 @@ import PassKit import SwiftUI import Combine import StripeApplePay +import os struct Payment { @@ -106,12 +107,14 @@ struct Payment { func onPaymentAuthPhase(selectedAmount: SelectedAmount, phase: PayWithApplePayButtonPaymentAuthorizationPhase) { switch phase { case .willAuthorize: + os_log("onPaymentAuthPhase: .willAuthorize") break case .didAuthorize(let payment, let resultHandler): + os_log("onPaymentAuthPhase: .didAuthorize") // call our server to get payment / setup intent and return the client.secret // async http call... Task { [resultHandler] in - + let paymentServer = StripeKiwix(endPoint: URL(string: "http://192.168.100.7:4242")!, payment: payment) do { @@ -128,8 +131,10 @@ struct Payment { resultHandler(result) } case .didFinish: + os_log("onPaymentAuthPhase: .didFinish") completeSubject.send(true) @unknown default: + os_log("onPaymentAuthPhase: @unknown default") break } } diff --git a/project.yml b/project.yml index 6af775285..85ff6cee5 100644 --- a/project.yml +++ b/project.yml @@ -62,6 +62,7 @@ targetTemplates: com.apple.security.files.user-selected.read-write: true com.apple.security.network.client: true com.apple.security.print: true + com.apple.developer.in-app-payments: [merchant.org.kiwix.apple] dependencies: - framework: CoreKiwix.xcframework embed: false From 44b0a74036eb9491586639ce0917e1044e42d565 Mon Sep 17 00:00:00 2001 From: Balazs Perlaki-Horvath Date: Thu, 14 Nov 2024 08:51:11 +0100 Subject: [PATCH 29/54] Add merchant session for macOS --- Model/Payment.swift | 22 +++++++++++++++++++++- Model/StripeKiwix.swift | 1 + 2 files changed, 22 insertions(+), 1 deletion(-) diff --git a/Model/Payment.swift b/Model/Payment.swift index b49f90b33..ff498dd59 100644 --- a/Model/Payment.swift +++ b/Model/Payment.swift @@ -24,6 +24,7 @@ struct Payment { let completeSubject = PassthroughSubject() + static let merchantSessionURL = URL(string: "https://apple-pay-gateway.apple.com" )! static let merchantId = "merchant.org.kiwix.apple" static let paymentSubscriptionManagingURL = "https://www.kiwix.org" static let supportedNetworks: [PKPaymentNetwork] = [ @@ -141,6 +142,25 @@ struct Payment { @available(macOS 13.0, *) func onMerchantSessionUpdate() async -> PKPaymentRequestMerchantSessionUpdate { - .init(status: .success, merchantSession: nil) + var request = URLRequest(url: Self.merchantSessionURL) + request.httpMethod = "GET" + request.setValue("application/json", forHTTPHeaderField: "Content-Type") + do { + let (data, response) = try await URLSession.shared.data(for: request) + guard let httpResponse = response as? HTTPURLResponse, + (200..<300).contains(httpResponse.statusCode), + let dict = try JSONSerialization.jsonObject(with: data, options: .allowFragments) as? [String: Any] else { + throw MerchantSessionError.invalidStatus + } + let session = PKPaymentMerchantSession(dictionary: dict) + return .init(status: .success, merchantSession: session) + } catch (let error) { + os_log("Merchant session not established: %@", type: .debug, error.localizedDescription) + return .init(status: .failure, merchantSession: nil) + } } } + +private enum MerchantSessionError: Error { + case invalidStatus +} diff --git a/Model/StripeKiwix.swift b/Model/StripeKiwix.swift index bfff6dbff..9e138dd30 100644 --- a/Model/StripeKiwix.swift +++ b/Model/StripeKiwix.swift @@ -41,6 +41,7 @@ struct StripeKiwix { func clientSecretForPayment(selectedAmount: SelectedAmount) async -> Result { do { + // TODO: for monthly this should create a setup-intent ! var request = URLRequest(url: endPoint.appending(path: "create-payment-intent")) request.httpMethod = "POST" request.setValue("application/json", forHTTPHeaderField: "Content-Type") From c4e5087a4a124f14dbede7892cdbdbd755feea34 Mon Sep 17 00:00:00 2001 From: Balazs Perlaki-Horvath Date: Fri, 15 Nov 2024 12:08:27 +0100 Subject: [PATCH 30/54] Change required postal address to email address --- Model/Payment.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Model/Payment.swift b/Model/Payment.swift index ff498dd59..c0232e732 100644 --- a/Model/Payment.swift +++ b/Model/Payment.swift @@ -84,7 +84,7 @@ struct Payment { request.countryCode = "CH" request.currencyCode = selectedAmount.currency request.supportedNetworks = Self.supportedNetworks - request.requiredBillingContactFields = [.postalAddress] + request.requiredBillingContactFields = [.emailAddress] let recurring: PKRecurringPaymentRequest? = if selectedAmount.isMonthly { PKRecurringPaymentRequest(paymentDescription: "payment.description.label".localized, regularBilling: .init(label: "payment.monthly_support.label".localized, From 60a1d1091b3d0793f712e8d7b3687bf0331253ac Mon Sep 17 00:00:00 2001 From: Balazs Perlaki-Horvath Date: Fri, 15 Nov 2024 12:57:58 +0100 Subject: [PATCH 31/54] Fixlint --- Model/Payment.swift | 19 ++++++++++--------- Model/StripeKiwix.swift | 2 +- Views/Payment/PaymentSummary.swift | 6 ++---- 3 files changed, 13 insertions(+), 14 deletions(-) diff --git a/Model/Payment.swift b/Model/Payment.swift index c0232e732..b5bbc5a83 100644 --- a/Model/Payment.swift +++ b/Model/Payment.swift @@ -34,7 +34,7 @@ struct Payment { .mada, .maestro, .masterCard, - .visa, + .visa ] static let capabilities: PKMerchantCapability = [.threeDSecure, .credit, .debit, .emv] @@ -109,11 +109,9 @@ struct Payment { switch phase { case .willAuthorize: os_log("onPaymentAuthPhase: .willAuthorize") - break case .didAuthorize(let payment, let resultHandler): os_log("onPaymentAuthPhase: .didAuthorize") // call our server to get payment / setup intent and return the client.secret - // async http call... Task { [resultHandler] in let paymentServer = StripeKiwix(endPoint: URL(string: "http://192.168.100.7:4242")!, @@ -121,14 +119,17 @@ struct Payment { do { let publicKey = try await paymentServer.publishableKey() StripeAPI.defaultPublishableKey = publicKey - } catch (let serverError) { + } catch let serverError { resultHandler(.init(status: .failure, errors: [serverError])) return } let stripe = StripeApplePaySimple() let result = await stripe.complete(payment: payment, - returnURLPath: nil, // TODO: update the return path for confirmations - usingClientSecretProvider: { await paymentServer.clientSecretForPayment(selectedAmount: selectedAmount) } ) + returnURLPath: nil, + // TODO: update the return path for confirmations + usingClientSecretProvider: { + await paymentServer.clientSecretForPayment(selectedAmount: selectedAmount) + }) resultHandler(result) } case .didFinish: @@ -136,7 +137,6 @@ struct Payment { completeSubject.send(true) @unknown default: os_log("onPaymentAuthPhase: @unknown default") - break } } @@ -149,12 +149,13 @@ struct Payment { let (data, response) = try await URLSession.shared.data(for: request) guard let httpResponse = response as? HTTPURLResponse, (200..<300).contains(httpResponse.statusCode), - let dict = try JSONSerialization.jsonObject(with: data, options: .allowFragments) as? [String: Any] else { + let dict = try JSONSerialization.jsonObject(with: data, + options: .allowFragments) as? [String: Any] else { throw MerchantSessionError.invalidStatus } let session = PKPaymentMerchantSession(dictionary: dict) return .init(status: .success, merchantSession: session) - } catch (let error) { + } catch let error { os_log("Merchant session not established: %@", type: .debug, error.localizedDescription) return .init(status: .failure, merchantSession: nil) } diff --git a/Model/StripeKiwix.swift b/Model/StripeKiwix.swift index 9e138dd30..1616b9315 100644 --- a/Model/StripeKiwix.swift +++ b/Model/StripeKiwix.swift @@ -53,7 +53,7 @@ struct StripeKiwix { } let json = try JSONDecoder().decode(ClientSecretKey.self, from: data) return .success(json.clientSecret) - } catch (let serverError) { + } catch let serverError { return .failure(serverError) } } diff --git a/Views/Payment/PaymentSummary.swift b/Views/Payment/PaymentSummary.swift index e4730bf2b..dcc95fd8b 100644 --- a/Views/Payment/PaymentSummary.swift +++ b/Views/Payment/PaymentSummary.swift @@ -49,10 +49,8 @@ struct PaymentSummary: View { buttonLabel, request: payment.donationRequest(for: selectedAmount), onPaymentAuthorizationChange: { - phase in payment.onPaymentAuthPhase( - selectedAmount: selectedAmount, - phase: phase - ) + phase in payment.onPaymentAuthPhase(selectedAmount: selectedAmount, + phase: phase) }, onMerchantSessionRequested: payment.onMerchantSessionUpdate ) From 3a61563f742661930043b33f183a771e9713ab9b Mon Sep 17 00:00:00 2001 From: rgaudin Date: Fri, 15 Nov 2024 15:38:17 +0000 Subject: [PATCH 32/54] Use api.donation.kiwix.org --- Model/Payment.swift | 2 +- Model/StripeKiwix.swift | 10 +++++----- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/Model/Payment.swift b/Model/Payment.swift index b5bbc5a83..1dbf1d001 100644 --- a/Model/Payment.swift +++ b/Model/Payment.swift @@ -114,7 +114,7 @@ struct Payment { // call our server to get payment / setup intent and return the client.secret Task { [resultHandler] in - let paymentServer = StripeKiwix(endPoint: URL(string: "http://192.168.100.7:4242")!, + let paymentServer = StripeKiwix(endPoint: URL(string: "https://api.donation.kiwix.org/v1/stripe")!, payment: payment) do { let publicKey = try await paymentServer.publishableKey() diff --git a/Model/StripeKiwix.swift b/Model/StripeKiwix.swift index 1616b9315..6583d6e22 100644 --- a/Model/StripeKiwix.swift +++ b/Model/StripeKiwix.swift @@ -36,13 +36,13 @@ struct StripeKiwix { throw StripeError.serverError } let json = try JSONDecoder().decode(PublishableKey.self, from: data) - return json.publishableKey + return json.publishable_key } func clientSecretForPayment(selectedAmount: SelectedAmount) async -> Result { do { // TODO: for monthly this should create a setup-intent ! - var request = URLRequest(url: endPoint.appending(path: "create-payment-intent")) + var request = URLRequest(url: endPoint.appending(path: "payment-intent")) request.httpMethod = "POST" request.setValue("application/json", forHTTPHeaderField: "Content-Type") request.httpBody = try JSONEncoder().encode(SelectedPaymentAmount(from: selectedAmount)) @@ -52,7 +52,7 @@ struct StripeKiwix { throw StripeError.serverError } let json = try JSONDecoder().decode(ClientSecretKey.self, from: data) - return .success(json.clientSecret) + return .success(json.secret) } catch let serverError { return .failure(serverError) } @@ -62,13 +62,13 @@ struct StripeKiwix { /// Response structure for GET {endPoint}/config /// {"publishableKey":"pk_test_..."} private struct PublishableKey: Decodable { - let publishableKey: String + let publishable_key: String } /// Response structure for POST {endPoint}/create-payment-intent /// {"clientSecret":"pi_..."} private struct ClientSecretKey: Decodable { - let clientSecret: String + let secret: String } private struct SelectedPaymentAmount: Encodable { From ddb5d0ac5dc0a176a0c9b1349401ae3b0fdc7709 Mon Sep 17 00:00:00 2001 From: Balazs Perlaki-Horvath Date: Sat, 16 Nov 2024 18:36:30 +0100 Subject: [PATCH 33/54] Fixlint --- Model/StripeKiwix.swift | 12 +++++++----- Views/Payment/PaymentSummary.swift | 6 +++--- 2 files changed, 10 insertions(+), 8 deletions(-) diff --git a/Model/StripeKiwix.swift b/Model/StripeKiwix.swift index 6583d6e22..bb862ec76 100644 --- a/Model/StripeKiwix.swift +++ b/Model/StripeKiwix.swift @@ -35,8 +35,10 @@ struct StripeKiwix { (200..<300).contains(httpResponse.statusCode) else { throw StripeError.serverError } - let json = try JSONDecoder().decode(PublishableKey.self, from: data) - return json.publishable_key + let decoder = JSONDecoder() + decoder.keyDecodingStrategy = .convertFromSnakeCase + let json = try decoder.decode(PublishableKey.self, from: data) + return json.publishableKey } func clientSecretForPayment(selectedAmount: SelectedAmount) async -> Result { @@ -60,13 +62,13 @@ struct StripeKiwix { } /// Response structure for GET {endPoint}/config -/// {"publishableKey":"pk_test_..."} +/// {"publishable_key":"pk_test_..."} private struct PublishableKey: Decodable { - let publishable_key: String + let publishableKey: String } /// Response structure for POST {endPoint}/create-payment-intent -/// {"clientSecret":"pi_..."} +/// {"secret":"pi_..."} private struct ClientSecretKey: Decodable { let secret: String } diff --git a/Views/Payment/PaymentSummary.swift b/Views/Payment/PaymentSummary.swift index dcc95fd8b..fc0b02226 100644 --- a/Views/Payment/PaymentSummary.swift +++ b/Views/Payment/PaymentSummary.swift @@ -48,9 +48,9 @@ struct PaymentSummary: View { PayWithApplePayButton( buttonLabel, request: payment.donationRequest(for: selectedAmount), - onPaymentAuthorizationChange: { - phase in payment.onPaymentAuthPhase(selectedAmount: selectedAmount, - phase: phase) + onPaymentAuthorizationChange: { phase in + payment.onPaymentAuthPhase(selectedAmount: selectedAmount, + phase: phase) }, onMerchantSessionRequested: payment.onMerchantSessionUpdate ) From 86e1518d3d7b5289efebe44765304c3b81817e9d Mon Sep 17 00:00:00 2001 From: Balazs Perlaki-Horvath Date: Sun, 17 Nov 2024 12:46:56 +0100 Subject: [PATCH 34/54] Show Thank You on donation success --- Model/Payment.swift | 6 ++++ Support/en.lproj/Localizable.strings | 2 ++ Views/Payment/PaymentSummary.swift | 17 +++++++-- Views/Payment/PaymentThankYou.swift | 54 ++++++++++++++++++++++++++++ Views/Settings/Settings.swift | 13 +++++-- 5 files changed, 88 insertions(+), 4 deletions(-) create mode 100644 Views/Payment/PaymentThankYou.swift diff --git a/Model/Payment.swift b/Model/Payment.swift index 1dbf1d001..e0aa5c040 100644 --- a/Model/Payment.swift +++ b/Model/Payment.swift @@ -23,6 +23,7 @@ import os struct Payment { let completeSubject = PassthroughSubject() + let successSubject = PassthroughSubject() static let merchantSessionURL = URL(string: "https://apple-pay-gateway.apple.com" )! static let merchantId = "merchant.org.kiwix.apple" @@ -131,6 +132,11 @@ struct Payment { await paymentServer.clientSecretForPayment(selectedAmount: selectedAmount) }) resultHandler(result) + if result.status == .success { + Task { @MainActor in + successSubject.send(()) + } + } } case .didFinish: os_log("onPaymentAuthPhase: .didFinish") diff --git a/Support/en.lproj/Localizable.strings b/Support/en.lproj/Localizable.strings index 5e9582138..d48a2e37d 100644 --- a/Support/en.lproj/Localizable.strings +++ b/Support/en.lproj/Localizable.strings @@ -292,3 +292,5 @@ "payment.selection.option.one_time" = "One time"; "payment.selection.option.monthly" = "Monthly"; "payment.support_fallback_message" = "We are sorry, your device does not support Apple Pay."; +"payment.success.title" = "Thank you so much for your donation."; +"payment.success.description" = "Your generosity means everything to us."; diff --git a/Views/Payment/PaymentSummary.swift b/Views/Payment/PaymentSummary.swift index fc0b02226..afac3b891 100644 --- a/Views/Payment/PaymentSummary.swift +++ b/Views/Payment/PaymentSummary.swift @@ -24,10 +24,14 @@ struct PaymentSummary: View { private let selectedAmount: SelectedAmount private let payment: Payment private let onComplete: () -> Void + private let onSuccess: () -> Void - init(selectedAmount: SelectedAmount, onComplete: @escaping () -> Void) { + init(selectedAmount: SelectedAmount, + onComplete: @escaping () -> Void, + onSuccess: @escaping () -> Void) { self.selectedAmount = selectedAmount self.onComplete = onComplete + self.onSuccess = onSuccess payment = Payment() } @@ -65,9 +69,18 @@ struct PaymentSummary: View { dismiss() onComplete() } + .onReceive(payment.successSubject) { + onSuccess() + } } } #Preview { - PaymentSummary(selectedAmount: SelectedAmount(value: 34, currency: "CHF", isMonthly: true), onComplete: {}) + PaymentSummary( + selectedAmount: SelectedAmount(value: 34, + currency: "CHF", + isMonthly: true), + onComplete: {}, + onSuccess: {} + ) } diff --git a/Views/Payment/PaymentThankYou.swift b/Views/Payment/PaymentThankYou.swift new file mode 100644 index 000000000..1cd29e0f8 --- /dev/null +++ b/Views/Payment/PaymentThankYou.swift @@ -0,0 +1,54 @@ +// This file is part of Kiwix for iOS & macOS. +// +// Kiwix is free software; you can redistribute it and/or modify it +// under the terms of the GNU General Public License as published by +// the Free Software Foundation; either version 3 of the License, or +// any later version. +// +// Kiwix is distributed in the hope that it will be useful, but +// WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +// General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with Kiwix; If not, see https://www.gnu.org/licenses/. + +import Foundation +import SwiftUI + +struct PaymentThankYou: View { + + @Environment(\.dismiss) var dismiss + @Environment(\.verticalSizeClass) var verticalSizeClass: UserInterfaceSizeClass? + + var body: some View { + Group { + // iPhone Landscape + if verticalSizeClass == .compact { + // needs a close button + HStack(alignment: .top) { + Spacer() + Button("", systemImage: "x.circle.fill") { + dismiss() + } + .font(.title2) + .foregroundStyle(.secondary) + .padding() + .buttonStyle(BorderlessButtonStyle()) + } + } + VStack(spacing: 16) { + Text("payment.success.title".localized) + .font(.title) + Text("payment.success.description".localized) + .font(.headline) + } + .multilineTextAlignment(.center) + } + } +} + +#Preview { + PaymentThankYou() +} + diff --git a/Views/Settings/Settings.swift b/Views/Settings/Settings.swift index 392d7fac0..2195d0a07 100644 --- a/Views/Settings/Settings.swift +++ b/Views/Settings/Settings.swift @@ -133,6 +133,7 @@ struct Settings: View { private var amountSelected = PassthroughSubject() @State private var selectedAmount: SelectedAmount? @State private var showDonationPopUp: Bool = false + @State private var showThankYou: Bool = false func openDonation() { showDonationPopUp = true } @@ -174,9 +175,11 @@ struct Settings: View { }, content: { Group { if let selectedAmount { - PaymentSummary(selectedAmount: selectedAmount) { + PaymentSummary(selectedAmount: selectedAmount, onComplete: { showDonationPopUp = false - } + }, onSuccess: { + showThankYou = true + }) } else { PaymentForm(amountSelected: amountSelected) } @@ -186,6 +189,12 @@ struct Settings: View { selectedAmount = value } }) + .sheet(isPresented: $showThankYou, onDismiss: { + showThankYou = false + }, content: { + PaymentThankYou() + .presentationDetents([.fraction(0.33)]) + }) } var readingSettings: some View { From f0fa920990e0897e0f90d211489aa9f9d3fd0698 Mon Sep 17 00:00:00 2001 From: Balazs Perlaki-Horvath Date: Tue, 19 Nov 2024 10:03:13 +0100 Subject: [PATCH 35/54] Show thank you on donation success --- App/App_macOS.swift | 34 ++++++++++++----- Model/Payment.swift | 33 +++++++++++------ Views/Payment/PaymentSummary.swift | 17 +++------ Views/Payment/PaymentThankYou.swift | 29 ++++++++++----- Views/Settings/Settings.swift | 57 ++++++++++++++++++++--------- 5 files changed, 110 insertions(+), 60 deletions(-) diff --git a/App/App_macOS.swift b/App/App_macOS.swift index 17701a876..87342aac6 100644 --- a/App/App_macOS.swift +++ b/App/App_macOS.swift @@ -30,6 +30,7 @@ final class AppDelegate: NSObject, NSApplicationDelegate { @main struct Kiwix: App { @NSApplicationDelegateAdaptor(AppDelegate.self) var appDelegate + @Environment(\.openWindow) var openWindow @StateObject private var libraryRefreshViewModel = LibraryViewModel() private let notificationCenterDelegate = NotificationCenterDelegate() private var amountSelected = PassthroughSubject() @@ -86,15 +87,12 @@ struct Kiwix: App { Window("payment.donate.title".localized, id: "donation") { Group { if let selectedAmount { - PaymentSummary(selectedAmount: selectedAmount) { - // after upgrading to macOS 14, use: - // @Environment(\.dismissWindow) var dismissWindow - // and call: - // dismissWindow(id: "donation") - NSApplication.shared.windows.first { window in - window.identifier?.rawValue == "donation" - }?.close() - } + PaymentSummary(selectedAmount: selectedAmount, onComplete: { + closeDonation() + if Payment.shouldShowThanks() { + openWindow(id: "donation-thank-you") + } + }) } else { PaymentForm(amountSelected: amountSelected) .frame(width: 320, height: 320) @@ -116,6 +114,24 @@ struct Kiwix: App { .windowStyle(.titleBar) .commandsRemoved() .defaultSize(width: 320, height: 400) + + Window("", id: "donation-thank-you") { + PaymentThankYou() + .padding() + } + .windowResizability(.contentMinSize) + .commandsRemoved() + .defaultSize(width: 320, height: 198) + } + + private func closeDonation() { + // after upgrading to macOS 14, use: + // @Environment(\.dismissWindow) var dismissWindow + // and call: + // dismissWindow(id: "donation") + NSApplication.shared.windows.first { window in + window.identifier?.rawValue == "donation" + }?.close() } private class NotificationCenterDelegate: NSObject, UNUserNotificationCenterDelegate { diff --git a/Model/Payment.swift b/Model/Payment.swift index e0aa5c040..64899f477 100644 --- a/Model/Payment.swift +++ b/Model/Payment.swift @@ -21,9 +21,20 @@ import StripeApplePay import os struct Payment { + + /// Decides if the Thank You pop up should be shown + /// - Returns: `True` only once + @MainActor + static func shouldShowThanks() -> Bool { + // make sure `true` is "read only once" + let value = Self.showThanks + Self.showThanks = false + return value + } + @MainActor + static private var showThanks: Bool = false - let completeSubject = PassthroughSubject() - let successSubject = PassthroughSubject() + let completeSubject = PassthroughSubject() static let merchantSessionURL = URL(string: "https://apple-pay-gateway.apple.com" )! static let merchantId = "merchant.org.kiwix.apple" @@ -106,15 +117,15 @@ struct Payment { return request } - func onPaymentAuthPhase(selectedAmount: SelectedAmount, phase: PayWithApplePayButtonPaymentAuthorizationPhase) { + func onPaymentAuthPhase(selectedAmount: SelectedAmount, + phase: PayWithApplePayButtonPaymentAuthorizationPhase) { switch phase { case .willAuthorize: os_log("onPaymentAuthPhase: .willAuthorize") case .didAuthorize(let payment, let resultHandler): os_log("onPaymentAuthPhase: .didAuthorize") // call our server to get payment / setup intent and return the client.secret - Task { [resultHandler] in - + Task { @MainActor [resultHandler] in let paymentServer = StripeKiwix(endPoint: URL(string: "https://api.donation.kiwix.org/v1/stripe")!, payment: payment) do { @@ -131,19 +142,19 @@ struct Payment { usingClientSecretProvider: { await paymentServer.clientSecretForPayment(selectedAmount: selectedAmount) }) + // calling any UI refreshing state / subject from here + // will block the UI in the payment state for ever + // therefore it's defered via static showThanks + Self.showThanks = result.status == .success resultHandler(result) - if result.status == .success { - Task { @MainActor in - successSubject.send(()) - } - } } case .didFinish: os_log("onPaymentAuthPhase: .didFinish") - completeSubject.send(true) + completeSubject.send(()) @unknown default: os_log("onPaymentAuthPhase: @unknown default") } + } @available(macOS 13.0, *) diff --git a/Views/Payment/PaymentSummary.swift b/Views/Payment/PaymentSummary.swift index afac3b891..d9c886ad5 100644 --- a/Views/Payment/PaymentSummary.swift +++ b/Views/Payment/PaymentSummary.swift @@ -23,15 +23,12 @@ struct PaymentSummary: View { private let selectedAmount: SelectedAmount private let payment: Payment - private let onComplete: () -> Void - private let onSuccess: () -> Void + private let onComplete: @MainActor () -> Void init(selectedAmount: SelectedAmount, - onComplete: @escaping () -> Void, - onSuccess: @escaping () -> Void) { + onComplete: @escaping @MainActor () -> Void) { self.selectedAmount = selectedAmount self.onComplete = onComplete - self.onSuccess = onSuccess payment = Payment() } @@ -65,13 +62,10 @@ struct PaymentSummary: View { .foregroundStyle(.red) .font(.callout) } - }.onReceive(payment.completeSubject) { _ in - dismiss() + }.onReceive(payment.completeSubject) { + debugPrint("PaymentSummary::payment.completeSubject") onComplete() } - .onReceive(payment.successSubject) { - onSuccess() - } } } @@ -80,7 +74,6 @@ struct PaymentSummary: View { selectedAmount: SelectedAmount(value: 34, currency: "CHF", isMonthly: true), - onComplete: {}, - onSuccess: {} + onComplete: {} ) } diff --git a/Views/Payment/PaymentThankYou.swift b/Views/Payment/PaymentThankYou.swift index 1cd29e0f8..e2367b412 100644 --- a/Views/Payment/PaymentThankYou.swift +++ b/Views/Payment/PaymentThankYou.swift @@ -19,24 +19,19 @@ import SwiftUI struct PaymentThankYou: View { @Environment(\.dismiss) var dismiss + #if os(iOS) @Environment(\.verticalSizeClass) var verticalSizeClass: UserInterfaceSizeClass? + #endif var body: some View { Group { + #if os(iOS) // iPhone Landscape if verticalSizeClass == .compact { // needs a close button - HStack(alignment: .top) { - Spacer() - Button("", systemImage: "x.circle.fill") { - dismiss() - } - .font(.title2) - .foregroundStyle(.secondary) - .padding() - .buttonStyle(BorderlessButtonStyle()) - } + closeButton } + #endif VStack(spacing: 16) { Text("payment.success.title".localized) .font(.title) @@ -46,6 +41,20 @@ struct PaymentThankYou: View { .multilineTextAlignment(.center) } } + + @ViewBuilder + var closeButton: some View { + HStack(alignment: .top) { + Spacer() + Button("", systemImage: "x.circle.fill") { + dismiss() + } + .font(.title2) + .foregroundStyle(.secondary) + .padding() + .buttonStyle(BorderlessButtonStyle()) + } + } } #Preview { diff --git a/Views/Settings/Settings.swift b/Views/Settings/Settings.swift index 2195d0a07..2d78d270f 100644 --- a/Views/Settings/Settings.swift +++ b/Views/Settings/Settings.swift @@ -130,10 +130,16 @@ import PassKit import Combine struct Settings: View { + + enum DonationPopupState { + case selection + case selectedAmount(SelectedAmount) + case thankYou + } + private var amountSelected = PassthroughSubject() - @State private var selectedAmount: SelectedAmount? @State private var showDonationPopUp: Bool = false - @State private var showThankYou: Bool = false + @State private var donationPopUpState: DonationPopupState = .selection func openDonation() { showDonationPopUp = true } @@ -171,30 +177,45 @@ struct Settings: View { } } .sheet(isPresented: $showDonationPopUp, onDismiss: { - selectedAmount = nil - }, content: { + if Payment.shouldShowThanks() { + Task { + // we need to close the sheet in order to dismiss ApplePay, + // and we need to re-open it again with a delay to show thank you state + // Swift UI cannot yet handle multiple sheets + try? await Task.sleep(for: .milliseconds(100)) + await MainActor.run { + showDonationPopUp = true + donationPopUpState = .thankYou + } + } + } else { + // reset + donationPopUpState = .selection + } + }) { Group { - if let selectedAmount { + switch donationPopUpState { + case .selection: + PaymentForm(amountSelected: amountSelected) + .presentationDetents([.fraction(0.65)]) + case .selectedAmount(let selectedAmount): PaymentSummary(selectedAmount: selectedAmount, onComplete: { showDonationPopUp = false - }, onSuccess: { - showThankYou = true }) - } else { - PaymentForm(amountSelected: amountSelected) + .presentationDetents([.fraction(0.65)]) + case .thankYou: + PaymentThankYou() + .presentationDetents([.fraction(0.33)]) } } - .presentationDetents([.fraction(0.65)]) .onReceive(amountSelected) { value in - selectedAmount = value + if let amount = value { + donationPopUpState = .selectedAmount(amount) + } else { + donationPopUpState = .selection + } } - }) - .sheet(isPresented: $showThankYou, onDismiss: { - showThankYou = false - }, content: { - PaymentThankYou() - .presentationDetents([.fraction(0.33)]) - }) + } } var readingSettings: some View { From 889af89b0aa91d6e0f087a62684588c9bcb4a923 Mon Sep 17 00:00:00 2001 From: Balazs Perlaki-Horvath Date: Fri, 22 Nov 2024 12:12:03 +0100 Subject: [PATCH 36/54] Remove support button for macOS --- App/App_macOS.swift | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/App/App_macOS.swift b/App/App_macOS.swift index 87342aac6..09bd9de20 100644 --- a/App/App_macOS.swift +++ b/App/App_macOS.swift @@ -182,11 +182,11 @@ struct RootView: View { } } .frame(minWidth: 160) - .safeAreaInset(edge: .bottom) { - SupportKiwixButton { - openWindow(id: "donation") - } - } +// .safeAreaInset(edge: .bottom) { +// SupportKiwixButton { +// openWindow(id: "donation") +// } +// } } detail: { switch navigation.currentItem { case .loading: From cfcb6fc4bb53baef82d395837e7d712b331c3856 Mon Sep 17 00:00:00 2001 From: Balazs Perlaki-Horvath Date: Fri, 22 Nov 2024 12:17:31 +0100 Subject: [PATCH 37/54] Disable monthly options from payments --- Views/Payment/PaymentForm.swift | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/Views/Payment/PaymentForm.swift b/Views/Payment/PaymentForm.swift index 03d305f3e..640f503e9 100644 --- a/Views/Payment/PaymentForm.swift +++ b/Views/Payment/PaymentForm.swift @@ -52,11 +52,12 @@ struct PaymentForm: View { #endif VStack { - Picker("", selection: $isMonthly) { - Label("payment.selection.option.one_time".localized, systemImage: "heart.circle").tag(false) - Label("payment.selection.option.monthly".localized, systemImage: "arrow.clockwise.heart").tag(true) - }.pickerStyle(.segmented) - .padding([.leading, .trailing, .bottom]) + // Re-enable as part of: https://github.com/kiwix/kiwix-apple/issues/1032 +// Picker("", selection: $isMonthly) { +// Label("payment.selection.option.one_time".localized, systemImage: "heart.circle").tag(false) +// Label("payment.selection.option.monthly".localized, systemImage: "arrow.clockwise.heart").tag(true) +// }.pickerStyle(.segmented) +// .padding([.leading, .trailing, .bottom]) ListOfAmounts(amountSelected: amountSelected, isMonthly: $isMonthly) } From af9a95b2371a1e0901aa692a1b208206dba5f7d8 Mon Sep 17 00:00:00 2001 From: Balazs Perlaki-Horvath Date: Fri, 22 Nov 2024 12:28:34 +0100 Subject: [PATCH 38/54] Add payment log --- Model/Payment.swift | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/Model/Payment.swift b/Model/Payment.swift index 64899f477..1e04370b9 100644 --- a/Model/Payment.swift +++ b/Model/Payment.swift @@ -143,10 +143,11 @@ struct Payment { await paymentServer.clientSecretForPayment(selectedAmount: selectedAmount) }) // calling any UI refreshing state / subject from here - // will block the UI in the payment state for ever + // will block the UI in the payment state forever // therefore it's defered via static showThanks Self.showThanks = result.status == .success resultHandler(result) + os_log("onPaymentAuthPhase: .didAuthorize: \(result.status == .success)") } case .didFinish: os_log("onPaymentAuthPhase: .didFinish") From 7b910dcab1a09a92343e860c7544d6d7fb4f6043 Mon Sep 17 00:00:00 2001 From: Balazs Perlaki-Horvath Date: Sat, 23 Nov 2024 10:24:34 +0100 Subject: [PATCH 39/54] Fixlint --- Model/Payment.swift | 3 ++- Model/StripeKiwix.swift | 3 ++- Views/Payment/PaymentThankYou.swift | 1 - Views/Settings/Settings.swift | 4 ++-- 4 files changed, 6 insertions(+), 5 deletions(-) diff --git a/Model/Payment.swift b/Model/Payment.swift index 1e04370b9..441f61704 100644 --- a/Model/Payment.swift +++ b/Model/Payment.swift @@ -135,10 +135,11 @@ struct Payment { resultHandler(.init(status: .failure, errors: [serverError])) return } + // we should update the return path for confirmations + // see: https://github.com/kiwix/kiwix-apple/issues/1032 let stripe = StripeApplePaySimple() let result = await stripe.complete(payment: payment, returnURLPath: nil, - // TODO: update the return path for confirmations usingClientSecretProvider: { await paymentServer.clientSecretForPayment(selectedAmount: selectedAmount) }) diff --git a/Model/StripeKiwix.swift b/Model/StripeKiwix.swift index bb862ec76..e52f5ccfd 100644 --- a/Model/StripeKiwix.swift +++ b/Model/StripeKiwix.swift @@ -43,7 +43,8 @@ struct StripeKiwix { func clientSecretForPayment(selectedAmount: SelectedAmount) async -> Result { do { - // TODO: for monthly this should create a setup-intent ! + // for monthly we should create a setup-intent: + // see: https://github.com/kiwix/kiwix-apple/issues/1032 var request = URLRequest(url: endPoint.appending(path: "payment-intent")) request.httpMethod = "POST" request.setValue("application/json", forHTTPHeaderField: "Content-Type") diff --git a/Views/Payment/PaymentThankYou.swift b/Views/Payment/PaymentThankYou.swift index e2367b412..a2d16685e 100644 --- a/Views/Payment/PaymentThankYou.swift +++ b/Views/Payment/PaymentThankYou.swift @@ -60,4 +60,3 @@ struct PaymentThankYou: View { #Preview { PaymentThankYou() } - diff --git a/Views/Settings/Settings.swift b/Views/Settings/Settings.swift index 2d78d270f..3a1cdb3a0 100644 --- a/Views/Settings/Settings.swift +++ b/Views/Settings/Settings.swift @@ -192,7 +192,7 @@ struct Settings: View { // reset donationPopUpState = .selection } - }) { + }, content: { Group { switch donationPopUpState { case .selection: @@ -216,7 +216,7 @@ struct Settings: View { } } } - } + }) var readingSettings: some View { let isSnippet = Binding { From b8f5f9d04d3cb6d9e7767d6d4a1a400cc910855b Mon Sep 17 00:00:00 2001 From: Balazs Perlaki-Horvath Date: Sat, 23 Nov 2024 10:37:20 +0100 Subject: [PATCH 40/54] Fix closures --- Views/Settings/Settings.swift | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/Views/Settings/Settings.swift b/Views/Settings/Settings.swift index 3a1cdb3a0..6518850d8 100644 --- a/Views/Settings/Settings.swift +++ b/Views/Settings/Settings.swift @@ -202,7 +202,7 @@ struct Settings: View { PaymentSummary(selectedAmount: selectedAmount, onComplete: { showDonationPopUp = false }) - .presentationDetents([.fraction(0.65)]) + .presentationDetents([.fraction(0.65)]) case .thankYou: PaymentThankYou() .presentationDetents([.fraction(0.33)]) @@ -215,8 +215,8 @@ struct Settings: View { donationPopUpState = .selection } } - } - }) + }) + } var readingSettings: some View { let isSnippet = Binding { From 701897847f3730b0de6879cb4b81c8cb5b187a81 Mon Sep 17 00:00:00 2001 From: Balazs Perlaki-Horvath Date: Sun, 24 Nov 2024 18:41:52 +0100 Subject: [PATCH 41/54] Try build with full bundleID --- project.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/project.yml b/project.yml index 85ff6cee5..54ad9d834 100644 --- a/project.yml +++ b/project.yml @@ -102,7 +102,7 @@ targets: settings: base: MARKETING_VERSION: "3.6.0" - PRODUCT_BUNDLE_IDENTIFIER: self.Kiwix + PRODUCT_BUNDLE_IDENTIFIER: ${DEVELOPMENT_TEAM}.self.Kiwix INFOPLIST_KEY_CFBundleDisplayName: Kiwix INFOPLIST_FILE: Support/Info.plist INFOPLIST_KEY_UILaunchStoryboardName: SplashScreenKiwix.storyboard From 90ee8c713c4bbbe608a9baaaf5c39296831505f2 Mon Sep 17 00:00:00 2001 From: Balazs Perlaki-Horvath Date: Sun, 24 Nov 2024 19:00:38 +0100 Subject: [PATCH 42/54] Revert "Try build with full bundleID" This reverts commit 7c17ea4bc5956fedc78982c51cd6df096aceba29. --- project.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/project.yml b/project.yml index 54ad9d834..85ff6cee5 100644 --- a/project.yml +++ b/project.yml @@ -102,7 +102,7 @@ targets: settings: base: MARKETING_VERSION: "3.6.0" - PRODUCT_BUNDLE_IDENTIFIER: ${DEVELOPMENT_TEAM}.self.Kiwix + PRODUCT_BUNDLE_IDENTIFIER: self.Kiwix INFOPLIST_KEY_CFBundleDisplayName: Kiwix INFOPLIST_FILE: Support/Info.plist INFOPLIST_KEY_UILaunchStoryboardName: SplashScreenKiwix.storyboard From efa35e6578250b9d6b5d301275d1218c15f60c1f Mon Sep 17 00:00:00 2001 From: Balazs Perlaki-Horvath Date: Sun, 24 Nov 2024 19:55:42 +0100 Subject: [PATCH 43/54] Remove in app payments merchant id --- project.yml | 1 - 1 file changed, 1 deletion(-) diff --git a/project.yml b/project.yml index 85ff6cee5..6af775285 100644 --- a/project.yml +++ b/project.yml @@ -62,7 +62,6 @@ targetTemplates: com.apple.security.files.user-selected.read-write: true com.apple.security.network.client: true com.apple.security.print: true - com.apple.developer.in-app-payments: [merchant.org.kiwix.apple] dependencies: - framework: CoreKiwix.xcframework embed: false From 281d618c640bc096457882006c3b82294c13bdbf Mon Sep 17 00:00:00 2001 From: Balazs Perlaki-Horvath Date: Sun, 24 Nov 2024 20:47:58 +0100 Subject: [PATCH 44/54] Add merchant id to Kiwix target only --- project.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/project.yml b/project.yml index 6af775285..919f166bf 100644 --- a/project.yml +++ b/project.yml @@ -98,6 +98,7 @@ targets: entitlements: properties: com.apple.security.files.downloads.read-write: true + com.apple.developer.in-app-payments: [merchant.org.kiwix.apple] settings: base: MARKETING_VERSION: "3.6.0" From 9f761d4e79425dd1da25694d8edf9cd315e22800 Mon Sep 17 00:00:00 2001 From: Balazs Perlaki-Horvath Date: Sun, 24 Nov 2024 21:21:21 +0100 Subject: [PATCH 45/54] Revert CI/CD to develop --- .github/workflows/cd.yml | 4 ---- .github/workflows/ci.yml | 4 ---- 2 files changed, 8 deletions(-) diff --git a/.github/workflows/cd.yml b/.github/workflows/cd.yml index d1e43276a..c47b1c3fd 100644 --- a/.github/workflows/cd.yml +++ b/.github/workflows/cd.yml @@ -52,10 +52,6 @@ jobs: - name: Install python dependencies run: pip install pyyaml==6.0.1 - - name: Remove Apple Pay capability for macOS - if: matrix.platform == 'macOS' - run: sed -i '' '/in-app-payments/d' project.yml - - name: Set VERSION from code shell: python run: | diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 7d0fe5b61..33fd0005a 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -41,10 +41,6 @@ jobs: if: matrix.platform == 'iOS' run: echo "EXTRA_XCODEBUILD=-sdk iphoneos ${{ env.APPLE_AUTH_PARAMS }}" >> $GITHUB_ENV - - name: Remove Apple Pay capability for macOS - if: matrix.platform == 'macOS' - run: sed -i '' '/in-app-payments/d' project.yml - - name: Build uses: ./.github/actions/xcbuild with: From 52727f586d55c62f63cba61f2c9fa8b4f18a37b3 Mon Sep 17 00:00:00 2001 From: Balazs Perlaki-Horvath Date: Tue, 26 Nov 2024 22:50:33 +0100 Subject: [PATCH 46/54] Fix donation button click area, header padding --- Views/Buttons/SupportKiwixButton.swift | 2 +- Views/Payment/PaymentForm.swift | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Views/Buttons/SupportKiwixButton.swift b/Views/Buttons/SupportKiwixButton.swift index 8e454b244..5fbd5bc3b 100644 --- a/Views/Buttons/SupportKiwixButton.swift +++ b/Views/Buttons/SupportKiwixButton.swift @@ -33,8 +33,8 @@ struct SupportKiwixButton: View { .padding(6) #endif } - .buttonStyle(BorderlessButtonStyle()) #if os(macOS) + .buttonStyle(BorderlessButtonStyle()) .padding() #endif } diff --git a/Views/Payment/PaymentForm.swift b/Views/Payment/PaymentForm.swift index 640f503e9..966211950 100644 --- a/Views/Payment/PaymentForm.swift +++ b/Views/Payment/PaymentForm.swift @@ -38,7 +38,7 @@ struct PaymentForm: View { Spacer() Text("payment.donate.title".localized) .font(.title) - .padding() + .padding(.init(top: 12, leading: 0, bottom: 8, trailing: 0)) Spacer() } .overlay(alignment: .topTrailing) { From 6ae88f6e534ca8e2de70cf7ab827b37271fc3920 Mon Sep 17 00:00:00 2001 From: Balazs Perlaki-Horvath Date: Tue, 26 Nov 2024 23:34:04 +0100 Subject: [PATCH 47/54] Add docs to Payment --- Model/Payment.swift | 23 +++++++++++++++++++++++ 1 file changed, 23 insertions(+) diff --git a/Model/Payment.swift b/Model/Payment.swift index 441f61704..232caecdc 100644 --- a/Model/Payment.swift +++ b/Model/Payment.swift @@ -20,6 +20,25 @@ import Combine import StripeApplePay import os +/// Payment processing based on: +/// Apple-Pay button: +/// https://developer.apple.com/documentation/passkit_apple_pay_and_wallet/apple_pay#2872687 +/// as described in: What’s new in Wallet and Apple Pay from WWDC 2022 +/// (https://developer.apple.com/videos/play/wwdc2022/10041/) +/// +/// Combined with Stripe's lightweight Apple Pay framework +/// https://github.com/stripe/stripe-ios/blob/master/StripeApplePay/README.md +/// based on the App Clip example project: +/// https://github.com/stripe/stripe-ios/tree/master/Example/AppClipExample +/// +/// Whereas the Stripe SDK is based on the older +/// PKPaymentAuthorizationController +/// https://developer.apple.com/documentation/passkit_apple_pay_and_wallet/apple_pay#2870963 +/// +/// The Stripe SDK has been brought up to date (with 2022 WWDC changes) +/// and modified to be compatible with macOS as well, see SPM dependencies +/// https://github.com/CodeLikeW/stripe-apple-pay +/// https://github.com/CodeLikeW/stripe-core struct Payment { /// Decides if the Thank You pop up should be shown @@ -77,6 +96,10 @@ struct Payment { .init(value: 10) ] + /// Checks Apple Pay capabilities, and returns the button label accrodingly + /// Setup button if no cards added yet, + /// nil if Apple Pay is not supported + /// or donation button, if all is OK static func paymentButtonType() -> PayWithApplePayButtonLabel? { if PKPaymentAuthorizationController.canMakePayments() { return PayWithApplePayButtonLabel.donate From d23e30ea94c46575c39774689a0f9e823f4d7c98 Mon Sep 17 00:00:00 2001 From: Balazs Perlaki-Horvath Date: Tue, 26 Nov 2024 23:35:11 +0100 Subject: [PATCH 48/54] Payment docs up --- Model/Payment.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Model/Payment.swift b/Model/Payment.swift index 232caecdc..0ffccc1ac 100644 --- a/Model/Payment.swift +++ b/Model/Payment.swift @@ -32,7 +32,7 @@ import os /// https://github.com/stripe/stripe-ios/tree/master/Example/AppClipExample /// /// Whereas the Stripe SDK is based on the older -/// PKPaymentAuthorizationController +/// PKPaymentAuthorizationController (before PayWithApplePayButton was available) /// https://developer.apple.com/documentation/passkit_apple_pay_and_wallet/apple_pay#2870963 /// /// The Stripe SDK has been brought up to date (with 2022 WWDC changes) From f18edc60f9f97f3398e394fac147298090eea73e Mon Sep 17 00:00:00 2001 From: Balazs Perlaki-Horvath Date: Tue, 26 Nov 2024 23:43:50 +0100 Subject: [PATCH 49/54] Revert CI / CD to main --- .github/workflows/cd.yml | 4 ++++ .github/workflows/ci.yml | 4 ++++ 2 files changed, 8 insertions(+) diff --git a/.github/workflows/cd.yml b/.github/workflows/cd.yml index c47b1c3fd..d1e43276a 100644 --- a/.github/workflows/cd.yml +++ b/.github/workflows/cd.yml @@ -52,6 +52,10 @@ jobs: - name: Install python dependencies run: pip install pyyaml==6.0.1 + - name: Remove Apple Pay capability for macOS + if: matrix.platform == 'macOS' + run: sed -i '' '/in-app-payments/d' project.yml + - name: Set VERSION from code shell: python run: | diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 33fd0005a..7d0fe5b61 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -41,6 +41,10 @@ jobs: if: matrix.platform == 'iOS' run: echo "EXTRA_XCODEBUILD=-sdk iphoneos ${{ env.APPLE_AUTH_PARAMS }}" >> $GITHUB_ENV + - name: Remove Apple Pay capability for macOS + if: matrix.platform == 'macOS' + run: sed -i '' '/in-app-payments/d' project.yml + - name: Build uses: ./.github/actions/xcbuild with: From 92b6f73e9aa0cea6b4ed8fa15764b5c2fe858ee0 Mon Sep 17 00:00:00 2001 From: Balazs Perlaki-Horvath Date: Tue, 26 Nov 2024 23:45:11 +0100 Subject: [PATCH 50/54] Revert --- App/App_iOS.swift | 2 -- 1 file changed, 2 deletions(-) diff --git a/App/App_iOS.swift b/App/App_iOS.swift index d8bc13f18..8b1b628e6 100644 --- a/App/App_iOS.swift +++ b/App/App_iOS.swift @@ -17,7 +17,6 @@ import SwiftUI import UserNotifications #if os(iOS) - @main struct Kiwix: App { @Environment(\.scenePhase) private var scenePhase @@ -95,7 +94,6 @@ struct Kiwix: App { } private class AppDelegate: NSObject, UIApplicationDelegate, UNUserNotificationCenterDelegate { - /// Storing background download completion handler sent to application delegate func application(_ application: UIApplication, handleEventsForBackgroundURLSession identifier: String, From a0a585a62ec4d7534771d793e73ffa459f3b2379 Mon Sep 17 00:00:00 2001 From: Balazs Perlaki-Horvath Date: Fri, 29 Nov 2024 00:47:10 +0100 Subject: [PATCH 51/54] Add all payment methods --- Model/Payment.swift | 18 +++++++++++++++++- 1 file changed, 17 insertions(+), 1 deletion(-) diff --git a/Model/Payment.swift b/Model/Payment.swift index 0ffccc1ac..210a04f24 100644 --- a/Model/Payment.swift +++ b/Model/Payment.swift @@ -60,12 +60,28 @@ struct Payment { static let paymentSubscriptionManagingURL = "https://www.kiwix.org" static let supportedNetworks: [PKPaymentNetwork] = [ .amex, + .bancomat, + .bancontact, + .cartesBancaires, + .chinaUnionPay, + .dankort, .discover, + .eftpos, .electron, + .elo, + .girocard, + .interac, + .idCredit, + .JCB, .mada, .maestro, .masterCard, - .visa + .mir, + .privateLabel, + .quicPay, + .suica, + .visa, + .vPay ] static let capabilities: PKMerchantCapability = [.threeDSecure, .credit, .debit, .emv] From 93351694ee7a890e0f963a7d08cd05f4d1f0e07f Mon Sep 17 00:00:00 2001 From: Balazs Perlaki-Horvath Date: Sat, 30 Nov 2024 14:09:32 +0100 Subject: [PATCH 52/54] Add Error pop up for donations --- App/App_macOS.swift | 16 ++++++++-- Model/Payment.swift | 31 +++++++++++++------ Support/en.lproj/Localizable.strings | 2 ++ ...hankYou.swift => PaymentResultPopUp.swift} | 28 +++++++++++++---- Views/Settings/Settings.swift | 24 ++++++++++---- 5 files changed, 78 insertions(+), 23 deletions(-) rename Views/Payment/{PaymentThankYou.swift => PaymentResultPopUp.swift} (69%) diff --git a/App/App_macOS.swift b/App/App_macOS.swift index 09bd9de20..8b5899415 100644 --- a/App/App_macOS.swift +++ b/App/App_macOS.swift @@ -89,8 +89,12 @@ struct Kiwix: App { if let selectedAmount { PaymentSummary(selectedAmount: selectedAmount, onComplete: { closeDonation() - if Payment.shouldShowThanks() { + switch Payment.showResult() { + case .none: break + case .thankYou: openWindow(id: "donation-thank-you") + case .error: + openWindow(id: "donation-error") } }) } else { @@ -116,7 +120,15 @@ struct Kiwix: App { .defaultSize(width: 320, height: 400) Window("", id: "donation-thank-you") { - PaymentThankYou() + PaymentResultPopUp(state: .thankYou) + .padding() + } + .windowResizability(.contentMinSize) + .commandsRemoved() + .defaultSize(width: 320, height: 198) + + Window("", id: "donation-error") { + PaymentResultPopUp(state: .error) .padding() } .windowResizability(.contentMinSize) diff --git a/Model/Payment.swift b/Model/Payment.swift index 210a04f24..d8c86aa88 100644 --- a/Model/Payment.swift +++ b/Model/Payment.swift @@ -40,18 +40,23 @@ import os /// https://github.com/CodeLikeW/stripe-apple-pay /// https://github.com/CodeLikeW/stripe-core struct Payment { - - /// Decides if the Thank You pop up should be shown - /// - Returns: `True` only once + + enum FinalResult { + case thankYou + case error + } + + /// Decides if the Thank You / Error pop up should be shown + /// - Returns: `FinalResult` only once @MainActor - static func shouldShowThanks() -> Bool { + static func showResult() -> FinalResult? { // make sure `true` is "read only once" - let value = Self.showThanks - Self.showThanks = false + let value = Self.finalResult + Self.finalResult = nil return value } @MainActor - static private var showThanks: Bool = false + static private var finalResult: Payment.FinalResult? = nil let completeSubject = PassthroughSubject() @@ -171,6 +176,7 @@ struct Payment { let publicKey = try await paymentServer.publishableKey() StripeAPI.defaultPublishableKey = publicKey } catch let serverError { + Self.finalResult = .error resultHandler(.init(status: .failure, errors: [serverError])) return } @@ -184,8 +190,15 @@ struct Payment { }) // calling any UI refreshing state / subject from here // will block the UI in the payment state forever - // therefore it's defered via static showThanks - Self.showThanks = result.status == .success + // therefore it's defered via static finalResult + switch result.status { + case .success: + Self.finalResult = .thankYou + case .failure: + Self.finalResult = .error + default: + Self.finalResult = nil + } resultHandler(result) os_log("onPaymentAuthPhase: .didAuthorize: \(result.status == .success)") } diff --git a/Support/en.lproj/Localizable.strings b/Support/en.lproj/Localizable.strings index d48a2e37d..9289bdf45 100644 --- a/Support/en.lproj/Localizable.strings +++ b/Support/en.lproj/Localizable.strings @@ -294,3 +294,5 @@ "payment.support_fallback_message" = "We are sorry, your device does not support Apple Pay."; "payment.success.title" = "Thank you so much for your donation."; "payment.success.description" = "Your generosity means everything to us."; +"payment.error.title" = "Well that's awkward."; +"payment.error.description" = "There has been an issue with your payment. Please try again."; diff --git a/Views/Payment/PaymentThankYou.swift b/Views/Payment/PaymentResultPopUp.swift similarity index 69% rename from Views/Payment/PaymentThankYou.swift rename to Views/Payment/PaymentResultPopUp.swift index a2d16685e..0d8ffda74 100644 --- a/Views/Payment/PaymentThankYou.swift +++ b/Views/Payment/PaymentResultPopUp.swift @@ -16,13 +16,20 @@ import Foundation import SwiftUI -struct PaymentThankYou: View { +struct PaymentResultPopUp: View { @Environment(\.dismiss) var dismiss #if os(iOS) @Environment(\.verticalSizeClass) var verticalSizeClass: UserInterfaceSizeClass? #endif + let state: State + + enum State { + case thankYou + case error + } + var body: some View { Group { #if os(iOS) @@ -33,10 +40,19 @@ struct PaymentThankYou: View { } #endif VStack(spacing: 16) { - Text("payment.success.title".localized) - .font(.title) - Text("payment.success.description".localized) - .font(.headline) + switch state { + case .thankYou: + Text("payment.success.title".localized) + .font(.title) + Text("payment.success.description".localized) + .font(.headline) + case .error: + Text("payment.error.title".localized) + .font(.title) + Text("payment.error.description".localized) + .font(.headline) + } + } .multilineTextAlignment(.center) } @@ -58,5 +74,5 @@ struct PaymentThankYou: View { } #Preview { - PaymentThankYou() + PaymentResultPopUp(state: .thankYou) } diff --git a/Views/Settings/Settings.swift b/Views/Settings/Settings.swift index 6518850d8..c86bc99f1 100644 --- a/Views/Settings/Settings.swift +++ b/Views/Settings/Settings.swift @@ -135,6 +135,7 @@ struct Settings: View { case selection case selectedAmount(SelectedAmount) case thankYou + case error } private var amountSelected = PassthroughSubject() @@ -177,20 +178,28 @@ struct Settings: View { } } .sheet(isPresented: $showDonationPopUp, onDismiss: { - if Payment.shouldShowThanks() { + let result = Payment.showResult() + switch result { + case .none: + // reset + donationPopUpState = .selection + return + case .some(let finalResult): Task { // we need to close the sheet in order to dismiss ApplePay, // and we need to re-open it again with a delay to show thank you state // Swift UI cannot yet handle multiple sheets try? await Task.sleep(for: .milliseconds(100)) await MainActor.run { + switch finalResult { + case .thankYou: + donationPopUpState = .thankYou + case .error: + donationPopUpState = .error + } showDonationPopUp = true - donationPopUpState = .thankYou } } - } else { - // reset - donationPopUpState = .selection } }, content: { Group { @@ -204,7 +213,10 @@ struct Settings: View { }) .presentationDetents([.fraction(0.65)]) case .thankYou: - PaymentThankYou() + PaymentResultPopUp(state: .thankYou) + .presentationDetents([.fraction(0.33)]) + case .error: + PaymentResultPopUp(state: .error) .presentationDetents([.fraction(0.33)]) } } From ea40c54204d83dd5973989fe685ad4b06e28101a Mon Sep 17 00:00:00 2001 From: Balazs Perlaki-Horvath Date: Sat, 30 Nov 2024 14:18:00 +0100 Subject: [PATCH 53/54] Add macOS payment flow, but still with disabled UI --- Model/Payment.swift | 22 +++++++--------------- Model/StripeKiwix.swift | 28 ++++++++++++++++++++++++++++ 2 files changed, 35 insertions(+), 15 deletions(-) diff --git a/Model/Payment.swift b/Model/Payment.swift index d8c86aa88..83033a4be 100644 --- a/Model/Payment.swift +++ b/Model/Payment.swift @@ -60,6 +60,7 @@ struct Payment { let completeSubject = PassthroughSubject() + static let kiwixPaymentServer = URL(string: "https://api.donation.kiwix.org/v1/stripe")! static let merchantSessionURL = URL(string: "https://apple-pay-gateway.apple.com" )! static let merchantId = "merchant.org.kiwix.apple" static let paymentSubscriptionManagingURL = "https://www.kiwix.org" @@ -140,6 +141,7 @@ struct Payment { request.countryCode = "CH" request.currencyCode = selectedAmount.currency request.supportedNetworks = Self.supportedNetworks + request.merchantCapabilities = .threeDSecure request.requiredBillingContactFields = [.emailAddress] let recurring: PKRecurringPaymentRequest? = if selectedAmount.isMonthly { PKRecurringPaymentRequest(paymentDescription: "payment.description.label".localized, @@ -170,7 +172,7 @@ struct Payment { os_log("onPaymentAuthPhase: .didAuthorize") // call our server to get payment / setup intent and return the client.secret Task { @MainActor [resultHandler] in - let paymentServer = StripeKiwix(endPoint: URL(string: "https://api.donation.kiwix.org/v1/stripe")!, + let paymentServer = StripeKiwix(endPoint: Self.kiwixPaymentServer, payment: payment) do { let publicKey = try await paymentServer.publishableKey() @@ -213,23 +215,13 @@ struct Payment { @available(macOS 13.0, *) func onMerchantSessionUpdate() async -> PKPaymentRequestMerchantSessionUpdate { - var request = URLRequest(url: Self.merchantSessionURL) - request.httpMethod = "GET" - request.setValue("application/json", forHTTPHeaderField: "Content-Type") - do { - let (data, response) = try await URLSession.shared.data(for: request) - guard let httpResponse = response as? HTTPURLResponse, - (200..<300).contains(httpResponse.statusCode), - let dict = try JSONSerialization.jsonObject(with: data, - options: .allowFragments) as? [String: Any] else { - throw MerchantSessionError.invalidStatus + guard let session = await StripeKiwix.stripeSession(endPoint: Self.kiwixPaymentServer) else { + await MainActor.run { + Self.finalResult = .error } - let session = PKPaymentMerchantSession(dictionary: dict) - return .init(status: .success, merchantSession: session) - } catch let error { - os_log("Merchant session not established: %@", type: .debug, error.localizedDescription) return .init(status: .failure, merchantSession: nil) } + return .init(status: .success, merchantSession: session) } } diff --git a/Model/StripeKiwix.swift b/Model/StripeKiwix.swift index e52f5ccfd..565246fc8 100644 --- a/Model/StripeKiwix.swift +++ b/Model/StripeKiwix.swift @@ -15,6 +15,7 @@ import Foundation import PassKit +import os struct StripeKiwix { @@ -60,6 +61,30 @@ struct StripeKiwix { return .failure(serverError) } } + + static func stripeSession(endPoint: URL) async -> PKPaymentMerchantSession? { + do { + var request = URLRequest(url: endPoint.appending(path: "payment-session")) + request.httpMethod = "POST" + request.setValue("application/json", forHTTPHeaderField: "Content-Type") + let encoder = JSONEncoder() + encoder.keyEncodingStrategy = .convertToSnakeCase + request.httpBody = try encoder.encode(SessionParams()) + let (data, response) = try await URLSession.shared.data(for: request) + guard let httpResponse = response as? HTTPURLResponse, + (200..<300).contains(httpResponse.statusCode) else { + throw StripeError.serverError + } + guard let dictionary = try JSONSerialization.jsonObject(with: data, options: []) as? [String: Any] else { + os_log("Merchant session not established: unable to decode server response", type: .debug) + return nil + } + return PKPaymentMerchantSession(dictionary: dictionary) + } catch let serverError { + os_log("Merchant session not established: %@", type: .debug, serverError.localizedDescription) + return nil + } + } } /// Response structure for GET {endPoint}/config @@ -88,3 +113,6 @@ private struct SelectedPaymentAmount: Encodable { assert(Payment.currencyCodes.contains(currency)) } } +private struct SessionParams: Encodable { + let validationUrl = "apple-pay-gateway-cert.apple.com" +} From 9692781840f13eccba13f40d579a684f20a727b2 Mon Sep 17 00:00:00 2001 From: Balazs Perlaki-Horvath Date: Sat, 30 Nov 2024 14:20:26 +0100 Subject: [PATCH 54/54] Fixlint --- Model/Payment.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Model/Payment.swift b/Model/Payment.swift index 83033a4be..28d2c491c 100644 --- a/Model/Payment.swift +++ b/Model/Payment.swift @@ -56,7 +56,7 @@ struct Payment { return value } @MainActor - static private var finalResult: Payment.FinalResult? = nil + static private var finalResult: Payment.FinalResult? let completeSubject = PassthroughSubject()