HomeGuidesReference
Log In

iOS Quickstart

Use the Forage iOS SDK to accept EBT SNAP payments in your iOS app.

In this guide, you’ll learn how to:

  1. Set up a Forage iOS app
  2. Add and style a Forage Element
  3. Perform payment operations
    1. Tokenize and store a payment method
    2. Check the balance of a payment method
    3. Capture a payment

Prerequisites

  • Sign up for a Forage account (contact us if you don’t yet have one)
  • Install:
    • XCode v14.1 or higher
    • iOS v13 or higher
    • Swift v5.5 or higher
    • The following third-party libraries:

Resources

Step 1: Set up a Forage iOS app

1. Add an endpoint to your server that creates a session token.

POST to the Forage /session_token/ endpoint creates a session token.

🚧

The following code snippets assume that environment variables are stored in a .env

Edit the relevant lines of code to match your token management preferences or dev environment. Just be careful to never expose sensitive data like authentication tokens in client-side requests.

// app.js 

const axios = require('axios')
const express = require('express')
const bodyParser = require('body-parser')
require('dotenv').config();

const app = express()
const port = 5000

// Middleware to parse the request body
app.use(bodyParser.urlencoded({ extended: true }))

app.post('/api/session_token', async (req, res) => {
  try {
    const apiUrl = 'https://api.sandbox.joinforage.app/api/session_token/'
    // Replace the below with your authentication token
    const authenticationToken = process.env.AUTHENTICATION_TOKEN
    // Replace the below with your Forage merchant ID
    const merchantId = process.env.MERCHANT_ID

    const headers = {
      Accept: 'application/json',
      Authorization: `Bearer ${authenticationToken}`,
      'Merchant-Account': merchantId,
      'Content-Type': 'application/x-www-form-urlencoded'
    }

    const response = await axios.post(apiUrl, null, { headers })

    res.json(response.data)
  } catch (error) {
    res
      .status(error.response ? error.response.status : 500)
      .json({ error: 'Something went wrong' })
  }
})

app.listen(port, () => {
  console.log(`Server is running on http://localhost:${port}`)
})
# app.rb

require 'sinatra'
require 'net/http'
require 'json'
require 'dotenv/load'

class ApiController < Sinatra::Base
  post '/session_token' do
    begin
      api_url = 'https://api.sandbox.joinforage.app/api/session_token/'
      # Replace the below with your authentication token
      authentication_token = ENV['AUTHENTICATION_TOKEN']
      # Replace the below with your Forage merchant ID
      merchant_id = ENV['MERCHANT_ID']

      uri = URI(api_url)
      http = Net::HTTP.new(uri.host, uri.port)
      http.use_ssl = true

      headers = {
        'Accept' => 'application/json',
        'Authorization' => "Bearer #{authentication_token}",
        'Merchant-Account' => merchant_id,
        'Content-Type' => 'application/x-www-form-urlencoded',
      }

      response = http.post(uri.path, nil, headers)

      if response.is_a?(Net::HTTPSuccess)
        json_response = JSON.parse(response.body)
        content_type :json
        json_response.to_json
      else
        status response.code
        { error: 'Something went wrong' }.to_json
      end
    rescue StandardError => e
      status e.respond_to?(:status) ? e.status : 500
      { error: 'Something went wrong' }.to_json
    end
  end
end

2. Install the iOS SDK.

You can either use CocoaPods or XCode’s built-in Swift Package Manager (SPM) to install forage-ios-sdk.

CocoaPods

If you haven’t already, install the latest version of CocoaPods. Run the following command if you don’t have an existing Podfile:

pod init

Add the below line to your Podfile:

pod 'ForageSDK', '~> 4.4.4'

Run the installation command:

pod install

To update to the latest version of the SDK, run:

pod repo update
pod update ForageSDK

Xcode

Navigate to Add Packages…

add_packages.png

Add the https://github.com/teamforage/forage-ios-sdk dependency. Set the Dependency Rule to Branch and type main for the branch name. Finally, click Add Package.

dependency_rule.png

On the next screen, select the package ForageSDK, and once again click Add Package.

add_package_again.png

For more detailed instructions, consult the official Apple SPM guide.

3. Import the SDK.

Add the following import to the top of every file that uses the library.

import ForageSDK

// your code here

4. Initialize the SDK.

Set up a request from your front-end to your backend that asks the server to generate a session token.

In your app’s primary ViewController.swift, call ForageSDK.setup to initialize the SDK. Pass a ForageSDK.Config object that specifies the merchantID and the server-generated sessionToken.

// ViewController.swift

