Contents

etranzact-global-reloaded/xcel-paygate-sdk-swift

Official Swift SDK for integrating XCEL PayGate payment solutions into your iOS application.

Features

Core SDK

  • Payment Link Generation - Create payment links via API
  • Transaction Status - Check payment status by code
  • XCEL Wallet Payments - Direct wallet-to-wallet transactions
  • Merchant Products - Fetch and manage merchant products
  • Type-Safe API - Full Swift type definitions with Sendable support
  • SwiftUI Views - Ready-to-use payment UI components
  • Async/Await - Modern Swift concurrency support
  • Actor Isolation - Thread-safe API clients

SwiftUI Components

  • 🎨 PaymentWebView - Embedded payment page with callbacks
  • 🎨 PaymentView - Complete payment flow with loading states
  • 🎨 ReceiptView - Beautiful receipt display
  • 🎨 Cross-Platform - Works on iOS and macOS

Requirements

  • iOS 15.0+ / macOS 12.0+ / watchOS 8.0+ / tvOS 15.0+
  • Swift 5.9+
  • Xcode 15.0+

Installation

Swift Package Manager

Add the following to your Package.swift file:

dependencies: [
    .package(url: "https://github.com/etranzact-global-reloaded/xcel-paygate-sdk-swift.git", from: "1.0.0")
]

Or in Xcode:

  1. Go to File > Add Package Dependencies
  2. Enter the repository URL
  3. Select version and add to your target

Quick Start

1. Initialize the SDK

import XcelPayGateSDK

let config = XcelPayGateConfig(
    merchantId: "YOUR_MERCHANT_ID",
    publicKey: "YOUR_PUBLIC_KEY"
)

let sdk = XcelPayGateSDK(config: config)

2. Generate Payment Link

let request = GeneratePaymentLinkRequest(
    amount: "10.00",
    currency: "GHS",
    clientTransactionId: "txn-\(UUID().uuidString)",
    customerEmail: "customer@example.com",
    customerPhone: "233244000000",
    description: "Payment for Order #123",
    channel: .mobile,
    redirectUrl: "myapp://payment/success",
    webhookUrl: "https://your-api.com/webhook"
)

do {
    let response = try await sdk.checkout.initiateCheckout(request)
    print("Payment Link: \(response.data.paymentLink)")
    print("Payment Code: \(response.data.paymentCode)")
} catch {
    print("Error: \(error)")
}

3. Display Payment Page (SwiftUI)

import SwiftUI
import XcelPayGateSDK

struct CheckoutView: View {
    @State private var showPayment = false
    @State private var paymentUrl: URL?

    var body: some View {
        Button("Pay Now") {
            initiatePayment()
        }
        .sheet(isPresented: $showPayment) {
            if let url = paymentUrl {
                NavigationView {
                    PaymentView(
                        paymentUrl: url,
                        merchantName: "My Store",
                        onSuccess: { receipt in
                            print("Payment successful: \(receipt.transactionId)")
                            showPayment = false
                        },
                        onFailure: { error in
                            print("Payment failed: \(error)")
                            showPayment = false
                        },
                        onCancel: {
                            showPayment = false
                        }
                    )
                }
            }
        }
    }

    func initiatePayment() {
        Task {
            let request = GeneratePaymentLinkRequest(
                amount: "50.00",
                currency: "GHS",
                clientTransactionId: "order-\(Date().timeIntervalSince1970)",
                channel: .mobile,
                redirectUrl: "myapp://payment/success"
            )

            do {
                let response = try await sdk.checkout.initiateCheckout(request)
                paymentUrl = URL(string: response.data.paymentLink)
                showPayment = true
            } catch {
                print("Error: \(error)")
            }
        }
    }
}

4. Poll Payment Status

let result = try await sdk.checkout.pollTransactionStatus(
    paymentCode: "PAYMENT_CODE",
    maxAttempts: 24,
    intervalSeconds: 5.0,
    onStatusChange: { transaction in
        print("Status: \(transaction.status)")
    }
)

