@enfinitos/sdk-mobile
EnfinitOS reference SDK for the MOBILE substrate. Four sibling implementations — iOS (Swift), Android (Kotlin), React Native (New Architecture), Flutter — all backed by the canonical Swift + Kotlin modules and built on top of @enfinitos/sdk-renderer-core.
Architecture
┌──────────────────────────────────────────┐
│ @enfinitos/sdk-renderer-core (TS) │
│ resolve / grant / event-ingest / health│
└──────────────────────────────────────────┘
▲
│ bridges
│
┌───────────────┬───────────────┼───────────────┬───────────────┐
│ │ │ │ │
┌───────┐ ┌─────────┐ ┌────────────┐ ┌───────────┐
│ iOS │ │ Android │ │ React Nat. │ │ Flutter │
│ Swift │ │ Kotlin │ │ TurboModule│ │ Dart │
└───────┘ └─────────┘ └────────────┘ └───────────┘
│ │ │ │
│ │ │ │
AppTracking- Privacy Both iOS & MethodChannel
Transparency, Sandbox Android via to canonical
SKAdNetwork v4, (Topics + Codegen spec Swift / Kotlin
Universal Links AR API), (Native…Spec.ts) modules.
Topics API,
App Links
The Swift + Kotlin modules are canonical: every feature gets implemented there first. React Native + Flutter wrap them via TurboModule (RN 0.76+ New Architecture) and MethodChannel respectively.
Getting started
iOS (Swift)
Add the package to your Xcode project via SwiftPM:
.package(url: "https://github.com/enfinitos/sdk-mobile-ios.git", from: "0.0.1")
The package ships its own PrivacyInfo.xcprivacy; SwiftPM bundles it into your app's resources at build time. Add the NSUserTrackingUsageDescription to your app's Info.plist:
<key>NSUserTrackingUsageDescription</key>
<string>We use anonymous data to show relevant ads.</string>
30-line minimum integration:
import EnfinitOSMobile
let client = EnfinitOSMobileClient(
apiBaseUrl: URL(string: "https://api.enfinitos.com")!,
appId: "com.acme.shop",
deviceId: sessionDeviceId,
apiKey: jwt,
)
try await client.start()
_ = await client.requestTrackingAuthorization()
let slot = MobileAdSlot(
position: .banner,
surfaceId: "home-feed-card-7",
size: SurfaceSize(widthPx: 320, heightPx: 50),
)
if let resolved = try await client.fetchAdSlot(slot) {
renderInBannerSurface(resolved.asset)
await client.reportImpression(resolved.asset)
}
client.stop()
Android (Kotlin)
Gradle:
implementation("com.enfinitos:sdk-mobile:0.0.1")
Manifest:
<!-- For App Links click-through tracking. -->
<intent-filter android:autoVerify="true">
<action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.BROWSABLE" />
<data android:scheme="https" android:host="link.enfinitos.com" />
</intent-filter>
30-line minimum integration:
val client = EnfinitOSMobileClient(
context = applicationContext,
apiBaseUrl = "https://api.enfinitos.com",
appId = "com.acme.shop",
deviceId = sessionDeviceId,
apiKey = jwt,
)
client.start()
val slot = MobileAdSlot(
position = AdSlotPosition.BANNER,
surfaceId = "home-feed-card-7",
size = SurfaceSize(320, 50),
)
val resolved = client.fetchAdSlot(slot)
if (resolved != null) {
renderInBannerSurface(resolved.asset)
client.reportImpression(resolved.asset)
}
client.stop()
React Native (0.76+ New Architecture)
npm install @enfinitos/sdk-mobile-react-native
cd ios && pod install
The TurboModule spec lives in src/NativeEnfinitOSMobileSpec.ts; the Codegen pipeline picks it up automatically at build time.
import { EnfinitOSMobileClient } from '@enfinitos/sdk-mobile-react-native';
import NativeEnfinitOSMobile from '@enfinitos/sdk-mobile-react-native/NativeEnfinitOSMobileSpec';
const client = new EnfinitOSMobileClient({
apiBaseUrl: 'https://api.enfinitos.com',
appId: 'com.acme.shop',
deviceId: sessionDeviceId,
apiKey: jwt,
bridge: makeBridgeFromTurboModule(NativeEnfinitOSMobile),
});
await client.start();
const resolved = await client.fetchAdSlot('banner');
Flutter
dependencies:
enfinitos_mobile: ^0.0.1
import 'package:enfinitos_mobile/enfinitos_mobile.dart';
final client = EnfinitOSMobileClient(
apiBaseUrl: 'https://api.enfinitos.com',
appId: 'com.acme.shop',
deviceId: sessionDeviceId,
apiKey: jwt,
);
await client.start();
final resolved = await client.fetchAdSlot(const MobileAdSlot(
position: AdSlotPosition.banner,
surfaceId: 'home-feed-card-7',
));
2026 platform notes
iOS
- Minimum target: iOS 17. Buys us the Observable macro, Swift Concurrency 5.10's stricter Sendable diagnostics, and SKAdNetwork v4's coarse-grain conversion callbacks without
#availableshims. iOS 16-targeted apps stick to v0.0.x; v0.1 onwards requires 17. - Swift Concurrency is mandatory. Every async method is
async throws. No(Result<T>) -> Voidcompletion handlers in the public API. Combine publisher + AsyncSequence accessors are available for reactive code (ConsentManager.publisher,ConsentManager.changes). - App Tracking Transparency uses the four-state enum:
notDetermined / restricted / denied / authorized. The SDK treats anything that isn'tauthorizedas "do not ship IDFA". - PrivacyInfo.xcprivacy ships in the package — declares the four Required Reason API categories (UserDefaults C617.1, system boot time DDA9.1, file timestamp 35F9.1, active keyboard 54BD.1) and the data types the SDK puts on the wire (Advertising Data + Device ID under the user's tracking-consented state, Crash + Performance diagnostics unlinked + non-tracking).
- SKAdNetwork v4 is the canonical post-IDFA attribution path. The conduit in
SKAdNetwork.swiftwires the host app's conversion events throughSKAdNetwork.updatePostbackConversionValue(the iOS 16.1+ async-throws API). The deprecated v3updateConversionValue:completion-handler path is never called. - visionOS is out of scope for this SDK — see
packages/sdks/spatial-ar/visionos/for the visionOS SDK.
Android
- Minimum SDK: 33 (Android 13 Tiramisu). Target SDK 34 (Android 14, the Play Store floor since Aug 2024). Compile SDK 35 (Android 15 Vanilla Ice Cream) so the build picks up the latest stub APIs.
- Kotlin Coroutines + Flow are the primary async surface. No RxJava, no LiveData. The consent state is exposed as
StateFlow<ConsentState>so host apps using Jetpack Compose cancollectAsState()it. - Privacy Sandbox is the modern attribution path: - Topics API: host app calls
TopicsManager.getTopicsAsync(via the AndroidX wrapper); the SDK records the assignment on the consent envelope. - Attribution Reporting API: source + trigger registration helpers inPrivacySandbox.kt. The OS dispatches aggregate reports to the platform's AR endpoint; no per-event data crosses domains. - Protected Audience API: outcome-only event "remarketing- auction-won" the host app fires when its mediation layer reports a winning bid. All three are runtime-feature-gated; a pre-Privacy-Sandbox device (or one whose user has opted out at the OS level) sees the conduit return null and the SDK ships its events without the Sandbox fields. - AAID deprecation: AAID is still readable on Android 13+ but the SDK prefers Privacy Sandbox primitives. AAID reads happen through a reflective
AdvertisingIdClientlookup so apps that don't linkplay-services-ads-identifierstill compile. - Jetpack Compose is the supported UI primitive. The optional consent dialog (
enfinitos-mobile-consent-uiartifact) is the only Compose-dependent surface; the core SDK has no UI. - Wear OS 5 uses Compose for Wear OS + Tiles 2.0; the wearables variant of this SDK lives under
packages/sdks/wearables/(future tranche).
React Native
- 0.76+ New Architecture only. Fabric + TurboModules + Codegen. The Old Architecture path (
NativeModules.*via async bridging) is deprecated and slated for removal in 0.80; this SDK ships no Old-Arch code. - Codegen spec lives in
src/NativeEnfinitOSMobileSpec.ts. The React Native CLI picks it up at build time and generates: - Objective-C++ protocol stubs (iOS). - Kotlin abstract-class stubs (Android). - JS bindings the SDK's TS code imports. - Hermes is the supported JS engine in 2026 — the SDK does not rely on JSC-specific features.
Flutter
- Flutter 3.22+ with Material 3 + Impeller renderer. Older Flutter versions stick to v0.0.x of the plugin.
- Dart 3.4+. The plugin uses sealed classes for the consent state machine and records for ad-hoc tuples; both are 3.0+ features.
- Federated plugin convention via
plugin_platform_interface. A futureenfinitos_mobile_websibling can be added for the browser case without breaking the existing API.
Attribution model
In 2026 the IDFA/AAID-as-primary-key world is mostly dead. The SDK defaults to server-side / cohort-based attribution:
- iOS: SKAdNetwork v4. The ad network registers as the
source; postbacks arrive at a backend the operator runs (NOT at the host app). The SDK conduit only manages the on-device conversion-value update. - Android: Privacy Sandbox Attribution Reporting. Source + trigger registration happens on-device; the OS dispatches aggregate reports to the platform's AR endpoint.
- Universal Links / App Links are the click-through path. The link carries
(campaignId, creativeId, surfaceId, nonce)— never a user identifier. The platform's server-side attribution joins the receipt to the proof-of-play stream by tuple.
ProofReporter ships the consent envelope on every event but the advertising id is zeroed by default; it's only attached when the user is authorized on iOS or has not enabled limit-ad-tracking on Android, and consent.deviceIdAllowed == true.
Endpoint surface
| SDK call | Platform endpoint | Status |
|---|---|---|
fetchAdSlot | POST /runtime/resolve then POST /runtime/grant | existing |
reportImpression/Click/Skip/Complete | POST /runtime/event-ingest | existing |
reportConversion | POST /runtime/event-ingest + (iOS) SKAdNetwork postback to network endpoint | existing |
setConsentState | side-effect on next event | existing |
parseClickThroughLink (host call) | POST /runtime/event-ingest with kind=click-through-receipt | existing |
| Privacy Sandbox Attribution Reporting register | OS-mediated; aggregate reports posted to POST /runtime/ar-aggregate | needs future API work |
Test plan
- iOS: XCTest under
ios/Tests/EnfinitOSMobileTests/. Run viaxcodebuild testor in Xcode. The tests inject a fakeURLProtocolso production code paths execute without socket traffic. - Android: JUnit 4 + kotlinx-coroutines-test under
android/src/test/. Run via./gradlew :sdk-mobile:testDebugUnitTest. - React Native: Vitest under
react-native/__tests__/. Run viapnpm --filter @enfinitos/sdk-mobile-react-native test. - Flutter:
flutter_testunderflutter/test/. Run viaflutter test.