Skip to main content

iOS Activation Integration

This framework extends the BlinkReceipt SDK to enable rewards and monetization functionality. You must first install the BlinkReceipt framework before proceeding.

Requirements

  • iOS: 15.0+
  • Swift: 5.9+
  • Xcode: 16.4+
  • Dependencies: Google-Mobile-Ads-SDK, BlinkReceipt SDK
  • Complete the BlinkReceipt integration for receipt scanning: BlinkReceipt Integration

Installation

  1. In Xcode, go to File > Add Package Dependencies
  2. Enter: https://github.com/BlinkReceipt/blinkengage-ios
  3. Select latest version and add BlinkEngage product

CocoaPods

pod 'BlinkEngage', '~> 1.7.0'

Then run: pod install

App Delegate Setup

Configure BlinkEngage in application(_:didFinishLaunchingWithOptions:). Keep initialization clean by separating scan manager and SDK configuration into dedicated methods.

import BlinkEngage
import BlinkReceipt
import GoogleMobileAds

class AppDelegate: UIResponder, UIApplicationDelegate {
func application(_ application: UIApplication,
didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
// Call before any BRScanManager.shared() calls
#if DEBUG
BlinkEngageSDK.start(debugMode: true)
#else
BlinkEngageSDK.start(debugMode: false)
#endif
configureScanManager()
configureRewardsAndAppearance()
MobileAds.shared.start()
return true
}

private func configureScanManager() {
BRScanManager.shared().licenseKey = "YOUR-BLINKRECEIPT-LICENSE-KEY"
BRScanManager.shared().prodIntelKey = "YOUR-BLINKRECEIPT-PRODINTEL-KEY"
}

private func configureRewardsAndAppearance() {

// User identity — manage via SessionManager (see below)
BlinkEngageSDK.shared.user.emailHash = "hashed_email_string"
BlinkEngageSDK.shared.user.phoneHash = "hashed_phone_string"
BlinkEngageSDK.shared.user.clientUserId = "your_client_user_id"

// Reward configuration — see Reward Currency for full options
BlinkEngageSDK.shared.rewardConfig = BlinkEngageRewardConfig(
currencyName: "points",
currencyPerDollar: 100.0,
userPayoutPercentage: 0.6,
rewardCallback: { context, rewardAmount, blinkReceiptId in
switch context {
case "ScanFinished":
return NSNumber(value: 10.0)
case "Promo", "Boost", "BarcodeCollection":
print("User earned \(rewardAmount?.doubleValue ?? 0) points (receipt: \(blinkReceiptId ?? "nil"))")
return nil
default:
return nil
}
}
)

// Theming — see Theming Guide for full options
BlinkEngageSDK.shared.appearance = Appearance(theme: BlinkEngageTheme())
}
}

BlinkEngageSDK.start(debugMode:) must be called before any BRScanManager.shared() calls. Pass true during development — it registers the device as a GAM test device and relaxes on-device receipt checks. Pass false for App Store and customer-facing builds.

User Session Management

Tie SDK user registration to your own auth flow. BlinkEngage attributes offers and rewards to the registered identity — always keep it in sync with your app's login state.

import BlinkEngage
import Foundation

class SessionManager {

static let shared = SessionManager()

private(set) var isLoggedIn = false

func logIn(emailHash: String, phoneHash: String, userId: String? = nil) {
BlinkEngageSDK.shared.user.emailHash = emailHash
BlinkEngageSDK.shared.user.phoneHash = phoneHash
BlinkEngageSDK.shared.user.clientUserId = userId
isLoggedIn = true
}

func logOut() {
BlinkEngageSDK.shared.user.emailHash = nil
BlinkEngageSDK.shared.user.phoneHash = nil
BlinkEngageSDK.shared.user.clientUserId = nil
isLoggedIn = false
}

func switchUser(emailHash: String, phoneHash: String, userId: String? = nil) {
logOut()
logIn(emailHash: emailHash, phoneHash: phoneHash, userId: userId)
}
}
  • On login: call SessionManager.shared.logIn(...) after your app authenticates the user. The SDK registers the identity automatically.
  • On logout: call logOut() — setting both hashes to nil ensures the SDK doesn't attribute subsequent activity to the previous user.
  • On account switch: switchUser clears the previous identity before registering the new one, so rewards are never cross-attributed.

Presenting the Offer Wall

Wrap the offer wall in a UINavigationController before presenting it. Tapping a store tile or a carousel section's "Show more" pushes a filtered offer list onto that stack — without a navigation controller those screens cannot appear.

