Skip to main content

iOS Integration Guide


Requirements​

ItemRequirement
Minimum iOSiOS 15.0+
LanguageSwift
Host UI frameworkUIKit or SwiftUI
SDK diagnostic UIUIKit
NetworkingAlamofire
Error monitoringSentry Cocoa
Glass object detectionTensorFlow Lite Task Vision
Motion / Drop DetectionCoreMotion
BluetoothCoreBluetooth
LocationCoreLocation
Wi-Fi informationNetworkExtension / CaptiveNetwork
Camera / microphoneAVFoundation
Background tasksBackgroundTasks
Push / notification statusUserNotifications

The SDK reads the host app version from CFBundleShortVersionString; the host app does not pass an app-version parameter to WarrantyLifeSDK.build(...).


Dependencies and SDK Integration​

Import the SDK from the host application:

import WLSDKIOS

The SDK source target currently depends on:

  • Alamofire
  • SwiftyJSON
  • GzipSwift
  • Sentry Cocoa
  • TensorFlowLiteTaskVision

The SDK also requires its resource bundle and TensorFlow Lite model used by the Glass Test. The current detector loads:

WLSDKIOS.bundle
cell_phone_and_face_WL_ODM.tflite

If the SDK is distributed as a binary package, make sure these dependencies and resources are included or resolved by the package. If it is integrated as source, link the dependencies to the WLSDKiOS target, not by adding import statements throughout the host app.

The host app normally imports only:

import WLSDKIOS

Host App Configuration​

Info.plist privacy keys​

The host application owns Apple privacy descriptions. Add the keys required by the features the app uses.

Recommended full configuration:

<key>NSBluetoothAlwaysUsageDescription</key>
<string>Bluetooth access is used for device diagnostics and Smart Saver network scanning.</string>

<key>NSLocationWhenInUseUsageDescription</key>
<string>Location access is used to read Wi-Fi information for diagnostics.</string>

<key>NSLocationAlwaysAndWhenInUseUsageDescription</key>
<string>Location access can be used to support Smart Saver background wake-up.</string>

<key>NSMotionUsageDescription</key>
<string>Motion access is required for sensor diagnostics and Drop Detection.</string>

<key>NSCameraUsageDescription</key>
<string>Camera access is required for device glass diagnostics.</string>

<key>NSMicrophoneUsageDescription</key>
<string>Microphone access is required when the configured Sensor Test records audio.</string>

NSMotionUsageDescription is required because the SDK uses Core Motion APIs including CMSensorRecorder.

Background task identifiers​

The SDK registers these two task identifiers internally:

com.warrantylife.sdk.dropdetection.processing
com.warrantylife.sdk.dropdetection.refresh

Add both to the host app's Info.plist:

<key>BGTaskSchedulerPermittedIdentifiers</key>
<array>
<string>com.warrantylife.sdk.dropdetection.processing</string>
<string>com.warrantylife.sdk.dropdetection.refresh</string>
</array>

Background Modes​

For the full current Drop Detection implementation, enable:

Target → Signing & Capabilities → Background Modes

Recommended selections:

  • Background fetch
  • Background processing
  • Remote notifications

Equivalent UIBackgroundModes values are:

<key>UIBackgroundModes</key>
<array>
<string>fetch</string>
<string>processing</string>
<string>remote-notification</string>
</array>

The current SDK uses startMonitoringSignificantLocationChanges() for the Location Always wake path. It does not use continuous startUpdatingLocation(), so the location background mode is not required by the current implementation.

Push Notifications​

If the host app uses FCM/APNs and silent push:

Target → Signing & Capabilities → + Capability → Push Notifications

The host app owns Firebase/APNs setup and system notification permission UI.

Access WiFi Information​

For current SSID/BSSID access, enable:

Target → Signing & Capabilities → + Capability → Access WiFi Information

Entitlement:

com.apple.developer.networking.wifi-info

This capability is required for reliable Wi-Fi information. Wi-Fi collection is optional for the overall Auto Test and Drop Detection flow.


SDK Initialisation​

Call WarrantyLifeSDK.build(...) exactly once, as early as possible in the application launch lifecycle.

The SDK lifecycle is application/process-level. It is not created separately for each UIWindowScene.

build(...) creates and connects the SDK's repositories, diagnostic managers, Drop Detection services, background task handlers, Sentry integration, AppSession refresh coordinator, and API-key validation. Background-task registration also occurs from this application-level initialization path, so initialization must not be deferred until a scene or SwiftUI view appears.

UIKit / AppDelegate​

For a traditional UIKit application, call build(...) from application(_:didFinishLaunchingWithOptions:):

import UIKit
import WLSDKIOS

@main
final class AppDelegate: UIResponder, UIApplicationDelegate {
var window: UIWindow?

func application(
_ application: UIApplication,
didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?
) -> Bool {
WarrantyLifeSDK.build(
clientCode: "YOUR_CLIENT_CODE",
apiKey: "YOUR_DEVELOPER_API_KEY",
sentryClientKey: "YOUR_SENTRY_DSN",
apiKeyValidationHandler: { result in
switch result {
case .valid(let name, let environment):
print("WarrantyLife API key valid:", name ?? "", environment ?? "")

case .invalid(let message):
print("WarrantyLife API key invalid:", message)

case .unavailable(let message, let statusCode):
print("WarrantyLife API key validation unavailable:", statusCode, message)
}
}
)

return true
}
}

UIKit + SceneDelegate​

Apps that use UIScene / SceneDelegate must still initialize the SDK from AppDelegate:

import UIKit
import WLSDKIOS

@main
final class AppDelegate: UIResponder, UIApplicationDelegate {
func application(
_ application: UIApplication,
didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?
) -> Bool {
WarrantyLifeSDK.build(
clientCode: "YOUR_CLIENT_CODE",
apiKey: "YOUR_DEVELOPER_API_KEY",
sentryClientKey: "YOUR_SENTRY_DSN"
)

return true
}
}

