Skip to content

Commit

Permalink
Merge pull request #694 from OneBusAway/apple-pay
Browse files Browse the repository at this point in the history
Apple pay
  • Loading branch information
aaronbrethorst authored Nov 20, 2023
2 parents c8a304f + e6fecb6 commit 6d29fc8
Show file tree
Hide file tree
Showing 7 changed files with 175 additions and 24 deletions.
11 changes: 7 additions & 4 deletions Apps/OneBusAway/project.yml
Original file line number Diff line number Diff line change
Expand Up @@ -64,16 +64,19 @@ targets:
AppGroup: group.org.onebusaway.iphone
BundledRegionsFileName: regions.json
DeepLinkServerBaseAddress: https://onebusaway.co
DonationsEnabled: true
Donations:
Enabled: true
ApplePayMerchantID: merchant.org.onebusaway.iphone
DonationManagementPortal: "https://onebusaway.co/manage-donations"
StripePublishableKeys:
test: pk_test_51O20R8BsTfxG3EeliQswtDI6etwUu6cdYX7AIT2HkyI7iMbI2Ltqz0R06EkM7SmRTYJfOPwum1iTJgrJpoLoB8ii00eUXFwHjT
production: pk_live_51O20R8BsTfxG3EelVtkEOa5zmx9MPoAXi2Jrcx8PvBUwYvKlXEJdL3nYHF7Vmd1UZ6tKyrZrKUx89bwMmSaN5HIr0096SVUJts
ExtensionURLScheme: onebusaway
PrivacyPolicyURL: https://onebusaway.org/privacy/
PushNotificationAPIKey: d5d0d28a-6091-46cd-9627-0ce01ffa9f9e
RESTServerAPIKey: org.onebusaway.iphone
RegionsServerBaseAddress: https://regions.onebusaway.org
RegionsServerAPIPath: /regions-v3.json
StripePublishableKeys:
test: pk_test_51O20R8BsTfxG3EeliQswtDI6etwUu6cdYX7AIT2HkyI7iMbI2Ltqz0R06EkM7SmRTYJfOPwum1iTJgrJpoLoB8ii00eUXFwHjT
production: pk_live_51O20R8BsTfxG3EelVtkEOa5zmx9MPoAXi2Jrcx8PvBUwYvKlXEJdL3nYHF7Vmd1UZ6tKyrZrKUx89bwMmSaN5HIr0096SVUJts
settings:
base:
DEVELOPMENT_TEAM: 4ZQCMA634J
Expand Down
79 changes: 72 additions & 7 deletions OBAKit/Donations/DonationModel.swift
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
import StripePaymentSheet
import SwiftUI
import OBAKitCore
import PassKit

