Skip to main content

Adding the SDKs to a React Native app

There's no separate React Native package. Passpoint onboarding needs the native iOS and Android SDKs, so you expose them to JavaScript with a small native module that wraps the same four verbs.

This guide gives you a drop-in module for both platforms and the JS API that sits on top.

Prerequisites

Set up the native SDKs first, exactly as a native app would. Nothing here replaces that.

  • iOSadd the Swift Package and complete iOS setup: App Attest, the Hotspot Configuration entitlement, the Keychain access group, and registering your Team ID + bundle ID.
  • Androidadd the AAR and complete Android setup: Play Integrity, the Cloud project number, and registering your app-signing cert SHA-256 + package name.

The entitlements, permissions, and registration in those pages all still apply. The bridge below just calls the SDKs; it doesn't change what they need.

1. The JavaScript API

Create a typed wrapper so the rest of your app never touches NativeModules directly. The error code on a rejected promise is the same string on both platforms.

// widas.ts
import { NativeModules } from 'react-native';

const { WiDASModule } = NativeModules;

export type WiDASState = 'none' | 'valid' | 'expired' | 'revoked';

export interface WiDASStatus {
state: WiDASState;
serial: string | null;
expiry: string | null; // ISO-8601, or null
}

export interface ConnectionResult {
serial: string;
notAfter: string; // ISO-8601
}

export interface ConfigureOptions {
baseUrl?: string;
keychainAccessGroup?: string; // iOS only
cloudProjectNumber?: number; // Android only
}

export const WiDAS = {
configure(id: string, options: ConfigureOptions = {}): Promise<void> {
return WiDASModule.configure(id, options);
},
connect(user: string, requestedValidity = 'P365D'): Promise<ConnectionResult> {
return WiDASModule.connect(user, requestedValidity);
},
status(): Promise<WiDASStatus> {
return WiDASModule.status();
},
disconnect(): Promise<void> {
return WiDASModule.disconnect();
},
};

Pass your clientId (iOS) or partnerId (Android) as id. Each platform reads only the options it needs and ignores the rest.

2. The iOS native module

Add two files to your iOS project (the ios/ folder, added to your app target). If your app has no Swift yet, Xcode will offer to create a bridging header when you add the first Swift file — accept it.

// ios/WiDASModule.swift
import Foundation
import WiDAS

@objc(WiDASModule)
final class WiDASModule: NSObject {
private var widas: WiDAS?

@objc static func requiresMainQueueSetup() -> Bool { false }

@objc(configure:options:resolver:rejecter:)
func configure(_ clientId: String,
options: [String: Any],
resolver resolve: @escaping RCTPromiseResolveBlock,
rejecter reject: @escaping RCTPromiseRejectBlock) {
let group = options["keychainAccessGroup"] as? String
if let raw = options["baseUrl"] as? String, let url = URL(string: raw) {
widas = WiDAS(clientId: clientId, baseURL: url, keychainAccessGroup: group)
} else {
widas = WiDAS(clientId: clientId, keychainAccessGroup: group)
}
resolve(nil)
}

@objc(connect:requestedValidity:resolver:rejecter:)
func connect(_ user: String,
requestedValidity: String,
resolver resolve: @escaping RCTPromiseResolveBlock,
rejecter reject: @escaping RCTPromiseRejectBlock) {
guard let widas else { return reject("notConfigured", "Call configure() first", nil) }
Task {
do {
let result = try await widas.connect(user: user, requestedValidity: requestedValidity)
resolve([
"serial": result.serial,
"notAfter": ISO8601DateFormatter().string(from: result.notAfter),
])
} catch let error as WiDASError {
reject(code(for: error), "\(error)", error)
} catch {
reject("unknown", error.localizedDescription, error)
}
}
}

@objc(status:rejecter:)
func status(_ resolve: @escaping RCTPromiseResolveBlock,
rejecter reject: @escaping RCTPromiseRejectBlock) {
guard let widas else { return reject("notConfigured", "Call configure() first", nil) }
Task {
let status = await widas.status()
let iso = ISO8601DateFormatter()
resolve([
"state": status.state.rawValue,
"serial": status.serial as Any,
"expiry": status.expiry.map { iso.string(from: $0) } as Any,
])
}
}

@objc(disconnect:rejecter:)
func disconnect(_ resolve: @escaping RCTPromiseResolveBlock,
rejecter reject: @escaping RCTPromiseRejectBlock) {
guard let widas else { return reject("notConfigured", "Call configure() first", nil) }
Task {
do { try await widas.disconnect(); resolve(nil) }
catch let error as WiDASError { reject(code(for: error), "\(error)", error) }
catch { reject("unknown", error.localizedDescription, error) }
}
}

private func code(for error: WiDASError) -> String {
switch error {
case .attestationUnavailable: return "attestationUnavailable"
case .attestationRejected: return "attestationRejected"
case .issuanceRejected: return "issuanceRejected"
case .serviceUnavailable: return "serviceUnavailable"
case .entitlementMissing: return "entitlementMissing"
case .passpointInstallFailed: return "passpointInstallFailed"
case .revoked: return "revoked"
case .network: return "network"
case .cancelled: return "cancelled"
}
}
}
// ios/WiDASModule.m
#import <React/RCTBridgeModule.h>

