Skip to main content

iOS callbacks and notifications

The iOS SDK communicates back through three distinct mechanisms, and knowing which one carries what saves a lot of searching:

MechanismUsed forWhere you handle it
Completion closuresResults of data method callsAt the call site
exitHandlerSDK exit, and deep payment dataThe closure you passed at launch
NotificationCenter broadcastsUser activity heartbeat, reward revealAn observer you register

Android uses listener interfaces for all three. iOS splits them, which means an integration ported from Android will silently miss the notifications unless you register observers explicitly.


Completion closures

Every data method on CheggoutApp takes a trailing closure that receives the result. They share three properties worth internalising:

  • No error parameter. There is no Result type and no Error. Failure surfaces as an empty or nil payload, or as a body whose status is false.
  • No queue guarantee. TBD The documentation does not state which queue the closure is called on. Assume a background queue and dispatch to the main queue before touching UI.
  • No retry. A dropped request is simply a closure that never fires or fires empty. If the data matters, add your own timeout.
CheggoutApp.getCheggoutTopBanners(
languageCode: "EN",
virtualId: virtualId,
placementId: "01",
additonalParams: "",
securityKey: ""
) { topBanners in
DispatchQueue.main.async {
guard let banners = topBanners, !banners.isEmpty else {
self.hideBannerCarousel() // a normal outcome, not an error state
return
}
self.render(banners)
}
}
MethodClosure delivers
getCheggoutTopBannersBanner creatives keyed by placement id
getCheggoutTopStoresAn array of stores with logo, title and discount
getCashbackPointscashbackPoints (a string) and deepLink
sendRewardCardRequestScratch card status, banner, deepLink and popup fields

impression has no closure — it fires a beacon and returns.

Displaying is the impression, not fetching

When a banner closure fires you have data, not an impression. Call CheggoutApp.impression(userID:impressionInfo:) when the creative is actually on screen. Firing on fetch inflates impressions for creatives the user never scrolled to, and misattributes the ones they did see.


exitHandler

exitHandler is passed into the CheggoutApp initialiser and called when the SDK exits. It carries two responsibilities and, in one flow, one payload.

cheggoutApp = CheggoutApp(
userID: virtualId,
sessionInfo: sessionId,
accessInfo: nil,
payload: payload,
exitHandler: { [weak self] result in
DispatchQueue.main.async {
self?.dismiss(animated: false)
self?.cheggoutApp = nil
}
}
)

Responsibility one — dismiss. The SDK does not tear down your presentation for you.

Responsibility two — release. Clear the initialised Cheggout instance. Holding it after exit keeps the SDK's state alive and makes the next launch ambiguous.

After control returns, where the user lands depends on your own session state: if your app ends the session the user goes to your login page; if you keep it alive using Last Activity Time or heartbeat, they go to your home page. See TrackUserActivity below for keeping that session alive.

Payment data arrives through exitHandler

In the deep payment flow, the SDK exits so that your app can collect the money. The payment data response is passed as a parameter to the exitHandler closure, Base64 encoded.

exitHandler: { [weak self] result in
guard
let encoded = result,
let data = Data(base64Encoded: encoded),
let json = try? JSONSerialization.jsonObject(with: data) as? [String: Any]
else {
DispatchQueue.main.async { self?.dismiss(animated: false) }
return
}

self?.handlePayment(json)
}

Decoded, it looks like this:

{
"deepLink": "https://click.cheggout.com?type=pwa&id=<giftcard url>?token=tokenvalue/cheggoutRefNo=YOURCHANNEL123456/paymentType=YOURCHANNEL/amount=540.00/transactionId=bankTransactionID/status=bankStatus",
"cheggoutRefNo": "YOURCHANNEL123456",
"amount": "54000",
"responseCode": "COLLECTPAYMENT",
"virtualId": "pub_8f14e45fceea167a",
"merchantName": "Cheggout",
"merchantId": "REPLACE_WITH_MERCHANT_ID"
}
FieldMeaning
deepLinkUsed to relaunch the SDK after payment. Replace bankTransactionID with your payment's transaction ID and bankStatus with the payment status.
cheggoutRefNoUnique reference number generated by Cheggout for the transaction. Store it — it is your reconciliation key.
amountPayment amount in paisa. "54000" means ₹540.00.
responseCodeCOLLECTPAYMENT.
virtualIdThe user this payment belongs to.
merchantNameIdentifies the transaction source within Cheggout, for example Cheggout.
merchantIdA unique identifier per merchant name, issued by the bank during onboarding.
amount is in paisa

