Integration Guide

Integrate CoinmeBridge in Your iOS App

Add Coinme's cryptocurrency purchase flow to your native iOS app in under 10 minutes.

TL;DR

import CoinmeBridge

var config = CoinmeConfig(rampId: "your-ramp-id", partnerKey: "your-partner-key")
config.destinationCurrency = "BTC"

let vc = CoinmeViewController(
    orchestratorURL: URL(string: "https://widget.coinme.com")!,
    config: config
)
vc.delegate = self   // adopt CoinmeDelegate
present(vc, animated: true)

Read on for the full step-by-step walkthrough.


Prerequisites

  • Xcode 15.0+ and Swift 5.7+
  • iOS 14.0+ deployment target
  • Your rampId and partnerKey (provided by Coinme)
  • Your Orchestrator URL (e.g. https://widget.coinme.com)

Step 1: Add the Package

Configure the Cloudsmith Swift registry (one-time setup — your Coinme integration specialist will provide the entitlement token):

swift package-registry set https://swift.cloudsmith.io/coinme/coinme-sdk-frontend/ --global
⚠️

Current Project Scope:

Omit --global to configure for the current project only.

swift package-registry login https://swift.cloudsmith.io/coinme/coinme-sdk-frontend/ --username token --password <ENTITLEMENT_TOKEN>
ℹ️

Note:

The login command must be a single line. Replace <ENTITLEMENT_TOKEN> with the token from your Coinme integration specialist.

In Xcode, go to File > Add Package Dependencies. In the search field, enter the package identifier:

coinme.coinme-native-bridge-ios

Xcode resolves it from the configured registry. Set the dependency rule to your desired version range (e.g. Up to Next Major), then add the CoinmeBridge library to your app target.

Or add it directly to your Package.swift:

dependencies: [
    .package(id: "coinme.coinme-native-bridge-ios", from: "1.0.0")
]

And add "CoinmeBridge" to your target's dependencies.

Step 2: Import the Module

import CoinmeBridge

Step 3: Create a Configuration

Build a CoinmeConfig with your credentials:

var config = CoinmeConfig(
    rampId: "your-ramp-id",
    partnerKey: "your-partner-key",
    environment: "prod"               // "dev", "stage", or "prod"
)

// Optional: pre-select a cryptocurrency
config.destinationCurrency = "BTC"

Step 4: Implement the Delegate

Adopt CoinmeDelegate to receive events and errors:

extension YourViewController: CoinmeDelegate {

    func coinme(_ vc: CoinmeViewController, didReceiveEvent event: CoinmeEvent) {
        switch event {
        case .transactionComplete:
            print("Transaction succeeded")
        case .transactionFailed:
            print("Transaction failed")
        case .userCancelled:
            vc.dismiss(animated: true)
        case .kycRequired:
            print("KYC verification required")
        case .sheetOpened:
            break // Orchestrator opened an info sheet
        case .sheetClosed:
            break // Orchestrator closed its info sheet
        case .unknown(let type, _):
            print("Unhandled event: \(type)")
        }
    }

    func coinme(_ vc: CoinmeViewController, didFailWithError error: CoinmeError) {
        print("Bridge error: \(error.localizedDescription)")
        vc.dismiss(animated: true)
    }
}

Step 5: Present the View Controller

let coinmeVC = CoinmeViewController(
    orchestratorURL: URL(string: "https://widget.coinme.com")!,
    config: config
)
coinmeVC.delegate = self
present(coinmeVC, animated: true)

That's it. CoinmeViewController handles the WebView, bridge handshake, and session lifecycle automatically. It presents full-screen by default.


Minimal Complete Example

import UIKit
import CoinmeBridge

class BuyViewController: UIViewController, CoinmeDelegate {

    func showCoinme() {
        var config = CoinmeConfig(
            rampId: "your-ramp-id",
            partnerKey: "your-partner-key"
        )
        config.destinationCurrency = "BTC"

        let vc = CoinmeViewController(
            orchestratorURL: URL(string: "https://widget.coinme.com")!,
            config: config
        )
        vc.delegate = self
        present(vc, animated: true)
    }

    // MARK: - CoinmeDelegate

    func coinme(_ vc: CoinmeViewController, didReceiveEvent event: CoinmeEvent) {
        if case .transactionComplete = event {
            vc.dismiss(animated: true)
        }
        if case .userCancelled = event {
            vc.dismiss(animated: true)
        }
    }

    func coinme(_ vc: CoinmeViewController, didFailWithError error: CoinmeError) {
        print("Error: \(error.localizedDescription)")
        vc.dismiss(animated: true)
    }
}

Customization

Permission Flags

Camera and location default to true to provide the full Coinme experience. Screen capture protection is on by default (allowsCapture = false).

FlagDefaultWhat it controlsInfo.plist keys required
allowsCameratrueCamera, microphone, and photo library access for KYC document/selfie capture and uploadNSCameraUsageDescription, NSMicrophoneUsageDescription, NSPhotoLibraryUsageDescription
allowsLocationtrueBrowser geolocation API (e.g. store finders)NSLocationWhenInUseUsageDescription
allowsCapturefalseWhen false, screen recording and screenshots of the WebView are blockedNone

Since allowsCamera and allowsLocation are true by default, you must add the corresponding Info.plist keys listed above. To opt out, set the flags to false:

config.allowsCamera = false   // Disables camera + microphone
config.allowsLocation = false // Disables navigator.geolocation in the WebView
config.allowsCapture = true   // Disables screen capture protection
ℹ️

Trade-offs of disabling permissions:

  • allowsCamera = false: The allowsCamera flag and its corresponding Info.plist permissions (NSCameraUsageDescription, NSMicrophoneUsageDescription, NSPhotoLibraryUsageDescription) are all required for identity-document workflows. Without them, the integration cannot fall back to documentary KYC and is unavailable in jurisdictions that require it.
  • allowsLocation = false: Users will not see nearby retail locations automatically and will have to specify their location manually.
⚠️

Important:

You need to communicate your decision about camera access and geolocation to your Coinme business development team so that your ramp can be properly configured. Not doing so may result in poor UX for a subset of your customers.

Theme and Language

config.theme = .dark       // .light (default) or .dark
config.language = "es"     // BCP 47 tag; defaults to "en"

Transaction Pre-fill

Lock fields to prevent user changes:

config.walletAddress = "bc1q..."
config.walletLock = true

config.sourceAmount = 100.0
config.sourceAmountLock = true

config.destinationCurrency = "BTC"
config.destinationCurrencyLock = true

SSO / Authentication Passthrough

Pass credentials at any time — they're queued until the session is ready:

coinmeVC.setAuth(token: "jwt-token", userId: "user-123", userEmail: "[email protected]")

Metadata and Tracking

var meta = CoinmeMetadata()
meta.source = "ios-app"
meta.campaign = "summer-promo"
config.metadata = meta

config.externalSessionId = "session-abc"
config.externalTransactionId = "tx-456"

Optional Delegate Methods

These are optional — default implementations are no-ops:

func coinmeSessionReady(_ vc: CoinmeViewController) {
    // Bridge handshake complete, session is active
}

func coinmeSessionEnded(_ vc: CoinmeViewController) {
    // Orchestrator signaled session end
    vc.dismiss(animated: true)
}

Troubleshooting

ProblemCauseFix
Bridge never connectsWrong Orchestrator URL or environmentVerify the URL matches your environment (dev, stage, prod)
Events not receivedDelegate not set, or set after presentationSet delegate before calling present(_:animated:)
Camera not workingMissing Info.plist keysAdd NSCameraUsageDescription and NSMicrophoneUsageDescription
Photo upload not workingMissing Info.plist keyAdd NSPhotoLibraryUsageDescription for photo library access
Blank white screenNetwork or URL errorCheck didFailWithError for .webViewLoadFailed and inspect the underlying error
Screen capture blocked unexpectedlyallowsCapture defaults to falseSet config.allowsCapture = true if capture is needed

Debugging with Safari Web Inspector

In DEBUG builds the SDK enables WebView inspection automatically (iOS 16.4+). To inspect:

  1. Open Safari on your Mac
  2. Go to Develop > [Your Device/Simulator] > [Your App]
  3. Use the console to see [CoinmeBridge] log messages and Orchestrator JavaScript output

Bridge Protocol Reference

The SDK handles all protocol details internally. This section is for reference only.

Message Flow

┌─────────────┐                           ┌──────────────┐
│   Native    │                           │ Orchestrator │
│     App     │                           │   (WebView)  │
└──────┬──────┘                           └──────┬───────┘
       │                                         │
       │  1. Load Orchestrator URL               │
       │────────────────────────────────────────>│
       │                                         │
       │  2. Handshake                           │
       │<────────────────────────────────────────│
       │     { type: "handshake" }               │
       │                                         │
       │  3. HandshakeAck                        │
       │────────────────────────────────────────>│
       │     { type: "handshakeAck" }            │
       │                                         │
       │  4. bridge-session-ready                │
       │<────────────────────────────────────────│
       │     { type: "event" }                   │
       │                                         │
       │  5. updateConfig                        │
       │────────────────────────────────────────>│
       │                                         │
       │  6. mount                               │
       │────────────────────────────────────────>│
       │                                         │

Key Differences from Web Integration

  • Message transport: Native uses window.postMessage() with targeted origins; web uses the post-robot library
  • Message handler: Native registers WKScriptMessageHandler on the "coinmeBridge" channel
  • Embedding: Native loads the Orchestrator directly in a WKWebView, not in an iframe