ForageSDK.setup(
  ForageSDK.Config(
    merchantID: "mid/d5685d56ed",
    sessionToken: "sandbox_eyJ0eXAiOiJKV1Qi..."
  )
)

Step 2: Add and style a Forage Element

A Forage Element is a secure, client-side entity that accepts and submits customer input for a transaction.

The iOS SDK includes an Element for collecting a card number, ForagePANTextField, and an Element to collect a card PIN, ForagePINTextField, both pictured below.

ForagePANTextField

ForagePANTextField.png

The DerivedCardInfoProtocol defines inferred information about the ForagePANTextField.

public protocol DerivedCardInfoProtocol {

    /// The US state that issued the EBT card, derived from the Issuer Identification Number (IIN),
    /// also known as BIN (Bank Identification Number).
    /// The IIN is the first 6 digits of the PAN.
    var usState: USState? { get }
}

This field can be accessed through the derivedCardInfo property.

foragePanTextField.derivedCardInfo.usState


ForagePINTextField

ForagePINTextField.png

1. Initialize the Element

To add an Element, call the method to initialize the Element in a View.swift for a specific payment operation. You can set styling properties after calling the method.

Example: ForagePANTextField

The below example initializes a ForagePANTextField Element via a call to ForagePANTextField() in a CardNumberView.swift and specifies some styling properties:

// CardNumberView.swift

private let foragePanTextField: ForagePANTextField = {
  let tf = ForagePANTextField()

  tf.placeholder = "Card Number"
  tf.borderColor = .black
  tf.borderWidth = 3
  tf.clearButtonMode = .whileEditing
  tf.cornerRadius = 4
  tf.font = .systemFont(ofSize: 22)

  return tf
}()

Example: ForagePINTextField

The below example initializes a ForagePINTextField Element via a call to ForagePANTextField() in a CheckBalanceView.swift and specifies some styling properties:

// CheckBalanceView.swift

private let foragePinTextField: ForagePINTextField = {
  let tf = ForagePINTextField()
  tf.borderRadius = 10
  tf.backgroundColor = .systemGray6
  tf.textAlignment = .center
  return tf
}()

📘

Related guide

2. Set up text input validation

Forage validates an Element’s input as a customer types. To notify customers of input validation errors, you'll need to conform to the ForageElementDelegate protocol, and listen for the Element's ObservableState.

The focusDidChange method is triggered when an Element gains or loses focus.

foragePanTextField.delegate = self
// signature
public protocol ForageElementDelegate: AnyObject {
    func focusDidChange(_ state: ObservableState)
    func textFieldDidChange(_ state: ObservableState)
}

// usage
extension ForagePanView: ForageElementDelegate {
    func focusDidChange(_ state: ObservableState) {
        if state.isFirstResponder { ... } // element gained focus
        else { ... } // element lost focus (blurred)
    }

    func textFieldDidChange(_ state: ObservableState) {
        // show an error message on blur if the input is invalid
        if !state.isValid && !state.isFirstResponder { ... }
    }
}

The ObservableState protocol defines properties reflecting the state of an Element.

public protocol ObservableState {
  /// Indicates whether the input is focused.
  var isFirstResponder: Bool { get }

  /// Indicates whether the input is empty.
  var isEmpty: Bool { get }

  /// Indicates whether all validation checks pass, excluding the the minimum length validator.
  var isValid: Bool { get }

  /// Indicates whether all validation checks pass and the ForageElement is ready for submission.
  var isComplete: Bool { get }
}

In the View.swift for the payment operation, indicate the delegate in the primary render() , as in the following example for the ForagePANTextField in the CardNumberView.swift :

// CardNumberView.swift

public func render() {
  foragePanTextField.delegate = self
  setupView()
  setupConstraints()
}

Then, specify the ForageElementDelegate as an extension in the file. Set the state to ObservableState. Define functions to handle state changes in the extension as well.

The below example sets up an extension for the ForagePANTextField in the CardNumberView.swift :

// CardNumberView.swift

extension CardNumberView: ForageElementDelegate {
  func focusDidChange(_ state: ObservableState) {
    updateState(state: state)
  }

  func textFieldDidChange(_ state: ObservableState) {
    updateState(state: state)
  }
}

3. Establish the ForageElement as the First Responder

Finally, in the Controller.swift for the payment operation, call becomeFirstResponder() to set focus on the Forage Element.

The following example calls the method as part of the lifecycle methods in a CardNumberViewController.swift :

class CardNumberViewController: UIViewController {
  override func viewDidAppear(_ animated: Bool) {
    super.viewDidAppear(animated)
    customView.foragePanTextField.becomeFirstResponder()
  }
}

Step 3: Perform payment operations

Tokenize and store a payment method

