react-native-nitro-fetch 0.2.1 → 0.3.0-beta.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (32) hide show
  1. package/android/src/main/java/com/margelo/nitro/nitrofetch/AutoPrefetcher.kt +203 -45
  2. package/android/src/main/java/com/margelo/nitro/nitrofetch/NativeStorage.kt +173 -77
  3. package/ios/NativeStorage.swift +122 -28
  4. package/ios/NitroAutoPrefetcher.swift +170 -20
  5. package/ios/NitroBootstrap.mm +5 -5
  6. package/lib/module/fetch.js +10 -26
  7. package/lib/module/fetch.js.map +1 -1
  8. package/lib/module/index.js +1 -0
  9. package/lib/module/index.js.map +1 -1
  10. package/lib/module/tokenRefresh.js +105 -0
  11. package/lib/module/tokenRefresh.js.map +1 -0
  12. package/lib/typescript/src/NitroFetch.nitro.d.ts +4 -0
  13. package/lib/typescript/src/NitroFetch.nitro.d.ts.map +1 -1
  14. package/lib/typescript/src/fetch.d.ts +0 -2
  15. package/lib/typescript/src/fetch.d.ts.map +1 -1
  16. package/lib/typescript/src/index.d.ts +2 -0
  17. package/lib/typescript/src/index.d.ts.map +1 -1
  18. package/lib/typescript/src/tokenRefresh.d.ts +36 -0
  19. package/lib/typescript/src/tokenRefresh.d.ts.map +1 -0
  20. package/nitrogen/generated/android/c++/JHybridNativeStorageSpec.cpp +13 -0
  21. package/nitrogen/generated/android/c++/JHybridNativeStorageSpec.hpp +3 -0
  22. package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitrofetch/HybridNativeStorageSpec.kt +12 -0
  23. package/nitrogen/generated/ios/c++/HybridNativeStorageSpecSwift.hpp +20 -0
  24. package/nitrogen/generated/ios/swift/HybridNativeStorageSpec.swift +3 -0
  25. package/nitrogen/generated/ios/swift/HybridNativeStorageSpec_cxx.swift +34 -0
  26. package/nitrogen/generated/shared/c++/HybridNativeStorageSpec.cpp +3 -0
  27. package/nitrogen/generated/shared/c++/HybridNativeStorageSpec.hpp +3 -0
  28. package/package.json +2 -2
  29. package/src/NitroFetch.nitro.ts +4 -0
  30. package/src/fetch.ts +10 -27
  31. package/src/index.tsx +9 -0
  32. package/src/tokenRefresh.ts +160 -0
@@ -4,13 +4,17 @@ import android.app.Application
4
4
  import android.content.Context
5
5
  import org.json.JSONArray
6
6
  import org.json.JSONObject
7
+ import java.net.HttpURLConnection
8
+ import java.net.URL
7
9
  import java.util.concurrent.CompletableFuture
8
10
 
9
11
 
