Skip to main content

Swift SDK

Build native Apple platform applications that integrate with Staqr Platform and Commerce APIs.

Security Notice

Never embed API keys directly in iOS applications.

Instead, use one of these patterns:

  1. OAuth2/OIDC user authentication
  2. Backend proxy for sensitive API calls
  3. Token-based authentication with refresh

See Mobile Security for detailed guidance.


Installation

Swift Package Manager

Add to your Package.swift:

dependencies: [
.package(url: "https://github.com/staqr/staqr-swift-sdk.git", from: "1.0.0")
]

Or in Xcode: File > Add Packages > Enter the repository URL.

CocoaPods

pod 'StaqrApiClient', '~> 1.0'

Quick Start

import StaqrApiClient

// Configuration - token from secure source (Keychain)
let token = KeychainService.retrieve(forKey: "staqr_token")

StaqrApiClient.basePath = "https://api.staqr.com"
StaqrApiClient.customHeaders["Authorization"] = "Bearer \(token ?? "")"

// List flows
FlowsAPI.listFlows(limit: 50) { data, error in
if let error = error {
print("Error: \(error.localizedDescription)")
return
}

if let flows = data {
print("Found \(flows.count) flows")
for flow in flows {
print(" - \(flow.name) (\(flow.id))")
}
}
}

Available Packages

PackageAPIPlatform Support
StaqrApiClientStaqr PlatformiOS 13+, macOS 10.15+, tvOS 13+, watchOS 6+
StaqrCommerceV0Commerce v0 (Legacy)iOS 13+, macOS 10.15+, tvOS 13+, watchOS 6+
StaqrCommerceV1Commerce v1iOS 13+, macOS 10.15+, tvOS 13+, watchOS 6+
StaqrCommerceV2Commerce v2iOS 13+, macOS 10.15+, tvOS 13+, watchOS 6+

Authentication

Keychain Storage

import Security

class KeychainService {

static func save(token: String, forKey key: String) -> Bool {
guard let data = token.data(using: .utf8) else { return false }

// Delete existing item first
delete(forKey: key)

let query: [String: Any] = [
kSecClass as String: kSecClassGenericPassword,
kSecAttrAccount as String: key,
kSecValueData as String: data,
kSecAttrAccessible as String: kSecAttrAccessibleWhenUnlockedThisDeviceOnly
]

let status = SecItemAdd(query as CFDictionary, nil)
return status == errSecSuccess
}

static func retrieve(forKey key: String) -> String? {
let query: [String: Any] = [
kSecClass as String: kSecClassGenericPassword,
kSecAttrAccount as String: key,
kSecReturnData as String: true,
kSecMatchLimit as String: kSecMatchLimitOne
]

var result: AnyObject?
let status = SecItemCopyMatching(query as CFDictionary, &result)

guard status == errSecSuccess,
let data = result as? Data,
let token = String(data: data, encoding: .utf8) else {
return nil
}

return token
}

static func delete(forKey key: String) {
let query: [String: Any] = [
kSecClass as String: kSecClassGenericPassword,
kSecAttrAccount as String: key
]
SecItemDelete(query as CFDictionary)
}
}

OAuth2 Integration with ASWebAuthenticationSession

import AuthenticationServices

class AuthService: NSObject, ASWebAuthenticationPresentationContextProviding {

func authenticate() async throws -> String {
let authURL = URL(string: "https://auth.staqr.com/authorize?client_id=your-client-id&redirect_uri=your-app://callback&response_type=code&scope=openid profile email")!

let callbackURLScheme = "your-app"

return try await withCheckedThrowingContinuation { continuation in
let session = ASWebAuthenticationSession(
url: authURL,
callbackURLScheme: callbackURLScheme
) { callbackURL, error in
if let error = error {
continuation.resume(throwing: error)
return
}

guard let callbackURL = callbackURL,
let code = URLComponents(url: callbackURL, resolvingAgainstBaseURL: false)?
.queryItems?.first(where: { $0.name == "code" })?.value else {
continuation.resume(throwing: AuthError.noCode)
return
}

// Exchange code for token
Task {
do {
let token = try await self.exchangeCodeForToken(code)
continuation.resume(returning: token)
} catch {
continuation.resume(throwing: error)
}
}
}

session.presentationContextProvider = self
session.prefersEphemeralWebBrowserSession = false
session.start()
}
}

func presentationAnchor(for session: ASWebAuthenticationSession) -> ASPresentationAnchor {
return UIApplication.shared.windows.first { $0.isKeyWindow }!
}

private func exchangeCodeForToken(_ code: String) async throws -> String {
// Exchange authorization code for access token
// Implementation depends on your auth server
fatalError("Implement token exchange")
}
}

enum AuthError: Error {
case noCode
case tokenExchangeFailed
}

SwiftUI Integration

ViewModel Pattern

import SwiftUI