To tokenize an EBT Card number, use the ForagePANTextField Element.

Building on the example CardNumberView.swift, call the ForageSDK.shared.tokenizeEBTCard method.

// CardNumberView.swift

ForageSDK.shared.tokenizeEBTCard(
  foragePanTextField: foragePanTextField,
  // NOTE: The following line is for testing purposes only and should not be used in production.
  // Please replace this line with a hashed customer ID value.
  customerID: UUID.init().uuidString,
  reusable: true
) { result in
  // Handle result and error here
  switch result {
  case .success(let paymentMethod):
    // Handle the PaymentMethod here (balance, card, reusable, etc.)
  case .failure(let error):
    if let forageError = error as? ForageError {
      // handle forageError.code, forageError.httpStatusCode and forageError.message
    }
  }
}

Check the balance of a payment method

To check the balance of a payment method, use the ForagePINTextField Element.

🚧

You must call clearText() after collecting a cardholder’s PIN.

FNS requires clearing the input text field upon submission.

🚧

FNS requirements for balance inquiries

FNS prohibits balance inquiries on sites and apps that offer guest checkout. Skip this transaction if your customers can opt for guest checkout.

If guest checkout is not an option, then it's up to you whether or not to add a balance inquiry feature. No FNS regulations apply.

Repeat Step 2 for a new ForagePINTextField Element, in summary:

  • Initialize the Element in a View.swift for the payment operation, in this case something like CheckBalanceView.swift
  • Set up text input validation for the ForagePINTextField
  • Establish the Element as the FirstResponder in a Controller.swift for the payment operation, in this case something like CheckBalanceViewController.swift

Then, from the CheckBalanceView.swift, call the checkBalance method. Passing in references to the ForagePINTextField and a previously created Forage PaymentMethod , as in the below:

// CheckBalanceView.swift

private func checkBalance(
  pinTextField: ForagePINTextField, paymentMethodReference: String, completion: @escaping () -> Void
) {
  DispatchQueue.global(qos: .userInitiated).async {
    ForageSDK.shared.checkBalance(
      foragePinTextField: pinTextField,
      paymentMethodReference: paymentMethodReference
    ) { [self] result in
      foragePinTextField.clearText()
      switch result {
      case .success(let balance):
        // Handle the Balance here
      case .failure(let error):
        if let forageError = error as? ForageError {
          // Handle forageError.code, forageError.httpStatusCode and forageError.message
        }
      }
    }
  }
}

Capture a payment

To capture a payment, use the ForagePINTextField Element.

🚧

You must call clearText() after collecting a cardholder’s PIN.

FNS requires clearing the input text field upon submission.

Repeat Step 2 for a new ForagePINTextField Element, in summary:

  • Initialize the Element in a View.swift for the payment operation, in this case something like CapturePaymentView.swift
  • Set up text input validation for the ForagePINTextField
  • Establish the Element as the FirstResponder in a Controller.swift for the payment operation, in this case something like CaptureViewController.swift

Then, you have two options for capturing the payment. You can either defer payment capture to the server or capture the payment immediately.

Option 1: Defer payment capture to the server

From the CapturePaymentView.swift, call the deferPaymentCapture method. Passing in references to the ForagePINTextField and a previously created Forage Payment and then calling clearText() on the Element, as in the following example:

ForageSDK.shared.deferPaymentCapture(
  foragePinTextField: foragePinTextField,
  paymentReference: paymentReference
) { result in
  pinTextField.clearText()
  switch result {
  case .success(let payment):
    // Handle the Payment here
  case .failure(let error):
    if let forageError = error as? ForageError {
      // Handle forageError.code, forageError.httpStatusCode and forageError.message
    }
  }
}

When you’re ready to capture the payment, from your server call the Forage Payments API endpoint to capture a payment.

📘

Option 2: Capture a payment immediately client-side

From the CapturePaymentView.swift, call the capturePayment method. Passing in references to the ForagePINTextField and a previously created Forage Payment and then calling clearText() on the Element, as in the following example:

// CapturePaymentView.swift

private func capturePayment(
  pinTextField: ForagePINTextField, paymentReference: String, completion: @escaping () -> Void
) {
  DispatchQueue.global(qos: .userInitiated).async {
    ForageSDK.shared.capturePayment(
      foragePinTextField: pinTextField,
      paymentReference: paymentReference
    ) { result in
      pinTextField.clearText()
      switch result {
      case .success(let payment):
        // Handle the Payment here
      case .failure(let error):
        if let forageError = error as? ForageError {
          // Handle forageError.code, forageError.httpStatusCode and forageError.message
        }
      }
    }
  }
}