10
12
  object AutoPrefetcher {
11
13
  @Volatile private var initialized = false
12
14
  private const val KEY_QUEUE = "nitrofetch_autoprefetch_queue"
13
- private const val PREFS_NAME = "nitro_fetch_storage"
15
+ private const val KEY_TOKEN_REFRESH = "nitro_token_refresh_fetch"
16
+ private const val KEY_TOKEN_CACHE = "nitro_token_refresh_fetch_cache"
17
+ private const val PREFS_NAME = NitroFetchSecureAtRest.PREFS_NAME
14
18
 
15
19
  fun prefetchOnStart(app: Application) {
16
20
  if (initialized) return
@@ -20,55 +24,209 @@ object AutoPrefetcher {
20
24
  val raw = prefs.getString(KEY_QUEUE, null) ?: ""
21
25
  if (raw.isEmpty()) return
22
26
  val arr = JSONArray(raw)
23
- for (i in 0 until arr.length()) {
24
- val o = arr.optJSONObject(i) ?: continue
25
- val url = o.optString("url", null) ?: continue
26
- val prefetchKey = o.optString("prefetchKey", null) ?: continue
27
- val headersObj = o.optJSONObject("headers") ?: JSONObject()
28
- val headersList = mutableListOf<Pair<String, String>>()
29
- headersObj.keys().forEachRemaining { k ->
30
- headersList.add(k to headersObj.optString(k, ""))
31
- }
32
- // Ensure prefetchKey header is present
33
- headersList.add("prefetchKey" to prefetchKey)
34
-
35
- val headerObjs = headersList.map { (k, v) -> NitroHeader(k, v) }.toTypedArray()
36
- val req = NitroRequest(
37
- url = url,
38
- method = null,
39
- headers = headerObjs,
40
- bodyString = null,
41
- bodyBytes = null,
42
- bodyFormData = null,
43
- timeoutMs = null,
44
- followRedirects = null,
45
- requestId = null
46
- )
47
-
48
- // If already pending or fresh, skip starting a new one
49
- if (FetchCache.getPending(prefetchKey) != null) continue
50
- if (FetchCache.hasFreshResult(prefetchKey, 5_000L)) continue
51
-
52
- val future = CompletableFuture<NitroResponse>()
53
- FetchCache.setPending(prefetchKey, future)
54
- NitroFetchClient.fetch(req,
55
- onSuccess = { res ->
56
- try {
57
- FetchCache.complete(prefetchKey, res)
58
- future.complete(res)
59
- } catch (t: Throwable) {
60
- FetchCache.completeExceptionally(prefetchKey, t)
61
- future.completeExceptionally(t)
27
+
28
+ val refreshRaw = NitroFetchSecureAtRest.getDecryptedForPrefs(prefs, KEY_TOKEN_REFRESH)
29
+
30
+ if (!refreshRaw.isNullOrEmpty()) {
31
+ // Token refresh requires a network call — run everything on a background thread
32
+ Thread {
33
+ try {
34
+ val refreshConfig = JSONObject(refreshRaw)
35
+ val onFailure = refreshConfig.optString("onFailure", "useStoredHeaders")
36
+ val refreshURL = refreshConfig.optString("url", "(unknown)")
37
+ android.util.Log.d("NitroFetch", "[TokenRefresh] Calling refresh endpoint: $refreshURL")
38
+
39
+ val refreshed = callTokenRefreshSync(refreshConfig)
40
+
41
+ val tokenHeaders: Map<String, String> = if (refreshed != null) {
42
+ android.util.Log.d("NitroFetch", "[TokenRefresh] ✅ Success — got ${refreshed.size} header(s)")
43
+ refreshed.forEach { (k, v) -> android.util.Log.d("NitroFetch", "[TokenRefresh] $k: $v") }
44
+ // Cache fresh token headers for useStoredHeaders fallback on next cold start
45
+ val cacheJson = JSONObject()
46
+ refreshed.forEach { (k, v) -> cacheJson.put(k, v) }
47
+ NitroFetchSecureAtRest.putEncrypted(prefs, KEY_TOKEN_CACHE, cacheJson.toString())
48
+ refreshed
49
+ } else {
50
+ android.util.Log.d("NitroFetch", "[TokenRefresh] ❌ Refresh failed — onFailure: $onFailure")
51
+ if (onFailure == "skip") {
52
+ android.util.Log.d("NitroFetch", "[TokenRefresh] Skipping all prefetches")
53
+ return@Thread
54
+ }
55
+ // Use last cached token headers (or empty map if none cached yet)
56
+ val cacheRaw = NitroFetchSecureAtRest.getDecryptedForPrefs(prefs, KEY_TOKEN_CACHE)
57
+ val cached = if (cacheRaw != null) {
58
+ try {
59
+ val co = JSONObject(cacheRaw)
60
+ co.keys().asSequence().associateWith { k -> co.optString(k, "") }
61
+ } catch (_: Throwable) { emptyMap() }
62
+ } else {
63
+ emptyMap()
64
+ }
65
+ android.util.Log.d("NitroFetch", "[TokenRefresh] Using cached headers (${cached.size} header(s))")
66
+ cached
62
67
  }
63
- },
64
- onFail = { err ->
65
- FetchCache.completeExceptionally(prefetchKey, err)
66
- future.completeExceptionally(err)
68
+
69
+ android.util.Log.d("NitroFetch", "[TokenRefresh] Injecting token headers into ${arr.length()} prefetch URL(s)")
70
+ startPrefetches(arr, tokenHeaders)
71
+ } catch (_: Throwable) {
72
+ // Best-effort — never crash the app
67
73
  }
68
- )
74
+ }.start()
75
+ } else {
76
+ // No token refresh config — proceed on current thread (Cronet is async)
77
+ startPrefetches(arr, emptyMap())
69
78
  }
70
79
  } catch (_: Throwable) {
71
80
  // ignore – prefetch-on-start is best-effort
72
81
  }
73
82
  }
83
+
84
+ private fun startPrefetches(arr: JSONArray, tokenHeaders: Map<String, String>) {
85
+ for (i in 0 until arr.length()) {
86
+ val o = arr.optJSONObject(i) ?: continue
87
+ val url = o.optString("url", null) ?: continue
88
+ val prefetchKey = o.optString("prefetchKey", null) ?: continue
89
+ val headersObj = o.optJSONObject("headers") ?: JSONObject()
90
+
91
+ // Merge: static headers first, token headers override
92
+ val merged = mutableMapOf<String, String>()
93
+ headersObj.keys().forEachRemaining { k ->
94
+ merged[k] = headersObj.optString(k, "")
95
+ }
96
+ tokenHeaders.forEach { (k, v) -> merged[k] = v }
97
+ merged["prefetchKey"] = prefetchKey
98
+
99
+ android.util.Log.d("NitroFetch", "[TokenRefresh] Prefetching $url with ${merged.size} header(s)")
100
+ merged.forEach { (k, v) -> android.util.Log.d("NitroFetch", "[TokenRefresh] $k: $v") }
101
+ val headerObjs = merged.map { (k, v) -> NitroHeader(k, v) }.toTypedArray()
102
+ val req = NitroRequest(
103
+ url = url,
104
+ method = null,
105
+ headers = headerObjs,
106
+ bodyString = null,
107
+ bodyBytes = null,
108
+ bodyFormData = null,
109
+ timeoutMs = null,
110
+ followRedirects = null,
111
+ requestId = null
112
+ )
113
+
114
+ if (FetchCache.getPending(prefetchKey) != null) continue
115
+ if (FetchCache.hasFreshResult(prefetchKey, 5_000L)) continue
116
+
117
+ val future = CompletableFuture<NitroResponse>()
118
+ FetchCache.setPending(prefetchKey, future)
119
+ NitroFetchClient.fetch(req,
120
+ onSuccess = { res ->
121
+ try {
122
+ FetchCache.complete(prefetchKey, res)
123
+ future.complete(res)
124
+ } catch (t: Throwable) {
125
+ FetchCache.completeExceptionally(prefetchKey, t)
126
+ future.completeExceptionally(t)
127
+ }
128
+ },
129
+ onFail = { err ->
130
+ FetchCache.completeExceptionally(prefetchKey, err)
131
+ future.completeExceptionally(err)
132
+ }
133
+ )
134
+ }
135
+ }
136
+
137
+ // MARK: - Token refresh (synchronous, runs on background thread)
138
+
139
+ private fun callTokenRefreshSync(config: JSONObject): Map<String, String>? {
140
+ return try {
141
+ val urlStr = config.optString("url", null) ?: return null
142
+ val method = config.optString("method", "POST")
143
+ val reqHeaders = config.optJSONObject("headers")
144
+ val body = config.optString("body", null)
145
+ val responseType = config.optString("responseType", "json")
146
+
147
+ val conn = URL(urlStr).openConnection() as HttpURLConnection
148
+ conn.requestMethod = method
149
+ conn.connectTimeout = 10_000
150
+ conn.readTimeout = 10_000
151
+ conn.doInput = true
152
+ if (body != null) conn.doOutput = true
153
+
154
+ reqHeaders?.keys()?.forEachRemaining { k ->
155
+ conn.setRequestProperty(k, reqHeaders.optString(k, ""))
156
+ }
157
+
158
+ if (body != null) {
159
+ conn.outputStream.use { it.write(body.toByteArray(Charsets.UTF_8)) }
160
+ }
161
+
162
+ val status = conn.responseCode
163
+ if (status !in 200..299) return null
164
+
165
+ val responseBody = conn.inputStream.use { it.bufferedReader(Charsets.UTF_8).readText() }
166
+
167
+ parseTokenResponse(responseBody, responseType, config)
168
+ } catch (_: Throwable) {
169
+ null
170
+ }
171
+ }
172
+
173
+ private fun parseTokenResponse(
174
+ body: String,
175
+ responseType: String,
176
+ config: JSONObject
177
+ ): Map<String, String> {
178
+ val result = mutableMapOf<String, String>()
179
+
180
+ if (responseType == "text") {
181
+ val textHeader = config.optString("textHeader", null)
182
+ if (textHeader != null) {
183
+ val textTemplate = config.optString("textTemplate", null)
184
+ result[textHeader] = textTemplate?.replace("{{value}}", body) ?: body
185
+ }
186
+ return result
187
+ }
188
+
189
+ // JSON
190
+ val json = try { JSONObject(body) } catch (_: Throwable) { return result }
191
+
192
+ val mappings = config.optJSONArray("mappings")
193
+ if (mappings != null) {
194
+ for (i in 0 until mappings.length()) {
195
+ val m = mappings.optJSONObject(i) ?: continue
196
+ val jsonPath = m.optString("jsonPath", null) ?: continue
197
+ val header = m.optString("header", null) ?: continue
198
+ val value = getNestedField(json, jsonPath) ?: continue
199
+ val tmpl = m.optString("valueTemplate", null)
200
+ result[header] = tmpl?.replace("{{value}}", value) ?: value
201
+ }
202
+ }
203
+
204
+ val compositeHeaders = config.optJSONArray("compositeHeaders")
205
+ if (compositeHeaders != null) {
206
+ for (i in 0 until compositeHeaders.length()) {
207
+ val comp = compositeHeaders.optJSONObject(i) ?: continue
208
+ val header = comp.optString("header", null) ?: continue
209
+ val template = comp.optString("template", null) ?: continue
210
+ val paths = comp.optJSONObject("paths") ?: continue
211
+ var built = template
212
+ paths.keys().forEachRemaining { ph ->
213
+ val val2 = getNestedField(json, paths.optString(ph, ""))
214
+ built = built.replace("{{$ph}}", val2 ?: "")
215
+ }
216
+ result[header] = built
217
+ }
218
+ }
219
+
220
+ return result
221
+ }
222
+
223
+ private fun getNestedField(obj: JSONObject, dotPath: String): String? {
224
+ val parts = dotPath.split(".")
225
+ var current: Any = obj
226
+ for (part in parts) {
227
+ if (current !is JSONObject) return null
228
+ current = current.opt(part) ?: return null
229
+ }
230
+ return current.toString()
231
+ }
74
232
  }
@@ -1,102 +1,198 @@
1
1
  package com.margelo.nitro.nitrofetch
2
2
 
3
- import android.app.Application
4
3
  import android.content.Context
5
4
  import android.content.SharedPreferences
5
+ import android.security.keystore.KeyGenParameterSpec
6
+ import android.security.keystore.KeyProperties
7
+ import android.util.Base64
6
8
  import android.util.Log
7
9
  import com.facebook.proguard.annotations.DoNotStrip
8
10
  import com.margelo.nitro.NitroModules
11
+ import java.security.KeyStore
12
+ import javax.crypto.Cipher
13
+ import javax.crypto.KeyGenerator
14
+ import javax.crypto.spec.GCMParameterSpec
9
15
 
16
+ /**
17
+ * Keystore-backed AES-GCM strings stored in [PREFS_NAME] with prefix [ENC_PREFIX].
18
+ * Keep [KEYSTORE_ALIAS] and [ENC_PREFIX] in sync with `NitroWebSocketAutoPrewarmer.kt`.
19
+ */
20
+ internal object NitroFetchSecureAtRest {
21
+ internal const val PREFS_NAME = "nitro_fetch_storage"
22
+ private const val KEYSTORE_ALIAS = "nitro_fetch_aes_gcm_v1"
23
+ private const val ANDROID_KEYSTORE = "AndroidKeyStore"
24
+ private const val TRANSFORMATION = "AES/GCM/NoPadding"
25
+ private const val GCM_IV_LENGTH = 12
26
+ private const val GCM_TAG_BITS = 128
27
+ const val ENC_PREFIX = "nfc1:"
10
28
 
29
+ private fun keyStore(): KeyStore =
30
+ KeyStore.getInstance(ANDROID_KEYSTORE).apply { load(null) }
31
+
32
+ private fun getOrCreateSecretKey(): javax.crypto.SecretKey {
33
+ val ks = keyStore()
34
+ if (ks.containsAlias(KEYSTORE_ALIAS)) {
35
+ return (ks.getEntry(KEYSTORE_ALIAS, null) as KeyStore.SecretKeyEntry).secretKey
36
+ }
37
+ val keyGenerator =
38
+ KeyGenerator.getInstance(KeyProperties.KEY_ALGORITHM_AES, ANDROID_KEYSTORE)
39
+ val spec =
40
+ KeyGenParameterSpec.Builder(
41
+ KEYSTORE_ALIAS,
42
+ KeyProperties.PURPOSE_ENCRYPT or KeyProperties.PURPOSE_DECRYPT
43
+ )
44
+ .setBlockModes(KeyProperties.BLOCK_MODE_GCM)
45
+ .setEncryptionPaddings(KeyProperties.ENCRYPTION_PADDING_NONE)
46
+ .setKeySize(256)
47
+ .build()
48
+ keyGenerator.init(spec)
49
+ return keyGenerator.generateKey()
50
+ }
51
+
52
+ private fun encrypt(plaintext: String): String {
53
+ val key = getOrCreateSecretKey()
54
+ val cipher = Cipher.getInstance(TRANSFORMATION)
55
+ cipher.init(Cipher.ENCRYPT_MODE, key)
56
+ val iv = cipher.iv
57
+ val ciphertext = cipher.doFinal(plaintext.toByteArray(Charsets.UTF_8))
58
+ val combined = ByteArray(iv.size + ciphertext.size)
59
+ System.arraycopy(iv, 0, combined, 0, iv.size)
60
+ System.arraycopy(ciphertext, 0, combined, iv.size, ciphertext.size)
61
+ return Base64.encodeToString(combined, Base64.NO_WRAP)
62
+ }
63
+
64
+ private fun decrypt(b64: String): String {
65
+ val combined = Base64.decode(b64, Base64.NO_WRAP)
66
+ if (combined.size < GCM_IV_LENGTH + 16) {
67
+ throw IllegalArgumentException("truncated")
68
+ }
69
+ val iv = combined.copyOfRange(0, GCM_IV_LENGTH)
70
+ val ciphertext = combined.copyOfRange(GCM_IV_LENGTH, combined.size)
71
+ val key = getOrCreateSecretKey()
72
+ val cipher = Cipher.getInstance(TRANSFORMATION)
73
+ cipher.init(Cipher.DECRYPT_MODE, key, GCMParameterSpec(GCM_TAG_BITS, iv))
74
+ return String(cipher.doFinal(ciphertext), Charsets.UTF_8)
75
+ }
76
+
77
+ /** Plaintext for JSON parsing, or null if key absent. Migrates legacy plaintext to encrypted. */
78
+ fun getDecryptedForPrefs(prefs: SharedPreferences, key: String): String? {
79
+ val raw = prefs.getString(key, null) ?: return null
80
+ if (raw.isEmpty()) return ""
81
+ return if (raw.startsWith(ENC_PREFIX)) {
82
+ try {
83
+ decrypt(raw.substring(ENC_PREFIX.length))
84
+ } catch (_: Throwable) {
85
+ raw
86
+ }
87
+ } else {
88
+ try {
89
+ putEncrypted(prefs, key, raw)
90
+ } catch (_: Throwable) {}
91
+ raw
92
+ }
93
+ }
94
+
95
+ fun putEncrypted(prefs: SharedPreferences, key: String, plain: String): Boolean {
96
+ val enc = ENC_PREFIX + encrypt(plain)
97
+ return prefs.edit().putString(key, enc).commit()
98
+ }
99
+
100
+ fun removeFromPrefs(prefs: SharedPreferences, key: String): Boolean {
101
+ return prefs.edit().remove(key).commit()
102
+ }
103
+ }
11
104
 
12
105
  @DoNotStrip
13
106
  class NativeStorage : HybridNativeStorageSpec() {
14
107
 
15
- companion object {
16
- private const val TAG = "HybridNativeStorage"
17
- private const val PREFS_NAME = "nitro_fetch_storage"
108
+ companion object {
109
+ private const val TAG = "HybridNativeStorage"
18
110
 
19
-
20
- private val sharedPreferences: SharedPreferences by lazy {
21
- val context = NitroModules.applicationContext ?: throw Error("Cannot get Android Context - No Context available!")
22
- context.getSharedPreferences(PREFS_NAME, Context.MODE_PRIVATE)
23
- }
111
+ private val sharedPreferences: SharedPreferences by lazy {
112
+ val context =
113
+ NitroModules.applicationContext
114
+ ?: throw Error("Cannot get Android Context - No Context available!")
115
+ context.getSharedPreferences(NitroFetchSecureAtRest.PREFS_NAME, Context.MODE_PRIVATE)
116
+ }
24
117
 
118
+ }
25
119
 
120
+ override fun getString(key: String): String {
121
+ return try {
122
+ val value = sharedPreferences.getString(key, null)
123
+ if (value != null) {
124
+ Log.d(TAG, "Retrieved value for key: $key")
125
+ value
126
+ } else {
127
+ Log.d(TAG, "Key not found: $key, returning empty string")
128
+ ""
129
+ }
130
+ } catch (t: Throwable) {
131
+ Log.e(TAG, "Error getting string for key: $key", t)
132
+ throw RuntimeException("Failed to get string for key: $key", t)
26
133
  }
134
+ }
27
135
 
28
- /**
29
- * Retrieves a string value for the given key.
30
- *
31
- * @param key The key to look up in storage
32
- * @return The stored string value, or empty string if key doesn't exist
33
- * @throws IllegalStateException if SharedPreferences is not available
34
- */
35
- override fun getString(key: String): String {
36
- return try {
37
- val value = sharedPreferences.getString(key, null)
38
- if (value != null) {
39
- Log.d(TAG, "Retrieved value for key: $key")
40
- value
41
- } else {
42
- Log.d(TAG, "Key not found: $key, returning empty string")
43
- ""
44
- }
45
- } catch (t: Throwable) {
46
- Log.e(TAG, "Error getting string for key: $key", t)
47
- throw RuntimeException("Failed to get string for key: $key", t)
48
- }
136
+ override fun setString(key: String, value: String) {
137
+ try {
138
+ val editor = sharedPreferences.edit()
139
+ editor.putString(key, value)
140
+ val success = editor.commit()
141
+ if (success) {
142
+ Log.d(TAG, "Successfully stored value for key: $key")
143
+ } else {
144
+ Log.e(TAG, "Failed to commit value for key: $key")
145
+ throw RuntimeException("Failed to store value for key: $key")
146
+ }
147
+ } catch (t: Throwable) {
148
+ Log.e(TAG, "Error setting string for key: $key", t)
149
+ throw RuntimeException("Failed to set string for key: $key", t)
49
150
  }
151
+ }
50
152
 
51
- /**
52
- * Stores a string value with the given key.
53
- *
54
- * @param key The key to store the value under
55
- * @param value The string value to store
56
- * @throws IllegalStateException if SharedPreferences is not available
57
- * @throws RuntimeException if the write operation fails
58
- */
59
- override fun setString(key: String, value: String) {
60
- try {
61
- val editor = sharedPreferences.edit()
62
- editor.putString(key, value)
63
- val success = editor.commit() // commit() is synchronous and returns boolean
64
- if (success) {
65
- Log.d(TAG, "Successfully stored value for key: $key")
66
- } else {
67
- Log.e(TAG, "Failed to commit value for key: $key")
68
- throw RuntimeException("Failed to store value for key: $key")
69
- }
70
- } catch (t: Throwable) {
71
- Log.e(TAG, "Error setting string for key: $key", t)
72
- throw RuntimeException("Failed to set string for key: $key", t)
73
- }
153
+ override fun removeString(key: String) {
154
+ try {
155
+ val editor = sharedPreferences.edit()
156
+ editor.remove(key)
157
+ val success = editor.commit()
158
+ if (success) {
159
+ Log.d(TAG, "Successfully deleted key: $key")
160
+ } else {
161
+ Log.e(TAG, "Failed to commit deletion for key: $key")
162
+ throw RuntimeException("Failed to delete key: $key")
163
+ }
164
+ } catch (t: Throwable) {
165
+ Log.e(TAG, "Error deleting key: $key", t)
166
+ throw RuntimeException("Failed to delete key: $key", t)
74
167
  }
168
+ }
75
169
 
76
- /**
77
- * Deletes the value associated with the given key.
78
- * If the key doesn't exist, this is a no-op.
79
- *
80
- * @param key The key to delete from storage
81
- * @throws IllegalStateException if SharedPreferences is not available
82
- * @throws RuntimeException if the delete operation fails
83
- */
84
- override fun removeString(key: String) {
85
- try {
86
- val editor = sharedPreferences.edit()
87
- editor.remove(key)
88
- val success = editor.commit() // commit() is synchronous and returns boolean
89
- if (success) {
90
- Log.d(TAG, "Successfully deleted key: $key")
91
- } else {
92
- Log.e(TAG, "Failed to commit deletion for key: $key")
93
- throw RuntimeException("Failed to delete key: $key")
94
- }
95
- } catch (t: Throwable) {
96
- Log.e(TAG, "Error deleting key: $key", t)
97
- throw RuntimeException("Failed to delete key: $key", t)
98
- }
170
+ override fun getSecureString(key: String): String {
171
+ return try {
172
+ NitroFetchSecureAtRest.getDecryptedForPrefs(sharedPreferences, key) ?: ""
173
+ } catch (t: Throwable) {
174
+ Log.e(TAG, "Error getSecureString for key: $key", t)
175
+ throw RuntimeException("Failed to get secure string for key: $key", t)
99
176
  }
100
- }
177
+ }
101
178
 
179
+ override fun setSecureString(key: String, value: String) {
180
+ try {
181
+ val ok = NitroFetchSecureAtRest.putEncrypted(sharedPreferences, key, value)
182
+ if (!ok) throw RuntimeException("commit failed")
183
+ } catch (t: Throwable) {
184
+ Log.e(TAG, "Error setSecureString for key: $key", t)
185
+ throw RuntimeException("Failed to set secure string for key: $key", t)
186
+ }
187
+ }
102
188
 
189
+ override fun removeSecureString(key: String) {
190
+ try {
191
+ val ok = NitroFetchSecureAtRest.removeFromPrefs(sharedPreferences, key)
192
+ if (!ok) throw RuntimeException("commit failed")
193
+ } catch (t: Throwable) {
194
+ Log.e(TAG, "Error removeSecureString for key: $key", t)
195
+ throw RuntimeException("Failed to remove secure string for key: $key", t)
196
+ }
197
+ }
198
+ }