Stripe Kit Save

A Swift on Server SDK for the Stripe API

Project README

StripeKit

Test

StripeKit is a Swift package used to communicate with the Stripe API for Server Side Swift Apps.

Version support

Stripe API version 2022-11-15 -> StripeKit: 22.0.0

Installation

To start using StripeKit, in your Package.swift, add the following

.package(url: "https://github.com/vapor-community/stripe-kit.git", from: "22.0.0")

Using the API

Initialize the StripeClient

let httpClient = HTTPClient(..)
let stripe = StripeClient(httpClient: httpClient, apiKey: "sk_12345")

And now you have acess to the APIs via stripe.

The APIs you have available correspond to what's implemented.

For example to use the charges API, the stripeclient has a property to access that API via routes.

do {
    let charge = try await stripe.charges.create(amount: 2500,
                                                 currency: .usd,
                                                 description: "A server written in swift.",
                                                 source: "tok_visa")
    if charge.status == .succeeded {
        print("New swift servers are on the way 🚀")
    } else {
        print("Sorry you have to use Node.js 🤢")
    }
} catch {
    // Handle error
}

Expandable objects

StripeKit supports expandable objects via 3 property wrappers:

@Expandable, @DynamicExpandable and @ExpandableCollection

All API routes that can return expanded objects have an extra parameter expand: [String]? that allows specifying which objects to expand.

Usage with @Expandable:

  1. Expanding a single field.
// Expanding a customer from creating a `PaymentIntent`.
     let paymentIntent = try await stripeclient.paymentIntents.create(amount: 2500, currency: .usd, expand: ["customer"])
     // Accessing the expanded `Customer` object
     paymentIntent.$customer.email
  1. Expanding multiple fields.
// Expanding a customer and payment method from creating a `PaymentIntent`.
let paymentIntent = try await stripeclient.paymentIntents.create(amount: 2500, currency: .usd, expand: ["customer", "paymentMethod"])
// Accessing the expanded `StripeCustomer` object   
 paymentIntent.$customer?.email // "[email protected]"
// Accessing the expanded `StripePaymentMethod` object
 paymentIntent.$paymentMethod?.card?.last4 // "1234"
  1. Expanding nested fields.
// Expanding a payment method and its nested customer from creating a `PaymentIntent`.
let paymentIntent = try await stripeclient.paymentIntents.create(amount: 2500, currency: .usd, expand: ["paymentMethod.customer"])
// Accessing the expanded `PaymentMethod` object
 paymentIntent.$paymentMethod?.card?.last4 // "1234"
// Accessing the nested expanded `Customer` object   
 paymentIntent.$paymentMethod?.$customer?.email // "[email protected]"
  1. Usage with list all.

Note: For list operations expanded fields must start with data

// Expanding a customer from listing all `PaymentIntent`s.
let list = try await stripeclient.paymentIntents.listAll(filter: ["expand": ["data.customer"...]])
// Accessing the first `StripePaymentIntent`'s expanded `Customer` property
list.data?.first?.$customer?.email // "[email protected]"

Usage with @DynamicExpandable:

Some objects in stripe can be expanded into different objects. For example:

An ApplicationFee has an originatingTransaction property that can be expanded into either a charge or a transfer.

When expanding it you can specify which object you expect by doing the following:

let applicationfee = try await stripeclient.applicationFees.retrieve(fee: "fee_1234", expand: ["originatingTransaction"])
// Access the originatingTransaction as a Charge
applicationfee.$originatingTransaction(as: Charge.self)?.amount // 2500
...
// Access the originatingTransaction as a Transfer
applicationfee.$originatingTransaction(as: Transfer.self)?.destination // acc_1234

Usage with @ExpandableCollection:

  1. Expanding an array of ids
let invoice = try await stripeClient.retrieve(invoice: "in_12345", expand: ["discounts"])

// Access the discounts array as `String`s
invoice.discounts.map { print($0) } // "","","",..

// Access the array of `Discount`s
invoice.$discounts.compactMap(\.id).map { print($0) } // "di_1","di_2","di_3",...  

