├── .gitignore ├── .swiftpm └── xcode │ └── package.xcworkspace │ ├── contents.xcworkspacedata │ └── xcshareddata │ └── IDEWorkspaceChecks.plist ├── INSTALL.md ├── Package.resolved ├── Package.swift ├── README.md ├── Sources └── iPaymentButton │ ├── DemoApplePay.swift │ ├── ExampleApp.png │ └── iPaymentButton.swift └── Tests ├── LinuxMain.swift └── iPaymentButtonTests ├── XCTestManifests.swift └── iPaymentButtonTests.swift /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | /.build 3 | /Packages 4 | /*.xcodeproj 5 | xcuserdata/ 6 | -------------------------------------------------------------------------------- /.swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /.swiftpm/xcode/package.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /INSTALL.md: -------------------------------------------------------------------------------- 1 | [*<<< Back to README*](https://github.com/benjaminsage/iPages) 2 | 3 | 4 | # Instructions 5 | 6 | ## **(1/3)** Open XCode. Go to [File > Swift Packges > Add Package Dependency...] 7 | ![](https://github.com/benjaminsage/iPages/blob/main/instructions/instructions-1.png) 8 | 9 | ## (2/3) Input https://github.com/benjaminsage/iPages.git & click Next. 10 | ![](https://github.com/benjaminsage/iPages/blob/main/instructions/instructions-2-iPages.png) 11 | 12 | ## (3/3) Select Version: Up to Next Major & click finish. 13 | ![](https://github.com/benjaminsage/iPages/blob/main/instructions/instructions-3-iPages.png) 14 | 15 | 16 | 17 | [*<<< Back to README*](https://github.com/benjaminsage/iPages) 18 | -------------------------------------------------------------------------------- /Package.resolved: -------------------------------------------------------------------------------- 1 | { 2 | "object": { 3 | "pins": [ 4 | { 5 | "package": "iGraphics", 6 | "repositoryURL": "https://github.com/benjaminsage/iGraphics.git", 7 | "state": { 8 | "branch": null, 9 | "revision": "eaeec808bbbac6df127fc87d6255cc8cfb91ec15", 10 | "version": "0.0.5" 11 | } 12 | } 13 | ] 14 | }, 15 | "version": 1 16 | } 17 | -------------------------------------------------------------------------------- /Package.swift: -------------------------------------------------------------------------------- 1 | // swift-tools-version:5.3 2 | // The swift-tools-version declares the minimum version of Swift required to build this package. 3 | 4 | import PackageDescription 5 | 6 | let package = Package( 7 | name: "iPaymentButton", 8 | defaultLocalization: "en", 9 | platforms: [ 10 | .iOS(.v13) 11 | ], 12 | products: [ 13 | // Products define the executables and libraries a package produces, and make them visible to other packages. 14 | .library( 15 | name: "iPaymentButton", 16 | targets: ["iPaymentButton"]), 17 | ], 18 | dependencies: [ 19 | .package(url: "https://github.com/benjaminsage/iGraphics.git", from: "0.0.1") 20 | // Dependencies declare other packages that this package depends on. 21 | // .package(url: /* package url */, from: "1.0.0"), 22 | ], 23 | targets: [ 24 | // Targets are the basic building blocks of a package. A target can define a module or a test suite. 25 | // Targets can depend on other targets in this package, and on products in packages this package depends on. 26 | .target( 27 | name: "iPaymentButton", 28 | dependencies: ["iGraphics"], 29 | resources: [.process("ExampleApp.png")]), 30 | .testTarget( 31 | name: "iPaymentButtonTests", 32 | dependencies: ["iPaymentButton"]), 33 | ] 34 | ) 35 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |

iPaymentButton 💵

2 |

Quickly implement & easily customize the Apple Pay button. 🤑

3 |

4 | Get Started | 5 | Examples | 6 | Customize | 7 | Install | 8 |

9 |

10 | CI 11 |

12 | 13 |
14 | 15 | ## Get Started 16 | 17 | 1. [Install](https://github.com/benjaminsage/iPaymentButton/blob/main/INSTALL.md) `iPages` 18 | 19 | 2. Add `iPaymentButton` to your project 20 | ```swift 21 | import SwiftUI 22 | import iPaymentButton 23 | 24 | struct ContentView: View { 25 | var body: some View { 26 | iPaymentButton(action: { 27 | // Add your custom payment code here 28 | }) 29 | } 30 | } 31 | ``` 32 | 33 | 3. Customize your `iPages` 34 | 35 | 36 | ## Examples 37 | ### Starter 38 | 39 | 40 | Use our built-in `applePayDemo()` to demo a purchase experience. 41 | 42 | ```swift 43 | import SwiftUI 44 | import iPaymentButton 45 | import iGraphics 46 | 47 | struct ContentView: View { 48 | var body: some View { 49 | iGraphicsBox().stack(3) 50 | 51 | iPaymentButton(action: { 52 | iPaymentButton.applePayDemo() 53 | // Add your custom payment code here 54 | }) 55 | } 56 | } 57 | ``` 58 | 59 | 60 | ### Modern 61 | 62 | Change the button type and style. 63 | 64 | ```swift 65 | import SwiftUI 66 | import iPaymentButton 67 | import iGraphics 68 | 69 | struct ContentView: View { 70 | var body: some View { 71 | iGraphicsView(.first) 72 | 73 | iPaymentButton(type: .support, style: .whiteOutline, action: { 74 | iPaymentButton.applePayDemo() 75 | // Add your custom payment code here 76 | }) 77 | } 78 | } 79 | ``` 80 | 81 | 82 | ## Customize 83 | `iPaymentButton` has one required parameter: an action to execute on button tap. `iPaymentButton` supports several custom initializers and one custom modifier. 84 | 85 | **Example**: Change the text, style and corner radius with the following code block: 86 | ```swift 87 | iPaymentButton(type: .support, style: .whiteOutline, action: { } ) 88 | .cornerRadius(25) 89 | ``` 90 | 91 | Use our exhaustive input list to customize your views. 92 | 93 | Modifier or Initializer | Description 94 | --- | --- 95 | `type: PKPaymentButtonType = .buy` | The text written on the button. 🆒 96 | `type: PKPaymentButtonStyle = .black` | The color that the button should be. 🎨 97 | `action: @escaping () -> Void` | The action to be performed when the user taps the button. 🎬▶️ 98 | `.cornerRadius(radius: CGFloat) -> iPaymentButton` | Modifies the corner radius of the payment button ⬛️⚫️. To remove the rounded courners, set this value to 0.0 0️⃣👌. The default value is set to 4.0 🍀. 99 | 100 | 101 | ## Install 102 | Use the Swift package manager to install. Find instructions [here](https://github.com/benjaminsage/iPaymentButton/blob/main/INSTALL.md)😀 103 | 104 | -------------------------------------------------------------------------------- /Sources/iPaymentButton/DemoApplePay.swift: -------------------------------------------------------------------------------- 1 | // 2 | // DemoApplePay.swift 3 | // DummyTest 4 | // 5 | // Created by Kalil Fine on 10/5/20. 6 | // 7 | 8 | // 9 | // ApplePay.swift 10 | // ConsumerApp 11 | // 12 | // Created by Benjamin Sage on 7/10/20. 13 | // Copyright © 2020 DeuceDrone. All rights reserved. 14 | // 15 | 16 | import UIKit 17 | import PassKit 18 | import SwiftUI 19 | import iGraphics 20 | 21 | public extension iPaymentButton { 22 | 23 | @available(iOS 13.0.0, *) 24 | public struct DemoDonation: View { 25 | public var body: some View { 26 | VStack { 27 | CardsApp() 28 | iPaymentButton(type: .donate, style: .whiteOutline, action: { 29 | applePayDemo() 30 | }) 31 | } 32 | } 33 | } 34 | 35 | @available(iOS 13.0.0, *) 36 | public struct DemoPrimary: View { 37 | public var body: some View { 38 | VStack { 39 | ShoppingApp() 40 | iPaymentButton(action: { 41 | applePayDemo() 42 | }) 43 | } 44 | } 45 | } 46 | 47 | 48 | @available(iOS 14.0.0, *) 49 | public struct DemoGraphics: View { 50 | public var body: some View { 51 | VStack { 52 | MediaApp() 53 | iPaymentButton(type: .support, style: .whiteOutline, action: { 54 | applePayDemo() 55 | }) 56 | } 57 | } 58 | } 59 | 60 | 61 | @available(iOS 13.0.0, *) 62 | public struct ShoppingApp: View { 63 | public var body: some View { 64 | iGraphicsBox() 65 | .stack(3) 66 | } 67 | } 68 | 69 | @available(iOS 13.0.0, *) 70 | public struct CardsApp: View { 71 | public var body: some View { 72 | iGraphicsBox() 73 | .stack([.card, .caption]) 74 | } 75 | } 76 | 77 | @available(iOS 13.0.0, *) 78 | public struct MediaApp: View { 79 | public var body: some View { 80 | iGraphicsView(.first) 81 | } 82 | } 83 | 84 | 85 | @available(iOS 11.0, *) 86 | public static func applePayDemo() { 87 | ExampleApplePayPopup().pay() 88 | } 89 | } 90 | 91 | public typealias PaymentCompletionHandler = (Bool) -> Void 92 | 93 | @available(iOS 11.0, *) 94 | public class ExampleApplePayPopup: NSObject { 95 | public override init() {} 96 | 97 | var paymentController: PKPaymentAuthorizationController? 98 | var paymentSummaryItems = [PKPaymentSummaryItem]() 99 | var paymentStatus = PKPaymentAuthorizationStatus.failure 100 | var completionHandler: PaymentCompletionHandler! 101 | 102 | public static let supportedNetworks: [PKPaymentNetwork] = [ 103 | .amex, 104 | .discover, 105 | .masterCard, 106 | .visa 107 | ] 108 | 109 | public class func applePayStatus() -> (canMakePayments: Bool, canSetupCards: Bool) { 110 | return (PKPaymentAuthorizationController.canMakePayments(), 111 | PKPaymentAuthorizationController.canMakePayments(usingNetworks: supportedNetworks)) 112 | } 113 | 114 | private func demoCompletion(_: Bool) {} 115 | 116 | // public func startPayment(completion: @escaping PaymentCompletionHandler) { 117 | public func pay() { 118 | completionHandler = demoCompletion 119 | 120 | let subtotal = PKPaymentSummaryItem(label: "Subtotal", amount: 87.24, type: .final) 121 | let serviceFee = PKPaymentSummaryItem(label: "Service Fee", amount: 4.02, type: .final) 122 | let tax = PKPaymentSummaryItem(label: "Tax", amount: 8.04, type: .final) 123 | let total = PKPaymentSummaryItem(label: "Demo Corp", amount: 99.30, type: .final) 124 | paymentSummaryItems = [subtotal, serviceFee, tax, total] 125 | 126 | // Create our payment request 127 | let paymentRequest = PKPaymentRequest() 128 | paymentRequest.paymentSummaryItems = paymentSummaryItems 129 | paymentRequest.merchantIdentifier = "merchant.com.demo" 130 | paymentRequest.merchantCapabilities = .capability3DS 131 | paymentRequest.countryCode = "US" 132 | paymentRequest.currencyCode = "USD" 133 | paymentRequest.requiredShippingContactFields = [.postalAddress, .phoneNumber] 134 | paymentRequest.supportedNetworks = ExampleApplePayPopup.supportedNetworks 135 | 136 | // Display our payment request 137 | paymentController = PKPaymentAuthorizationController(paymentRequest: paymentRequest) 138 | paymentController?.delegate = self 139 | paymentController?.present(completion: { (presented: Bool) in 140 | if presented { 141 | // debugPrint("Presented payment controller") 142 | } else { 143 | // debugPrint("Failed to present payment controller") 144 | self.completionHandler(false) 145 | } 146 | }) 147 | 148 | } 149 | } 150 | 151 | /* 152 | PKPaymentAuthorizationControllerDelegate conformance. 153 | */ 154 | @available(iOS 11.0, *) 155 | extension ExampleApplePayPopup: PKPaymentAuthorizationControllerDelegate { 156 | 157 | public func paymentAuthorizationController(_ controller: PKPaymentAuthorizationController, didAuthorizePayment payment: PKPayment, handler completion: @escaping (PKPaymentAuthorizationResult) -> Void) { 158 | 159 | // Perform some very basic validation on the provided contact information 160 | let errors = [Error]() 161 | let status = PKPaymentAuthorizationStatus.success 162 | // if payment.shippingContact?.postalAddress?.isoCountryCode != "US" { 163 | // let pickupError = PKPaymentRequest.paymentShippingAddressUnserviceableError(withLocalizedDescription: "Sample App only picks up in the United States") 164 | // let countryError = PKPaymentRequest.paymentShippingAddressInvalidError(withKey: CNPostalAddressCountryKey, localizedDescription: "Invalid country") 165 | // errors.append(pickupError) 166 | // errors.append(countryError) 167 | // status = .failure 168 | // } else { 169 | // // Here you would send the payment token to your server or payment provider to process 170 | // // Once processed, return an appropriate status in the completion handler (success, failure, etc) 171 | // } 172 | 173 | self.paymentStatus = status 174 | print(payment) 175 | completion(PKPaymentAuthorizationResult(status: status, errors: errors)) 176 | 177 | 178 | } 179 | 180 | public func paymentAuthorizationControllerDidFinish(_ controller: PKPaymentAuthorizationController) { 181 | controller.dismiss { 182 | print("payment controller did finish") 183 | // We are responsible for dismissing the payment sheet once it has finished 184 | DispatchQueue.main.async { 185 | if self.paymentStatus == .success { 186 | self.completionHandler!(true) 187 | } else { 188 | self.completionHandler!(false) 189 | } 190 | } 191 | 192 | } 193 | } 194 | 195 | public func paymentAuthorizationController(_ controller: PKPaymentAuthorizationController, didSelectPaymentMethod paymentMethod: PKPaymentMethod, handler completion: @escaping (PKPaymentRequestPaymentMethodUpdate) -> Void) { 196 | // The didSelectPaymentMethod delegate method allows you to make changes when the user updates their payment card 197 | // Here we're applying a $2 discount when a debit card is selected 198 | guard paymentMethod.type == .debit else { 199 | completion(PKPaymentRequestPaymentMethodUpdate(paymentSummaryItems: paymentSummaryItems)) 200 | return 201 | } 202 | 203 | // var discountedSummaryItems = paymentSummaryItems 204 | // let discount = PKPaymentSummaryItem(label: "Debit Card Discount", amount: NSDecimalNumber(string: "-2.00")) 205 | // discountedSummaryItems.insert(discount, at: paymentSummaryItems.count - 1) 206 | // if let total = paymentSummaryItems.last { 207 | // total.amount = total.amount.subtracting(NSDecimalNumber(string: "2.00")) 208 | // } 209 | // completion(PKPaymentRequestPaymentMethodUpdate(paymentSummaryItems: discountedSummaryItems)) 210 | } 211 | } 212 | -------------------------------------------------------------------------------- /Sources/iPaymentButton/ExampleApp.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/blsage/iPaymentButton/33348c69e980a48e93d1468a010dc4b2c6721b0b/Sources/iPaymentButton/ExampleApp.png -------------------------------------------------------------------------------- /Sources/iPaymentButton/iPaymentButton.swift: -------------------------------------------------------------------------------- 1 | import UIKit 2 | import PassKit 3 | import SwiftUI 4 | 5 | @available(iOS 13.0, *) 6 | /// A **payment button** 💵 wrapper view around Apple's PassKit `PKPaymentButton` ☑️ 7 | /// which allows the button to be drawn and rendered complely using SwiftUI. 🙌 8 | public struct iPaymentButton: View { 9 | 10 | private var type: PKPaymentButtonType 11 | private var style: PKPaymentButtonStyle 12 | private var cornerRadius: CGFloat = 4.0 13 | private var action: () -> Void 14 | 15 | /// Creates a new payment button. 🏗👷‍♀️💰 16 | /// - Parameters: 17 | /// - type: The **text** written on the button 🆒 18 | /// - style: The **color** that the button should be 🎨 19 | /// - action: The **action** to be performed when the user taps the button 🎬▶️ 20 | public init(type: PKPaymentButtonType = .buy, 21 | style: PKPaymentButtonStyle = .black, 22 | action: @escaping () -> Void) 23 | { 24 | self.type = type 25 | self.style = style 26 | self.action = action 27 | } 28 | 29 | public var body: some View { 30 | Button(action: action, label: { EmptyView() } ) 31 | .buttonStyle(iPaymentButtonStyle(type: type, style: style, cornerRadius: cornerRadius)) 32 | } 33 | } 34 | 35 | public extension iPaymentButton { 36 | /// Modifies the **corner radius** of the payment button. ⬛️⚫️ 37 | /// 38 | /// To remove the rounded courners, set this value to 0.0. 0️⃣👌 39 | /// 40 | /// The default value is set to 4.0 🍀4️⃣ 41 | /// - Parameter radius: The desired corner radius in points ⬜️⚪️ 42 | /// - Returns: A payment button with the desired corner radius 📄 43 | func cornerRadius(_ radius: CGFloat) -> iPaymentButton { 44 | var view = self 45 | view.cornerRadius = radius 46 | return view 47 | } 48 | } 49 | 50 | @available(iOS 13.0.0, *) 51 | fileprivate struct iPaymentButtonStyle: ButtonStyle { 52 | var type: PKPaymentButtonType 53 | var style: PKPaymentButtonStyle 54 | var cornerRadius: CGFloat 55 | func makeBody(configuration: Self.Configuration) -> some View { 56 | return iPaymentButtonHelper(type: type, style: style, cornerRadius: cornerRadius) 57 | } 58 | } 59 | 60 | @available(iOS 13.0.0, *) 61 | fileprivate struct iPaymentButtonHelper: View { 62 | var type: PKPaymentButtonType 63 | var style: PKPaymentButtonStyle 64 | var cornerRadius: CGFloat 65 | var body: some View { 66 | iPaymentButtonRepresentable(type: type, style: style, cornerRadius: cornerRadius) 67 | .frame(minWidth: 100, maxWidth: 400) 68 | .frame(height: 60) 69 | .frame(maxWidth: .infinity) 70 | } 71 | } 72 | 73 | @available(iOS 13.0.0, *) 74 | extension iPaymentButtonHelper { 75 | struct iPaymentButtonRepresentable: UIViewRepresentable { 76 | var type: PKPaymentButtonType 77 | var style: PKPaymentButtonStyle 78 | var cornerRadius: CGFloat 79 | 80 | var button: PKPaymentButton { 81 | let button = PKPaymentButton(paymentButtonType: type, paymentButtonStyle: style) 82 | button.cornerRadius = cornerRadius 83 | return button 84 | } 85 | 86 | func makeUIView(context: Context) -> PKPaymentButton { 87 | return button 88 | } 89 | func updateUIView(_ uiView: PKPaymentButton, context: Context) { } 90 | } 91 | } 92 | -------------------------------------------------------------------------------- /Tests/LinuxMain.swift: -------------------------------------------------------------------------------- 1 | import XCTest 2 | 3 | import iPaymentButtonTests 4 | 5 | var tests = [XCTestCaseEntry]() 6 | tests += iPaymentButtonTests.allTests() 7 | XCTMain(tests) 8 | -------------------------------------------------------------------------------- /Tests/iPaymentButtonTests/XCTestManifests.swift: -------------------------------------------------------------------------------- 1 | import XCTest 2 | 3 | #if !canImport(ObjectiveC) 4 | public func allTests() -> [XCTestCaseEntry] { 5 | return [ 6 | testCase(iPaymentButtonTests.allTests), 7 | ] 8 | } 9 | #endif 10 | -------------------------------------------------------------------------------- /Tests/iPaymentButtonTests/iPaymentButtonTests.swift: -------------------------------------------------------------------------------- 1 | import XCTest 2 | @testable import iPaymentButton 3 | 4 | final class iPaymentButtonTests: XCTestCase { 5 | func testExample() { 6 | // This is an example of a functional test case. 7 | // Use XCTAssert and related functions to verify your tests produce the correct 8 | // results. 9 | // XCTAssertEqual(iPaymentButton().text, "Hello, World!") 10 | } 11 | 12 | static var allTests = [ 13 | ("testExample", testExample), 14 | ] 15 | } 16 | --------------------------------------------------------------------------------