iOS / Swift Shield SDK

AkedlyShield provides Proof-of-Work solving and Turnstile token retrieval for Akedly V1.2 on iOS and macOS. Uses Swift concurrency (async/await) and CryptoKit for native performance.

Installation

Add via Swift Package Manager in Xcode:

  1. File > Add Package Dependencies
  2. Enter: https://github.com/Akedly-Org/akedly-shield-swift
  3. Select version 1.0.0 or later

Or add to Package.swift:

dependencies: [
    .package(url: "https://github.com/Akedly-Org/akedly-shield-swift", from: "1.0.0")
]

Requirements: iOS 13+ (CryptoKit)


Quick Start

Import AkedlyShield and use the async solvePow function to solve challenges on a background thread. For Turnstile, use AkedlyTurnstile which creates a hidden WKWebView. PoW and Turnstile run on-device; only non-sensitive proofs travel to your backend.

Quick Start

SWIFT
// Express proxy — deploy this on your server, not inside the iOS app.
import express from 'express';

const app = express();
app.use(express.json());
// Optional — only needed if you want per-end-user-IP rate limiting.
// Drop this line and the x-end-user-ip header below if you don't.
// Makes req.ip the real client IP behind a reverse proxy (1 = trust first hop).
app.set('trust proxy', 1);

app.get('/auth/akedly/challenge', async (_req, res) => {
  const r = await fetch(
    `https://api.akedly.io/api/v1.2/transactions/challenge` +
    `?APIKey=${process.env.AKEDLY_API_KEY}` +
    `&pipelineID=${process.env.AKEDLY_PIPELINE_ID}`
  );
  res.status(r.status).json(await r.json());
});

app.post('/auth/akedly/send', async (req, res) => {
  const { phoneNumber, powSolution, turnstileToken } = req.body;
  const r = await fetch('https://api.akedly.io/api/v1.2/transactions/send', {
    method: 'POST',
    headers: {
      'Content-Type': 'application/json',
      'x-end-user-ip': req.ip,
    },
    body: JSON.stringify({
      APIKey: process.env.AKEDLY_API_KEY,
      pipelineID: process.env.AKEDLY_PIPELINE_ID,
      verificationAddress: { phoneNumber },
      powSolution,
      turnstileToken,
    }),
  });
  res.status(r.status).json(await r.json());
});

app.post('/auth/akedly/verify', async (req, res) => {
  const { transactionReqID, otp } = req.body;
  const r = await fetch('https://api.akedly.io/api/v1.2/transactions/verify', {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify({ transactionReqID, otp }),
  });
  res.status(r.status).json(await r.json());
});

API Methods

solvePow(challenge:difficulty:) async -> Int

Asynchronous PoW solver using Swift concurrency. Executes on a background thread via Task.detached and yields every 10,000 iterations for cooperative cancellation.

  • Name
    challenge
    Type
    String
    Description

    64-character hex string from the server.

  • Name
    difficulty
    Type
    Int
    Description

    Number of leading hex zeros required.

Returns Int (the nonce).

solvePowSync(challenge:difficulty:) -> Int

Synchronous blocking solver. Use on background queues only.

  • Name
    challenge
    Type
    String
    Description

    64-character hex string from the server.

  • Name
    difficulty
    Type
    Int
    Description

    Number of leading hex zeros required.

Returns Int (the nonce). Blocks until found.

AkedlyTurnstile

Creates a hidden WKWebView to retrieve Cloudflare Turnstile tokens.

  • Name
    init(bridgeDomain:)
    Type
    constructor
    Description

    Optional bridge domain. Defaults to turnstile.akedly.io.

  • Name
    getToken(siteKey:) async throws -> String
    Type
    method
    Description

    Loads the Turnstile bridge page and returns the verification token.


SwiftUI Example

SwiftUI

SWIFT
OTPView.swift
import SwiftUI
import AkedlyShield

// Calls YOUR backend proxy; see Quick Start for the Node.js server.
let backendUrl = "https://yourapp.com"

struct OTPView: View {
    let phoneNumber: String

    @State private var transactionReqID: String?
    @State private var otp = ""
    @State private var loading = false
    @State private var error: String?

    var body: some View {
        VStack(spacing: 20) {
            if transactionReqID == nil {
                Text("Send OTP to \(phoneNumber)")
                Button(loading ? "Sending..." : "Send OTP") {
                    Task { await sendOTP() }
                }
                .disabled(loading)
            } else {
                TextField("Enter 6-digit OTP", text: $otp)
                    .keyboardType(.numberPad)
                    .textFieldStyle(.roundedBorder)
                Button(loading ? "Verifying..." : "Verify") {
                    Task { await verifyOTP() }
                }
                .disabled(loading || otp.count < 6)
            }
            if let error {
                Text(error).foregroundColor(.red)
            }
        }
        .padding()
    }