A normal SceneDelegate does not need to initialize or rebuild the SDK:

final class SceneDelegate: UIResponder, UIWindowSceneDelegate {
var window: UIWindow?

func scene(
_ scene: UIScene,
willConnectTo session: UISceneSession,
options connectionOptions: UIScene.ConnectionOptions
) {
// Configure the host app scene only.
// Do not call WarrantyLifeSDK.build(...) here.
}
}

Do not call WarrantyLifeSDK.build(...) from scene(_:willConnectTo:options:), sceneDidBecomeActive(_:), or other per-scene callbacks.

SwiftUI lifecycle​

SwiftUI applications should bridge an UIApplicationDelegate with @UIApplicationDelegateAdaptor and initialize the SDK from that delegate.

import SwiftUI

@main
struct MyApp: App {
@UIApplicationDelegateAdaptor(AppDelegate.self)
private var appDelegate

var body: some Scene {
WindowGroup {
ContentView()
}
}
}
import UIKit
import WLSDKIOS

final class AppDelegate: NSObject, UIApplicationDelegate {
func application(
_ application: UIApplication,
didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?
) -> Bool {
WarrantyLifeSDK.build(
clientCode: "YOUR_CLIENT_CODE",
apiKey: "YOUR_DEVELOPER_API_KEY",
sentryClientKey: "YOUR_SENTRY_DSN"
)

return true
}
}

Do not initialize the SDK from:

  • a SwiftUI View.init;
  • body;
  • .onAppear;
  • scenePhase changes;
  • a SceneDelegate callback.

Those locations can execute more than once and are not the application-level launch point required by the SDK.

Application lifecycle ownership​

The SDK owns one process-level lifecycle coordinator. The host application does not need to forward ordinary foreground/background scene transitions to the SDK.

The SDK observes application lifecycle state and coordinates:

  • AppSession foreground refresh;
  • Drop Detection foreground/background eligibility;
  • pending Drop sync;
  • background-task scheduling;
  • significant-location-change monitoring when eligible;
  • Sensor Test haptic recovery after normal app/audio interruptions.

A host app may still observe scenePhase or SceneDelegate for its own UI behavior, but that should not trigger a second SDK initialization.

Scene-aware SDK UI presentation​

The SDK resolves the active UIWindowScene, key window, and top-most presented view controller when SDK-owned diagnostic UI must be shown.

The standard Glass Test presentation flow supports hosts whose visible UI is based on:

  • a UIKit root view controller;
  • a presented UIKit view controller;
  • a SwiftUI UIHostingController;
  • a SwiftUI sheet or other presented SwiftUI content.

The host app does not need to manually pass a UIViewController to the standard Glass Test APIs.

Main entry points​

After build:

let repository = WarrantyLifeSDK.getApiRepository()
let appDataManager = WarrantyLifeSDK.getAppDataManager()
let testManager = WarrantyLifeSDK.getTestManager()

The host app should not construct internal SDK managers directly.


API Key Validation and Sentry​

Developer API Key​

apiKey is required.

The production SDK contract sends:

X-WL-Developer-Key: <apiKey>

with WarrantyLife server requests.

An empty API key is treated as invalid.

Public status:

WarrantyLifeSDK.getAPIKeyValidationStatus()

Possible values:

.notValidated
.validating
.valid
.invalid
.unavailable

You may register or replace the callback later:

WarrantyLifeSDK.setAPIKeyValidationHandler { result in
// Handle the latest result
}

If a result was already received, the latest result is delivered to the new handler.

Manual retry:

WarrantyLifeSDK.retryAPIKeyValidation()

Behavior:

ResultMeaningSDK behavior
validKey acceptedNormal requests continue
invalidServer returned 401 for key validationSDK blocks normal WarrantyLife server requests locally
unavailableNetwork/server/temporary validation problemKey is not treated as invalid; normal requests remain available and validation may retry

The SDK automatically retries an .unavailable validation when the app becomes active again.

Internal DEBUG-only test overrides are not part of the public integration contract. Production integrations must use a valid production Developer API key.

Sentry​

sentryClientKey is optional.

Disable Sentry:

sentryClientKey: nil

or:

sentryClientKey: ""

Enable Sentry:

sentryClientKey: "YOUR_SENTRY_DSN"

The same DSN already used by the host WarrantyLife app may be passed to the SDK; a separate Sentry Project is not required unless the product team wants SDK events isolated in a different project.

The current SDK:

  • initializes Sentry itself;
  • disables Sentry automatic failed-request capture;
  • sends SDK HTTP error messages through its own ApiClient / SentryManager;
  • removes query strings from the endpoint recorded in Sentry;
  • does not intentionally include Authorization tokens or Developer API keys in the custom event text.

Because Sentry Cocoa uses a process-wide client, avoid calling SentrySDK.start(...) a second time in the host app after the SDK has taken ownership of Sentry initialisation. Existing host-app SentrySDK.capture... calls can continue to use the initialized client.

Current HTTP-status filtering is an implementation detail and should not be treated as a public API guarantee. The current iOS build may report both 4xx and 5xx HTTP failures.


Permission Ownership​

Core rule​

The host application requests permissions. The SDK validates permissions and decides whether a test or Drop Detection operation can continue.

The SDK does not present a complete permission-onboarding UI for the host app.

Diagnostic permissions​

FeatureHost permission/configuration
Auto Test BluetoothBluetooth permission + Bluetooth usage description
Auto Test Wi-FiLocation When In Use or Always + Access WiFi Information capability
Sensor Test motionMotion & Fitness
Sensor Test audioMicrophone, when audio recording is enabled by server configuration
Glass TestCamera
Drop DetectionSee formula below

For Auto Test, Location permission is used only for Wi-Fi information. If Location/Wi-Fi information is unavailable, the SDK can still return the other diagnostic results.

Drop Detection permission rule​

