Skip to content

amirsaam/PatreonAPI-Swift

Repository files navigation

PatreonAPI-Swift

The missing library for interacting with Patreon's API in Swift.

Swift Version License

Since I needed to show the Patreon campaign in one of our projects and there was no Swift Package or base code for Patreon's API in Swift, I needed to write my own and I think there was need for it in the community, so I just made it a package in order to make fellow developers' life easier since the API is a total nightmare!

Installation

Add this project on your Package.swift

import PackageDescription

let package = Package(
    dependencies: [
        .package(url: "https://github.com/amirsaam/PatreonAPI-Swift.git", from: "0.9.00"),
    ]
)

Usage example

Surely import the package

import PatreonAPI

Initialise the Class

let patreonAPI = PatreonAPI(clientID: "YOUR CLIENT ID",
                            clientSecret: "YOUR CLIENT SECRET",
                            creatorAccessToken: "YOUR CREATOR'S ACCESS TOKEN",
                            creatorRefreshToken: "YOUR CREATOR'S REFRESH TOKEN",
                            redirectURI: "YOUR CLIENT REDIRECT URL",
                            campaignID: "YOUR CAMPAIGN ID")

Use built-in Functions

// For Authentication: 
patreonAPI.doOAuth() // Uses Client ID and Redirect URI from Initialiser
var oauthData = await patreonAPI.getOAuthTokens(callbackCode: "CALLBACK CODE FROM doOAuth()") // Uses Client ID, Client Secret and Redirect URI from Initialiser
oauthData = await patreonAPI.refreshOAuthTokens(userRefreshToken: oauthData?.refresh_token ?? "") // Uses Client Secret and Redirect URI from Initialiser

// For Retrieving Data:
let userIdentity = await patreonAPI.getUserIdentity(userAccessToken: oauthData?.access_token ?? "")
let userCampaigns = await patreonAPI.getUserOwnedCampaigns(userAccessToken: oauthData?.access_token ?? "")
let creatorCampaign = await patreonAPI.getDataForCampaign() // Uses Creator Access Token and Campaign ID from Initialiser
let creatorCampaignMembersList = await patreonAPI.getMembersForCampaign() // Uses Creator Access Token and Campaign ID from Initialiser
let specificMemberFromCreatorCampaign = await patreonAPI.getMemberForCampaignByID(memberID: "MEMBER OF CAMPAIGN ID") // Uses Creator Access Token from Initialiser. `memberID` should be retrieved from `membersList`

Handling doOAuth():

  • First you need to handle the redirect, rather storing the returning data to your online database or redirect them to your app with url scheme that first needs to be redirected to your website because Patreon doesn't support App URL Scheme.
  • If you choosed URL Scheme, after redirect to it the function will return a URL with some queries (read more in Patreon docs)
  • You can parse the url manually or you can use such code as below for automatic parse:
extension URL {
    func params() -> [String : Any] {
        var dict = [String : Any]()
        if let components = URLComponents(url: self, resolvingAgainstBaseURL: false) {
            if let queryItems = components.queryItems {
                for item in queryItems {
                    dict[item.name] = item.value!
                }
            }
            return dict
        } else {
            return [:]
        }
    }
}

that can be used like this for the future usages:

.onOpenURL { url in
  let callback = url.params()
  if callback.isEmpty {
      // handle callback being unsuccessful
  } else {
      let patreonCallbackCode = callback["code"] as! String
      let patreonCallbackState = callback["state"] as! String
  }
}

then you can pass patreonCallbackCode to the getOAuthTokens function.

A Problem in Patreon's API

Unfortunately, when I was trying to retrieve all data about Creator's campaign, I have encountered a bizarre situation where returning data with same name with no any distinguishing ability could have various structures and I needed to just use them with native Swift experience with decoding structs. So I made something called CodableAny (that is in this package's dependencies) for fixing my own issue. Basically when we call getDataForCampaign function, we will return:

public struct PatreonCampaignInfo: Codable {
    public let data: CampaignData
    public let included: [CampaignIncludedAny]
    public let links: SelfLink
}

and CampaignIncludedAny is:

public struct CampaignIncludedAny: Codable {
    public let attributes: [String: CodableAny]
    public let id: String
    public let type: String
}

So we retrived any data that is in the Included part of the API call in one network call, but it is not decoded so an extra local step is required! In your ViewModel for Patreon you can use such thing to decode that Included part based on if it's a Tier data or a Benefit data:

@Published var campaignTiers: [CampaignIncludedTier] = []
@Published var campaignBenefits: [CampaignIncludedBenefit] = []
@Published var patreonCampaign: PatreonCampaignInfo? {
    didSet {
        if let campaign = patreonCampaign {
            campaignTiers = extractCampaignTiers(from: campaign.included)
            campaignBenefits = extractCampaignBenefits(from: campaign.included)
        } else {
            campaignTiers = []
            campaignBenefits = []
        }
    }
}

func extractCampaignTiers(from campaign: [CampaignIncludedAny]) -> [CampaignIncludedTier] {
    var decodedArray = [CampaignIncludedTier]()
    for campaignIncluded in campaign {
        if campaignIncluded.type == "tier" {
            let decoded = try? JSONDecoder().decode(CampaignIncludedTier.self, from: try JSONEncoder().encode(campaignIncluded))
            if let decoded = decoded {
                decodedArray.append(decoded)
            }
        }
    }
    return decodedArray
}

func extractCampaignBenefits(from campaign: [CampaignIncludedAny]) -> [CampaignIncludedBenefit] {
    var decodedArray = [CampaignIncludedBenefit]()
    for campaignIncluded in campaign {
        if campaignIncluded.type == "benefit" {
            let decoded = try? JSONDecoder().decode(CampaignIncludedBenefit.self, from: try JSONEncoder().encode(campaignIncluded))
            if let decoded = decoded {
                decodedArray.append(decoded)
            }
        }
    }
    return decodedArray
}

For more information about decoding structs please go to this directory

To Do

  • Add Missing API Calls(?)
  • Write Xcode Tests
  • Make Pod
  • Deploy Github Action

Dependencies

Meta

Amir Mohammadi – @amirsaam – amirsaam [at] me [dot] com

Distributed under the MIT license. See LICENSE for more information.

https://github.com/amirsaam/PatreonAPI-Swift/