Troubleshooting

Troubleshooting

Common issues and how to fix them.


Enabling debug logs

The SDK provides two mechanisms for debugging.

On-screen log overlay (SwiftUI)

Add the debug config viewer to any SwiftUI view:

import SwiftUI
import Payrails

struct DebugView: View {
    var body: some View {
        Payrails.Debug.configViewer()
    }
}

This renders the parsed SDK configuration and recent log entries on screen. Requires an active session.

Console logging

The SDK writes to LogStore.shared and also calls Swift.print. To see logs in the Xcode console, ensure the scheme is not suppressing standard output.


Installation issues

Package resolution fails

  • Confirm the package URL is exactly https://github.com/payrails/ios-sdk.git.
  • Click File → Packages → Reset Package Caches in Xcode, then resolve again.

"the checksum of the downloaded artifact does not match"

  • Swift Package Manager verifies the downloaded Payrails.xcframework.zip against the checksum in the package manifest, and the two disagree.
  • Almost always a stale cache. Click File → Packages → Reset Package Caches, clear the Swift Package Manager cache at ~/Library/Caches/org.swift.swiftpm, then resolve again.
  • Payrails never re-uploads a published zip, so a mismatch that survives a cleared cache is worth a support ticket with the SDK version.

Package.resolved still pins a source revision after upgrading to 3.0.0

  • 3.0.0 replaces the source target with a binary target, and Package.resolved pins the revision that was resolved before the upgrade.
  • Click File → Packages → Reset Package Caches, then File → Packages → Update to Latest Package Versions. Resolving again rewrites the pin.

Linker error: PayrailsCSE not found

  • PayrailsCSE is linked statically into the framework binary, so this is not a missing-code problem. The generated .swiftinterface carries import PayrailsCSE, so the Swift compiler needs that module present to build against the binary. That holds even though no public Payrails API exposes a CSE type.
  • Confirm the ios-cse package appears in the project's resolved packages. If it is missing, resolve the Payrails package again. Its manifest declares both ios-cse and paypalcheckout-ios.
  • The CSE code is therefore present twice: once absorbed into Payrails.framework, once as the separately resolved ios-cse package. Inside the framework that is 176 exported PayrailsCSE symbols, plus 626 from JOSESwift, which arrives transitively through ios-cse. That matters when you measure binary size or integrate PayrailsCSE directly. PayPalCheckout is the opposite case: it ships as a prebuilt binary XCFramework, so it stays a genuine external dynamic dependency.

Compiler cannot load the framework's module interface

  • Each release is built with Xcode 16.4. Swift's .swiftinterface format is not backward compatible across toolchains, so an older Xcode cannot read the module. Xcode 16.4 is the minimum, not a recommendation.
  • Upgrade Xcode, or open a support ticket with the Xcode version.

SDK initialization issues

"Provided configuration data is invalid and cannot be parsed"

  • The init payload data field is not valid base64-encoded JSON
  • Ensure your backend passes the exact string returned by the Payrails POST /checkout/initialize endpoint without modification

"SDK has not been properly initialized"

  • Payrails.createCardForm() or another factory was called before Payrails.createSession() completed
  • These calls trigger a precondition failure if called without an active session — always await session creation first

Session init works in debug but fails in release

  • Check that your backend call succeeds in the production environment
  • Verify that the env option matches your backend environment (.production vs .test)

Card form issues

Card form appears but cannot submit — payment button stays unresponsive

  • Ensure Payrails.createCardForm() is called before Payrails.createCardPaymentButton()
  • The button holds a strong reference to the form at creation time; order matters

Validation errors not shown

  • Check that the card form has enough vertical space — error labels require height to render
  • Use CardFormConfig(showRequiredAsterisk: true) to make required fields explicit

Card icon not appearing

  • Pass showCardIcon: true in CardFormConfig.
  • Icons load from the asset catalog inside the framework bundle, which the package resolves automatically. If icons are missing after a clean resolve, open a support ticket with the SDK version.

Layout falls back to default unexpectedly (console logs "falling back to default layout")

  • Your custom CardLayoutConfig is missing required fields (.CARD_NUMBER, .CVV, and at least one expiry field)
  • All three are required for a valid submission

Apple Pay issues

Apple Pay button is not visible

  • session.isApplePayAvailable is a device-capability check only (returns false when PKPaymentAuthorizationController.canMakePayments() is false). It does not inspect the merchant config.
  • For a combined "configured and device capable" check, compose:
    let canShow = session.isApplePayAvailable
        && !session.getPaymentMethodConfig(.specific("apple_pay")).isEmpty
  • The PKPaymentButton hides itself when PKPaymentAuthorizationViewController.canMakePayments() returns false.