The exact iOS rule is:

Motion & Fitness
AND
(
Notifications
OR
Location Always
)

All three are not required.

In code terms:

Motion && (Notification || LocationAlways)

Examples:

MotionNotificationsLocation AlwaysRequired permissions satisfied
YesYesNoYes
YesNoYesYes
YesYesYesYes
YesNoNoNo
NoYesYesNo

Notification .authorized and .provisional are accepted. On newer iOS versions, .ephemeral is also accepted where available.

Location Always is additionally required if the app wants the SDK's significant-location-change wake path. If Notifications are granted but Location Always is not granted, Drop Detection can still be eligible; only the location wake source is unavailable.


Registration Flow​

The current iOS registration flow is conceptually:

voucher
→ required diagnostics
→ authentication
→ optional receipt
→ IMEI + registerDevice
→ optional purchase date
→ Smart Saver selection
→ createWarranty / updateWarranty

Step 1 — Voucher Code​

guard let repository = WarrantyLifeSDK.getApiRepository() else { return }

repository.getVoucherByVoucherCode(
voucherCode: voucherCode,
isPlanTransfer: false,
activeWarrantiesInCurrentDevice: 0
) { response in
switch response {
case .success(let success):
guard let voucher = success.data else { return }
print(voucher.action)
print(voucher.registrationType)

case .error(let error):
print(error.errorCode, error.errorMessage ?? error.error)
}
}

The voucher call stores the current voucher, warranty identifier, diagnostic requirements, receipt requirement, purchase-date requirement, Smart Saver offer state, and original-device information inside the SDK. If the voucher contains a Sensor Test configuration, that configuration is also stored by the SDK and becomes available through AppDataManager.isSensorTestAvailable() and the normal TestManager.runSensorTest() flow.

VoucherAction​

Current public actions:

.normalRegistration
.deviceTransferablePopup
.deviceNonTransferablePopup
.userNotLoggedInClaimed
.differentEmailClaimed
.activeWarrantyConflict
.warrantyNotTransferable
.startWarrantyTransfer

activeWarrantyConflict and warrantyNotTransferable return isError == true.

For a plan transfer, use:

isPlanTransfer: true

The iOS SDK does not expose Android's setDeviceTransferMode(...) API. Route the host UI using the returned Voucher.action and use the iOS plan-transfer lookup/API flow.

RegistrationType​

.normalRegistration
.passwordlessRegistration
.lockedEmailRegistration

Use this value to decide which registration fields the host UI should show.

Step 2 — Required Tests​

Read current requirements from AppDataManager:

let data = WarrantyLifeSDK.getAppDataManager()

let autoRequired = data?.isAutoTestRequired() ?? false
let sensorRequired = data?.isSensorTestRequired() ?? false
let frontRequired = data?.isFrontCameraTestRequired() ?? false
let backRequired = data?.isBackCameraTestRequired() ?? false
let openFaceRequired = data?.isOpenFaceTestRequired() ?? false

The current voucher mapping exposes Open Face Test support through TestManager, but the standard current voucher mapping does not mark Open Face as a required registration test.

Step 3 — Authentication​

Login:

repository.getTokenViaUserPassword(
username: email,
password: password
) { response in
// Handle ApiResponse<LoginResponse>
}

A successful login stores SDK authentication and triggers an immediate authenticated AppSession refresh.

Register new user:

let user = WLRegisterNewUserModel(
email: email,
password: password,
firstName: firstName,
lastName: lastName,
company: company,
streetAddress: street,
country: countryId,
countryCode: countryCode,
city: city,
zip: zip,
state: stateId,
stateCode: stateCode,
phone: phone
)

repository.registerNewUser(user: user) { response in
// Handle ApiResponse<LoginResponse>
}

Logout:

repository.logout()

Do not manually clear the SDK's internal UserDefaults. Logout stops Drop Detection processing and clears authenticated Smart Saver session state while preserving the SDK device identity.

Step 4 — Receipt Image​

If:

appDataManager.isReceiptRequired()

store a local receipt-image path before registerDevice:

appDataManager.setReceiptImagePath(receiptURL.path)

If a receipt is required and no receipt data/path exists, registerDevice returns:

.validationError(.missingReceiptImage)

Step 5 — IMEI / Serial and Register Device​

Set IMEI:

appDataManager.setIMEI(imei)

Then:

WarrantyLifeSDK.registerDevice { state in
switch state {
case .submitted:
break

case .progress(let percentage):
print("Registration upload:", percentage)

case .success:
print("Registration diagnostic upload complete")

case .validationError(let error):
print("Missing registration data:", error)

case .uploadError(let message, let code):
print("Upload failed:", code, message)
}
}

Current progress stages are approximately:

25 → Item resolved
50 → Sensor Event resolved
75 → Sensor upload stage complete
100 → Registration upload complete

Validation errors:

.missingAutoSystemTestData
.missingFrontGlassImage
.missingBackGlassImage
.missingOpenFaceTestImage
.missingSensorTestData
.missingReceiptImage
.missingIMEI

Step 6 — Purchase Date​

If:

appDataManager.isPurchaseDateRequired()

store:

appDataManager.setReceiptDate("yyyy-MM-dd")

and also pass the date in the warranty model when required.

Step 7 — Smart Saver Selection​

During an active registration or device-transfer flow, setSSR, setSSD, and setTradeIn save the user's selection without applying the normal post-registration active-coverage permission gate.

Example:

appDataManager.setSSR(true) { result in
// Handle OptInResult
}

See §8 for normal post-registration behavior.

Step 8 — Create / Update Warranty​

Create a new warranty:

let model = WLWarrantyRequest(
firstName: firstName,
lastName: lastName,
company: company,
streetAddress: street,
city: city,
zip: zip,
state: stateId,
country: countryId,
phone: phone,
dateOfPurchase: purchaseDate,
acceptedMarketing: acceptedMarketing,
isDeviceInsured: isDeviceInsured
)