@MainActor
class FlowsViewModel: ObservableObject {
@Published var flows: [Flow] = []
@Published var isLoading = false
@Published var error: Error?

func loadFlows() async {
isLoading = true
error = nil

defer { isLoading = false }

do {
flows = try await withCheckedThrowingContinuation { continuation in
FlowsAPI.listFlows(limit: 100) { data, error in
if let error = error {
continuation.resume(throwing: error)
} else {
continuation.resume(returning: data ?? [])
}
}
}
} catch {
self.error = error
}
}
}

struct FlowsListView: View {
@StateObject private var viewModel = FlowsViewModel()

var body: some View {
NavigationView {
Group {
if viewModel.isLoading {
ProgressView()
} else if let error = viewModel.error {
VStack {
Text("Error loading flows")
.font(.headline)
Text(error.localizedDescription)
.font(.caption)
.foregroundColor(.secondary)
Button("Retry") {
Task { await viewModel.loadFlows() }
}
}
} else {
List(viewModel.flows, id: \.id) { flow in
NavigationLink(destination: FlowDetailView(flow: flow)) {
VStack(alignment: .leading) {
Text(flow.name)
.font(.headline)
Text(flow.id)
.font(.caption)
.foregroundColor(.secondary)
}
}
}
}
}
.navigationTitle("Flows")
}
.task {
await viewModel.loadFlows()
}
}
}

Using Async/Await Extensions

import Foundation

extension FlowsAPI {

static func listFlowsAsync(limit: Int? = nil) async throws -> [Flow] {
try await withCheckedThrowingContinuation { continuation in
listFlows(limit: limit) { data, error in
if let error = error {
continuation.resume(throwing: error)
} else {
continuation.resume(returning: data ?? [])
}
}
}
}

static func getFlowAsync(id: String) async throws -> Flow {
try await withCheckedThrowingContinuation { continuation in
getFlow(id: id) { data, error in
if let error = error {
continuation.resume(throwing: error)
} else if let flow = data {
continuation.resume(returning: flow)
} else {
continuation.resume(throwing: StaqrError.notFound)
}
}
}
}
}

enum StaqrError: Error {
case notFound
case unauthorized
case networkError
}

Combine Integration

import Combine

extension FlowsAPI {

static func listFlowsPublisher(limit: Int? = nil) -> AnyPublisher<[Flow], Error> {
Future { promise in
listFlows(limit: limit) { data, error in
if let error = error {
promise(.failure(error))
} else {
promise(.success(data ?? []))
}
}
}
.eraseToAnyPublisher()
}
}

// Usage
class FlowsStore: ObservableObject {
@Published var flows: [Flow] = []

private var cancellables = Set<AnyCancellable>()

func loadFlows() {
FlowsAPI.listFlowsPublisher(limit: 100)
.receive(on: DispatchQueue.main)
.sink(
receiveCompletion: { completion in
if case .failure(let error) = completion {
print("Error: \(error)")
}
},
receiveValue: { [weak self] flows in
self?.flows = flows
}
)
.store(in: &cancellables)
}
}

Error Handling

import Foundation

func handleAPIError(_ error: Error) {
if let urlError = error as? URLError {
switch urlError.code {
case .notConnectedToInternet:
showAlert("No Internet Connection", message: "Please check your network settings.")
case .timedOut:
showAlert("Request Timed Out", message: "The server took too long to respond.")
default:
showAlert("Network Error", message: "Please try again later.")
}
} else if let httpError = error as? ErrorResponse {
switch httpError {
case .error(let statusCode, _, _, _):
switch statusCode {
case 401:
// Token expired - trigger re-authentication
NotificationCenter.default.post(name: .tokenExpired, object: nil)
case 403:
showAlert("Access Denied", message: "You don't have permission to perform this action.")
case 404:
showAlert("Not Found", message: "The requested resource was not found.")
case 500...599:
showAlert("Server Error", message: "Something went wrong. Please try again later.")
default:
showAlert("Error", message: "An unexpected error occurred.")
}
}
} else {
showAlert("Error", message: error.localizedDescription)
}
}

extension Notification.Name {
static let tokenExpired = Notification.Name("tokenExpired")
}

App Transport Security

Ensure your Info.plist allows HTTPS connections:

<key>NSAppTransportSecurity</key>
<dict>
<key>NSAllowsArbitraryLoads</key>
<false/>
<key>NSExceptionDomains</key>
<dict>
<key>api.staqr.com</key>
<dict>
<key>NSExceptionRequiresForwardSecrecy</key>
<true/>
<key>NSIncludesSubdomains</key>
<true/>
</dict>
</dict>
</dict>

Requirements

  • Swift 5.5+
  • iOS 13.0+ / macOS 10.15+ / tvOS 13.0+ / watchOS 6.0+
  • Xcode 13.0+

Dependencies

The SDK uses:

  • Alamofire for HTTP networking
  • Foundation for JSON parsing

Next Steps