Every other amount in the Cheggout contract is in rupees to two decimals. This one is in paisa. Convert explicitly and assert on the result before charging anything.

Once the payment completes, relaunch the SDK using the deep link, with bankTransactionID and bankStatus substituted. Full flow in Deep payment.

One closure, two meanings

exitHandler fires both on an ordinary back-out and on a payment handoff. Branch on whether the parameter decodes to a payload with responseCode: "COLLECTPAYMENT"; an ordinary exit will not.


TrackUserActivity — the heartbeat

Android exposes a CHEGHeartBeatListener interface. iOS instead posts a notification, which your app observes.

The SDK posts Notification.Name("TrackUserActivity") with a userInfo dictionary containing LastUserActivity and an event key with the value "tap".

NotificationCenter.default.addObserver(
forName: Notification.Name("TrackUserActivity"),
object: nil,
queue: .main
) { notification in
// The user is active inside the Cheggout SDK.
// Reset your own inactivity timer so the session does not expire mid-shop.
SessionManager.shared.recordActivity()

if let info = notification.userInfo {
let last = info["LastUserActivity"]
let event = info["event"] // "tap"
_ = (last, event)
}
}

Register the observer before you launch the SDK, and remove it when your hosting view controller goes away.

Why this matters: without it, a user who spends fifteen minutes browsing inside Cheggout looks idle to your app's session timer. They come back to a login screen instead of your home screen, and they blame the shopping module for logging them out.

Unconfirmed details

TBD The type and format of LastUserActivity, the full set of possible event values beyond "tap", and the posting cadence are not documented. Treat each notification as "activity happened just now" and reset your timer, rather than computing elapsed time from the values or from the gap between posts.


HandleDominosScDetails — reward reveal

When a reward is revealed inside the SDK, the scratch card details are broadcast as a notification so the parent app can react — refresh a points balance, log an analytics event, or show your own confirmation.

The SDK sends the scratch card details (scDetails) via a notification broadcast named HandleDominosScDetails. Listen for it and handle scDetails in your handler.

NotificationCenter.default.addObserver(
self,
selector: #selector(handleNotification(_:)),
name: Notification.Name("HandleDominosScDetails"),
object: nil
)

@objc func handleNotification(_ notification: Notification) {
let scDetails = notification.userInfo?["scDetails"]
print("Reveal details: \(String(describing: scDetails))")
rewardsRepository.refresh()
}
The notification name is literal

HandleDominosScDetails is the actual broadcast name in the SDK. It reads like a leftover from a specific campaign, but it is the name you must observe. Do not rename it, and do not assume it is brand-specific — it carries reveal details for any reward.

TBD The exact structure of scDetails, and the key it is filed under in userInfo, are not documented. Log the notification in UAT to see what your build delivers, and do not parse fields you have not observed. The Android equivalent is the CHEGscratchRevealCallBack interface; see Android callbacks.


Push notifications

Push is not a callback mechanism — Cheggout needs to reach the user when your app is not running — and the two integration options are identical across platforms. Rather than duplicating them, they are documented once:

Push notification options

In short: either Cheggout calls your push API with the user's VirtualID, a message and the deep-link information, or you pass the Firebase identifier as pushnotificationID in the launch payload and hand Cheggout your FCM credentials so it sends directly.

On tap, whichever option you chose, launch the SDK with the deep link in the payload's DeeplinkInfo:

let payload: [String: Any] = [
"Component": "CHEGGOUT",
"IsEmployee": "0",
"Language": "EN",
"DeeplinkInfo": deepLinkFromNotification
]

Next

  • Launching — the initialiser and the payload the deep link goes into.
  • SDK methods — the calls these closures belong to.
  • Deep payment — the full payment round trip.