repository.createWarranty(model: model) { response in
// Handle ApiResponse<Warranty>
}

For the current transfer/update path:

repository.updateWarranty(model: model) { response in
// Handle ApiResponse<Warranty>
}

On successful warranty finalisation, the SDK:

  • persists the warranty;
  • marks active coverage;
  • clears registration-in-progress state;
  • clears the current voucher;
  • sends Smart Saver enabled Sensor Events for selected offers;
  • refreshes Drop Detection lifecycle state.

Use Country.id and Province.id values returned by getAppConfig() for country and state, not only the display name/abbreviation.


Smart Saver Opt-Ins​

Current public methods:

appDataManager.setSSR(true) { result in ... }
appDataManager.setSSD(true) { result in ... }
appDataManager.setTradeIn(true) { result in ... }

Read values:

appDataManager.getSSR()
appDataManager.getSSD()
appDataManager.getTradeIn()

Check whether each option is offered:

appDataManager.hasSsr()
appDataManager.hasSsd()
appDataManager.isTipOffered()

Normal post-registration validation​

When not in a registration/transfer flow, enabling an option requires:

  1. User is logged in
  2. Active coverage exists
  3. The option is offered by the current warranty/plan
  4. Required Drop Detection permissions are satisfied
  5. Current Item ID exists
  6. The Smart Saver Sensor Event upload succeeds

Example:

appDataManager.setSSR(true) { result in
switch result {
case .success:
print("SSR enabled")

case .failure(let error):
print(error.message)
print(error.errorCode as Any)
}
}

Possible errors:

.notLoggedIn
.noActiveCoverage
.optionNotOffered(...)
.requiredPermissionsNotGranted(...)
.sensorEventFailed(...)

When the last enabled offered Smart Saver option is disabled, the SDK disables Drop Detection.


Drop Detection and Background Execution​

iOS architecture​

The iOS implementation is intentionally different from Android.

Android uses a foreground service. The current iOS SDK uses:

  • CMSensorRecorder recorded accelerometer data;
  • SDK lifecycle coordination;
  • foreground wake opportunities;
  • BGProcessingTask;
  • BGAppRefreshTask;
  • silent push;
  • significant-location-change wake when Location Always is available.

The host application should not create a duplicate continuous accelerometer Drop Detection loop.

The lifecycle coordinator is application/process-level. UIKit scenes and SwiftUI scenePhase transitions do not create independent Drop Detection lifecycles and should not be used to call WarrantyLifeSDK.build(...) again.

Eligibility / gating​

Before new Drop Detection processing can run, the SDK requires:

  1. persisted Drop Detection state is enabled;
  2. user is logged in;
  3. active coverage exists;
  4. at least one offered Smart Saver option is opted in;
  5. required permissions are satisfied.

Public state helpers:

appDataManager.isSmartSaverEnabled()
appDataManager.canProcessDropDetection()

Foreground session refresh​

On a new foreground session, the SDK refreshes server state before starting new foreground Drop Detection work.

Logged-in flow:

getItemByDeviceID
→ getAllWarranties
→ restore active coverage / Smart Saver state
→ allow or block Drop Detection processing

Logged-out flow:

getItemByDeviceIdPublic
→ refresh public coverage state

This prevents Drop Detection from processing stale local state when the app returns to the foreground.

Wake sources​

Current wake opportunities include:

Foreground
SDK start
BGProcessingTask
BGAppRefreshTask
Silent push
Location significant-change
Internal lifecycle refresh

Pending Drop data is synced before new recorded-data processing is attempted.

Manual sync is available:

WarrantyLifeSDK.syncDropData { success in
print("Drop sync:", success)
}

Drop Detection JavaScript​

The Drop Detection JavaScript version is stored as a String, for example:

2.0.22

The host app does not need to download or refresh the Drop Detection JavaScript as part of normal integration. Registration, Smart Saver Sensor Events, and Drop Detection resolve the JavaScript/version through the SDK's internal flow when required.

Significant location wake​

The SDK starts startMonitoringSignificantLocationChanges() only when:

  • Drop Detection gating is satisfied;
  • required permissions are satisfied;
  • Location Services are enabled;
  • Location authorization is Always.

If Location Always is not granted, the SDK simply disables this wake source. Notification permission may still satisfy the overall Drop Detection permission rule.

Bluetooth / Wi-Fi tracking attached to Sensor Events​

Network collection is server-controlled through voucher diagnostic configuration.

Bluetooth scan collection occurs only when:

  • server configuration enables Bluetooth scanning;
  • app is active in the foreground;
  • Bluetooth permission/configuration is available;
  • no older unsent Bluetooth scan is already pending.

Wi-Fi collection occurs only when:

  • server configuration enables Wi-Fi scanning;
  • required Wi-Fi/location capability is available;
  • no older unsent Wi-Fi result is already pending.

The SDK keeps at most the pending unsent data needed for the next Sensor Event.

When createSensorEvent runs:

pending Bluetooth/Wi-Fi data
→ reserved
→ attached to Sensor Event
→ cleared only after successful API response

If the Sensor Event upload fails, the pending network data is retained for a later attempt.

This prevents the same scan payload from being attached repeatedly after a confirmed successful upload.


Diagnostic Tests​

Get the manager:

guard let testManager = WarrantyLifeSDK.getTestManager() else { return }

Auto System Test​

Set the result handler:

testManager.setAutoResultHandler { result in
switch result {
case .result(let isPassed, let details):
print("Auto Test:", isPassed)
print(details.model)

case .failure(let error, let details):
print(error.message)
print(details.model)
}
}

Run:

testManager.runAutoSystemTest()

Current details include:

  • make/model
  • Bluetooth status
  • cellular status / carrier
  • Wi-Fi status / SSID
  • accelerometer status
  • gyroscope status
  • battery status / level
  • jailbreak result