@interface RCT_EXTERN_MODULE(WiDASModule, NSObject)

RCT_EXTERN_METHOD(configure:(NSString *)clientId
options:(NSDictionary *)options
resolver:(RCTPromiseResolveBlock)resolve
rejecter:(RCTPromiseRejectBlock)reject)

RCT_EXTERN_METHOD(connect:(NSString *)user
requestedValidity:(NSString *)requestedValidity
resolver:(RCTPromiseResolveBlock)resolve
rejecter:(RCTPromiseRejectBlock)reject)

RCT_EXTERN_METHOD(status:(RCTPromiseResolveBlock)resolve
rejecter:(RCTPromiseRejectBlock)reject)

RCT_EXTERN_METHOD(disconnect:(RCTPromiseResolveBlock)resolve
rejecter:(RCTPromiseRejectBlock)reject)

@end

3. The Android native module

Add the module and a package, then register the package in your app.

// android/app/src/main/java/com/yourapp/widas/WiDASModule.kt
package com.yourapp.widas

import com.facebook.react.bridge.Arguments
import com.facebook.react.bridge.Promise
import com.facebook.react.bridge.ReactApplicationContext
import com.facebook.react.bridge.ReactContextBaseJavaModule
import com.facebook.react.bridge.ReactMethod
import com.facebook.react.bridge.ReadableMap
import com.widas.sdk.WiDAS
import com.widas.sdk.WiDASException
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.SupervisorJob
import kotlinx.coroutines.cancel
import kotlinx.coroutines.launch

