Field Track 360

Developer guide

Integrate the SDK

Native SDKs for Android and iOS, with bridges for React Native and Flutter. Pick your platform - the setup genuinely differs, so these are not one page.

Install

iOS 17+, Swift 6. Five binary frameworks; hosts never receive source.

In Xcode: File > Add Package Dependencies, then paste the distribution URL. Or in a Package.swift:

.package(url: "https://github.com/Bhargav-4793/trackit-ios-dist", from: "1.0.1")

Add only the products you need. These are binary targets, so each product already carries what it depends on - adding TrackItCore gives you TrackItGeo too.

The products

  • TrackItCore - required. Capture, storage, background execution, permissions.
  • TrackItGeo - ships with every product. The public types: TrackPoint, Track, MotionState.
  • TrackItMaps - optional. MapKit rendering for finished and live tracks.
  • TrackItSnap - optional. OSRM map matching behind RoadSnapProvider.
  • TrackItSync - optional. Upload with a retry queue and 401 teardown.

Host configuration - not optional

Without these Info.plist keys the SDK cannot capture in the background, and registration raises an exception no Swift catch can reach.

<key>NSLocationWhenInUseUsageDescription</key>
<string>Records your route while you are using the app.</string>
<key>NSLocationAlwaysAndWhenInUseUsageDescription</key>
<string>Records your route in the background so your trips are complete even when the app is closed.</string>
<key>NSMotionUsageDescription</key>
<string>Detects when you start and stop moving, to save battery.</string>

<key>UIBackgroundModes</key>
<array>
    <string>location</string>
    <string>processing</string>
</array>

<key>BGTaskSchedulerPermittedIdentifiers</key>
<array>
    <string>com.devstree.trackit.backstop</string>
    <string>com.devstree.trackit.sync</string>
</array>

The identifiers must appear verbatim. A mismatch is not a degraded backstop - it is an exception at registration.

Quick start

ready() belongs in your App initialiser, not a view task.

ready() registers the background task handler, and registering after launch completes throws.

import SwiftUI
import TrackItCore

@main
struct MyApp: App {
    init() {
        Task { _ = await TrackIt.shared.ready() }
    }

    var body: some Scene {
        WindowGroup { ContentView() }
    }
}

The permission ladder is ordered

Always can only be requested from When-In-Use.

let tier = await TrackIt.shared.permissions().requestWhenInUse()
guard tier != .none else { return }

switch await TrackIt.shared.permissions().requestAlways() {
case .alreadyGranted, .granted: break
case .needsWhenInUseFirst, .denied: return
case .needsSettings(let url): await UIApplication.shared.open(url); return
@unknown default: return
}

switch await TrackIt.shared.start(tag: "delivery-run") {
case .success(let session): print("recording \(session.id)")
case .failure(let code, let message): print("refused - \(code.rawValue): \(message)")
@unknown default: break
}

Why @unknown default

The SDK never throws into your task; it returns TrackItResult. Every public enum is non-frozen, because this ships as a binary with library evolution enabled. Your switch needs @unknown default so a newer SDK adding a case does not break your build - and fold the unknown into the conservative branch, because an outcome you cannot read is not evidence that something succeeded.

Geofences and dwell

Circular regions that fire whether or not you are recording - plus the two ways crossings arrive.

addGeofence needs ready() and location authorization, but not a session. A fence fires whether or not you are recording, and keeps firing after stop().

A fence armed around where you already are fires enter immediately. CoreLocation reports transitions only, so without this a fence created at your current position would report nothing until you left and came back.

Crossings arrive two ways, and you need both

While your app runs, events() carries them live. But iOS relaunches a terminated app to deliver a crossing, and at that moment nothing is subscribed - the event stream has no replay. Every crossing is therefore also written to disk:

let crossings = try await TrackIt.shared.getGeofenceEvents(limit: 50)

What the platform imposes

  • 20 regions per app, shared with anything you monitor yourself. The SDK reserves one, so 19 are available.
  • ~100 m minimum radius. Smaller regions fire unreliably; a smaller fence is accepted and emits a diagnostic.
  • Always authorization is needed to wake a terminated app.
  • Re-using an id replaces that fence, with no window where neither exists.

Dwell

iOS has no dwell transition, so the SDK synthesises it. What that costs you is timing, not truth: the recorded event's timeMs is the moment the condition was met, not the moment the SDK noticed. A dwell reported at 15:10 for a condition met at 14:32 says 14:32.

Licensing

Checked once in ready(), entirely offline. Development needs no token.

A licence token is issued for your bundle identifier plus its .dev and .staging variants, and checked once in ready() with no server call.

The simulator and any build run from Xcode skip the check, so you can evaluate the whole SDK before buying. A token is required only in distributed builds: App Store, TestFlight, ad-hoc and enterprise.

Installing the token

Info.plist is the recommended route, and needs no code change:

<key>TrackItLicense</key>
<string>TRACKIT-eyJ…</string>

Or pass it in code when the token arrives at runtime:

await TrackIt.shared.ready(
    TrackItConfig.builder()
        .license("TRACKIT-eyJ…")
        .buildUnchecked()
)

When both are set, the code value wins. The token is not a secret - it only works for the bundle identifiers it was issued for - so committing it in the plist is fine.

If ready() refuses

  • licenseMissing - no token found. Purchase a licence for this app.
  • licenseInvalid - malformed or altered. Re-copy from the issue email; the message says what failed.
  • licenseBundleMismatch - genuine token, different app. The message names both identifiers.

A refused ready() never crashes. It returns the code and message, and your app decides what to show.

Why we verify licences on the device, not on our servers

A tracking app that stops because a licence server was unreachable is worse than a licence that survives a few hours too long.

Every Field Track 360 licence is verified on the device, with no network call. The token carries its own signature, the SDK carries the public key, and the check happens locally in milliseconds.

The trade we made

We also check periodically to see whether a licence has been revoked. That check can fail - no signal, a flat DNS, our own outage. When it does, the SDK keeps working on its cached verdict.

That is deliberate, and it costs us something: a revoked licence can survive until the device next reaches the network. We think that is the right side to fail on. A field team's tracking going dark mid-shift because a server blinked is a far worse outcome than a licence lasting a few hours longer than it should.

What it means for you

Your app starts tracking on a plane, on first launch, in a tunnel, after a reboot. There is no licence server in the path of your users doing their jobs.

Application keys: what one key actually covers

One key, one app identifier, both platforms, forever. Here is why it is bound that way and what to do when the id changes.

An application key licenses one app identifier - your Android applicationId or your iOS CFBundleIdentifier - on both platforms.

Why is it bound to the identifier? Because the identifier is cryptographically signed into the token itself. That is what makes a key impossible to copy into a second app: change the identifier and the signature no longer matches. It also means the binding cannot be edited afterwards, by us or by anyone else.

Debug and staging builds

Variants are included, as long as they extend your identifier with a dot. com.acme.app.dev and com.acme.app.staging are covered by a key for com.acme.app. com.acme.appX is a different application and is not - the dot is what separates a variant from a namesake.

When the identifier changes

It happens: a rebrand, a store migration, a typo caught late. Generate a corrected key from your account and the old one is revoked automatically. There is a limit per plan, because a revoked key still verifies offline, and support can help beyond it.

Try it before you buy

A 30-day trial licence for one application, issued instantly. Development builds are licence-waived, so you can evaluate the whole SDK first.

Get a trial key

Verification API

The SDK handles licensing for you. This is documented for tooling.

POST https://fieldtrack360-sdk.devstree.in/api/v1/verify