The Auto Test collects as much information as possible before completing. Wi-Fi information is optional. Bluetooth permission/configuration problems can return a diagnostic failure while still providing collected details.

Status values used internally by the current diagnostic report:

PASS = 1
FAIL = 0
NA = -1
FNA = -5

Sensor Test​

The Sensor Test configuration comes from the validated voucher/server configuration. The host application should not manufacture or write the SDK's private Sensor Test configuration directly.

Recommended flow:

voucher lookup
→ SDK stores diagnostic requirements and Sensor Test configuration
→ isSensorTestAvailable()
→ register Sensor Test state/result handlers
→ runSensorTest()

A normal voucher lookup is sufficient to prepare the configuration when the server response contains a Sensor Test:

guard
let repository = WarrantyLifeSDK.getApiRepository(),
let appDataManager = WarrantyLifeSDK.getAppDataManager(),
let testManager = WarrantyLifeSDK.getTestManager()
else {
return
}

repository.getVoucherByVoucherCode(
voucherCode: voucherCode,
isPlanTransfer: false,
activeWarrantiesInCurrentDevice: 0
) { response in
switch response {
case .success:
guard appDataManager.isSensorTestAvailable() else {
print("Sensor Test configuration is not available")
return
}

testManager.runSensorTest()

case .error(let error):
print(error.errorMessage ?? error.error)
}
}

Check whether the currently stored voucher/server configuration contains a usable Sensor Test:

appDataManager.isSensorTestAvailable()

Register handlers before starting the test:

testManager.setSensorStateHandler { state in
switch state {
case .waitingForPosition:
break

case .countdown(let seconds):
print("Sensor Test countdown:", seconds)

case .started:
break

case .progress(let percent, let phase):
print("Sensor Test:", percent, phase)

case .finished:
break
}
}

testManager.setSensorResultHandler { result in
switch result {
case .started:
break

case .result(let isPassed):
print("Sensor Test:", isPassed)

case .failure(let error):
print(error.message)
}
}

Run:

testManager.runSensorTest()

The test may use motion, altitude, haptics, tone output, and optional audio recording depending on the server-provided Sensor Test configuration.

The host app owns required permission UX. Motion & Fitness must be available for the motion-based Sensor Test. If the server configuration enables audio recording, Microphone permission must already be granted.

The SDK handles normal foreground/background and audio-session interruptions for its Sensor Test haptic engine internally. The host app should not rebuild the SDK after returning to the foreground.

Glass Tests​

Set handlers before running:

testManager.setGlassStateHandler { state in
print(state.message)
}

testManager.setGlassResultHandler { result in
switch result {
case .result(let isPassed, let validationMessage):
print("Glass Test:", isPassed)
print(validationMessage?.message ?? "")

case .failure(let error):
print(error.message)
}
}

testManager.setGlassCapturedImageHandler { image in
// Optional preview / host UI use
}

Run:

testManager.runFrontGlassTest()
testManager.runBackGlassTest()
testManager.runOpenFaceTest()

The Glass Test presents its own SDK camera view controller. Presentation is scene-aware: the SDK resolves the active UIWindowScene, key window, and top-most presenter. This supports standard UIKit, UIKit + SceneDelegate, and SwiftUI hosts, including presentation while a SwiftUI sheet is already visible.

If Camera permission is not granted, the run method returns an immediate failure result:

.failure(.missingCameraPermission)

Otherwise completion is delivered through the configured result handler.

Custom validation text:

testManager.setValidationMessages(
config: ValidationMessageConfig(
tooFarMessage: "...",
tooCloseMessage: "...",
outOfBoxMessage: "...",
takePictureMessage: "..."
)
)

Stop active diagnostic UI/work​

testManager.stop()

Remote Notifications and FCM Token​

CM token​

The host app owns Firebase configuration.

Whenever Firebase provides or refreshes a token:

WarrantyLifeSDK.updateFCMToken(fcmToken) { response in
// Optional API result handling
}

The SDK always stores the latest token locally.

The server update is sent only when the SDK currently has:

  • logged-in user state;
  • Item ID;
  • Device ID.

If Firebase produced the token before login/item registration, call updateFCMToken(...) again after the Item exists so the current server Item receives the token.

Silent push forwarding​

Forward background remote notifications from AppDelegate. SwiftUI hosts should forward the notification from the UIApplicationDelegate used by @UIApplicationDelegateAdaptor:

func application(
_ application: UIApplication,
didReceiveRemoteNotification userInfo: [AnyHashable: Any],
fetchCompletionHandler completionHandler: @escaping (UIBackgroundFetchResult) -> Void
) {
WarrantyLifeSDK.handleRemoteNotification(
userInfo: userInfo,
completionHandler: completionHandler
)
}

The SDK classifies the wake state and uses the opportunity for pending Drop sync and eligible Drop Detection processing.

The host app is responsible for:

  • APNs/Firebase registration;
  • silent-push payload configuration;
  • Push Notifications capability;
  • Background Modes → Remote notifications.

API Reference​

All asynchronous API calls return:

ApiResponse<T>
enum ApiResponse<T> {
case success(ApiSuccessResponse<T>)
case error(ApiErrorResponse)
}

Account​

FunctionReturns
emailRegisteredCheck(email:completion:)ApiResponse<Data>
recoverLostPassword(email:completion:)ApiResponse<Data>
getTokenViaUserPassword(username:password:completion:)ApiResponse<LoginResponse>
registerNewUser(user:completion:)ApiResponse<LoginResponse>
logout()synchronous
getMe(completion:)ApiResponse<Data>

App / Configuration​

FunctionReturns
getAppConfig(completion:)ApiResponse<AppConfig>
postSurvey(body:completion:)ApiResponse<Data>
fetchOffers(country:state:zip:city:completion:)ApiResponse<Data>

AppConfig contains supported Country and Province values. For warranty requests, use the returned IDs.

Voucher​