class YourViewController: UIViewController {

func showOfferWall() {
let offerWall = OffersWallViewController(offerWallViewType: .all)
offerWall.delegate = self
offerWall.title = "Offers"
offerWall.navigationItem.leftBarButtonItem = UIBarButtonItem(
barButtonSystemItem: .close,
target: self,
action: #selector(dismissOfferWall)
)
let nav = UINavigationController(rootViewController: offerWall)
configureNavigationBar(nav)
nav.modalPresentationStyle = .fullScreen
present(nav, animated: true)
}

@objc private func dismissOfferWall() {
dismiss(animated: true)
}

private func configureNavigationBar(_ nav: UINavigationController) {
let appearance = UINavigationBarAppearance()
appearance.configureWithOpaqueBackground()
appearance.backgroundColor = UIColor(named: "BrandPrimary") ?? .systemBlue
appearance.titleTextAttributes = [.foregroundColor: UIColor.white]
nav.navigationBar.standardAppearance = appearance
nav.navigationBar.scrollEdgeAppearance = appearance
nav.navigationBar.tintColor = .white
}
}

extension YourViewController: OffersWallViewControllerDelegate {

// Triggered when the user taps the floating "Scan Receipt" button
// Also fires from store and carousel-group offer lists pushed onto the stack
func offerWallDidSelectFloatingAction(_ viewController: OffersWallViewController) {
let scanOptions = BRScanOptions()
scanOptions.enableBlinkEngage = true
BRScanManager.shared().startStaticCamera(
from: self,
cameraType: .standard,
scanOptions: scanOptions,
with: self
)
}

func offerWallShouldDisplayFloatingAction(_ viewController: OffersWallViewController) -> Bool {
return true
}

// Called on initial load and whenever the user clips or unclips an offer
func offerWall(_ viewController: OffersWallViewController, didUpdateClippedOffersCount count: Int) {
tabBarItem.badgeValue = count > 0 ? "\(count)" : nil
}
}

Tab Bar Embedding

Embed "Offers" and "Saved" tabs in a UITabBarController. The didUpdateClippedOffersCount delegate keeps the Saved badge current without polling.

import BlinkEngage
import BlinkReceipt
import UIKit

class MainTabBarController: UITabBarController, OffersWallViewControllerDelegate {

override func viewDidLoad() {
super.viewDidLoad()

let allOffers = OffersWallViewController(offerWallViewType: .all)
allOffers.delegate = self
allOffers.tabBarItem = UITabBarItem(title: "Offers", image: UIImage(systemName: "tag"), tag: 0)

let clippedOffers = OffersWallViewController(offerWallViewType: .clipped)
clippedOffers.delegate = self
clippedOffers.tabBarItem = UITabBarItem(title: "Saved", image: UIImage(systemName: "heart"), tag: 1)

let homeVC = UIViewController()
homeVC.tabBarItem = UITabBarItem(title: "Home", image: UIImage(systemName: "house"), tag: 2)

// Wrap each offer wall tab in its own UINavigationController so store
// and carousel-group offer lists can push onto the stack.
viewControllers = [
homeVC,
UINavigationController(rootViewController: allOffers),
UINavigationController(rootViewController: clippedOffers),
]
}

func offerWallDidSelectFloatingAction(_ viewController: OffersWallViewController) {
let scanOptions = BRScanOptions()
scanOptions.enableBlinkEngage = true
BRScanManager.shared().startStaticCamera(
from: self,
cameraType: .standard,
scanOptions: scanOptions,
with: self
)
}

func offerWall(_ viewController: OffersWallViewController,
didUpdateClippedOffersCount count: Int) {
if let savedTab = viewControllers?.first(where: { $0.tabBarItem.tag == 1 }) {
savedTab.tabBarItem.badgeValue = count > 0 ? "\(count)" : nil
}
}
}

Offer wall view types:

  • .all — Show all available offers.
  • .clipped — Show only offers the user has clipped/saved.

Receipt Scanning

Start a scan from a button tap or from the offer wall's floating action delegate:

func scanReceipt() {
let scanOptions = BRScanOptions()
scanOptions.enableBlinkEngage = true
BRScanManager.shared().startStaticCamera(
from: self,
cameraType: .standard,
scanOptions: scanOptions,
with: self
)
}

With enableBlinkEngage = true on BRScanOptions, BlinkEngage intercepts scan results automatically. The didFinishScanning:withScanResults: delegate fires but scanResults is always nil — BlinkEngage handles the post-scan flow (loading screen, backend call, receipt summary).

Styling (Theme)

Control colors, fonts, and images across the SDK by implementing the Theme protocol:

class BlinkEngageTheme: NSObject, Theme {
var isMerchantIconEnabled: Bool { true }

var globalFontMatrix: NSDictionary? {
[
NSNumber(value: UIFont.Weight.regular.rawValue): "YourFont-Regular",
NSNumber(value: UIFont.Weight.medium.rawValue): "YourFont-Medium",
NSNumber(value: UIFont.Weight.semibold.rawValue): "YourFont-SemiBold",
NSNumber(value: UIFont.Weight.bold.rawValue): "YourFont-Bold",
] as NSDictionary
}

func color(forKey key: AppearanceColorKey) -> UIColor? {
switch key {
case .offerWallFloatingButtonBackground: return UIColor(named: "BrandAccent")
case .offerWallFloatingButtonLabel: return .white
case .offerClipButtonBackground: return UIColor(named: "BrandAccent")
case .offerClippedButtonBackground: return UIColor(named: "BrandPrimary")
case .postScanHeaderBackground: return UIColor(named: "BrandPrimary")
case .postScanFooterButtonTitle: return .white
case .errorModalBackButtonLabel: return UIColor(named: "BrandPrimary")
default: return nil
}
}

func fontName(forKey key: AppearanceFontNameKey) -> String? {
switch key {
case .postScanSuccessTitleLabel: return "YourFont-Black"
default: return nil
}
}

func image(forKey key: AppearanceIconKey) -> UIImage? {
switch key {
case .offerWallFloatingButtonIcon: return UIImage(systemName: "barcode.viewfinder")
case .postScanReceiptButtonIcon: return UIImage(systemName: "doc.text.magnifyingglass")
default: return nil
}
}

func text(forKey key: AppearanceTextKey) -> String? {
return nil // return a String to override SDK defaults
}
}

// Apply before presenting any SDK UI:
BlinkEngageSDK.shared.appearance = Appearance(theme: BlinkEngageTheme())

Return nil from any method to use the SDK default for that key. See the Theming Guide for the full reference of all color, font, and icon keys.

Production Reward Handling

Analytics Logging

The reward callback is your integration point for crediting users and feeding analytics. The four context values map to distinct user actions:

ContextDirectionWhat to do
"ScanFinished"SDK asks host app — return NSNumberReturn the base scan reward amount
"Promo"SDK informs host app — rewardAmount is setCredit rewardAmount to the user
"Boost"SDK informs host app — rewardAmount is setCredit rewardAmount to the user
"BarcodeCollection"SDK informs host app — rewardAmount is setCredit rewardAmount to the user
rewardCallback: { context, rewardAmount, blinkReceiptId in
switch context {
case "ScanFinished":
let reward = 10
Analytics.log("scan_reward", properties: [
"amount": reward,
"receipt_id": blinkReceiptId ?? "unknown"
])
return NSNumber(value: reward)

case "Promo":
Analytics.log("promo_reward", properties: [
"amount": rewardAmount?.doubleValue ?? 0,
"receipt_id": blinkReceiptId ?? "unknown"
])
return nil

case "Boost":
Analytics.log("boost_reward", properties: [
"amount": rewardAmount?.doubleValue ?? 0,
"receipt_id": blinkReceiptId ?? "unknown"
])
return nil

case "BarcodeCollection":
Analytics.log("barcode_reward", properties: [
"amount": rewardAmount?.doubleValue ?? 0,
"receipt_id": blinkReceiptId ?? "unknown"
])
return nil

default:
return nil
}
}

Server-Authoritative Rewards

For server-determined scan rewards, delegate the decision to a reward service:

rewardCallback: { context, rewardAmount, blinkReceiptId in
switch context {
case "ScanFinished":
let scanReward = RewardService.shared.scanRewardForCurrentUser()
RewardService.shared.credit(amount: scanReward, source: context, receiptId: blinkReceiptId)
return NSNumber(value: scanReward)

case "Promo", "Boost", "BarcodeCollection":
if let earned = rewardAmount {
RewardService.shared.credit(amount: earned.intValue, source: context, receiptId: blinkReceiptId)
}
return nil

default:
return nil
}
}

The callback runs on the main thread — keep it fast. Defer heavy work (network calls, database writes) to a background queue.

Client Events

Subscribe to SDK-side events for analytics and debugging. Set eventCallback once during app launch — it fires for the lifetime of the app session.

BlinkEngageSDK.shared.eventCallback = { eventName, metadata in
MyAnalytics.track(eventName, properties: metadata)
}
Event nameWhen it firesMetadata keys
scan_session_startedA BlinkReceipt scan session begins
ad_loading_startedAd loading screen is shown
post_scan_viewedPost-scan (receipt summary) screen is shown
offer_wall_viewedAll-offers wall is shown (.all type only; once per app session)
offer_detail_viewedOffer detail screen is opened (once per unique offer per app session)offer_title
offer_clippedUser clips an offeroffer_title

metadata is a [String: Any] dictionary. For events with no metadata the dictionary is empty. For offer_detail_viewed and offer_clipped, metadata["offer_title"] is a String.