class WiDASModule(private val reactContext: ReactApplicationContext) :
ReactContextBaseJavaModule(reactContext) {

private val scope = CoroutineScope(SupervisorJob() + Dispatchers.Main)
private var widas: WiDAS? = null

override fun getName() = "WiDASModule"

@ReactMethod
fun configure(partnerId: String, options: ReadableMap, promise: Promise) {
val baseUrl = options.getString("baseUrl")
?: return promise.reject("badArgs", "baseUrl is required")
val cloud = if (options.hasKey("cloudProjectNumber"))
options.getDouble("cloudProjectNumber").toLong() else null
widas = WiDAS.configure(reactContext, partnerId, baseUrl, cloud)
promise.resolve(null)
}

@ReactMethod
fun connect(user: String, requestedValidity: String?, promise: Promise) {
val sdk = widas ?: return promise.reject("notConfigured", "Call configure() first")
scope.launch {
try {
val result = sdk.connect(user, requestedValidity)
promise.resolve(Arguments.createMap().apply {
putString("serial", result.serial)
putString("notAfter", result.notAfter.toString())
})
} catch (e: WiDASException) {
promise.reject(codeFor(e), e.message ?: e::class.simpleName, e)
}
}
}

@ReactMethod
fun status(promise: Promise) {
val sdk = widas ?: return promise.reject("notConfigured", "Call configure() first")
scope.launch {
val status = sdk.status()
promise.resolve(Arguments.createMap().apply {
putString("state", status.state.name.lowercase())
putString("serial", status.serial)
putString("expiry", status.expiry?.toString())
})
}
}

@ReactMethod
fun disconnect(promise: Promise) {
val sdk = widas ?: return promise.reject("notConfigured", "Call configure() first")
scope.launch {
try {
sdk.disconnect()
promise.resolve(null)
} catch (e: WiDASException) {
promise.reject(codeFor(e), e.message, e)
}
}
}

override fun invalidate() {
scope.cancel()
super.invalidate()
}

private fun codeFor(e: WiDASException): String = when (e) {
is WiDASException.AttestationUnavailable -> "attestationUnavailable"
is WiDASException.AttestationRejected -> "attestationRejected"
is WiDASException.IssuanceRejected -> "issuanceRejected"
is WiDASException.EntitlementMissing -> "entitlementMissing"
is WiDASException.PasspointInstallFailed -> "passpointInstallFailed"
is WiDASException.Revoked -> "revoked"
is WiDASException.Network -> "network"
is WiDASException.Cancelled -> "cancelled"
}
}
// android/app/src/main/java/com/yourapp/widas/WiDASPackage.kt
package com.yourapp.widas

import com.facebook.react.ReactPackage
import com.facebook.react.bridge.NativeModule
import com.facebook.react.bridge.ReactApplicationContext
import com.facebook.react.uimanager.ViewManager

class WiDASPackage : ReactPackage {
override fun createNativeModules(reactContext: ReactApplicationContext): List<NativeModule> =
listOf(WiDASModule(reactContext))

override fun createViewManagers(reactContext: ReactApplicationContext): List<ViewManager<*, *>> =
emptyList()
}

Register the package in MainApplication (getPackages()), adding WiDASPackage() to the list.

4. Use it in your app

connect() is idempotent, so call it on launch and after any expired or revoked status.

import { WiDAS } from './widas';

async function onLaunch(userId: string) {
await WiDAS.configure('partner-acme', {
baseUrl: 'https://partner-api.wi-das.com/',
keychainAccessGroup: '<TeamID>.com.apple.networkextensionsharing', // iOS
cloudProjectNumber: 123456789, // Android
});

try {
await WiDAS.connect(userId, 'P365D');
} catch (e: any) {
if (e.code === 'attestationUnavailable') {
// e.g. iOS Simulator, or a device with no Play Services — degrade the UI.
} else {
// Branch on e.code — see the table below.
}
}

const status = await WiDAS.status();
// status.state: 'none' | 'valid' | 'expired' | 'revoked'
}

Error codes

A rejected promise carries the same code on both platforms. These match the native iOS and Android errors.

codeMeaningRetry?
attestationUnavailableAttestation can't run here (Simulator, or no Play Services).No — degrade the UI.
attestationRejectedAttestation didn't match a registered app.No — fix registration.
issuanceRejectedIssuance refused (bad CSR/validity/challenge, unknown partner).No — fix the config.
serviceUnavailableTemporary server-side fault (iOS reports this specifically).Yes — back off.
entitlementMissingA required capability or a disabled partner.No — fix setup/registration.
passpointInstallFailedThe Passpoint profile wouldn't install.No — check entitlements/permissions.
revokedThe certificate is revoked.No — disconnect() and tell the user.
networkTransport/connectivity failure.Yes.
cancelledThe operation was cancelled.Optional.
notConfiguredconnect/status/disconnect called before configure.No — call configure() first.
Test on real devices

Attestation needs real hardware. App Attest doesn't run on the iOS Simulator, and Play Integrity needs Google Play Services, so connect() will reject with attestationUnavailable on an emulator or Simulator. Confirm onboarding on a physical device.