getVoucherByVoucherCode(
voucherCode:isPlanTransfer:activeWarrantiesInCurrentDevice:completion:
)

Returns:

ApiResponse<Voucher>

Item / Device​

FunctionReturns
getItemByDeviceIdPublic(completion:)ApiResponse<DeviceDetail>
getItemByDeviceID(completion:)ApiResponse<ItemByDeviceIdResponse>
createSensorEvent(itemId:body:completion:)ApiResponse<UpdateItemResponse>
updateFCMToken(itemId:fcmToken:completion:)ApiResponse<Data>

Normal host apps should prefer the top-level:

WarrantyLifeSDK.updateFCMToken(...)

rather than manually managing Item IDs.

Device insights​

Fetch the current device's drop rankings, battery rankings, coaching hints, monthly warranty rewards, and Trade-In Protection (TIP) data in one request through ApiRepository. The method uses the SDK's device identity and authenticated session, following the existing device lookup methods. Call it after SDK initialization, sign-in, and device registration.

FunctionReturns
getInsightData(completion:)ApiResponse<InsightData> through the completion handler
// ApiRepository
public func getInsightData(
completion: @escaping (ApiResponse<InsightData>) -> Void
)

Obtain the repository through the normal SDK entry point:

guard let repository = WarrantyLifeSDK.getApiRepository() else { return }

repository.getInsightData { response in
DispatchQueue.main.async {
switch response {
case .success(let success):
guard let insights = success.data else {
showInsightsUnavailable()
return
}

// Use insights.dropRank, insights.batteryRank,
// insights.coaching, insights.rewards, and insights.tip
// to populate the screen.
renderInsights(insights)

case .error(let error):
showError(error.errorMessage ?? error.error)
}
}
}

renderInsights, showInsightsUnavailable, and showError represent host app UI handling. Dispatch UI updates to the main queue. A successful response can still have no data; show an unavailable state in that case. Handle failures through the existing ApiErrorResponse fields, including errorCode when needed.

Insight models​

The SDK exposes these public models with the same field names as the payload. dropRank and batteryRank share the same history type. rewards.current, rewards.projected, and each rewards.history entry share the same monthly reward type. tip.current and each tip.daily entry share the same dated value type.

public struct InsightData: Codable {
public let generatedAt: String
public let timeZone: String
public let dropRank: InsightRankHistory
public let batteryRank: InsightRankHistory
public let coaching: InsightCoaching
public let rewards: InsightRewards
public let tip: InsightTipHistory
}

public struct InsightRankHistory: Codable {
public let hourly: [HourlyInsightRank]
public let daily: [DailyInsightRank]
public let weekly: [WeeklyInsightRank]
public let monthly: [MonthlyInsightRank]
}

public struct HourlyInsightRank: Codable {
public let hour: Int
public let date: String
public let rank: Int
public let isProjected: Bool
}

public struct DailyInsightRank: Codable {
public let date: String
public let rank: Int
public let isProjected: Bool
}

public struct WeeklyInsightRank: Codable {
public let startDate: String
public let rank: Int
public let isProjected: Bool
}

public struct MonthlyInsightRank: Codable {
public let month: String
public let rank: Int
public let isProjected: Bool
}

public struct InsightCoaching: Codable {
public let generalHints: [String]
public let batteryHints: [String]
public let tipHints: [String]
}

public struct InsightRewards: Codable {
public let current: InsightReward?
public let projected: InsightReward?
public let history: [InsightReward]
}

public struct InsightReward: Codable {
public let month: String
public let amount: Double
}

public struct InsightTipHistory: Codable {
public let current: DailyInsightTip
public let daily: [DailyInsightTip]
public let weekly: [WeeklyInsightTip]
public let monthly: [MonthlyInsightTip]
}

public struct DailyInsightTip: Codable {
public let date: String
public let percentage: Double
public let value: Double
}

public struct WeeklyInsightTip: Codable {
public let startDate: String
public let percentage: Double
public let value: Double
}

public struct MonthlyInsightTip: Codable {
public let month: String
public let percentage: Double
public let value: Double
}

Reading the insight data​

FieldMeaning / display guidance
generatedAtServer snapshot timestamp in ISO 8601 format, for example 2026-09-11T22:17:43Z. Indicates when the data was generated.
timeZoneTime zone for the calendar periods, for example America/Toronto. Use this zone when interpreting history dates and hours. Since the time zone used is based off of the device's timezone, the pertinent data is already given in the device's timezone in order to avoid the need to do any timezone conversion.
dropRank, batteryRankSeparate ranking history data, each with hourly, daily, weekly, and monthly arrays.
rankServer-provided integer rank, range from 1 to 100.
isProjectedWhether that individual rank is for a current time period for which the final result is not yet determined. A projected rank can still change until the end of the time period.
coaching.generalHints, coaching.batteryHints, coaching.tipHintsArrays of translated, display-ready hints grouped by topic appropriate to the user's performance.
rewards.currentReward currently assigned to the warranty, including the UTC month in which it became effective, or nil when the device has no active reward-eligible warranty or no current reward.
rewards.projectedNon-persisted estimate for the next UTC month's reward, or nil when the estimate cannot be calculated. It can change until the monthly reward is granted.
rewards.historyPersisted monthly rewards for up to the current and previous 11 UTC months, ordered by month. The array can be empty and months are not guaranteed to be consecutive.
rewards.*.monthEffective UTC month in YYYY-MM format.
rewards.*.amountNumeric warranty reward amount. Currency is not included; format it using the warranty or integration currency.
tip.currentCurrent Trade-In Prediction (TIP) data.
tip.daily, tip.weekly, tip.monthlyTIP history. Note that TIP has no hourly series or isProjected field in this payload.
percentagePercentage in percentage units: 82.0 is displayed as 82%, not multiplied by 100.
valueNumeric TIP value. No currency is included in this payload; use the currency agreed for TIP values in your integration when formatting an amount.