extension PaymentSheetResult: Equatable {
public static func == (lhs: PaymentSheetResult, rhs: PaymentSheetResult) -> Bool {
Expand Down Expand Up @@ -37,6 +38,23 @@ class DonationModel: ObservableObject {
@MainActor
func donate(_ amountInCents: Int, recurring: Bool) async {
analytics?.reportEvent?(.userAction, label: AnalyticsLabels.donateButtonTapped, value: String(amountInCents))

let paymentSheet = PaymentSheet(
intentConfiguration: buildIntentConfiguration(amountInCents, recurring: recurring),
configuration: buildPaymentSheetConfiguration(amountInCents, recurring: recurring)
)

guard let presenter = UIApplication.shared.keyWindowFromScene?.topViewController else {
fatalError()
}

paymentSheet.present(from: presenter) { [weak self] result in
self?.result = result
self?.donationComplete = true
}
}

private func buildIntentConfiguration(_ amountInCents: Int, recurring: Bool) -> PaymentSheet.IntentConfiguration {
let mode = PaymentSheet.IntentConfiguration.Mode.payment(
amount: amountInCents,
currency: "USD",
Expand All @@ -48,29 +66,76 @@ class DonationModel: ObservableObject {
await strongSelf?.handleConfirm(amountInCents, recurring, intentCreationCallback)
}
}
return intentConfig
}

private func buildPaymentSheetConfiguration(_ amountInCents: Int, recurring: Bool) -> PaymentSheet.Configuration {
var configuration = PaymentSheet.Configuration()

configuration.billingDetailsCollectionConfiguration.name = .always
configuration.billingDetailsCollectionConfiguration.email = .always
configuration.applePay = buildApplePayConfiguration(amountInCents, recurring: recurring)

if let extensionURLScheme = Bundle.main.extensionURLScheme {
configuration.returnURL = "\(extensionURLScheme)://stripe-redirect"
}

let paymentSheet = PaymentSheet(intentConfiguration: intentConfig, configuration: configuration)
return configuration
}

guard let presenter = UIApplication.shared.keyWindowFromScene?.topViewController else {
fatalError()
private func buildApplePayConfiguration(_ amountInCents: Int, recurring: Bool) -> PaymentSheet.ApplePayConfiguration? {
guard
let merchantID = Bundle.main.applePayMerchantID,
let managementURL = Bundle.main.donationManagementPortal
else {
return nil
}

paymentSheet.present(from: presenter) { [weak self] result in
self?.result = result
self?.donationComplete = true
let recurringHandlers: PaymentSheet.ApplePayConfiguration.Handlers?

if recurring {
recurringHandlers = PaymentSheet.ApplePayConfiguration.Handlers(
paymentRequestHandler: { request in
let applePayLabel = OBALoc(
"donations.apple_pay.recurring_donation_label",
value: "OneBusAway Recurring Donation",
comment: "Required label for Apple Pay for a recurring donation"
)
let billing = PKRecurringPaymentSummaryItem(
label: applePayLabel,
amount: NSDecimalNumber(decimal: Decimal(amountInCents) / 100)
)

// Payment starts today
billing.startDate = Date()

// Pay once a month.
billing.intervalUnit = .month
billing.intervalCount = 1

request.recurringPaymentRequest = PKRecurringPaymentRequest(
paymentDescription: applePayLabel,
regularBilling: billing,
managementURL: managementURL
)
request.paymentSummaryItems = [billing]

return request
}
)
}
else {
recurringHandlers = nil
}

return PaymentSheet.ApplePayConfiguration(
merchantId: merchantID,
merchantCountryCode: "US",
customHandlers: recurringHandlers
)
}

func handleConfirm(_ amountInCents: Int, _ recurring: Bool, _ intentCreationCallback: @escaping (Result<String, Error>) -> Void) async {
private func handleConfirm(_ amountInCents: Int, _ recurring: Bool, _ intentCreationCallback: @escaping (Result<String, Error>) -> Void) async {
do {
let intent = try await obacoService.postCreatePaymentIntent(donationAmountInCents: amountInCents, recurring: recurring)
intentCreationCallback(.success(intent.clientSecret))
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
//

import Foundation
import OBAKitCore

/// Manages the visibility of donation requests.
public class DonationsManager {
Expand Down Expand Up @@ -65,6 +66,10 @@ public class DonationsManager {

// MARK: - State

public var donationsEnabled: Bool {
bundle.donationsEnabled
}

/// When true, it means the app should show an inline donation request UI.
public var shouldRequestDonations: Bool {
if !bundle.donationsEnabled { return false }
Expand All @@ -75,4 +80,16 @@ public class DonationsManager {

return donationRequestDismissedDate == nil
}

// MARK: - UI

public static func buildDonationThankYouAlert() -> UIAlertController {
let alert = UIAlertController(
title: Strings.donationThankYouTitle,
message: Strings.donationThankYouBody,
preferredStyle: .alert)
alert.addAction(UIAlertAction(title: Strings.dismiss, style: .default))

return alert
}
}
40 changes: 40 additions & 0 deletions OBAKit/Settings/MoreViewController.swift
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,7 @@ public class MoreViewController: UIViewController,
public func items(for listView: OBAListView) -> [OBAListViewSection] {
return [
headerSection,
donateSection,
updatesAndAlertsSection,
myLocationSection,
helpOutSection,
Expand All @@ -80,6 +81,45 @@ public class MoreViewController: UIViewController,
return OBAListViewSection(id: "header", contents: [MoreHeaderItem()])
}

// MARK: Donate section
var donateSection: OBAListViewSection? {
guard application.donationsManager.donationsEnabled else { return nil }

let header = OBALoc(
"more_controller.donate",
value: "Donate to OneBusAway",
comment: "Header for the donate section."
)

return OBAListViewSection(id: "donate", title: header, contents: [
OBAListRowView.DefaultViewModel(
title: OBALoc(
"more_controller.donate_description",
value: "Your support helps us improve OneBusAway",
comment: "The call to action for the More controller's donate buton"),
onSelectAction: { [weak self] _ in
self?.showDonationUI()
}
)
])
}

private func showDonationUI() {
guard let obacoService = application.obacoService else { return }
let donationModel = DonationModel(obacoService: obacoService, analytics: application.analytics)
let learnMoreView = DonationLearnMoreView { [weak self] donated in
guard donated else { return }

DispatchQueue.main.asyncAfter(deadline: .now() + 1.0) {
self?.present(DonationsManager.buildDonationThankYouAlert(), animated: true)
}
}
.environmentObject(donationModel)
.environmentObject(AnalyticsModel(application.analytics))

present(UIHostingController(rootView: learnMoreView), animated: true)
}

// MARK: Updates and alerts section
var updatesAndAlertsSection: OBAListViewSection {
let header = OBALoc(
Expand Down
8 changes: 1 addition & 7 deletions OBAKit/Stops/StopViewController.swift
Original file line number Diff line number Diff line change
Expand Up @@ -650,14 +650,8 @@ public class StopViewController: UIViewController,
let learnMoreView = DonationLearnMoreView { [weak self] donated in
guard donated else { return }

let alert = UIAlertController(
title: OBALoc("donations.thank_you_alert.title", value: "Thank you for your support!", comment: "The title of the alert shown when the user donates."),
message: nil,
preferredStyle: .alert)
alert.addAction(UIAlertAction(title: Strings.dismiss, style: .default))

DispatchQueue.main.asyncAfter(deadline: .now() + 1.0) {
self?.present(alert, animated: true)
self?.present(DonationsManager.buildDonationThankYouAlert(), animated: true)
self?.application.donationsManager.dismissDonationsRequests()
self?.refresh()
}
Expand Down
40 changes: 34 additions & 6 deletions OBAKitCore/Extensions/FoundationExtensions.swift
Original file line number Diff line number Diff line change
Expand Up @@ -45,19 +45,28 @@ public extension Bundle {
return URL(string: str)
}

/// A helper method for accessing the bundle's `DonationsEnabled`
/// A helper method for accessing the bundle's `Donations.Enabled` setting
var donationsEnabled: Bool {
guard let dict = OBAKitConfig, let val = dict["DonationsEnabled"] as? Bool else {
guard let dict = donationsConfig, let val = dict["Enabled"] as? Bool else {
return false
}

return val
}

/// A helper method for accessing the bundle's `StripePublishableKey.production` value, if defined.
/// A helper method for accessing the bundle's `Donations.ApplePayMerchantID` setting.
var applePayMerchantID: String? {
guard let dict = donationsConfig else {
return nil
}

return dict["ApplePayMerchantID"] as? String
}

/// A helper method for accessing the bundle's `Donations.StripePublishableKey.production` value, if defined.
var stripePublishableProductionKey: String? {
guard
let dict = OBAKitConfig,
let dict = donationsConfig,
let keys = dict["StripePublishableKeys"] as? [String: String]
else {
return nil
Expand All @@ -66,10 +75,10 @@ public extension Bundle {
return keys["production"]
}

/// A helper method for accessing the bundle's `StripePublishableKeys.test` value, if defined.
/// A helper method for accessing the bundle's `Donations.StripePublishableKeys.test` value, if defined.
var stripePublishableTestKey: String? {
guard
let dict = OBAKitConfig,
let dict = donationsConfig,
let keys = dict["StripePublishableKeys"] as? [String: String]
else {
return nil
Expand All @@ -78,6 +87,17 @@ public extension Bundle {
return keys["test"]
}

var donationManagementPortal: URL? {
guard
let dict = donationsConfig,
let urlString = dict["DonationManagementPortal"] as? String
else {
return nil
}

return URL(string: urlString)
}

/// A helper method for accessing the bundle's `ExtensionURLScheme`.
///
/// `extensionURLScheme` is used as an `init()` parameter on `URLSchemeRouter`.
Expand Down Expand Up @@ -142,6 +162,14 @@ public extension Bundle {
optionalValue(for: "OBAKitConfig", type: [AnyHashable: Any].self)
}

private var donationsConfig: [AnyHashable: Any]? {
guard let OBAKitConfig else {
return nil
}

return OBAKitConfig["Donations"] as? [AnyHashable: Any]
}

private func url(for key: String) -> URL? {
guard let address = optionalValue(for: key, type: String.self) else { return nil }
return URL(string: address)
Expand Down
4 changes: 4 additions & 0 deletions OBAKitCore/Strings/Strings.swift
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,10 @@ public class Strings: NSObject {

public static let donate = OBALoc("common.donate", value: "Donate", comment: "The verb 'to donate', as in to give money to a charitable cause.")

public static let donationThankYouTitle = OBALoc("common.donation_thank_you.title", value: "Thank you for your support!", comment: "Title of an alert to display to the user after they donate to OTSF")

public static let donationThankYouBody = OBALoc("common.donation_thank_you.body", value: "Your support will help us continue to improve OneBusAway.", comment: "The body of the message to display to the user after they donate to OTSF")

public static let confirmDelete = OBALoc("common.confirmdelete", value: "Confirm Delete", comment: "Asks the user to confirm a delete action. Typically this is showned when the user taps a 'delete' button on an important item.")

public static let dismiss = OBALoc("common.dismiss", value: "Dismiss", comment: "The verb 'to dismiss', as in to hide or get rid of something. Used as a button title on alerts.")
Expand Down

0 comments on commit 6d29fc8

Please sign in to comment.