Skip to content

Commit b2927ee

Browse files
committed
feat: expose cold-start fetch token-refresh outcome to JS
- Android AutoPrefetcher: persist last outcome to SharedPreferences (plaintext key nitro_token_refresh_fetch_last_outcome) at each exit: success, failed_skip, failed_cache, none, not_run, error. - iOS NitroAutoPrefetcher: mirror outcome writes to UserDefaults. - JS: getFetchTokenRefreshLastOutcome(); clearTokenRefresh removes the outcome key. Depends on Android CookieManager sync PR for a clean AutoPrefetcher history. Made-with: Cursor
1 parent c343524 commit b2927ee

4 files changed

Lines changed: 50 additions & 3 deletions

File tree

packages/react-native-nitro-fetch/android/src/main/java/com/margelo/nitro/nitrofetch/AutoPrefetcher.kt

Lines changed: 15 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,15 +15,24 @@ object AutoPrefetcher {
1515
private const val KEY_QUEUE = "nitrofetch_autoprefetch_queue"
1616
private const val KEY_TOKEN_REFRESH = "nitro_token_refresh_fetch"
1717
private const val KEY_TOKEN_CACHE = "nitro_token_refresh_fetch_cache"
18+
/** Plaintext outcome for debug / JS — same key as `tokenRefresh.ts` */
19+
private const val KEY_LAST_FETCH_TOKEN_REFRESH_OUTCOME = "nitro_token_refresh_fetch_last_outcome"
1820
private const val PREFS_NAME = NitroFetchSecureAtRest.PREFS_NAME
1921

22+
private fun setFetchTokenRefreshOutcome(prefs: android.content.SharedPreferences, value: String) {
23+
prefs.edit().putString(KEY_LAST_FETCH_TOKEN_REFRESH_OUTCOME, value).apply()
24+
}
25+
2026
fun prefetchOnStart(app: Application) {
2127
if (initialized) return
2228
initialized = true
2329
try {
2430
val prefs = app.getSharedPreferences(PREFS_NAME, Context.MODE_PRIVATE)
2531
val raw = prefs.getString(KEY_QUEUE, null) ?: ""
26-
if (raw.isEmpty()) return
32+
if (raw.isEmpty()) {
33+
setFetchTokenRefreshOutcome(prefs, "not_run")
34+
return
35+
}
2736
val arr = JSONArray(raw)
2837

2938
val refreshRaw = NitroFetchSecureAtRest.getDecryptedForPrefs(prefs, KEY_TOKEN_REFRESH)
@@ -41,7 +50,7 @@ object AutoPrefetcher {
4150

4251
val tokenHeaders: Map<String, String> = if (refreshed != null) {
4352
android.util.Log.d("NitroFetch", "[TokenRefresh] ✅ Success — got ${refreshed.size} header(s)")
44-
refreshed.forEach { (k, v) -> android.util.Log.d("NitroFetch", "[TokenRefresh] $k: $v") }
53+
setFetchTokenRefreshOutcome(prefs, "success")
4554
// Cache fresh token headers for useStoredHeaders fallback on next cold start
4655
val cacheJson = JSONObject()
4756
refreshed.forEach { (k, v) -> cacheJson.put(k, v) }
@@ -51,6 +60,7 @@ object AutoPrefetcher {
5160
android.util.Log.d("NitroFetch", "[TokenRefresh] ❌ Refresh failed — onFailure: $onFailure")
5261
if (onFailure == "skip") {
5362
android.util.Log.d("NitroFetch", "[TokenRefresh] Skipping all prefetches")
63+
setFetchTokenRefreshOutcome(prefs, "failed_skip")
5464
return@Thread
5565
}
5666
// Use last cached token headers (or empty map if none cached yet)
@@ -64,16 +74,19 @@ object AutoPrefetcher {
6474
emptyMap()
6575
}
6676
android.util.Log.d("NitroFetch", "[TokenRefresh] Using cached headers (${cached.size} header(s))")
77+
setFetchTokenRefreshOutcome(prefs, "failed_cache")
6778
cached
6879
}
6980

7081
android.util.Log.d("NitroFetch", "[TokenRefresh] Injecting token headers into ${arr.length()} prefetch URL(s)")
7182
startPrefetches(arr, tokenHeaders)
7283
} catch (_: Throwable) {
84+
setFetchTokenRefreshOutcome(prefs, "error")
7385
// Best-effort — never crash the app
7486
}
7587
}.start()
7688
} else {
89+
setFetchTokenRefreshOutcome(prefs, "none")
7790
// No token refresh config — proceed on current thread (Cronet is async)
7891
startPrefetches(arr, emptyMap())
7992
}

packages/react-native-nitro-fetch/ios/NitroAutoPrefetcher.swift

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,14 +7,24 @@ public final class NitroAutoPrefetcher: NSObject {
77
private static let suiteName = "nitro_fetch_storage"
88
private static let tokenRefreshKey = "nitro_token_refresh_fetch"
99
private static let tokenCacheKey = "nitro_token_refresh_fetch_cache"
10+
/// Plaintext outcome for debug / JS (`NativeStorage.getString`). Same key as `tokenRefresh.ts`.
11+
private static let lastFetchTokenRefreshOutcomeKey = "nitro_token_refresh_fetch_last_outcome"
12+
13+
private static func setFetchTokenRefreshOutcome(_ value: String, defaults: UserDefaults) {
14+
defaults.set(value, forKey: lastFetchTokenRefreshOutcomeKey)
15+
defaults.synchronize()
16+
}
1017

1118
@objc
1219
public static func prefetchOnStart() {
1320
if initialized { return }
1421
initialized = true
1522

1623
let userDefaults = UserDefaults(suiteName: suiteName) ?? UserDefaults.standard
17-
guard let raw = userDefaults.string(forKey: queueKey), !raw.isEmpty else { return }
24+
guard let raw = userDefaults.string(forKey: queueKey), !raw.isEmpty else {
25+
setFetchTokenRefreshOutcome("not_run", defaults: userDefaults)
26+
return
27+
}
1828
guard let data = raw.data(using: .utf8) else { return }
1929
guard let arr = try? JSONSerialization.jsonObject(with: data, options: []) as? [Any] else { return }
2030

@@ -33,6 +43,7 @@ public final class NitroAutoPrefetcher: NSObject {
3343
let refreshed = try? await callTokenRefresh(config: refreshObj)
3444
if let refreshed = refreshed {
3545
print("[NitroFetch][TokenRefresh] ✅ Success — got \(refreshed.count) header(s)")
46+
setFetchTokenRefreshOutcome("success", defaults: userDefaults)
3647
for (k, v) in refreshed { print("[NitroFetch][TokenRefresh] \(k): \(v)") }
3748
// Cache fresh token headers for useStoredHeaders fallback on next cold start
3849
if let cacheData = try? JSONSerialization.data(withJSONObject: refreshed),
@@ -44,6 +55,7 @@ public final class NitroAutoPrefetcher: NSObject {
4455
print("[NitroFetch][TokenRefresh] ❌ Refresh failed — onFailure: \(onFailure)")
4556
if onFailure == "skip" {
4657
print("[NitroFetch][TokenRefresh] Skipping all prefetches")
58+
setFetchTokenRefreshOutcome("failed_skip", defaults: userDefaults)
4759
return
4860
}
4961
var cached: [String: String] = [:]
@@ -54,9 +66,11 @@ public final class NitroAutoPrefetcher: NSObject {
5466
cached = cacheObj
5567
}
5668
print("[NitroFetch][TokenRefresh] Using cached headers (\(cached.count) header(s))")
69+
setFetchTokenRefreshOutcome("failed_cache", defaults: userDefaults)
5770
tokenHeaders = cached
5871
}
5972
} else {
73+
setFetchTokenRefreshOutcome("none", defaults: userDefaults)
6074
tokenHeaders = [:]
6175
}
6276

packages/react-native-nitro-fetch/src/index.tsx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ export {
2222
clearTokenRefresh,
2323
callRefreshEndpoint,
2424
getStoredTokenRefreshConfig,
25+
getFetchTokenRefreshLastOutcome,
2526
getNestedField,
2627
applyTemplate,
2728
} from './tokenRefresh';

packages/react-native-nitro-fetch/src/tokenRefresh.ts

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,8 @@ const KEY_WS = 'nitro_token_refresh_websocket';
55
const KEY_FETCH = 'nitro_token_refresh_fetch';
66
const KEY_WS_CACHE = 'nitro_token_refresh_ws_cache';
77
const KEY_FETCH_CACHE = 'nitro_token_refresh_fetch_cache';
8+
/** Plaintext; written by native cold-start autoprefetch (`NitroAutoPrefetcher` / `AutoPrefetcher`). */
9+
const KEY_FETCH_LAST_OUTCOME = 'nitro_token_refresh_fetch_last_outcome';
810

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

@@ -143,6 +145,11 @@ export function clearTokenRefresh(target?: TokenRefreshTarget): void {
143145
if (t === 'fetch' || t === 'all') {
144146
NativeStorageSingleton.removeSecureString(KEY_FETCH);
145147
NativeStorageSingleton.removeSecureString(KEY_FETCH_CACHE);
148+
try {
149+
NativeStorageSingleton.removeString(KEY_FETCH_LAST_OUTCOME);
150+
} catch (_error) {
151+
/* ignore */
152+
}
146153
}
147154
}
148155

@@ -158,3 +165,15 @@ export function getStoredTokenRefreshConfig(
158165
return null;
159166
}
160167
}
168+
169+
/**
170+
* Outcome of the last native cold-start fetch token refresh (before JS runs).
171+
* Values: `success` | `failed_skip` | `failed_cache` | `none` | `not_run` | `error` | `''` if unset.
172+
*/
173+
export function getFetchTokenRefreshLastOutcome(): string {
174+
try {
175+
return NativeStorageSingleton.getString(KEY_FETCH_LAST_OUTCOME).trim();
176+
} catch (_error) {
177+
return '';
178+
}
179+
}

0 commit comments

Comments
 (0)