Apple Pay sheet dismisses immediately

  • The merchant identifier in your app's entitlements does not match the one in the Payrails merchant configuration
  • Verify the capability is enabled under Signing & Capabilities in Xcode

"incorrectPaymentSetup" error for Apple Pay

  • The init payload does not include an Apple Pay payment option configuration
  • Contact your Payrails integration engineer to enable Apple Pay in your merchant account

PayPal issues

PayPal button tap does nothing

  • Ensure the PayPalCheckout SDK is properly linked (check the build phases)
  • The PayPal SDK requires a client ID in the init payload config; verify the payload includes it

PayPal checkout WebView dismissed with no result

  • The user cancelled — onAuthorizeFailed(_:failure:) fires with failure.code == .userCancelled. In parallel, the SDK invokes the onSessionExpired closure supplied at createSession time to refresh the underlying execution; the merchant's cached Session reference keeps working.

Payment issues

Payment failed — how do I know why?

  • onAuthorizeFailed(_ button:, failure:) carries an AuthorizationFailure struct. Switch on failure.code to give each case the right UX, and read failure.message / failure.rawError for detail:
    • .userCancelled — user dismissed the 3DS sheet. Show neutral copy, no error banner.
    • .authorizationError — authorization rejected (issuer decline, 3DS rejected, fraud blocked, etc.). failure.message is the backend's errors[0].reason.result, falling back to "Authorization failed" when no detail is provided.
    • .authenticationError — session token rejected (HTTP 401 / 403). The SDK also fires its onSessionExpired refresh in the background.
    • .unknownError — network, SDK, or other unexpected failure. Inspect failure.rawError for the underlying error.

My retry on the same card doesn't work after a failure

  • Payrails executions are single-use. Once a payment terminates (success or failure), the same execution cannot be retried. Supply the onSessionExpired closure to Payrails.createSession(with:onSessionExpired:) so the SDK can mint a fresh execution in-place when needed; the merchant's Session reference and cached buttons keep working unchanged.

3DS challenge never appears / presentPayment(_:) not called

  • Confirm that payButton.presenter = self is set on the CardPaymentButton
  • Confirm your view controller conforms to both PaymentPresenter and PayrailsCardPaymentFormDelegate if needed

Long-polling timeout errors

  • PayrailsError.finalStatusNotFoundAfterLongPoll — the payment status was not confirmed within the polling window
  • This is typically a transient network or backend issue; prompt the user to check their payment status through your order history

Pre-authorization gate issues

Payments are blocked and no request reaches the backend

The onRequestStart handler supplied at createSession answered false, or did not answer at all.
Check failure.code == .validationFailed in onAuthorizeFailed(_:failure:) to confirm — a block is
reported with that code and never as .authorizationError.

The most common cause is a handler that only answers on the branch it cares about. The gate fires
for every payment method on the session, so any branch that does not call completion(.proceed)
blocks that method:

onRequestStart: { context, completion in
    guard context.paymentMethodCode == "payPal" else {
        completion(.proceed)   // ← omitting this blocks card, wallets, everything else
        return
    }
    myBackend.validate { completion($0) }
}

A payment is blocked roughly ten seconds after tapping

The handler never called its completion. The SDK stops the attempt rather than leaving the element
spinning, and logs:

⚠️ onRequestStart did not answer within 10s for <method>; the payment was blocked.

Enable debug logs (see above) to see it. Check every path through the handler — including error and
early-return branches — calls completion exactly once.

Only PayPal or Apple Pay report a block without a reason

Those two delegates still expose a deprecated onAuthorizeFailed(_ button:) with no payload, and a
default implementation forwards to it for backwards compatibility. Implement
onAuthorizeFailed(_ button:, failure:) instead — it is the only variant carrying the code that
distinguishes a gate block from an issuer decline.


Stored instruments issues

Payrails.getStoredInstruments() returns an empty array

  • The init payload does not include any stored instruments for this holder reference
  • Instruments with status other than "enabled" or "created" are filtered out

StoredInstruments view renders nothing

  • Same as above — the list silently renders nothing when there are no eligible instruments
  • Optionally check the count before adding the view: Payrails.getStoredInstruments().isEmpty

Crash report issues

Payrails frames arrive as raw addresses

  • The SDK ships as a binary, so a crash reporter needs the SDK's debug symbols to name those frames.
  • Payrails builds Payrails.dSYMs.zip for every release. Request the file for the version you shipped through a support ticket, then upload it to your crash reporter alongside your app's own dSYMs.
  • Symbols only resolve for the SDK version in the build. dSYMs from another version do not match.

Getting help

  1. Enable debug logs and capture the output
  2. Reproduce the issue with env: .test to rule out production-only configuration issues
  3. Check the PayrailsError.errorDescription for the specific failure reason
  4. Open a support ticket with your executionId (from Payrails.query(.executionId)) and the full error description

Did this page help you?