Repository: appcraftstudio/buymeacoffee
Branch: master
Commit: 20c574ac7dc0
Files: 32
Total size: 35.2 KB
Directory structure:
gitextract_qlxitl2w/
├── .github/
│ └── workflows/
│ └── swift.yml
├── .gitignore
├── .swiftpm/
│ └── xcode/
│ ├── package.xcworkspace/
│ │ ├── contents.xcworkspacedata
│ │ └── xcshareddata/
│ │ ├── IDEWorkspaceChecks.plist
│ │ └── WorkspaceSettings.xcsettings
│ └── xcshareddata/
│ └── xcschemes/
│ └── BuyMeACoffee.xcscheme
├── Bundle.swift
├── BuyMeACoffee.podspec
├── CODE_OF_CONDUCT.md
├── LICENSE
├── Package.swift
├── README.md
├── Sources/
│ └── BuyMeACoffee/
│ ├── BMCButton.swift
│ ├── BMCColor.swift
│ ├── BMCFont.swift
│ ├── BMCManager.swift
│ └── Resources/
│ └── Assets.xcassets/
│ ├── Colors/
│ │ ├── Contents.json
│ │ ├── black.colorset/
│ │ │ └── Contents.json
│ │ ├── blue.colorset/
│ │ │ └── Contents.json
│ │ ├── green.colorset/
│ │ │ └── Contents.json
│ │ ├── orange.colorset/
│ │ │ └── Contents.json
│ │ ├── pink.colorset/
│ │ │ └── Contents.json
│ │ ├── purple.colorset/
│ │ │ └── Contents.json
│ │ ├── red.colorset/
│ │ │ └── Contents.json
│ │ ├── white.colorset/
│ │ │ └── Contents.json
│ │ └── yellow.colorset/
│ │ └── Contents.json
│ ├── Contents.json
│ ├── cup-white.imageset/
│ │ └── Contents.json
│ └── cup-yellow.imageset/
│ └── Contents.json
└── Tests/
├── BuyMeACoffeeTests/
│ └── XCTestManifests.swift
├── LinuxMain.swift
└── buymeacoffeeTests/
└── BuyMeACoffeeTests.swift
================================================
FILE CONTENTS
================================================
================================================
FILE: .github/workflows/swift.yml
================================================
name: Swift
on:
push:
branches: [ master ]
pull_request:
branches: [ master ]
jobs:
build:
runs-on: macos-latest
steps:
- name: Checkout
uses: actions/checkout@v2
- name: Select Xcode 12
run: sudo xcode-select -s /Applications/Xcode_12.app && xcodebuild -version
- name: Linting podspec
run: pod lib lint
- name: Build
run: swift build -v
- name: Run tests
run: swift test -v
================================================
FILE: .gitignore
================================================
.DS_Store
/.build
/Packages
/*.xcodeproj
xcuserdata/
================================================
FILE: .swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata
================================================
Copyright © 2020 App Craft Studio. All rights reserved.
================================================
FILE: Sources/BuyMeACoffee/BMCButton.swift
================================================
//
// BMCButton.swift
//
// Created by François Boulais on 26/06/2020.
// Copyright © 2020 App Craft Studio. All rights reserved.
//
#if canImport(UIKit)
import UIKit
@IBDesignable
public class BMCButton: UIButton {
public struct Configuration {
let color: BMCColor
let font: BMCFont
let title: String
public init(color: BMCColor, font: BMCFont, title: String = "Buy me a coffee") {
self.color = color
self.font = font
self.title = title
}
public static let `default`: Self = .init(color: .default, font: .default)
}
private lazy var numberFormatter: NumberFormatter = {
let numberFormatter = NumberFormatter()
numberFormatter.numberStyle = .currency
return numberFormatter
}()
private var _configuration: Configuration = .default
public override var isHighlighted: Bool {
didSet {
alpha = isHighlighted ? 0.85 : 1
}
}
public convenience init(configuration: Configuration) {
self.init(type: .custom)
self._configuration = configuration
}
override init(frame: CGRect) {
super.init(frame: frame)
setup()
}
required init?(coder: NSCoder) {
super.init(coder: coder)
setup()
}
// MARK: - Interface Builder
public override func prepareForInterfaceBuilder() {
super.prepareForInterfaceBuilder()
setup()
}
// MARK: - Public functions
public func configure(with configuration: Configuration) {
titleLabel?.font = configuration.font.value
setTitleColor(configuration.color.title, for: .normal)
setImage(configuration.color.cup, for: .normal)
backgroundColor = configuration.color.background
var title = configuration.title
if let product = BMCManager.shared.product {
numberFormatter.locale = product.priceLocale
if let price = numberFormatter.string(from: product.price) {
title.append(" (\(price))")
}
}
setTitle(title, for: .normal)
}
// MARK: - Private functions
private func setup() {
layer.shadowOffset = .init(width: 4, height: 4)
layer.shadowColor = UIColor.black.cgColor
layer.shadowOpacity = 0.1
layer.shadowRadius = 2
layer.cornerRadius = 5
contentEdgeInsets = .init(top: 8, left: 12, bottom: 8, right: 12)
titleEdgeInsets = .init(top: 0, left: 6, bottom: 0, right: -6)
imageEdgeInsets = .init(top: 0, left: -6, bottom: 0, right: 6)
imageView?.contentMode = .scaleAspectFit
registerFonts()
adjustsImageWhenHighlighted = false
addTarget(BMCManager.shared, action: #selector(BMCManager.shared.start), for: .touchUpInside)
configure(with: _configuration)
}
private func registerFonts() {
if let url = Bundle.module.url(forResource: "Cookie-Regular", withExtension: "ttf") {
CTFontManagerRegisterFontsForURL(url as CFURL, .process, nil)
}
if let url = Bundle.module.url(forResource: "Lato-Regular", withExtension: "ttf") {
CTFontManagerRegisterFontsForURL(url as CFURL, .process, nil)
}
}
}
#endif
================================================
FILE: Sources/BuyMeACoffee/BMCColor.swift
================================================
//
// BMCColor.swift
//
// Created by François Boulais on 30/06/2020.
// Copyright © 2020 App Craft Studio. All rights reserved.
//
#if canImport(UIKit)
import UIKit
public enum BMCColor: String, CaseIterable {
case orange
case yellow
case purple
case black
case white
case blue
case green
case red
case pink
public static let `default`: Self = .yellow
internal var background: UIColor? {
UIColor(named: rawValue, in: .module, compatibleWith: nil)
}
internal var title: UIColor {
switch self {
case .orange, .purple, .black, .blue, .green, .red, .pink:
return .white
case .yellow, .white:
return .black
}
}
internal var cup: UIImage? {
switch self {
case .yellow:
return UIImage(named: "cup-white", in: .module, compatibleWith: nil)
default:
return UIImage(named: "cup-yellow", in: .module, compatibleWith: nil)
}
}
}
#endif
================================================
FILE: Sources/BuyMeACoffee/BMCFont.swift
================================================
//
// BMCFont.swift
//
// Created by François Boulais on 30/06/2020.
// Copyright © 2020 App Craft Studio. All rights reserved.
//
#if canImport(UIKit)
import UIKit
public enum BMCFont: String, CaseIterable {
case cookie
case lato
case arial
public static let `default`: Self = .cookie
internal var value: UIFont? {
switch self {
case .cookie:
return UIFont(name: rawValue.capitalized, size: 26)
case .lato:
return UIFont(name: rawValue.capitalized, size: 20)
case .arial:
return UIFont(name: rawValue.capitalized, size: 20)
}
}
}
#endif
================================================
FILE: Sources/BuyMeACoffee/BMCManager.swift
================================================
//
// BMCManager.swift
//
// Created by François Boulais on 30/06/2020.
// Copyright © 2020 App Craft Studio. All rights reserved.
//
#if canImport(UIKit)
import UIKit
import SafariServices
import StoreKit
public final class BMCManager: NSObject, SKProductsRequestDelegate, SKPaymentTransactionObserver {
public static let shared = BMCManager()
// MARK: - Public properties
/// The view controller used to present donation flow.
public var presentingViewController: UIViewController?
/// This text is displayed to supporters immediately after they make a payment.
public var thankYouMessage: String?
// MARK: - Internal properties
internal var product: SKProduct?
// MARK: - Private properties
private var username = "appcraftstudio"
private var productsRequest: SKProductsRequest?
private lazy var loadingViewController: UIViewController = {
let activityIndicatorView: UIActivityIndicatorView
if #available(iOS 13.0, *) {
activityIndicatorView = UIActivityIndicatorView(style: .large)
} else {
activityIndicatorView = UIActivityIndicatorView(style: .whiteLarge)
}
activityIndicatorView.translatesAutoresizingMaskIntoConstraints = false
activityIndicatorView.color = BMCColor.default.background
activityIndicatorView.startAnimating()
let viewController = UIViewController()
viewController.view.backgroundColor = .white
viewController.view.addSubview(activityIndicatorView)
viewController.modalPresentationStyle = .formSheet
if #available(iOS 13.0, *) {
viewController.isModalInPresentation = true
}
NSLayoutConstraint.activate([
activityIndicatorView.centerXAnchor.constraint(equalTo: viewController.view.centerXAnchor),
activityIndicatorView.centerYAnchor.constraint(equalTo: viewController.view.centerYAnchor)
])
return viewController
}()
private var url: URL? {
URL(string: "https://www.buymeacoffee.com/".appending(username))
}
// MARK: - Public functions
/**
Configure the manager for future donation requests.
- parameters:
- username: The username you've chosen on www.buymeacoffee.com.
*/
public func configure(username: String) {
self.username = username
guard SKPaymentQueue.canMakePayments() else {
return
}
if let productIdentifier = Bundle.main.bundleIdentifier?.appending(".buymeacoffee") {
productsRequest?.cancel()
productsRequest = SKProductsRequest(productIdentifiers: [productIdentifier])
productsRequest?.delegate = self
productsRequest?.start()
}
}
/**
Start the donation flow on presenting view controller.
*/
@objc public func start() {
guard let presentingViewController = presentingViewController else {
fatalError("presentingViewController must be set.")
}
guard SKPaymentQueue.canMakePayments(), let product = product else {
return fallback()
}
presentingViewController.present(loadingViewController, animated: true) {
let payment = SKPayment(product: product)
SKPaymentQueue.default().add(self)
SKPaymentQueue.default().add(payment)
}
}
// MARK: - Private functions
private override init() { }
private func fallback() {
guard let url = url else {
return
}
let viewController = SFSafariViewController(url: url)
viewController.preferredControlTintColor = BMCColor.default.background
viewController.modalPresentationStyle = .formSheet
presentingViewController?.present(viewController, animated: true)
}
private func showPurchasedMessage() {
let message = thankYouMessage ?? "Thank you for supporting 🎉"
let alertController = UIAlertController(title: "Buy Me a Coffee", message: message, preferredStyle: .alert)
alertController.addAction(.init(title: "Close", style: .default, handler: { [weak self] _ in
self?.loadingViewController.dismiss(animated: true)
}))
alertController.view.tintColor = BMCColor.default.background
loadingViewController.present(alertController, animated: true)
}
// MARK: - SKProductsRequestDelegate
public func productsRequest(_ request: SKProductsRequest, didReceive response: SKProductsResponse) {
product = response.products.first
productsRequest = nil
}
// MARK: - SKPaymentTransactionObserver
public func paymentQueue(_ queue: SKPaymentQueue, updatedTransactions transactions: [SKPaymentTransaction]) {
for transaction in transactions {
switch transaction.transactionState {
case .purchased:
showPurchasedMessage()
SKPaymentQueue.default().finishTransaction(transaction)
case .failed:
loadingViewController.dismiss(animated: true) { [weak self] in
if (transaction.error as? SKError)?.code != .paymentCancelled {
self?.fallback()
}
}
case .restored:
break
case .deferred:
break
case .purchasing:
break
@unknown default:
break
}
}
}
public func paymentQueue(_ queue: SKPaymentQueue, shouldAddStorePayment payment: SKPayment, for product: SKProduct) -> Bool {
true
}
}
#endif
================================================
FILE: Sources/BuyMeACoffee/Resources/Assets.xcassets/Colors/Contents.json
================================================
{
"info" : {
"author" : "xcode",
"version" : 1
}
}
================================================
FILE: Sources/BuyMeACoffee/Resources/Assets.xcassets/Colors/black.colorset/Contents.json
================================================
{
"colors" : [
{
"color" : {
"color-space" : "srgb",
"components" : {
"alpha" : "1.000",
"blue" : "0.000",
"green" : "0.000",
"red" : "0.000"
}
},
"idiom" : "universal"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}
================================================
FILE: Sources/BuyMeACoffee/Resources/Assets.xcassets/Colors/blue.colorset/Contents.json
================================================
{
"colors" : [
{
"color" : {
"color-space" : "srgb",
"components" : {
"alpha" : "1.000",
"blue" : "1.000",
"green" : "0.498",
"red" : "0.373"
}
},
"idiom" : "universal"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}
================================================
FILE: Sources/BuyMeACoffee/Resources/Assets.xcassets/Colors/green.colorset/Contents.json
================================================
{
"colors" : [
{
"color" : {
"color-space" : "srgb",
"components" : {
"alpha" : "1.000",
"blue" : "0.710",
"green" : "0.839",
"red" : "0.475"
}
},
"idiom" : "universal"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}
================================================
FILE: Sources/BuyMeACoffee/Resources/Assets.xcassets/Colors/orange.colorset/Contents.json
================================================
{
"colors" : [
{
"color" : {
"color-space" : "srgb",
"components" : {
"alpha" : "1.000",
"blue" : "0.247",
"green" : "0.506",
"red" : "1.000"
}
},
"idiom" : "universal"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}
================================================
FILE: Sources/BuyMeACoffee/Resources/Assets.xcassets/Colors/pink.colorset/Contents.json
================================================
{
"colors" : [
{
"color" : {
"color-space" : "srgb",
"components" : {
"alpha" : "1.000",
"blue" : "1.000",
"green" : "0.443",
"red" : "0.957"
}
},
"idiom" : "universal"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}
================================================
FILE: Sources/BuyMeACoffee/Resources/Assets.xcassets/Colors/purple.colorset/Contents.json
================================================
{
"colors" : [
{
"color" : {
"color-space" : "srgb",
"components" : {
"alpha" : "1.000",
"blue" : "1.000",
"green" : "0.373",
"red" : "0.741"
}
},
"idiom" : "universal"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}
================================================
FILE: Sources/BuyMeACoffee/Resources/Assets.xcassets/Colors/red.colorset/Contents.json
================================================
{
"colors" : [
{
"color" : {
"color-space" : "srgb",
"components" : {
"alpha" : "1.000",
"blue" : "0.373",
"green" : "0.373",
"red" : "1.000"
}
},
"idiom" : "universal"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}
================================================
FILE: Sources/BuyMeACoffee/Resources/Assets.xcassets/Colors/white.colorset/Contents.json
================================================
{
"colors" : [
{
"color" : {
"color-space" : "srgb",
"components" : {
"alpha" : "1.000",
"blue" : "1.000",
"green" : "1.000",
"red" : "1.000"
}
},
"idiom" : "universal"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}
================================================
FILE: Sources/BuyMeACoffee/Resources/Assets.xcassets/Colors/yellow.colorset/Contents.json
================================================
{
"colors" : [
{
"color" : {
"color-space" : "srgb",
"components" : {
"alpha" : "1.000",
"blue" : "0.000",
"green" : "0.867",
"red" : "1.000"
}
},
"idiom" : "universal"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}
================================================
FILE: Sources/BuyMeACoffee/Resources/Assets.xcassets/Contents.json
================================================
{
"info" : {
"author" : "xcode",
"version" : 1
}
}
================================================
FILE: Sources/BuyMeACoffee/Resources/Assets.xcassets/cup-white.imageset/Contents.json
================================================
{
"images" : [
{
"filename" : "cup-white.pdf",
"idiom" : "universal"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}
================================================
FILE: Sources/BuyMeACoffee/Resources/Assets.xcassets/cup-yellow.imageset/Contents.json
================================================
{
"images" : [
{
"filename" : "cup-yellow.pdf",
"idiom" : "universal"
}
],
"info" : {
"author" : "xcode",
"version" : 1
},
"properties" : {
"template-rendering-intent" : "original"
}
}
================================================
FILE: Tests/BuyMeACoffeeTests/XCTestManifests.swift
================================================
import XCTest
#if !canImport(ObjectiveC)
public func allTests() -> [XCTestCaseEntry] {
return [
testCase(buymeacoffeeTests.allTests),
]
}
#endif
================================================
FILE: Tests/LinuxMain.swift
================================================
import XCTest
import BuyMeACoffeeTests
var tests = [XCTestCaseEntry]()
#if canImport(UIKit)
tests += BuyMeACoffeeTests.allTests()
#endif
XCTMain(tests)
================================================
FILE: Tests/buymeacoffeeTests/BuyMeACoffeeTests.swift
================================================
import Foundation
import XCTest
@testable import BuyMeACoffee
#if canImport(UIKit)
final class BuyMeACoffeeTests: XCTestCase {
private let fileManager: FileManager = .default
func testSnapshotButton() {
guard let url = fileManager.urls(for: .cachesDirectory, in: .userDomainMask).first else {
XCTFail()
return
}
var isDirectory: ObjCBool = false
if !fileManager.fileExists(atPath: url.path, isDirectory: &isDirectory) || !isDirectory.boolValue {
XCTAssertNoThrow(try fileManager.createDirectory(at: url, withIntermediateDirectories: false))
}
let button = BMCButton(frame: .init(x: 0, y: 0, width: 200, height: 50))
button.configure(with: .default)
if let data = button.snapshot().pngData() {
let url = url.appendingPathComponent("snapshot-bmc-button").appendingPathExtension("png")
print(url.path)
XCTAssertNoThrow(try data.write(to: url))
} else {
XCTFail()
}
}
static var allTests = [
("snapshot button", testSnapshotButton),
]
}
fileprivate extension BMCButton {
var size: CGSize {
bounds.insetBy(dx: -layer.shadowOffset.width, dy: -layer.shadowOffset.height).size
}
func snapshot() -> UIImage {
UIGraphicsImageRenderer(size: size).image { context in
layer.render(in: context.cgContext)
}
}
}
#endif