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:
@@ -22,6 +22,12 @@ object DailyNotificationConstants {
|
||||
// Permission Request Codes
|
||||
// ============================================================
|
||||
|
||||
/**
|
||||
* Maximum number of distinct JWT strings allowed in [configureNativeFetcher] `jwtTokens` / pool.
|
||||
* Host apps (e.g. TimeSafari) use a pool for background prefetch; cap avoids oversized bridge payloads.
|
||||
*/
|
||||
const val JWT_TOKEN_POOL_MAX = 128
|
||||
|
||||
/**
|
||||
* Request code for notification permission requests
|
||||
* Used by ActivityCompat.requestPermissions()
|
||||
|
||||
@@ -519,12 +519,22 @@ open class DailyNotificationPlugin : Plugin() {
|
||||
val activeDid = options.getString("activeDid") ?: return call.reject("activeDid is required")
|
||||
val jwtToken = options.getString("jwtToken") ?: options.getString("jwtSecret") ?: return call.reject("jwtToken or jwtSecret is required")
|
||||
|
||||
val jwtTokenPool: List<String>? = try {
|
||||
parseJwtTokenPool(options)
|
||||
} catch (e: IllegalArgumentException) {
|
||||
return call.reject(e.message ?: "Invalid jwt token pool")
|
||||
}
|
||||
|
||||
val nativeFetcher = getNativeFetcherStatic()
|
||||
if (nativeFetcher == null) {
|
||||
return call.reject("No native fetcher registered. Host app must register a NativeNotificationContentFetcher.")
|
||||
}
|
||||
|
||||
Log.i(TAG, "Configuring native fetcher: apiBaseUrl=$apiBaseUrl, activeDid=$activeDid")
|
||||
Log.i(
|
||||
TAG,
|
||||
"Configuring native fetcher: apiBaseUrl=$apiBaseUrl, activeDid=$activeDid" +
|
||||
if (jwtTokenPool != null) ", jwtTokenPoolSize=${jwtTokenPool.size}" else ""
|
||||
)
|
||||
|
||||
// Call the native fetcher's configure method FIRST
|
||||
// This configures the fetcher instance with API credentials for background operations
|
||||
@@ -533,7 +543,7 @@ open class DailyNotificationPlugin : Plugin() {
|
||||
|
||||
try {
|
||||
Log.d(TAG, "FETCHER|CONFIGURE_START apiBaseUrl=$apiBaseUrl, activeDid=${activeDid.take(30)}...")
|
||||
nativeFetcher.configure(apiBaseUrl, activeDid, jwtToken)
|
||||
nativeFetcher.configure(apiBaseUrl, activeDid, jwtToken, jwtTokenPool)
|
||||
configureSuccess = true
|
||||
Log.i(TAG, "FETCHER|CONFIGURE_COMPLETE success=true")
|
||||
} catch (e: Exception) {
|
||||
@@ -566,6 +576,12 @@ open class DailyNotificationPlugin : Plugin() {
|
||||
// Only store JWT token if explicitly opted-in
|
||||
if (persistToken) {
|
||||
put("jwtToken", jwtToken)
|
||||
if (jwtTokenPool != null && jwtTokenPool.isNotEmpty()) {
|
||||
put("jwtTokenPool", JSONArray(jwtTokenPool))
|
||||
Log.w(TAG, "JWT token pool stored (size=${jwtTokenPool.size}, persistToken=true).")
|
||||
} else {
|
||||
put("jwtTokenPool", JSONArray())
|
||||
}
|
||||
Log.w(TAG, "JWT token stored in database (persistToken=true). " +
|
||||
"Database is NOT encrypted - token is stored in plain text.")
|
||||
} else {
|
||||
@@ -607,6 +623,34 @@ open class DailyNotificationPlugin : Plugin() {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Optional JWT pool from `jwtTokens` (bridge array) or `jwtTokenPoolJson` (JSON array string).
|
||||
* Prefer `jwtTokens` when both are present. Empty array → null (same as omitting).
|
||||
*/
|
||||
private fun parseJwtTokenPool(options: JSObject): List<String>? {
|
||||
val arr: JSONArray = when {
|
||||
options.has("jwtTokens") -> options.optJSONArray("jwtTokens")
|
||||
!options.optString("jwtTokenPoolJson", "").isNullOrBlank() ->
|
||||
JSONArray(options.getString("jwtTokenPoolJson"))
|
||||
else -> null
|
||||
} ?: return null
|
||||
if (arr.length() == 0) return null
|
||||
if (arr.length() > DailyNotificationConstants.JWT_TOKEN_POOL_MAX) {
|
||||
throw IllegalArgumentException(
|
||||
"jwtTokens must have at most ${DailyNotificationConstants.JWT_TOKEN_POOL_MAX} entries"
|
||||
)
|
||||
}
|
||||
val out = ArrayList<String>(arr.length())
|
||||
for (i in 0 until arr.length()) {
|
||||
if (arr.isNull(i)) continue
|
||||
val s = arr.optString(i)
|
||||
if (s.isNotEmpty()) {
|
||||
out.add(s)
|
||||
}
|
||||
}
|
||||
return if (out.isEmpty()) null else out
|
||||
}
|
||||
|
||||
@PluginMethod
|
||||
fun getNotificationStatus(call: PluginCall) {
|
||||
CoroutineScope(Dispatchers.IO).launch {
|
||||
|
||||
@@ -18,6 +18,7 @@
|
||||
package org.timesafari.dailynotification;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import java.util.List;
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
|
||||
@@ -142,5 +143,21 @@ public interface NativeNotificationContentFetcher {
|
||||
// This allows fetchers that don't need TypeScript-provided configuration
|
||||
// to ignore this method without implementing an empty body.
|
||||
}
|
||||
|
||||
/**
|
||||
* Optional overload: distinct JWT strings for background use (e.g. one per day slot).
|
||||
* Persisted when {@code persistToken} is true; host fetchers (e.g. TimeSafari) should choose
|
||||
* which entry to use per fetch (e.g. day index modulo pool size).
|
||||
* Default delegates to {@link #configure(String, String, String)} so existing fetchers stay compatible.
|
||||
*
|
||||
* @param jwtTokenPool validated list (max length 128), or null if the host omitted the pool or sent an empty array
|
||||
*/
|
||||
default void configure(
|
||||
String apiBaseUrl,
|
||||
String activeDid,
|
||||
String jwtToken,
|
||||
@Nullable List<String> jwtTokenPool) {
|
||||
configure(apiBaseUrl, activeDid, jwtToken);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -249,16 +249,45 @@ public class DailyNotificationPlugin: CAPPlugin {
|
||||
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,8 +623,11 @@ 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 {
|
||||
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))...)")
|
||||
|
||||
@@ -623,6 +655,20 @@ public class DailyNotificationPlugin: CAPPlugin {
|
||||
etag: nil
|
||||
)
|
||||
}
|
||||
} else {
|
||||
// Config present but no bearer (empty jwtToken and pool)
|
||||
print("DNP-FETCH: Using dummy content (no bearer token)")
|
||||
content = NotificationContent(
|
||||
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: nil,
|
||||
etag: nil
|
||||
)
|
||||
}
|
||||
} else {
|
||||
// Fallback: Dummy content fetch (no network)
|
||||
print("DNP-FETCH: Using dummy content (native fetcher not configured)")
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@timesafari/daily-notification-plugin",
|
||||
"version": "2.1.5",
|
||||
"version": "2.2.0",
|
||||
"description": "TimeSafari Daily Notification Plugin - Enterprise-grade daily notification functionality with dual scheduling, callback support, TTL-at-fire logic, and comprehensive observability across Mobile (Capacitor) and Desktop (Electron) platforms",
|
||||
"main": "dist/plugin.js",
|
||||
"module": "dist/esm/index.js",
|
||||
|
||||
@@ -458,7 +458,18 @@ export interface DailyNotificationPlugin {
|
||||
configureNativeFetcher(options: {
|
||||
apiBaseUrl: string;
|
||||
activeDid: string;
|
||||
jwtToken: string; // Pre-generated JWT token (ES256K signed) from TypeScript
|
||||
/** Primary token; keep for backward compatibility and single-JWT flows. */
|
||||
jwtToken: string;
|
||||
/**
|
||||
* Optional. Distinct JWT strings for background use (e.g. one per day slot).
|
||||
* Persisted with native config when `persistToken` is true (same as primary token).
|
||||
* If omitted or empty, behavior matches a single `jwtToken` only.
|
||||
*/
|
||||
jwtTokens?: string[];
|
||||
/**
|
||||
* Optional alternative to `jwtTokens` when bridge payload size matters: JSON array string of JWT strings.
|
||||
*/
|
||||
jwtTokenPoolJson?: string;
|
||||
}): Promise<void>;
|
||||
|
||||
// Rolling window management
|
||||
|
||||
@@ -27,6 +27,7 @@ import java.io.OutputStream;
|
||||
import java.net.HttpURLConnection;
|
||||
import java.net.URL;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import androidx.annotation.Nullable;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.HashMap;
|
||||
@@ -92,9 +93,17 @@ public class TestNativeFetcher implements NativeNotificationContentFetcher {
|
||||
*/
|
||||
@Override
|
||||
public void configure(String apiBaseUrl, String activeDid, String jwtToken) {
|
||||
configure(apiBaseUrl, activeDid, jwtToken, null);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void configure(String apiBaseUrl, String activeDid, String jwtToken, @Nullable List<String> jwtTokenPool) {
|
||||
this.apiBaseUrl = apiBaseUrl;
|
||||
this.activeDid = activeDid;
|
||||
this.jwtToken = jwtToken;
|
||||
if (jwtTokenPool != null && !jwtTokenPool.isEmpty()) {
|
||||
Log.i(TAG, "TestNativeFetcher: jwtTokenPool size=" + jwtTokenPool.size());
|
||||
}
|
||||
|
||||
// Enhanced logging for JWT diagnostic purposes
|
||||
Log.i(TAG, "TestNativeFetcher: Configured with API: " + apiBaseUrl);
|
||||
|
||||
@@ -46,7 +46,7 @@
|
||||
},
|
||||
"../..": {
|
||||
"name": "@timesafari/daily-notification-plugin",
|
||||
"version": "1.0.11",
|
||||
"version": "2.2.0",
|
||||
"license": "MIT",
|
||||
"workspaces": [
|
||||
"packages/*"
|
||||
|
||||
Reference in New Issue
Block a user