Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -14,15 +14,24 @@ object AutoPrefetcher {
private const val KEY_QUEUE = "nitrofetch_autoprefetch_queue"
private const val KEY_TOKEN_REFRESH = "nitro_token_refresh_fetch"
private const val KEY_TOKEN_CACHE = "nitro_token_refresh_fetch_cache"
/** Plaintext outcome for debug / JS — same key as `tokenRefresh.ts` */
private const val KEY_LAST_FETCH_TOKEN_REFRESH_OUTCOME = "nitro_token_refresh_fetch_last_outcome"
private const val PREFS_NAME = NitroFetchSecureAtRest.PREFS_NAME

private fun setFetchTokenRefreshOutcome(prefs: android.content.SharedPreferences, value: String) {
prefs.edit().putString(KEY_LAST_FETCH_TOKEN_REFRESH_OUTCOME, value).apply()
}

fun prefetchOnStart(app: Application) {
if (initialized) return
initialized = true
try {
val prefs = app.getSharedPreferences(PREFS_NAME, Context.MODE_PRIVATE)
val raw = prefs.getString(KEY_QUEUE, null) ?: ""
if (raw.isEmpty()) return
if (raw.isEmpty()) {
setFetchTokenRefreshOutcome(prefs, "not_run")
return
}
val arr = JSONArray(raw)

val refreshRaw = NitroFetchSecureAtRest.getDecryptedForPrefs(prefs, KEY_TOKEN_REFRESH)
Expand All @@ -40,7 +49,7 @@ object AutoPrefetcher {

val tokenHeaders: Map<String, String> = if (refreshed != null) {
android.util.Log.d("NitroFetch", "[TokenRefresh] ✅ Success — got ${refreshed.size} header(s)")
refreshed.forEach { (k, v) -> android.util.Log.d("NitroFetch", "[TokenRefresh] $k: $v") }
setFetchTokenRefreshOutcome(prefs, "success")
// Cache fresh token headers for useStoredHeaders fallback on next cold start
val cacheJson = JSONObject()
refreshed.forEach { (k, v) -> cacheJson.put(k, v) }
Expand All @@ -50,6 +59,7 @@ object AutoPrefetcher {
android.util.Log.d("NitroFetch", "[TokenRefresh] ❌ Refresh failed — onFailure: $onFailure")
if (onFailure == "skip") {
android.util.Log.d("NitroFetch", "[TokenRefresh] Skipping all prefetches")
setFetchTokenRefreshOutcome(prefs, "failed_skip")
return@Thread
}
// Use last cached token headers (or empty map if none cached yet)
Expand All @@ -63,16 +73,19 @@ object AutoPrefetcher {
emptyMap()
}
android.util.Log.d("NitroFetch", "[TokenRefresh] Using cached headers (${cached.size} header(s))")
setFetchTokenRefreshOutcome(prefs, "failed_cache")
cached
}

android.util.Log.d("NitroFetch", "[TokenRefresh] Injecting token headers into ${arr.length()} prefetch URL(s)")
startPrefetches(arr, tokenHeaders)
} catch (_: Throwable) {
setFetchTokenRefreshOutcome(prefs, "error")
// Best-effort — never crash the app
}
}.start()
} else {
setFetchTokenRefreshOutcome(prefs, "none")
// No token refresh config — proceed on current thread (Cronet is async)
startPrefetches(arr, emptyMap())
}
Expand Down
16 changes: 15 additions & 1 deletion packages/react-native-nitro-fetch/ios/NitroAutoPrefetcher.swift
Original file line number Diff line number Diff line change
Expand Up @@ -7,14 +7,24 @@ public final class NitroAutoPrefetcher: NSObject {
private static let suiteName = "nitro_fetch_storage"
private static let tokenRefreshKey = "nitro_token_refresh_fetch"
private static let tokenCacheKey = "nitro_token_refresh_fetch_cache"
/// Plaintext outcome for debug / JS (`NativeStorage.getString`). Same key as `tokenRefresh.ts`.
private static let lastFetchTokenRefreshOutcomeKey = "nitro_token_refresh_fetch_last_outcome"

private static func setFetchTokenRefreshOutcome(_ value: String, defaults: UserDefaults) {
defaults.set(value, forKey: lastFetchTokenRefreshOutcomeKey)
defaults.synchronize()
}

@objc
public static func prefetchOnStart() {
if initialized { return }
initialized = true

let userDefaults = UserDefaults(suiteName: suiteName) ?? UserDefaults.standard
guard let raw = userDefaults.string(forKey: queueKey), !raw.isEmpty else { return }
guard let raw = userDefaults.string(forKey: queueKey), !raw.isEmpty else {
setFetchTokenRefreshOutcome("not_run", defaults: userDefaults)
return
}
guard let data = raw.data(using: .utf8) else { return }
guard let arr = try? JSONSerialization.jsonObject(with: data, options: []) as? [Any] else { return }

Expand All @@ -33,6 +43,7 @@ public final class NitroAutoPrefetcher: NSObject {
let refreshed = try? await callTokenRefresh(config: refreshObj)
if let refreshed = refreshed {
print("[NitroFetch][TokenRefresh] ✅ Success — got \(refreshed.count) header(s)")
setFetchTokenRefreshOutcome("success", defaults: userDefaults)
for (k, v) in refreshed { print("[NitroFetch][TokenRefresh] \(k): \(v)") }
// Cache fresh token headers for useStoredHeaders fallback on next cold start
if let cacheData = try? JSONSerialization.data(withJSONObject: refreshed),
Expand All @@ -44,6 +55,7 @@ public final class NitroAutoPrefetcher: NSObject {
print("[NitroFetch][TokenRefresh] ❌ Refresh failed — onFailure: \(onFailure)")
if onFailure == "skip" {
print("[NitroFetch][TokenRefresh] Skipping all prefetches")
setFetchTokenRefreshOutcome("failed_skip", defaults: userDefaults)
return
}
var cached: [String: String] = [:]
Expand All @@ -54,9 +66,11 @@ public final class NitroAutoPrefetcher: NSObject {
cached = cacheObj
}
print("[NitroFetch][TokenRefresh] Using cached headers (\(cached.count) header(s))")
setFetchTokenRefreshOutcome("failed_cache", defaults: userDefaults)
tokenHeaders = cached
}
} else {
setFetchTokenRefreshOutcome("none", defaults: userDefaults)
tokenHeaders = [:]
}

Expand Down
1 change: 1 addition & 0 deletions packages/react-native-nitro-fetch/src/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ export {
clearTokenRefresh,
callRefreshEndpoint,
getStoredTokenRefreshConfig,
getFetchTokenRefreshLastOutcome,
getNestedField,
applyTemplate,
} from './tokenRefresh';
Expand Down
19 changes: 19 additions & 0 deletions packages/react-native-nitro-fetch/src/tokenRefresh.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@ const KEY_WS = 'nitro_token_refresh_websocket';
const KEY_FETCH = 'nitro_token_refresh_fetch';
const KEY_WS_CACHE = 'nitro_token_refresh_ws_cache';
const KEY_FETCH_CACHE = 'nitro_token_refresh_fetch_cache';
/** Plaintext; written by native cold-start autoprefetch (`NitroAutoPrefetcher` / `AutoPrefetcher`). */
const KEY_FETCH_LAST_OUTCOME = 'nitro_token_refresh_fetch_last_outcome';

type TokenRefreshTarget = 'websocket' | 'fetch' | 'all';

Expand Down Expand Up @@ -143,6 +145,11 @@ export function clearTokenRefresh(target?: TokenRefreshTarget): void {
if (t === 'fetch' || t === 'all') {
NativeStorageSingleton.removeSecureString(KEY_FETCH);
NativeStorageSingleton.removeSecureString(KEY_FETCH_CACHE);
try {
NativeStorageSingleton.removeString(KEY_FETCH_LAST_OUTCOME);
} catch (_error) {
/* ignore */
}
}
}

Expand All @@ -158,3 +165,15 @@ export function getStoredTokenRefreshConfig(
return null;
}
}

/**
* Outcome of the last native cold-start fetch token refresh (before JS runs).
* Values: `success` | `failed_skip` | `failed_cache` | `none` | `not_run` | `error` | `''` if unset.
*/
export function getFetchTokenRefreshLastOutcome(): string {
try {
return NativeStorageSingleton.getString(KEY_FETCH_LAST_OUTCOME).trim();
} catch (_error) {
return '';
}
}