This document describes the features and functionality of the Approov Service for React Native. It provides details on how to interact with the service layer and customize its behavior to suit your application's needs.
You can initialize the ApproovService with an empty configuration string if you want the React Native environment to behave as a standard set of networking APIs (like fetch or XMLHttpRequest) without active Approov protection. This is useful for remote activation of Approov or when you need a bypass state (e.g., for specific deployment environments).
// Initialize with an empty string to operate as a standard network client ApproovService.initialize("");
When initialized this way, all network requests made through the native React Native networking module will proceed without Approov token injection, message signing, secure string substitution, or dynamic pinning. You can enable full Approov protection later in the application lifecycle by calling ApproovService.initialize(config) with a valid configuration string.
The Approov SDK must fully complete its native initialization sequence and receive your configuration string before it can protect your network traffic.
If your application executes a fetch() or axios request before ApproovService.initialize() has successfully completed, that specific request may proceed without an Approov token and without a reliable pinning guarantee. On iOS, the passive +load probe may still log evidence that startup networking primitives already existed, but the request itself is not recoverable after it has left the device.
You must await
useApproov()/approovReady(orawait ApproovService.initialize(...)) before making protectedfetch()calls. A request that leaves the device before initialization completes may be forwarded without an Approov token. If you intentionally initialize with"", that request path is treated as an explicit no-Approov bootstrap mode: requests will proceed without Approov protection until you later callApproovService.initialize("<valid-config>"). This empty-first then valid-config-later flow is supported for advanced service-layer integrations, but switching directly between different non-empty config strings is still rejected. The extended session metadata ledger exposed bygetSessionDiagnostics()is intended only for development and troubleshooting startup/interception issues. On iOS, turn it off for production withApproovService.setSessionMetadataCollectionEnabled(false)as part of startup. Android currently does not persist an equivalent session ledger; this toggle is retained there only for API parity. The iOS ledger now has an internal safety cap of about 1 MB, but that cap is only a backstop. It is not a reason to leave session metadata collection enabled in release builds.
To guarantee all requests are protected, you must strictly gate your network activity behind the initialization state. We provide two ways to do this:
If you are using the <ApproovProvider> wrapper at the root of your app, the provider automatically handles initialization. You can use the useApproov() hook in your child components to wait until Approov is fully ready before fetching data or rendering.
import React, { useEffect, useState } from 'react';
import { ActivityIndicator, View } from 'react-native';
import { useApproov } from '@approov/approov-service-react-native';
const MainScreen = () => {
// Extract the async initialization state
const { approovReady, approovError } = useApproov();
const [data, setData] = useState(null);
useEffect(() => {
// Only fetch your critical API data once Approov is definitively initialized
if (approovReady && !approovError) {
fetch("https://api.example.com/data")
.then(res => res.json())
.then(setData);
}
}, [approovReady, approovError]);
if (approovError) return <Text>Approov failed to start</Text>;
if (!approovReady) return <ActivityIndicator size="large" />;
return <View> /* Render your data */ </View>;
}If you are manually initializing Approov outside of the React component tree (e.g., in a background service or a dedicated API wrapper module), ApproovService.initialize() returns a Promise. You should await it before making any subsequent network calls.
import { Platform } from 'react-native';
import { ApproovService } from '@approov/approov-service-react-native';
async function bootstrapAppAndFetch() {
try {
// 1. Wait for Approov to finish initializing its Native modules
await ApproovService.initialize("<your-config-string>");
// 2. Capture startup diagnostics metadata.
// On Android this is the pre-flight health check. On iOS this is a baseline.
const diagnostics = await ApproovService.getPinningDiagnostics();
console.log("Approov startup diagnostics:", diagnostics);
// 3. CRITICAL ON ANDROID: heal the shared OkHttpClient before the first request.
if (Platform.OS === "android" &&
diagnostics &&
(!diagnostics.isInterceptorPresent || !diagnostics.isPinnerPresent)) {
console.warn("Approov hooks are missing from the active OkHttpClient. Healing...");
await ApproovService.updateClientFactory(true);
}
// 4. Make the first protected request
const response = await fetch("https://api.example.com/secure-data");
// 5. On iOS, inspect session metadata immediately after the first protected request.
const afterFirstRequest = await ApproovService.getPinningDiagnostics();
console.log("Approov post-request diagnostics:", afterFirstRequest);
} catch (error) {
console.error("Failed to initialize Approov", error);
}
}During development, staging, and the first production rollout of a new app build, you should capture ApproovService.getPinningDiagnostics() metadata twice:
- Immediately after initialization and before the first protected request
- Immediately after the first protected request
This gives you visibility into two different failure classes:
- Android factory override problems before the first request
- iOS session registration, delegate, and pinning-verification problems after the first request
If you are also using ApproovService.getSessionDiagnostics(), treat it as a temporary troubleshooting aid. It is most useful while validating a new integration, tracking down third-party NSURLSessionDelegate conflicts, or confirming that a startup request escaped before initialization completed. After that work is done, disable the extended ledger in production:
ApproovService.setSessionMetadataCollectionEnabled(false);Do not leave
getSessionDiagnostics()metadata collection enabled in production on iOS. The ledger is capped internally to about 1 MB to prevent unbounded growth, but it still stores extra diagnostic state that should only exist during development, staging, or short-lived rollout debugging. On Android this toggle is currently a no-op for ledger storage.
import { Platform } from 'react-native';
import { ApproovService } from '@approov/approov-service-react-native';
async function captureApproovDiagnostics(stage) {
const diagnostics = await ApproovService.getPinningDiagnostics();
console.log(`[Approov ${stage}]`, JSON.stringify(diagnostics, null, 2));
if (Platform.OS === 'android' &&
(!diagnostics.isInterceptorPresent || !diagnostics.isPinnerPresent)) {
await ApproovService.updateClientFactory(true);
}
return diagnostics;
}
await ApproovService.initialize("<your-config-string>");
await captureApproovDiagnostics("pre-first-fetch");
await fetch("https://api.example.com/secure-data");
await captureApproovDiagnostics("post-first-fetch");Interpret the metadata as follows:
- Android:
isInterceptorPresentandisPinnerPresentmust both betruebefore the first protected request. If not, another SDK likely replaced the sharedOkHttpClientFactory. - iOS: a pre-request baseline with zero sessions is normal. The important signal is the metadata after the first protected request. If
sessionsWithoutPinningis greater than0, then Approov observed requests on intercepted sessions but pinning was not verified for those sessions. - iOS limitation:
getPinningDiagnostics()only knows about sessions that actually reached the Approov interceptor. A completely bypassed first request may still require inspection of nativeApproovServicelogs forIMP CONFLICT,SKIPPING session creation, orskipping ... unregistered session.
You should send this metadata to your observability platform during early adoption. It is one of the fastest ways to spot request mutation or pinning regressions introduced by new monitoring SDKs, networking wrappers, or startup ordering changes.
React Native heavily optimizes Android networking by using a single, globally shared OkHttpClient. If another observability or analytics SDK (e.g., Datadog, New Relic) initializes after Approov does, that SDK might programmatically overwrite the networking factory, completely stripping away the Approov protection without throwing an error.
For robust Android deployments, it is highly recommended to perform the ApproovService.getPinningDiagnostics() health-check exactly once, just before you execute the very first API request of the application session, as demonstrated in Option 2 above. If the interceptor is missing, calling ApproovService.updateClientFactory(true) will instantly heal the active client, preserving the foreign SDK's hooks while adding the Approov protection back on top.
It is possible to sign HTTP requests using Approov to ensure message integrity and authenticity. There are two types of message signing available:
- Installation Message Signing: Uses an installation-specific key (held in the device's Secure Enclave/TEE) to sign requests. This provides strong non-repudiation as the signing key never leaves the device and is unique to that specific installation.
- Account Message Signing: Uses a shared account-specific secret key (HMAC-SHA256) to sign requests. This key is delivered to the SDK only upon successful attestation.
Advantages of Message Signing:
- Integrity: Ensures that the request parameters (headers, body, URL) have not been tampered with during transit.
- Authenticity: Proves that the request originated from a genuine, attested application instance.
Message signing is enabled automatically by the default ApproovService configuration. If your Approov account has message signing enabled, the SDK will automatically add the message signature headers to your outbound requests.
For more details on how to configure or override message signing behavior, see the Approov Service Mutator section below.
Token Binding allows you to bind the Approov token to a specific piece of data, such as an OAuth token or a user session identifier. This adds an extra layer of security by ensuring that the Approov token can only be used in conjunction with the bound data. The ApproovService calculates a hash of the binding data locally and includes this hash in the Approov token claims. It is important to note that the actual binding data is never sent to the Approov cloud service; only the hash is transmitted.
To set up token binding, specify a header name. The value of this header in your requests will be used for the binding.
// Bind the Approov token to the Authorization header (e.g., for OAuth)
ApproovService.setBindingHeader("Authorization");If the value of the binding header changes (e.g., the user logs in and gets a new OAuth token), the SDK automatically invalidates the current Approov token and fetches a new one with the updated binding on the next request.
In some cases, you might want to send the Approov fetch status (e.g., NO_NETWORK, MITM_DETECTED) to your backend when an actual token cannot be obtained. This allows your backend to distinguish between different failure reasons even when the Approov-Token would otherwise be empty or missing.
To enable this feature:
ApproovService.setUseApproovStatusIfNoToken(true);When enabled, the Approov-Token header is populated with the status string (with the configured prefix) only when the mutator allows the request to proceed without a token (for example, default NO_APPROOV_SERVICE handling, or custom mutator overrides). If the mutator blocks the request, no outbound request is made.
By default, the @approov/approov-service-react-native package uses swizzling (iOS) and OkHttpClient factory overrides (Android) to automatically intercept all React Native fetch() calls and inject Approov tokens.
However, in some complex applications, other observability SDKs (like New Relic, Datadog, or Firebase) might aggressively hook into the same networking layer in a way that conflicts with or bypasses Approov's security checks.
If you encounter such conflicts, you can use the ApproovService.fetchWithApproov API. It is a secure fetch-compatible API for sensitive calls, executed natively on isolated, protected HTTP clients that cannot be interfered with by other React Native modules.
import { ApproovService } from '@approov/approov-service-react-native';
// Use it exactly like you would use standard `fetch`
const response = await ApproovService.fetchWithApproov('https://api.yourdomain.com/data', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({ key: 'value' })
});
const data = await response.json();The fetchWithApproov wrapper is designed for standard JSON and text API payloads. Because it bypasses React Native's full NetworkingModule stack, it does not support:
- Multipart form uploads (
FormData, including file/URI-backed form parts) - Binary request bodies (
BloborArrayBuffer) - Request cancellation via
AbortController - React Native networking event model features (such as upload/download progress hooks)
- Streaming enormous files (the entire response is buffered into memory)
In practice, pass string bodies (JSON.stringify(...) or plain text) and use this API for security-critical REST calls rather than complex form/file transfer flows.
For critical security and authentication calls, fetchWithApproov provides guaranteed protection.
The ApproovServiceMutator allows you to customize the behavior of the Approov network layer at key points in the request lifecycle.
Important Architecture Note: Because the React Native bridge does not support passing complex executable code or classes, custom mutators must be implemented and registered directly in your platform-native code (Java/Kotlin for Android, Swift/Objective-C for iOS).
By default, the Approov Service is configured with a default mutator (ApproovDefaultMessageSigning) that automatically performs HTTP Message Signing. It fetches the Approov token, adds the Approov-Token header to requests, and generates the required message signature headers.
If you need to change other networking behaviors, you can do so natively by setting a custom ApproovServiceMutator.
You may want to modify the network behavior to suit specific app requirements. A common use case is handling NO_APPROOV_SERVICE statuses to enforce that an Approov Token must always be present, or proceeding on failures while preserving HTTP Message Signing.
The example below shows a custom mutator that:
- uses the normal Approov token flow on
SUCCESS - allows requests to continue on
MITM_DETECTEDandNO_APPROOV_SERVICE - signs the final outbound request with
ApproovDefaultMessageSigning
To send the failure reason in the token header when the request is allowed to continue without a real token, you must also enable:
ApproovService.setUseApproovStatusIfNoToken(true);With that setting enabled, the service layer places the failure status string into the configured token header, for example Approov-Token: MITM_DETECTED or Approov-Token: NO_APPROOV_SERVICE.
package com.yourcompany.yourapp;
import com.criticalblue.approovsdk.Approov;
import io.approov.reactnative.ApproovDefaultMessageSigning;
import io.approov.reactnative.ApproovException;
import io.approov.reactnative.ApproovService;
import io.approov.reactnative.ApproovServiceMutator;
public class ProceedOnSelectedStatusesMutator extends ApproovDefaultMessageSigning {
public ProceedOnSelectedStatusesMutator() {
setDefaultFactory(ApproovDefaultMessageSigning.generateDefaultSignatureParametersFactory());
}
@Override
public boolean handleInterceptorFetchTokenResult(ApproovService service,
Approov.TokenFetchResult approovResults,
String url) throws ApproovException {
Approov.TokenFetchStatus status = approovResults.getStatus();
if (status == Approov.TokenFetchStatus.SUCCESS ||
status == Approov.TokenFetchStatus.MITM_DETECTED ||
status == Approov.TokenFetchStatus.NO_APPROOV_SERVICE) {
return true;
}
return ApproovServiceMutator.super
.handleInterceptorFetchTokenResult(service, approovResults, url);
}
}Register it before React Native networking starts:
import android.app.Application;
import io.approov.reactnative.ApproovService;
public class MainApplication extends Application {
@Override
public void onCreate() {
super.onCreate();
ApproovService.setServiceMutator(new ProceedOnSelectedStatusesMutator());
}
}import Foundation
import approov_service_react_native
final class ProceedOnSelectedStatusesMutator: ApproovServiceMutator {
private let signer: ApproovDefaultMessageSigning
init() {
let signer = ApproovDefaultMessageSigning()
_ = signer.setDefaultFactory(
ApproovDefaultMessageSigning.generateDefaultSignatureParametersFactory()
)
self.signer = signer
}
func handleInterceptorFetchTokenResult(_ approovResults: ApproovTokenFetchResult,
url: String) throws -> Bool {
switch approovResults.status {
case .success, .mitmDetected, .noApproovService:
return true
default:
return try ApproovServiceMutatorDefault.shared
.handleInterceptorFetchTokenResult(approovResults, url: url)
}
}
func handleInterceptorProcessedRequest(_ request: URLRequest,
changes: ApproovRequestMutations) throws -> URLRequest {
return try signer.handleInterceptorProcessedRequest(request, changes: changes)
}
}Register it during app startup:
import approov_service_react_native
@UIApplicationMain
class AppDelegate: UIResponder, UIApplicationDelegate {
func application(_ application: UIApplication,
didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
ApproovServiceMutatorBridge.shared.serviceMutator = ProceedOnSelectedStatusesMutator()
return true
}
}In JavaScript, enable status-as-token before making protected requests:
import { ApproovService } from '@approov/approov-service-react-native';
await ApproovService.initialize('<your-config-string>');
ApproovService.setUseApproovStatusIfNoToken(true);Since ApproovDefaultMessageSigning is the default mutator, you must ensure that your custom mutator continues to invoke the message signer. If you want custom logic (like enforcing tokens), you pass the message signer into your custom mutator so they compose together.
Below are examples of how to implement and register a custom mutator that enforces tokens and simultaneously preserves message signing.
On Android, create a class implementing io.approov.reactnative.ApproovServiceMutator.
package com.yourcompany.yourapp;
import io.approov.reactnative.ApproovServiceMutator;
import io.approov.reactnative.ApproovServiceMutatorDefault;
import com.approov.service.ApproovService;
import com.approov.service.TokenFetchResult;
import okhttp3.Request;
import java.io.IOException;
public class EnforceTokenMutator implements ApproovServiceMutator {
private ApproovServiceMutator signer;
public EnforceTokenMutator(ApproovServiceMutator signer) {
this.signer = signer;
}
@Override
public boolean handleInterceptorFetchTokenResult(TokenFetchResult approovResults, String url) throws IOException {
if (approovResults.getStatus() == TokenFetchResult.Status.NO_APPROOV_SERVICE) {
throw new IOException("Approov service not available. Token required.");
}
return ApproovServiceMutatorDefault.getInstance().handleInterceptorFetchTokenResult(approovResults, url);
}
@Override
public boolean handleInterceptorShouldProcessRequest(Request request) throws IOException {
return ApproovServiceMutatorDefault.getInstance().handleInterceptorShouldProcessRequest(request);
}
@Override
public Request handleInterceptorProcessedRequest(Request request, ApproovServiceMutator.ApproovRequestMutations changes) throws IOException {
// We MUST call the signer here so that message signing still works!
if (signer != null) {
return signer.handleInterceptorProcessedRequest(request, changes);
}
return request;
}
}Register this mutator combined with the message signer configuration before initializing your React Native application (e.g., in MainApplication.java inside onCreate):
import io.approov.reactnative.ApproovService;
import io.approov.reactnative.ApproovDefaultMessageSigning;
public class MainApplication extends Application implements ReactApplication {
@Override
public void onCreate() {
super.onCreate();
// 1. Configure the signer natively (or skip if disabling signing)
ApproovDefaultMessageSigning.SignatureParametersFactory factory =
ApproovDefaultMessageSigning.generateDefaultSignatureParametersFactory();
ApproovDefaultMessageSigning signer = new ApproovDefaultMessageSigning();
signer.setDefaultFactory(factory);
// 2. Wrap it with your custom mutator
EnforceTokenMutator myMutator = new EnforceTokenMutator(signer);
// 3. Register custom mutator BEFORE React Native networking starts
ApproovService.setServiceMutator(myMutator);
// ... remaining React Native init
}
}On iOS, you can define your mutator in Swift using the ApproovServiceMutator protocol from approov_service_react_native.
import Foundation
import approov_service_react_native
class EnforceTokenMutator: ApproovServiceMutator {
private let signer: ApproovServiceMutator?
init(signer: ApproovServiceMutator?) {
self.signer = signer
}
func handleInterceptorFetchTokenResult(_ approovResults: ApproovTokenFetchResult, url: String) throws -> Bool {
if approovResults.status == .noApproovService {
throw NSError(domain: "ApproovService", code: 1, userInfo: [NSLocalizedDescriptionKey: "Approov service not available."])
}
return true
}
func handleInterceptorProcessedRequest(_ request: URLRequest, changes: ApproovRequestMutations) throws -> URLRequest {
if let signer = signer {
return try signer.handleInterceptorProcessedRequest(request, changes: changes)
}
return request
}
func handleInterceptorShouldProcessRequest(_ request: URLRequest) throws -> Bool { return true }
func handlePrecheckResult(_ approovResults: ApproovTokenFetchResult) throws {}
func handleFetchTokenResult(_ approovResults: ApproovTokenFetchResult) throws {}
func handleFetchSecureStringResult(_ approovResults: ApproovTokenFetchResult, operation: String, key: String) throws {}
func handleFetchCustomJWTResult(_ approovResults: ApproovTokenFetchResult) throws {}
func handleInterceptorHeaderSubstitutionResult(_ approovResults: ApproovTokenFetchResult, header: String) throws -> Bool { return true }
func handleInterceptorQueryParamSubstitutionResult(_ approovResults: ApproovTokenFetchResult, queryKey: String) throws -> Bool { return true }
func handlePinningShouldProcessRequest(_ request: URLRequest) -> Bool { return true }
}Register your mutator in AppDelegate.swift or AppDelegate.mm:
import approov_service_react_native
@UIApplicationMain
class AppDelegate: UIResponder, UIApplicationDelegate {
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
// 1. Configure the signer natively
let factory = ApproovDefaultMessageSigning.generateDefaultSignatureParametersFactory()
let signer = ApproovDefaultMessageSigning()
_ = signer.setDefaultFactory(factory)
// 2. Wrap it
let myMutator = EnforceTokenMutator(signer: signer)
// 3. Register it with the React Native Bridge BEFORE React Native init
ApproovServiceMutatorBridge.shared.serviceMutator = myMutator
return true
}
}This example implementation demonstrates how to customize the native ApproovServiceMutator to apply different options to API requests based on the hostname. Note that because custom mutators run directly on the underlying native HTTP client hooks, you must implement them in Java/Kotlin (Android) and Swift/Objective-C (iOS).
Android Implementation (Java)
package com.yourcompany.yourapp;
import io.approov.reactnative.ApproovServiceMutator;
import io.approov.reactnative.ApproovService;
import io.approov.reactnative.ApproovException;
import com.criticalblue.approovsdk.Approov;
import okhttp3.Request;
import java.util.Arrays;
import java.util.HashSet;
import java.util.Set;
import java.net.URL;
public class PolicyDrivenMutator implements ApproovServiceMutator {
private ApproovServiceMutator signer;
private Set<String> protectedHosts;
private Set<String> allowOfflineForHosts;
private Set<String> skipPinningHosts;
public PolicyDrivenMutator(ApproovServiceMutator signer) {
this.signer = signer;
this.protectedHosts = new HashSet<>(Arrays.asList("api.example.com"));
this.allowOfflineForHosts = new HashSet<>(Arrays.asList("status.example.com"));
this.skipPinningHosts = new HashSet<>(Arrays.asList("metrics.example.com"));
}
@Override
public boolean handleInterceptorShouldProcessRequest(ApproovService service, Request request) throws ApproovException {
String host = request.url().host();
if (!protectedHosts.contains(host)) {
return false;
}
return ApproovServiceMutator.DEFAULT.handleInterceptorShouldProcessRequest(service, request);
}
@Override
public boolean handleInterceptorFetchTokenResult(ApproovService service, Approov.TokenFetchResult approovResults, String url) throws ApproovException {
Approov.TokenFetchStatus status = approovResults.getStatus();
if (status == Approov.TokenFetchStatus.NO_NETWORK || status == Approov.TokenFetchStatus.POOR_NETWORK) {
try {
String host = new URL(url).getHost();
if (allowOfflineForHosts.contains(host)) {
return false;
}
} catch (Exception e) {
// Ignore URL parsing errors and fall through
}
}
return ApproovServiceMutator.DEFAULT.handleInterceptorFetchTokenResult(service, approovResults, url);
}
@Override
public Request handleInterceptorProcessedRequest(ApproovService service, Request request, ApproovServiceMutator.ApproovRequestMutations changes) throws ApproovException {
Request req = request;
if (signer != null) {
req = signer.handleInterceptorProcessedRequest(service, req, changes);
}
return req.newBuilder().header("X-Client-Platform", "android").build();
}
@Override
public boolean handlePinningShouldProcessRequest(Request request) {
String host = request.url().host();
return !skipPinningHosts.contains(host);
}
}iOS Implementation (Swift)
import Foundation
import approov_service_react_native
class PolicyDrivenMutator: ApproovServiceMutator {
private let signer: ApproovServiceMutator?
private let protectedHosts: Set<String>
private let allowOfflineForHosts: Set<String>
private let skipPinningHosts: Set<String>
init(
signer: ApproovServiceMutator? = nil,
protectedHosts: Set<String> = ["api.example.com"],
allowOfflineForHosts: Set<String> = ["status.example.com"],
skipPinningHosts: Set<String> = ["metrics.example.com"]
) {
self.signer = signer
self.protectedHosts = protectedHosts
self.allowOfflineForHosts = allowOfflineForHosts
self.skipPinningHosts = skipPinningHosts
}
func handleInterceptorShouldProcessRequest(_ request: URLRequest) throws -> Bool {
guard let host = request.url?.host, protectedHosts.contains(host) else { return false }
return try ApproovServiceMutatorDefault.shared.handleInterceptorShouldProcessRequest(request)
}
func handleInterceptorFetchTokenResult(_ approovResults: ApproovTokenFetchResult,
url: String) throws -> Bool {
if approovResults.status == .noNetwork || approovResults.status == .poorNetwork,
let host = URL(string: url)?.host, allowOfflineForHosts.contains(host) {
return false
}
return try ApproovServiceMutatorDefault.shared
.handleInterceptorFetchTokenResult(approovResults, url: url)
}
func handleInterceptorProcessedRequest(_ request: URLRequest,
changes: ApproovRequestMutations) throws -> URLRequest {
var req = request
if let signer = signer {
req = try signer.handleInterceptorProcessedRequest(req, changes: changes)
}
req.setValue("ios", forHTTPHeaderField: "X-Client-Platform")
return req
}
func handlePinningShouldProcessRequest(_ request: URLRequest) -> Bool {
guard let host = request.url?.host else { return true }
return !skipPinningHosts.contains(host)
}
// Other required protocol methods with default behavior:
func handlePrecheckResult(_ approovResults: ApproovTokenFetchResult) throws {}
func handleFetchTokenResult(_ approovResults: ApproovTokenFetchResult) throws {}
func handleFetchSecureStringResult(_ approovResults: ApproovTokenFetchResult, operation: String, key: String) throws {}
func handleFetchCustomJWTResult(_ approovResults: ApproovTokenFetchResult) throws {}
func handleInterceptorHeaderSubstitutionResult(_ approovResults: ApproovTokenFetchResult, header: String) throws -> Bool { return true }
func handleInterceptorQueryParamSubstitutionResult(_ approovResults: ApproovTokenFetchResult, queryKey: String) throws -> Bool { return true }
}Monitoring and analyzing rejections is a key part of your security strategy. Ideally, your backend should be customized to include the ARC (Approov Rejection Code) and Device ID in its error responses (e.g., in a JSON body or a custom header) when it rejects a request due to a missing or invalid Approov token.
While you can obtain these values directly from the SDK using ApproovService.getLastARC(), it is generally safer and more reliable to log them from the server response for several reasons:
- Avoid Misleading Network Events: On poor network connections, a call to
getLastARC()can inadvertently trigger a background network event that successfully completes a delayed attestation. This might provide an ARC associated with a successful attestation that occurred after your original request failed, creating confusing telemetry. - Corporate Firewall & MITM Bypass: If your custom native mutator allows a request to proceed on
MITM_DETECTED(a common result of corporate firewalls), the request is sent without a token. In this state,getLastARC()will not yet have a rejection code available for that specific attempt. - Accuracy and Correlation: Logging the ARC that the server actually observed and used as the basis for rejection ensures perfect correlation in your monitoring dashboards.
If you must log from the client, ensure you have a fallback strategy for when the server doesn't provide the code.
import { ApproovService } from '@approov/approov-service-react-native'; async function makeProtectedRequest(url, options) { try { const response = await fetch(url, options); if (response.ok) { return response; } else { // Preferred: Extract ARC and Device ID from your own server's response const serverArc = response.headers.get("X-Approov-Error-ARC"); // ALTERNATIVE: (DISCOURAGED) Obtain from SDK ONLY if server-side retrieval is impossible. // Note: This may trigger background network events and return misleading results. const arc = serverArc || await ApproovService.getLastARC(); const deviceID = await ApproovService.getDeviceID(); console.log(`Request rejected. ARC: ${arc}, Device ID: ${deviceID}`); return response; } } catch (error) { console.error("Network request failed", error); throw error; } }
- Keep mutator logic fast and side-effect safe. These native hooks run on the request path and blocking them will hang the network traffic.
- Use
ApproovServiceMutator.DEFAULT(Android) orApproovServiceMutatorDefault.shared(iOS) to preserve the existing behavior and layer your changes on top. - If you override multiple hooks, keep them focused (one concern per hook) for easier testing and maintenance.