@@ -203,6 +203,17 @@ public synchronized boolean getUseApproovStatusIfNoToken() {
203203 // The mutator instance used to control ApproovService behavior
204204 private static ApproovServiceMutator serviceMutator ;
205205
206+ // Cached failure result from the last Approov token fetch that returned a
207+ // failure status.
208+ // Protected by failureCacheLock for thread-safe access. This avoids redundant
209+ // ~1s SDK calls
210+ // when the platform is in a sustained failure state (e.g. no network, MITM
211+ // detected).
212+ private static final Object failureCacheLock = new Object ();
213+ private static Approov .TokenFetchResult cachedFailureResult = null ;
214+ private static long cachedFailureTimeMs = 0 ;
215+ private static final long FAILURE_CACHE_TTL_MS = 500 ; // 0.5 seconds
216+
206217 static {
207218 ApproovDefaultMessageSigning signer = new ApproovDefaultMessageSigning ();
208219 signer .setDefaultFactory (ApproovDefaultMessageSigning .generateDefaultSignatureParametersFactory ());
@@ -234,13 +245,65 @@ public static ApproovServiceMutator getServiceMutator() {
234245 }
235246
236247 /**
237- * Fetches an Approov token for the given URL.
248+ * Returns a cached failure result if one exists and hasn't expired.
249+ * Returns null if no cache exists or it has expired (caller should fetch from
250+ * SDK).
251+ */
252+ private static Approov .TokenFetchResult getCachedFailure () {
253+ synchronized (failureCacheLock ) {
254+ if (cachedFailureResult != null
255+ && (System .currentTimeMillis () - cachedFailureTimeMs ) < FAILURE_CACHE_TTL_MS ) {
256+ return cachedFailureResult ;
257+ }
258+ // Cache miss or expired — clear and allow a fresh SDK call
259+ cachedFailureResult = null ;
260+ cachedFailureTimeMs = 0 ;
261+ return null ;
262+ }
263+ }
264+
265+ /**
266+ * Caches a failure result. Only failure statuses are cached; success is never
267+ * cached.
268+ */
269+ private static void cacheFailureIfNeeded (Approov .TokenFetchResult result ) {
270+ switch (result .getStatus ()) {
271+ case NO_NETWORK :
272+ case POOR_NETWORK :
273+ case MITM_DETECTED :
274+ case NO_APPROOV_SERVICE :
275+ synchronized (failureCacheLock ) {
276+ cachedFailureResult = result ;
277+ cachedFailureTimeMs = System .currentTimeMillis ();
278+ }
279+ break ;
280+ default :
281+ // Success and other statuses are never cached
282+ break ;
283+ }
284+ }
285+
286+ /**
287+ * Performs a cached Approov token fetch. If a failure result is cached and
288+ * within
289+ * the TTL window, the cached failure is returned instantly. Otherwise a fresh
290+ * SDK
291+ * call is made and the result is cached if it is a failure.
238292 *
239293 * @param url is the URL giving the domain for the token fetch
240294 * @return the token fetch result
241295 */
242- public Approov .TokenFetchResult fetchApproovTokenAndWait (String url ) {
243- return Approov .fetchApproovTokenAndWait (url );
296+ public Approov .TokenFetchResult fetchApproovTokenCached (String url ) {
297+ Approov .TokenFetchResult cached = getCachedFailure ();
298+ if (cached != null ) {
299+ log (LOG_DEBUG , TAG , "Using cached failure: " + cached .getStatus ().toString ());
300+ return cached ;
301+ }
302+
303+ // Normal execution
304+ Approov .TokenFetchResult result = Approov .fetchApproovTokenAndWait (url );
305+ cacheFailureIfNeeded (result );
306+ return result ;
244307 }
245308
246309 /**
@@ -694,6 +757,10 @@ public void initialize(String config, String comment, Promise promise) {
694757 initialConfig = config ;
695758 isInitialized = true ;
696759 clearEarliestNetworkRequestTime ();
760+ synchronized (failureCacheLock ) {
761+ cachedFailureResult = null ;
762+ cachedFailureTimeMs = 0 ;
763+ }
697764 if (isApproovEnabled ()) {
698765 log (LOG_INFO , TAG , "initialized on deviceID " + Approov .getDeviceID ());
699766 notifyPinChangeListeners ();
@@ -768,6 +835,11 @@ public void isApproovEnabled(Promise promise) {
768835 @ ReactMethod
769836 public void getLastARC (Promise promise ) {
770837 log (LOG_INFO , TAG , "ApproovService: getLastARC" );
838+ if (!isInitialized || !isApproovEnabled ()) {
839+ promise .resolve ("" );
840+ return ;
841+ }
842+
771843 // Get the dynamic pins from Approov
772844 Map <String , List <String >> approovPins = Approov .getPins ("public-key-sha256" );
773845 if (approovPins == null || approovPins .isEmpty ()) {
@@ -1318,14 +1390,6 @@ public void approovCallback(Approov.TokenFetchResult result) {
13181390 */
13191391 @ ReactMethod
13201392 public void precheck (Promise promise ) {
1321- if (!isInitialized ) {
1322- promise .reject ("approov_error" , "Approov is not initialized" , getErrorUserInfo (false ));
1323- return ;
1324- }
1325- if (!isApproovEnabled ()) {
1326- promise .reject ("approov_error" , "Approov is disabled" , getErrorUserInfo (false ));
1327- return ;
1328- }
13291393 try {
13301394 Approov .fetchSecureString (new PrecheckHandler (promise ), "precheck-dummy-key" , null );
13311395 } catch (IllegalStateException e ) {
@@ -1413,14 +1477,6 @@ public void getDeviceID(Promise promise) {
14131477 */
14141478 @ ReactMethod
14151479 public void setDataHashInToken (String data , Promise promise ) {
1416- if (!isInitialized ) {
1417- promise .reject ("approov_error" , "Approov is not initialized" , getErrorUserInfo (false ));
1418- return ;
1419- }
1420- if (!isApproovEnabled ()) {
1421- promise .reject ("approov_error" , "Approov is disabled" , getErrorUserInfo (false ));
1422- return ;
1423- }
14241480 try {
14251481 Approov .setDataHashInToken (data );
14261482 log (LOG_DEBUG , TAG , "setDataHashInToken" );
@@ -1449,14 +1505,6 @@ public void setDataHashInToken(String data, Promise promise) {
14491505 */
14501506 @ ReactMethod
14511507 public void fetchToken (String url , Promise promise ) {
1452- if (!isInitialized ) {
1453- promise .reject ("approov_error" , "Approov is not initialized" , getErrorUserInfo (false ));
1454- return ;
1455- }
1456- if (!isApproovEnabled ()) {
1457- promise .reject ("approov_error" , "Approov is disabled" , getErrorUserInfo (false ));
1458- return ;
1459- }
14601508 try {
14611509 Approov .fetchApproovToken (new FetchTokenHandler (promise ), url );
14621510 } catch (IllegalStateException e ) {
@@ -1556,14 +1604,6 @@ public void getMessageSignature(String message, Promise promise) {
15561604 */
15571605 @ ReactMethod
15581606 public void fetchSecureString (String key , String newDef , Promise promise ) {
1559- if (!isInitialized ) {
1560- promise .reject ("approov_error" , "Approov is not initialized" , getErrorUserInfo (false ));
1561- return ;
1562- }
1563- if (!isApproovEnabled ()) {
1564- promise .reject ("approov_error" , "Approov is disabled" , getErrorUserInfo (false ));
1565- return ;
1566- }
15671607 // determine the type of operation as the values themselves cannot be logged
15681608 String type = "lookup" ;
15691609 if (newDef != null )
@@ -1662,21 +1702,6 @@ else if ((result.getStatus() != Approov.TokenFetchStatus.SUCCESS) &&
16621702 */
16631703 @ ReactMethod
16641704 public void fetchCustomJWT (String payload , Promise promise ) {
1665- if (!isInitialized ) {
1666- promise .reject ("approov_error" , "Approov is not initialized" , getErrorUserInfo (false ));
1667- return ;
1668- }
1669- if (!isApproovEnabled ()) {
1670- promise .reject ("approov_error" , "Approov is disabled" , getErrorUserInfo (false ));
1671- return ;
1672- }
1673- try {
1674- new org .json .JSONObject (payload );
1675- } catch (org .json .JSONException e ) {
1676- promise .reject ("fetchCustomJWT" , "IllegalArgument: Malformed JSON payload" , getErrorUserInfo (false ));
1677- return ;
1678- }
1679-
16801705 try {
16811706 Approov .fetchCustomJWT (new FetchCustomJWTHandler (promise ), payload );
16821707 } catch (IllegalStateException e ) {
@@ -1989,6 +2014,52 @@ public void getMaxReswizzleAttempts(Promise promise) {
19892014 promise .resolve (0 );
19902015 }
19912016
2017+ /**
2018+ * Exposes the native Approov logging facility to Javascript.
2019+ *
2020+ * @param message the string message to log natively
2021+ * @param level the integer log level (e.g., ApproovService.Log.INFO)
2022+ */
2023+ @ ReactMethod
2024+ public void logMessage (String message , Integer level ) {
2025+ int mappedLevel = (level != null ) ? level : LOG_INFO ;
2026+ log (mappedLevel , TAG , "JS: " + (message != null ? message : "null" ));
2027+ }}}
2028+
2029+ promise .resolve (responseMap );}
2030+
2031+ }catch (
2032+
2033+ Exception e )
2034+ {
2035+ log (LOG_ERROR , TAG , "fetchWithApproov failed: " + e .getMessage ());
2036+ promise .reject ("network_error" , e .getMessage (), e );
2037+ }
2038+ }).start ();}
2039+
2040+ /**
2041+ * iOS-specific method to set the max reswizzle attempts.
2042+ * This is a no-op on Android since the OkHttp integration
2043+ * does not rely on method swizzling or +load races.
2044+ *
2045+ * @param attempts the maximum number of recovery attempts.
2046+ */
2047+ @ ReactMethod
2048+ public void setMaxReswizzleAttempts (Integer attempts ) {
2049+ log (LOG_DEBUG , TAG , "setMaxReswizzleAttempts: no-op on Android" );
2050+ }
2051+
2052+ /**
2053+ * iOS-specific method to get the max reswizzle attempts.
2054+ * Always returns 0 on Android since swizzling is not used.
2055+ *
2056+ * @param promise promise to be fulfilled with 0
2057+ */
2058+ @ ReactMethod
2059+ public void getMaxReswizzleAttempts (Promise promise ) {
2060+ promise .resolve (0 );
2061+ }
2062+
19922063 /**
19932064 * Exposes the native Approov logging facility to Javascript.
19942065 *
0 commit comments