Nuances with parameters and type safety

Stripe has a habit of changing APIs and having dynamic parameters for a lot of their APIs. To accomadate for these changes, certain routes that take arguments that are hashs or Dictionaries, are represented by a Swift dictionary [String: Any].

For example consider the Connect account API.

// We define a custom dictionary to represent the paramaters stripe requires.
// This allows us to avoid having to add updates to the library when a paramater or structure changes.
let individual: [String: Any] = ["address": ["city": "New York",
					     "country": "US",
                                             "line1": "1551 Broadway",
                                             "postal_code": "10036",
	                  	             "state": "NY"],
				 "first_name": "Taylor",
			         "last_name": "Swift",
                                 "ssn_last_4": "0000",
				 "dob": ["day": "13",
					 "month": "12",
					 "year": "1989"]] 
												 
let businessSettings: [String: Any] = ["payouts": ["statement_descriptor": "SWIFTFORALL"]]

let tosDictionary: [String: Any] = ["date": Int(Date().timeIntervalSince1970), "ip": "127.0.0.1"]

let connectAccount = try await stripe.connectAccounts.create(type: .custom,									
                                  country: "US",
				  email: "[email protected]",
				  businessType: .individual,
			          defaultCurrency: .usd,
				  externalAccount: "bank_token",
			          individual: individual,
				  requestedCapabilities: ["platform_payments"],
				  settings: businessSettings,
				  tosAcceptance: tosDictionary)
print("New Stripe Connect account ID: \(connectAccount.id)")

Authentication via the Stripe-Account header

The first, preferred, authentication option is to use your (the platform account’s) secret key and pass a Stripe-Account header identifying the connected account for which the request is being made. The example request performs a refund of a charge on behalf of a connected account using a builder style API:

   stripe.refunds
    .addHeaders(["Stripe-Account": "acc_12345",
             "Authorization": "Bearer different_api_key",
             "Stripe-Version": "older-api-version"])
    .create(charge: "ch_12345", reason: .requestedByCustomer)

NOTE: The modified headers will remain on the route instance (refunds in this case) of the StripeClient if a reference to it is held. If you're accessing the StripeClient in the scope of a function, the headers will not be retained.

Idempotent Requests

Similar to the account header, you can use the same builder style API to attach Idempotency Keys to your requests.

    let key = UUID().uuidString
    stripe.refunds
    .addHeaders(["Idempotency-Key": key])
    .create(charge: "ch_12345", reason: .requestedByCustomer)

Webhooks

The webhooks API is available to use in a typesafe way to pull out entities. Here's an example of listening for the payment intent webhook.

func handleStripeWebhooks(req: Request) async throws -> HTTPResponse {

    let signature = req.headers["Stripe-Signature"]

    try StripeClient.verifySignature(payload: req.body, header: signature, secret: "whsec_1234") 
    // Stripe dates come back from the Stripe API as epoch and the StripeModels convert these into swift `Date` types.
    // Use a date and key decoding strategy to successfully parse out the `created` property and snake case strpe properties. 
    let decoder = JSONDecoder()
    decoder.dateDecodingStrategy = .secondsSince1970
    decoder.keyDecodingStrategy = .convertFromSnakeCase
    
    let event = try decoder.decode(StripeEvent.self, from: req.bodyData)
    
    switch (event.type, event.data?.object) {
    case (.paymentIntentSucceeded, .paymentIntent(let paymentIntent)):
        print("Payment capture method: \(paymentIntent.captureMethod?.rawValue)")
        return HTTPResponse(status: .ok)
        
    default: return HTTPResponse(status: .ok)
    }
}

Using with Vapor

StripeKit is pretty easy to use but to better integrate with Vapor these are some helpful extensions

import Vapor
import StripeKit

extension Application {
    public var stripe: StripeClient {
        guard let stripeKey = Environment.get("STRIPE_API_KEY") else {
            fatalError("STRIPE_API_KEY env var required")
        }
        return .init(httpClient: self.http.client.shared, apiKey: stripeKey)
    }
}

