react-native-nitro-fetch 0.2.0 → 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.
- package/android/src/main/java/com/margelo/nitro/nitrofetch/AutoPrefetcher.kt +203 -45
- package/android/src/main/java/com/margelo/nitro/nitrofetch/NativeStorage.kt +173 -77
- package/ios/NativeStorage.swift +122 -28
- package/ios/NitroAutoPrefetcher.swift +170 -20
- package/ios/NitroBootstrap.mm +5 -5
- package/lib/module/fetch.js +10 -26
- package/lib/module/fetch.js.map +1 -1
- package/lib/module/index.js +1 -0
- package/lib/module/index.js.map +1 -1
- package/lib/module/tokenRefresh.js +105 -0
- package/lib/module/tokenRefresh.js.map +1 -0
- package/lib/typescript/src/NitroFetch.nitro.d.ts +4 -0
- package/lib/typescript/src/NitroFetch.nitro.d.ts.map +1 -1
- package/lib/typescript/src/fetch.d.ts +0 -2
- package/lib/typescript/src/fetch.d.ts.map +1 -1
- package/lib/typescript/src/index.d.ts +2 -0
- package/lib/typescript/src/index.d.ts.map +1 -1
- package/lib/typescript/src/tokenRefresh.d.ts +36 -0
- package/lib/typescript/src/tokenRefresh.d.ts.map +1 -0
- package/nitrogen/generated/android/c++/JHybridNativeStorageSpec.cpp +13 -0
- package/nitrogen/generated/android/c++/JHybridNativeStorageSpec.hpp +3 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitrofetch/HybridNativeStorageSpec.kt +12 -0
- package/nitrogen/generated/ios/c++/HybridNativeStorageSpecSwift.hpp +20 -0
- package/nitrogen/generated/ios/swift/HybridNativeStorageSpec.swift +3 -0
- package/nitrogen/generated/ios/swift/HybridNativeStorageSpec_cxx.swift +34 -0
- package/nitrogen/generated/shared/c++/HybridNativeStorageSpec.cpp +3 -0
- package/nitrogen/generated/shared/c++/HybridNativeStorageSpec.hpp +3 -0
- package/package.json +5 -2
- package/src/NitroFetch.nitro.ts +4 -0
- package/src/fetch.ts +10 -27
- package/src/index.tsx +9 -0
- 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
|
|
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
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
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
|
-
|
|
65
|
-
|
|
66
|
-
|
|
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
|
-
|
|
16
|
-
|
|
17
|
-
private const val PREFS_NAME = "nitro_fetch_storage"
|
|
108
|
+
companion object {
|
|
109
|
+
private const val TAG = "HybridNativeStorage"
|
|
18
110
|
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
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
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
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
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
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
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
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
|
+
}
|