switch result.status {
case .success:
    print("Payment completed!")
case .failed:
    print("Payment failed")
case .pending:
    print("Still pending...")
default:
    break
}

Advanced Usage

XCEL Wallet Integration

For direct wallet-to-wallet payments:

// 1. Verify customer's XCEL account
let verification = try await sdk.wallet.verifyAccount(
    countryCode: "GH",
    phoneNumber: "233542023469"
)

if let account = verification.data.first {
    print("Account found: \(account.firstName) \(account.lastName)")
}

// 2. Create transaction
let products = [
    CreateXcelTransactionRequest.TransactionProduct(
        productId: "PROD_001",
        amount: "50.00",
        merchantFees: "1.50"
    )
]

let transaction = try await sdk.wallet.createTransaction(
    merchantId: config.merchantId,
    payerWalletNo: "PAYER_WALLET",
    posWalletNo: "POS_WALLET",
    posSchemeCode: "SCHEME_CODE",
    referenceId: "REF-\(UUID().uuidString)",
    amount: "50.00",
    fees: "1.50",
    products: products
)

// 3. Poll for payment completion
let result = try await sdk.wallet.pollWalletPayment(
    merchantId: config.merchantId,
    externalReference: "REF-123",
    intervals: [5, 15, 10],
    onStatusChange: { transaction in
        print("Paid: \(transaction.paid)")
    }
)

if result.data.paid {
    print("Payment completed!")
}

Custom Transaction Options

Customize the checkout experience:

let customTxn = CustomTransaction(
    editAmt: true,
    minAmt: 5.0,
    maxAmt: 1500.5,
    borderTheme: "#9c27b0",
    receiptSxMsg: "Thank you for your payment!",
    receiptFeedbackPhone: "233241234567",
    receiptFeedbackEmail: "support@example.com",
    payLinkExpiryInDays: 15,
    payLinkCanPayMultipleTimes: false,
    displayPicture: "https://example.com/logo.png",
    xtraCustomerInput: [
        CustomTransactionInput(
            label: "Order Number",
            placeHolder: "Enter order number",
            type: .input,
            required: true
        )
    ]
)

let request = GeneratePaymentLinkRequest(
    amount: "50.00",
    currency: "GHS",
    clientTransactionId: "txn-123",
    channel: .web,
    redirectUrl: "myapp://success",
    customTxn: customTxn
)

Fetch Merchant Details and Products

// Get merchant details
let details = try await sdk.getMerchantDetails()
print("Merchant: \(details.data.data.regName)")
print("Currency: \(details.data.data.currency.currencyCode)")

// Get merchant products
let products = try await sdk.getMerchantProducts()
for product in products.data.data {
    print("Product: \(product.name) - \(product.productId)")
}

// Get merchant fees
let fees = try await sdk.getMerchantFees()
print("Total Amount: \(fees.data.totalAmount)")

Multiple Products in Payment

let request = GeneratePaymentLinkRequest(
    amount: "100.00",
    currency: "XAF",
    clientTransactionId: "txn-123",
    channel: .web,
    redirectUrl: "myapp://success",
    products: [
        PaymentProduct(productId: "PROD_001", amount: "50"),
        PaymentProduct(productId: "PROD_002", amount: "50")
    ]
)

URL Parsing Utilities

Parse payment completion URLs manually:

// Parse URL parameters
if let data = PaymentURLParser.parsePaymentCompletionUrl(urlString) {
    print("Payment Code: \(data.paymentCode ?? "")")
    print("Amount: \(data.amount ?? "")")
    print("Status: \(data.status ?? "")")
}

// Check URL status
if PaymentURLParser.isPaymentSuccessUrl(urlString) {
    print("Payment succeeded!")
}

if PaymentURLParser.isPaymentFailureUrl(urlString) {
    print("Payment failed!")
}