Hourly ranks use date (YYYY-MM-DD) plus hour (0–23); daily entries use date; weekly entries use startDate (YYYY-MM-DD); monthly entries use month (YYYY-MM). These date/time values are in the device's timezone. Use the returned weekly start date rather than assuming a particular weekday.

Handle empty history and hint arrays as empty states. Treat rewards.current and rewards.projected as optional, and do not synthesize a reward when either is nil. Do not assume a fixed history length, consecutive periods, or that separate series contain matching dates. Use the period fields when aligning chart data, and leave missing periods unavailable rather than inventing zero values. Fetch a new snapshot when the insights screen loads.

Warranty​

FunctionReturns
getAllWarranties(completion:)ApiResponse<[Warranty]>
createWarranty(model:completion:)ApiResponse<Warranty>
updateWarranty(model:completion:)ApiResponse<Warranty>
getWarrantyById(warrantyId:completion:)ApiResponse<Warranty>
updateWarrantyById(warrantyId:rewardAmount:completion:)ApiResponse<Warranty>

Registration​

Recommended entry point:

WarrantyLifeSDK.registerDevice(stateHandler:)

The repository also exposes:

repository.registerDevice(stateHandler:)

Prefer the top-level SDK method unless the host already owns a repository reference.

Diagnostic upload APIs​

The repository exposes lower-level diagnostic upload APIs, including:

uploadSensorReadingsWithRetry(...)
uploadSensorReadings(...)
createItemDiagnosticReport(...)
uploadImageForItem(...)
uploadImageForReference(...)

Normal registration UI should call registerDevice(...) and let the SDK orchestrate these stages instead of manually reproducing the upload workflow.

Feedback​

repository.postFeedback(model: completion:)

AppDataManager Reference​

Obtain:

guard let appDataManager = WarrantyLifeSDK.getAppDataManager() else { return }

Do not read or write the SDK's private UserDefaults keys directly.

Authentication / device​

isUserLoggedIn()
getIdentity()

getDeviceID()
setDeviceID(_:)
getItemID()

hasActiveCoverage()

setDeviceID(_:) is public but should not normally be used by a production host app. The SDK initializes its device identity from identifierForVendor and persists it in Keychain.

Voucher / registration​

getCurrentVoucherCode()
isVoucherConfirmed()
isVoucherCorrect()
isTransferable()

isReceiptRequired()
isPurchaseDateRequired()

isDeviceTransferable()
askIfDeviceIsInsured()
skipRegistrationForm()
getRegistrationDeadlineDays()
getImeiLocked()
isDeviceTransferInProgress()
getEncodedWarrantyID()

IMEI / receipt​

setIMEI(_:)
getIMEI()

setReceiptDate(_:)
getReceiptDate()

setReceiptImagePath(_:)
getReceiptImagePath()

Warranty / reward​

getWarrantyId()
getWarrantyStatus()
getRegistrationCode()

getPlanCurrency()
getRewardAmount()
getRewardAmountMax()
getRewardType()
getTradeInScore()

getCountWarrantyOnCurrentDevice()

Smart Saver​

setSSR(_:completion:)
getSSR()
hasSsr()

setSSD(_:completion:)
getSSD()
hasSsd()

setTradeIn(_:completion:)
getTradeIn()
isTipOffered()

isSmartSaverEnabled()
canProcessDropDetection()

Diagnostic requirements​

isAutoTestRequired()
isSensorTestRequired()
isFrontCameraTestRequired()
isBackCameraTestRequired()
isOpenFaceTestRequired()
isSensorTestAvailable()

Upload status​

getDiagnosticsId()
isDiagnosticReportUploaded()

isFrontGlassTestUploaded()
isBackGlassTestUploaded()
isOpenFaceTestUploaded()
isReceiptUploaded()
isSensorTestUploaded()

Registration Upload Reliability​

The current iOS registration uploader is designed so a retry does not blindly recreate every previously successful object.

This behavior is important for host integration: if registerDevice returns an upload error, the host may retry registerDevice; the SDK will reuse already-confirmed upload stages when the underlying data is unchanged.

Sensor Event basic data​

The registration Sensor Event is keyed by its current:

eventAt

If the same Sensor Test is retried and eventAt matches a previously successful Sensor Event upload, the SDK reuses the stored Sensor Event ID instead of creating another basic-data Sensor Event.

A new Sensor Test produces a new eventAt, so it is automatically treated as new data.

Sensor Readings​

Sensor Readings upload status is tracked separately from Sensor Event basic data.

Therefore:

Basic Data succeeded
Readings failed

does not cause Basic Data to be recreated during the next retry.

The SDK reuses the Sensor Event ID and retries the missing Sensor Readings stage.

Sensor audio​

Sensor audio is also tracked separately using the current Sensor Test eventAt.

If the audio upload already succeeded for that test, a later retry skips it.

Diagnostic report​

The SDK generates a SHA-256 fingerprint from the sorted diagnostic request JSON.

If the diagnostic request is unchanged:

same request hash
→ reuse Diagnostic ID

If diagnostic data changes:

different request hash
→ create a new Diagnostic report

Images and receipt​

Front-glass, back-glass, and receipt uploads use a SHA-256 content fingerprint together with the current Diagnostic ID.

An image is skipped only when both are true:

same image content
AND
same Diagnostic ID

If the photo changes, or a new Diagnostic report is created, the image is uploaded again.

Retry policy​

Sensor Readings and diagnostic image uploads currently retry up to 4 total attempts for transient errors including:

network/no response (-1)
408
429
500
502
503
504

Backoff:

1 second
2 seconds
4 seconds

Non-retryable errors are returned to the registration state handler immediately.


Error Handling​

Standard API errors​

public struct ApiErrorResponse {
public let error: String
public let errorMessage: String?
public let errorCode: Int
}

Use:

error.errorMessage ?? error.error

as the user/debug-readable message.

A network/no-response condition may use:

errorCode = -1

