feat(configureNativeFetcher): optional JWT pool for background native fetch

Add jwtTokens / jwtTokenPoolJson to the TypeScript API, parse and validate
(max 128) on Android and iOS, persist jwtTokenPool with native_fetcher_config
when persistToken is true (Android), and extend NativeNotificationContentFetcher
with a four-argument configure overload delegating to the existing three-arg
default. iOS stores the pool in UserDefaults JSON and uses primary jwt or first
pool entry in the plugin background fetch path. Bump version to 2.2.0. Update
TestNativeFetcher to exercise the new configure overload.
This commit is contained in:
Jose Olarte III
2026-03-27 16:30:31 +08:00
parent 469167a55f
commit 9121b1e0f7
8 changed files with 159 additions and 26 deletions

View File

@@ -248,17 +248,46 @@ public class DailyNotificationPlugin: CAPPlugin {
call.reject("jwtToken or jwtSecret is required")
return
}
let jwtTokenPoolMax = 128
var jwtTokenPool: [String]? = nil
if let rawTokens = options["jwtTokens"] as? [Any] {
let parsed = rawTokens.compactMap { $0 as? String }.filter { !$0.isEmpty }
if parsed.count > jwtTokenPoolMax {
call.reject("jwtTokens must have at most \(jwtTokenPoolMax) entries")
return
}
jwtTokenPool = parsed.isEmpty ? nil : parsed
} else if let jsonStr = options["jwtTokenPoolJson"] as? String, !jsonStr.isEmpty {
guard let data = jsonStr.data(using: .utf8),
let parsed = try? JSONSerialization.jsonObject(with: data) as? [String] else {
call.reject("jwtTokenPoolJson must be a JSON array of strings")
return
}
let filtered = parsed.filter { !$0.isEmpty }
if filtered.count > jwtTokenPoolMax {
call.reject("jwtTokens must have at most \(jwtTokenPoolMax) entries")
return
}
jwtTokenPool = filtered.isEmpty ? nil : filtered
}
print("DNP-PLUGIN: Configuring native fetcher: apiBaseUrl=\(apiBaseUrl), activeDid=\(activeDid.prefix(30))...")
if let pool = jwtTokenPool {
print("DNP-PLUGIN: jwtTokenPool size=\(pool.count)")
}
// Store configuration in database for persistence across app restarts
// Note: iOS native fetcher interface not yet implemented, but we store config for future use
let configId = "native_fetcher_config"
let configValue: [String: Any] = [
var configValue: [String: Any] = [
"apiBaseUrl": apiBaseUrl,
"activeDid": activeDid,
"jwtToken": jwtToken
]
if let pool = jwtTokenPool {
configValue["jwtTokenPool"] = pool
}
// Convert to JSON string for storage
guard let jsonData = try? JSONSerialization.data(withJSONObject: configValue, options: []),
@@ -594,32 +623,49 @@ public class DailyNotificationPlugin: CAPPlugin {
let configData = configJson.data(using: .utf8),
let config = try? JSONSerialization.jsonObject(with: configData) as? [String: Any],
let apiBaseUrl = config["apiBaseUrl"] as? String,
let activeDid = config["activeDid"] as? String,
let jwtToken = config["jwtToken"] as? String {
// Phase 3: JWT-signed fetcher is configured - attempt HTTP fetch
print("DNP-FETCH: Using JWT-signed fetcher (apiBaseUrl=\(apiBaseUrl), activeDid=\(activeDid.prefix(30))...)")
// Attempt to fetch content from TimeSafari API
// Note: This is a minimal implementation - can be extended with full API client
do {
let fetchedContent = try await fetchContentFromAPI(
apiBaseUrl: apiBaseUrl,
activeDid: activeDid,
jwtToken: jwtToken
)
content = fetchedContent
print("DNP-FETCH: Successfully fetched content from API")
} catch {
// Fallback to dummy content on fetch failure
print("DNP-FETCH: API fetch failed (\(error.localizedDescription)), using fallback content")
let activeDid = config["activeDid"] as? String {
let jwtFromPrimary = (config["jwtToken"] as? String).flatMap { $0.isEmpty ? nil : $0 }
let jwtFromPool = (config["jwtTokenPool"] as? [String])?.first { !$0.isEmpty }
let bearerToken = jwtFromPrimary ?? jwtFromPool
if let jwtToken = bearerToken {
// Phase 3: JWT-signed fetcher is configured - attempt HTTP fetch
print("DNP-FETCH: Using JWT-signed fetcher (apiBaseUrl=\(apiBaseUrl), activeDid=\(activeDid.prefix(30))...)")
// Attempt to fetch content from TimeSafari API
// Note: This is a minimal implementation - can be extended with full API client
do {
let fetchedContent = try await fetchContentFromAPI(
apiBaseUrl: apiBaseUrl,
activeDid: activeDid,
jwtToken: jwtToken
)
content = fetchedContent
print("DNP-FETCH: Successfully fetched content from API")
} catch {
// Fallback to dummy content on fetch failure
print("DNP-FETCH: API fetch failed (\(error.localizedDescription)), using fallback content")
content = NotificationContent(
id: "fallback_\(Date().timeIntervalSince1970)",
title: "Daily Update",
body: "Your daily notification is ready",
scheduledTime: Int64(Date().timeIntervalSince1970 * 1000) + (5 * 60 * 1000),
fetchedAt: Int64(Date().timeIntervalSince1970 * 1000),
url: nil,
payload: ["fetchError": error.localizedDescription],
etag: nil
)
}
} else {
// Config present but no bearer (empty jwtToken and pool)
print("DNP-FETCH: Using dummy content (no bearer token)")
content = NotificationContent(
id: "fallback_\(Date().timeIntervalSince1970)",
id: "dummy_\(Date().timeIntervalSince1970)",
title: "Daily Update",
body: "Your daily notification is ready",
scheduledTime: Int64(Date().timeIntervalSince1970 * 1000) + (5 * 60 * 1000),
fetchedAt: Int64(Date().timeIntervalSince1970 * 1000),
url: nil,
payload: ["fetchError": error.localizedDescription],
payload: nil,
etag: nil
)
}