iOS Integration Guide
Requirements
| Item | Requirement |
|---|---|
| Minimum iOS | iOS 15.0+ |
| Language | Swift |
| Host UI framework | UIKit or SwiftUI |
| SDK diagnostic UI | UIKit |
| Networking | Alamofire |
| Error monitoring | Sentry Cocoa |
| Glass object detection | TensorFlow Lite Task Vision |
| Motion / Drop Detection | CoreMotion |
| Bluetooth | CoreBluetooth |
| Location | CoreLocation |
| Wi-Fi information | NetworkExtension / CaptiveNetwork |
| Camera / microphone | AVFoundation |
| Background tasks | BackgroundTasks |
| Push / notification status | UserNotifications |
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;scenePhasechanges;- a
SceneDelegatecallback.
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:
| Result | Meaning | SDK behavior |
|---|---|---|
valid | Key accepted | Normal requests continue |
invalid | Server returned 401 for key validation | SDK blocks normal WarrantyLife server requests locally |
unavailable | Network/server/temporary validation problem | Key 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
| Feature | Host permission/configuration |
|---|---|
| Auto Test Bluetooth | Bluetooth permission + Bluetooth usage description |
| Auto Test Wi-Fi | Location When In Use or Always + Access WiFi Information capability |
| Sensor Test motion | Motion & Fitness |
| Sensor Test audio | Microphone, when audio recording is enabled by server configuration |
| Glass Test | Camera |
| Drop Detection | See 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:
| Motion | Notifications | Location Always | Required permissions satisfied |
|---|---|---|---|
| Yes | Yes | No | Yes |
| Yes | No | Yes | Yes |
| Yes | Yes | Yes | Yes |
| Yes | No | No | No |
| No | Yes | Yes | No |
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:
- User is logged in
- Active coverage exists
- The option is offered by the current warranty/plan
- Required Drop Detection permissions are satisfied
- Current Item ID exists
- 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:
CMSensorRecorderrecorded 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:
- persisted Drop Detection state is enabled;
- user is logged in;
- active coverage exists;
- at least one offered Smart Saver option is opted in;
- 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
| Function | Returns |
|---|---|
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
| Function | Returns |
|---|---|
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
| Function | Returns |
|---|---|
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.
| Function | Returns |
|---|---|
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
| Field | Meaning / display guidance |
|---|---|
generatedAt | Server snapshot timestamp in ISO 8601 format, for example 2026-09-11T22:17:43Z. Indicates when the data was generated. |
timeZone | Time 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, batteryRank | Separate ranking history data, each with hourly, daily, weekly, and monthly arrays. |
rank | Server-provided integer rank, range from 1 to 100. |
isProjected | Whether 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.tipHints | Arrays of translated, display-ready hints grouped by topic appropriate to the user's performance. |
rewards.current | Reward 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.projected | Non-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.history | Persisted 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.*.month | Effective UTC month in YYYY-MM format. |
rewards.*.amount | Numeric warranty reward amount. Currency is not included; format it using the warranty or integration currency. |
tip.current | Current Trade-In Prediction (TIP) data. |
tip.daily, tip.weekly, tip.monthly | TIP history. Note that TIP has no hourly series or isProjected field in this payload. |
percentage | Percentage in percentage units: 82.0 is displayed as 82%, not multiplied by 100. |
value | Numeric 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
| Function | Returns |
|---|---|
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.
Deep Link Integration
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+
-
WLSDKIOSlinked 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.bundleand TFLite model included -
WarrantyLifeSDK.build(...)called exactly once during application launch - UIKit + SceneDelegate apps initialize from
AppDelegate, notSceneDelegate - SwiftUI apps use
@UIApplicationDelegateAdaptorfor application-level SDK initialization - SwiftUI views /
scenePhasedo not initialize or rebuild the SDK - Valid
clientCodesupplied - Valid WarrantyLife Developer
apiKeysupplied -
sentryClientKeysupplied or intentionally nil/empty
Privacy / permissions
-
NSBluetoothAlwaysUsageDescription -
NSLocationWhenInUseUsageDescription -
NSLocationAlwaysAndWhenInUseUsageDescriptionif Location Always may be requested -
NSMotionUsageDescription -
NSCameraUsageDescription -
NSMicrophoneUsageDescriptionwhen 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
RegisterDeviceStatecases - Store purchase date when required
- Save Smart Saver selections
- Call
createWarrantyorupdateWarrantyfor 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
TestManagerresult/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.