extension Request {
    private struct StripeKey: StorageKey {
        typealias Value = StripeClient
    }
    
    public var stripe: StripeClient {
        if let existing = application.storage[StripeKey.self] {
            return existing
        } else {
            guard let stripeKey = Environment.get("STRIPE_API_KEY") else {
                fatalError("STRIPE_API_KEY env var required")
            }
            let new = StripeClient(httpClient: self.application.http.client.shared, apiKey: stripeKey)
            self.application.storage[StripeKey.self] = new
            return new
        }
    }
}

extension StripeClient {
    /// Verifies a Stripe signature for a given `Request`. This automatically looks for the header in the headers of the request and the body.
    /// - Parameters:
    ///     - req: The `Request` object to check header and body for
    ///     - secret: The webhook secret used to verify the signature
    ///     - tolerance: In seconds the time difference tolerance to prevent replay attacks: Default 300 seconds
    /// - Throws: `StripeSignatureError`
    public static func verifySignature(for req: Request, secret: String, tolerance: Double = 300) throws {
        guard let header = req.headers.first(name: "Stripe-Signature") else {
            throw StripeSignatureError.unableToParseHeader
        }
        
        guard let data = req.body.data else {
            throw StripeSignatureError.noMatchingSignatureFound
        }
        
        try StripeClient.verifySignature(payload: Data(data.readableBytesView), header: header, secret: secret, tolerance: tolerance)
    }
}

extension StripeSignatureError: AbortError {
    public var reason: String {
        switch self {
        case .noMatchingSignatureFound:
            return "No matching signature was found"
        case .timestampNotTolerated:
            return "Timestamp was not tolerated"
        case .unableToParseHeader:
            return "Unable to parse Stripe-Signature header"
        }
    }
    
    public var status: HTTPResponseStatus {
        .badRequest
    }
}

Whats Implemented

Core Resources

  • Balance
  • Balance Transactions
  • Charges
  • Customers
  • Disputes
  • Events
  • Files
  • File Links
  • Mandates
  • PaymentIntents
  • SetupIntents
  • SetupAttempts
  • Payouts
  • Refunds
  • Tokens
  • EphemeralKeys

Payment Methods

  • Payment Methods
  • Bank Accounts
  • Cash Balance
  • Cards
  • Sources

Products

  • Products
  • Prices
  • Coupons
  • Promotion Codes
  • Discounts
  • Tax Codes
  • Tax Rates
  • Shipping Rates

Checkout

  • Sessions

  • Payment Links

Billing

  • Credit Notes
  • Customer Balance Transactions
  • Customer Portal
  • Customer Tax IDs
  • Invoices
  • Invoice Items
  • Plans
  • Quotes
  • Quote Line Items
  • Subscriptions
  • Subscription items
  • Subscription Schedule
  • Test Clocks
  • Usage Records

Connect

  • Account
  • Account Links
  • Account Sessions
  • Application Fees
  • Application Fee Refunds
  • Capabilities
  • Country Specs
  • External Accounts
  • Persons
  • Top-ups
  • Transfers
  • Transfer Reversals
  • Secret Management

Fraud

  • Early Fraud Warnings
  • Reviews
  • Value Lists
  • Value List Items

Issuing

  • Authorizations
  • Cardholders
  • Cards
  • Disputes
  • Funding Instructions
  • Transactions

Terminal

  • Connection Tokens
  • Locations
  • Readers
  • Hardware Orders
  • Hardware Products
  • Hardware SKUs
  • Hardware Shipping Methods
  • Configurations

Sigma

  • Scheduled Queries

Reporting

  • Report Runs
  • Report Types

Identity

  • VerificationSessions
  • VerificationReports

Webhooks

  • Webhook Endpoints
  • Signature Verification

Idempotent Requests

License

StripeKit is available under the MIT license. See the LICENSE file for more info.

Open Source Agenda is not affiliated with "Stripe Kit" Project. README Source: vapor-community/stripe-kit
Stars
112
Open Issues
12
Last Commit
2 weeks ago
License
MIT

Open Source Agenda Badge

Open Source Agenda Rating