// Extract payment code
if let code = PaymentURLParser.extractPaymentCode(urlString) {
    print("Code: \(code)")
}

Receipt Generation

Format transaction data as receipt:

let receipt = PaymentReceiptFormatter.formatTransactionReceipt(
    transaction: transactionData,
    merchantName: "My Store"
)

// Generate formatted text
let receiptText = PaymentReceiptFormatter.formatReceiptText(receipt)
print(receiptText)

// Display in SwiftUI
NavigationLink("View Receipt") {
    ReceiptView(receipt: receipt)
}

SwiftUI Components

PaymentWebView

Basic web view for payment pages:

PaymentWebView(
    url: paymentUrl,
    onNavigationChange: { url in
        print("Navigated to: \(url)")
    },
    onComplete: { result in
        switch result {
        case .success(let data):
            print("Payment succeeded: \(data.paymentCode ?? "")")
        case .failure(let error):
            print("Payment failed: \(error)")
        }
    }
)

PaymentView

Complete payment flow with states:

PaymentView(
    paymentUrl: url,
    merchantName: "ACME Corp",
    onSuccess: { receipt in
        // Navigate to receipt screen
        showReceipt = true
    },
    onFailure: { error in
        // Show error alert
        alertMessage = error.localizedDescription
    },
    onCancel: {
        // Dismiss payment
        dismiss()
    }
)

ReceiptView

Display payment receipt:

ReceiptView(receipt: receipt)

Payment Flow

Checkout URL Flow

  1. Generate Payment Link - Call initiateCheckout() with payment details
  2. Display Payment Page - Use PaymentWebView or open in Safari
  3. Customer Pays - Customer completes payment on XCEL's hosted page
  4. Monitor Status - Use pollTransactionStatus() to track payment
  5. Handle Result - Process success/failure callbacks

XCEL Wallet Flow

  1. Verify Account - Verify customer's XCEL wallet account
  2. Generate Dynamic Link - Create OTP for transaction
  3. Create Transaction - Initialize the payment transaction
  4. Poll Status - Check transaction status at intervals
  5. Confirm Payment - Verify paid: true in transaction response

Error Handling

All async methods throw typed errors:

do {
    let response = try await sdk.checkout.initiateCheckout(request)
    // Success
} catch XcelPayGateError.invalidResponse {
    print("Invalid server response")
} catch XcelPayGateError.httpError(let code) {
    print("HTTP error: \(code)")
} catch XcelPayGateError.apiError(let message) {
    print("API error: \(message)")
} catch {
    print("Unknown error: \(error)")
}

Testing

Test the integration using the provided test credentials:

  • Test Merchant ID: 0pf9ztq7q
  • Test Paygate URL: https://paygate.xcelapp.com/pay/0pf9ztq7q

Best Practices

  1. Use Async/Await - All network calls are async
  2. Handle Errors - Always catch and handle errors appropriately
  3. Validate Payment Codes - Check payment codes in callbacks
  4. Store Transaction IDs - Keep records for reconciliation
  5. Secure Credentials - Never hardcode API keys in production
  6. Handle Expiry - Payment links expire after 24 hours by default
  7. Monitor Webhooks - Implement webhook handlers for reliable status updates
  8. Use HTTPS - Ensure redirect and webhook URLs use HTTPS

Thread Safety

All API clients use Swift actors for thread safety:

// Safe to call from any thread/task
Task {
    let response = try await sdk.checkout.initiateCheckout(request)
}

Task.detached {
    let status = try await sdk.checkout.checkTransactionStatus("CODE")
}

Example App

Check out the example app in the Examples directory for a complete integration demonstration.

Support

License

MIT License - see LICENSE file for details.

Contributing

Contributions are welcome! Please feel free to submit a Pull Request.

Package Metadata

Repository: etranzact-global-reloaded/xcel-paygate-sdk-swift

Default branch: main

README: README.md