Some SDK local validation errors may use code 0 or -1 depending on the API path.

Register Device errors​

Two classes are intentionally different:

.validationError(...)

means required local registration data is missing before the upload can proceed.

.uploadError(message, code)

means the network/upload workflow could not complete.

Smart Saver errors​

Use:

error.message
error.errorCode

from OptInError.

Diagnostic errors​

WLDiagnosticError includes categories for:

  • microphone/sensor;
  • camera/glass;
  • Bluetooth;
  • Location/Wi-Fi;
  • orientation/movement;
  • test configuration.

Each error exposes:

error.message

Sentry​

When Sentry is enabled, HTTP failures handled by the SDK networking layer are also eligible for Sentry reporting. Sentry reporting is supplementary; the normal ApiResponse.error must still be handled by the host app.


The current iOS SDK does not own deep-link routing.

The host app may use:

  • custom URL schemes; or
  • Universal Links / Associated Domains.

The URL should contain the voucher code, for example:

warrantylife://voucher?code=ABC-1234-XYZ

After extracting the code, call:

repository.getVoucherByVoucherCode(
voucherCode: code,
isPlanTransfer: false
) { response in
// Navigate using Voucher.action
}

Example custom-scheme handling in an AppDelegate-based app:

func application(
_ app: UIApplication,
open url: URL,
options: [UIApplication.OpenURLOptionsKey: Any] = [:]
) -> Bool {
guard
let components = URLComponents(url: url, resolvingAgainstBaseURL: false),
let code = components.queryItems?.first(where: { $0.name == "code" })?.value,
let repository = WarrantyLifeSDK.getApiRepository()
else {
return false
}

repository.getVoucherByVoucherCode(
voucherCode: code,
isPlanTransfer: false
) { response in
// Route registration UI
}

return true
}

For Universal Links, the host app owns Associated Domains configuration and NSUserActivity handling.

The current iOS SDK does not provide an Android-style Install Referrer/deferred-deep-link implementation. If deferred deep linking after App Store installation is required, implement it at the host application/product-link layer.


Integration Checklist​

Core​

  • SDK deployment target supports iOS 15.0+
  • WLSDKIOS linked to host app
  • Alamofire linked to SDK target
  • SwiftyJSON linked to SDK target
  • GzipSwift linked to SDK target
  • Sentry Cocoa linked to SDK target
  • TensorFlowLiteTaskVision linked to SDK target
  • WLSDKIOS.bundle and TFLite model included
  • WarrantyLifeSDK.build(...) called exactly once during application launch
  • UIKit + SceneDelegate apps initialize from AppDelegate, not SceneDelegate
  • SwiftUI apps use @UIApplicationDelegateAdaptor for application-level SDK initialization
  • SwiftUI views / scenePhase do not initialize or rebuild the SDK
  • Valid clientCode supplied
  • Valid WarrantyLife Developer apiKey supplied
  • sentryClientKey supplied or intentionally nil/empty

Privacy / permissions​

  • NSBluetoothAlwaysUsageDescription
  • NSLocationWhenInUseUsageDescription
  • NSLocationAlwaysAndWhenInUseUsageDescription if Location Always may be requested
  • NSMotionUsageDescription
  • NSCameraUsageDescription
  • NSMicrophoneUsageDescription when Sensor Test audio is used
  • Host app owns permission request timing

Capabilities​

  • Access WiFi Information
  • Push Notifications
  • Background Modes → Background fetch
  • Background Modes → Background processing
  • Background Modes → Remote notifications
  • Both WarrantyLife BGTask identifiers listed in BGTaskSchedulerPermittedIdentifiers

Registration​

  • Validate voucher before registration
  • Route UI using Voucher.action
  • Route registration form using Voucher.registrationType
  • Run only required diagnostics
  • Store receipt image path when required
  • Set IMEI before registerDevice
  • Handle all RegisterDeviceState cases
  • Store purchase date when required
  • Save Smart Saver selections
  • Call createWarranty or updateWarranty for the selected flow
  • On registration retry, let the SDK reuse completed upload stages

Smart Saver / Drop Detection​

  • Understand required permission formula: Motion && (Notification || LocationAlways)
  • Do not require all three permissions
  • Do not build a duplicate host accelerometer Drop Detection loop
  • Forward silent push to WarrantyLifeSDK.handleRemoteNotification
  • Allow SDK AppSession foreground refresh to restore current coverage/offer state
  • Call repository.logout() for logout rather than clearing SDK storage manually

Diagnostics​

  • Register TestManager result/state handlers before starting tests
  • Load a voucher/server configuration before running Sensor Test when configuration is required
  • Check isSensorTestAvailable() before starting Sensor Test
  • Camera permission granted before Glass Test
  • Microphone permission granted when configured Sensor Test uses audio
  • Handle WLDiagnosticError
  • Keep Glass Test resource bundle/model packaged with SDK

FCM​

  • Forward current FCM token with WarrantyLifeSDK.updateFCMToken
  • Forward token again after Item registration if the token was first received before an Item existed
  • Forward future token refreshes

Platform Differences from the Android SDK​

The following Android concepts are not part of the current iOS public SDK and should not be copied into an iOS integration:

  • Hilt dependency injection setup
  • WorkManager / WLWorkerFactory
  • Android foreground-service permissions
  • Android foreground notification channels/customization
  • ProGuard / R8 rules
  • Android ActivityLifecycleCallbacks
  • Android Install Referrer API
  • Android setDeviceTransferMode(...)
  • Android Try Before You Buy APIs currently shown in the Android guide
  • Android separate Trade-In registration flow currently shown in the Android guide
  • Android device-change callback API

The equivalent iOS behavior is implemented through one application-level lifecycle coordinator, UIKit application notifications, Scene-aware UIKit presentation, SwiftUI application-delegate bridging, BackgroundTasks, Core Location significant-change monitoring, silent push, SDK-owned storage, and the iOS public APIs documented above.