    func sendOTP() async {
        loading = true
        error = nil
        do {
            let url = URL(string: "\(backendUrl)/auth/akedly/challenge")!
            let (data, _) = try await URLSession.shared.data(from: url)
            let json = try JSONSerialization.jsonObject(with: data) as! [String: Any]
            let d = json["data"] as! [String: Any]

            let nonce = await solvePow(
                challenge: d["challenge"] as! String,
                difficulty: d["difficulty"] as! Int
            )

            var turnstileToken: String?
            if let ts = d["turnstile"] as? [String: Any],
               ts["required"] as? Bool == true {
                turnstileToken = try await AkedlyTurnstile()
                    .getToken(siteKey: ts["siteKey"] as! String)
            }

            var request = URLRequest(url: URL(string: "\(backendUrl)/auth/akedly/send")!)
            request.httpMethod = "POST"
            request.setValue("application/json", forHTTPHeaderField: "Content-Type")
            var body: [String: Any] = [
                "phoneNumber": phoneNumber,
                "powSolution": ["challengeToken": d["challengeToken"]!, "nonce": nonce]
            ]
            if let token = turnstileToken { body["turnstileToken"] = token }
            request.httpBody = try JSONSerialization.data(withJSONObject: body)

            let (resData, _) = try await URLSession.shared.data(for: request)
            let result = try JSONSerialization.jsonObject(with: resData) as! [String: Any]
            transactionReqID = (result["data"] as? [String: Any])?["transactionReqID"] as? String
        } catch {
            self.error = error.localizedDescription
        }
        loading = false
    }

    func verifyOTP() async {
        loading = true
        error = nil
        do {
            var request = URLRequest(url: URL(string: "\(backendUrl)/auth/akedly/verify")!)
            request.httpMethod = "POST"
            request.setValue("application/json", forHTTPHeaderField: "Content-Type")
            request.httpBody = try JSONSerialization.data(withJSONObject: ["transactionReqID": transactionReqID!, "otp": otp])
            let (data, _) = try await URLSession.shared.data(for: request)
            let result = try JSONSerialization.jsonObject(with: data) as! [String: Any]
            if result["status"] as? String == "success" {
                // Handle success
            } else {
                self.error = "Invalid OTP"
            }
        } catch {
            self.error = error.localizedDescription
        }
        loading = false
    }
}

UIKit Example

UIKit

SWIFT
OTPViewController.swift
import UIKit
import AkedlyShield

// Calls YOUR backend proxy; see Quick Start for the Node.js server.
let backendUrl = "https://yourapp.com"

class OTPViewController: UIViewController {
    var phoneNumber: String = ""

    private var transactionReqID: String?

    func sendOTP() {
        Task {
            do {
                let url = URL(string: "\(backendUrl)/auth/akedly/challenge")!
                let (data, _) = try await URLSession.shared.data(from: url)
                let json = try JSONSerialization.jsonObject(with: data) as! [String: Any]
                let d = json["data"] as! [String: Any]

                let nonce = await solvePow(
                    challenge: d["challenge"] as! String,
                    difficulty: d["difficulty"] as! Int
                )

                var body: [String: Any] = [
                    "phoneNumber": phoneNumber,
                    "powSolution": ["challengeToken": d["challengeToken"]!, "nonce": nonce]
                ]

                if let ts = d["turnstile"] as? [String: Any],
                   ts["required"] as? Bool == true {
                    let token = try await AkedlyTurnstile()
                        .getToken(siteKey: ts["siteKey"] as! String)
                    body["turnstileToken"] = token
                }

                var request = URLRequest(url: URL(string: "\(backendUrl)/auth/akedly/send")!)
                request.httpMethod = "POST"
                request.setValue("application/json", forHTTPHeaderField: "Content-Type")
                request.httpBody = try JSONSerialization.data(withJSONObject: body)

                let (resData, _) = try await URLSession.shared.data(for: request)
                let result = try JSONSerialization.jsonObject(with: resData) as! [String: Any]
                transactionReqID = (result["data"] as? [String: Any])?["transactionReqID"] as? String
            } catch {
                print("Error: \(error)")
            }
        }
    }

    func verifyOTP(_ otp: String) {
        guard let transactionReqID else { return }
        Task {
            var request = URLRequest(url: URL(string: "\(backendUrl)/auth/akedly/verify")!)
            request.httpMethod = "POST"
            request.setValue("application/json", forHTTPHeaderField: "Content-Type")
            request.httpBody = try JSONSerialization.data(withJSONObject: ["transactionReqID": transactionReqID, "otp": otp])
            let (data, _) = try await URLSession.shared.data(for: request)
            let result = try JSONSerialization.jsonObject(with: data) as! [String: Any]
            if result["status"] as? String == "success" {
                // Handle success
            }
        }
    }
}

Was this page helpful?