chore: commit to move to laptop
This commit is contained in:
30
BUILDING.md
30
BUILDING.md
@@ -879,6 +879,36 @@ rm -rf ~/.gradle/caches ~/.gradle/daemon
|
||||
./scripts/build-native.sh --platform android
|
||||
```
|
||||
|
||||
#### Capacitor Settings Path Fix (Test App)
|
||||
|
||||
**Problem**: `capacitor.settings.gradle` is auto-generated with incorrect plugin path.
|
||||
The plugin module is in `android/plugin/` but Capacitor generates a path to `android/`.
|
||||
|
||||
**Automatic Solution** (Test App Only):
|
||||
```bash
|
||||
# Use the wrapper script that auto-fixes after sync:
|
||||
npm run cap:sync
|
||||
|
||||
# This automatically:
|
||||
# 1. Runs npx cap sync android
|
||||
# 2. Fixes capacitor.settings.gradle path (android -> android/plugin/)
|
||||
# 3. Fixes capacitor.plugins.json registration
|
||||
```
|
||||
|
||||
**Manual Fix** (if needed):
|
||||
```bash
|
||||
# After running npx cap sync android directly:
|
||||
node scripts/fix-capacitor-plugins.js
|
||||
|
||||
# Or for plugin development (root project):
|
||||
./scripts/fix-capacitor-build.sh
|
||||
```
|
||||
|
||||
**Automatic Fix on Install**:
|
||||
The test app has a `postinstall` hook that automatically fixes these issues after `npm install`.
|
||||
|
||||
**Note**: The fix script is idempotent - it only changes what's needed and won't break correct configurations.
|
||||
|
||||
#### Android Studio Issues
|
||||
```bash
|
||||
# Problem: Android Studio can't find SDK
|
||||
|
||||
@@ -7,7 +7,8 @@ android {
|
||||
}
|
||||
}
|
||||
|
||||
apply from: "../capacitor-cordova-android-plugins/cordova.variables.gradle"
|
||||
// Plugin development project - no Capacitor integration files needed
|
||||
// apply from: "../capacitor-cordova-android-plugins/cordova.variables.gradle"
|
||||
dependencies {
|
||||
|
||||
|
||||
|
||||
@@ -290,9 +290,9 @@ public class DailyNotificationFetchWorker extends Worker {
|
||||
|
||||
// Save content to storage
|
||||
storage.saveNotificationContent(content);
|
||||
|
||||
// Schedule notification if not already scheduled
|
||||
scheduleNotificationIfNeeded(content);
|
||||
|
||||
// Schedule notification if not already scheduled
|
||||
scheduleNotificationIfNeeded(content);
|
||||
scheduledCount++;
|
||||
|
||||
} catch (Exception e) {
|
||||
@@ -389,7 +389,7 @@ public class DailyNotificationFetchWorker extends Worker {
|
||||
long baseDelay = backoff.minMs;
|
||||
double exponentialMultiplier = Math.pow(backoff.factor, retryCount - 1);
|
||||
long exponentialDelay = (long) (baseDelay * exponentialMultiplier);
|
||||
|
||||
|
||||
// Cap at maxMs
|
||||
long cappedDelay = Math.min(exponentialDelay, backoff.maxMs);
|
||||
|
||||
|
||||
@@ -182,10 +182,15 @@ public class DailyNotificationPlugin extends Plugin {
|
||||
* await DailyNotification.configureNativeFetcher({
|
||||
* apiBaseUrl: 'http://10.0.2.2:3000',
|
||||
* activeDid: 'did:ethr:0x0000694B58C2cC69658993A90D3840C560f2F51F',
|
||||
* jwtSecret: 'test-jwt-secret-for-development'
|
||||
* jwtToken: 'eyJhbGciOiJFUzI1Nksi...' // Pre-generated JWT token
|
||||
* });
|
||||
* }</pre>
|
||||
*
|
||||
* <p><b>Architecture Note:</b> JWT tokens should be generated in TypeScript using
|
||||
* TimeSafari's {@code createEndorserJwtForKey()} function (which uses DID-based ES256K
|
||||
* signing), then passed to this method. This avoids the complexity of implementing
|
||||
* DID-based JWT signing in Java.</p>
|
||||
*
|
||||
* @param call Plugin call containing configuration parameters:
|
||||
* <ul>
|
||||
* <li>{@code apiBaseUrl} (required): Base URL for API server.
|
||||
@@ -194,8 +199,9 @@ public class DailyNotificationPlugin extends Plugin {
|
||||
* Production: "https://api.timesafari.com"</li>
|
||||
* <li>{@code activeDid} (required): Active DID for authentication.
|
||||
* Format: "did:ethr:0x..."</li>
|
||||
* <li>{@code jwtSecret} (required): JWT secret for signing tokens.
|
||||
* Keep secure in production.</li>
|
||||
* <li>{@code jwtToken} (required): Pre-generated JWT token (ES256K signed).
|
||||
* Generated in TypeScript using TimeSafari's {@code createEndorserJwtForKey()}
|
||||
* function. Token format: "Bearer {token}" will be added automatically.</li>
|
||||
* </ul>
|
||||
*
|
||||
* @throws PluginException if configuration fails (rejected via call.reject())
|
||||
@@ -207,10 +213,10 @@ public class DailyNotificationPlugin extends Plugin {
|
||||
try {
|
||||
String apiBaseUrl = call.getString("apiBaseUrl");
|
||||
String activeDid = call.getString("activeDid");
|
||||
String jwtSecret = call.getString("jwtSecret");
|
||||
String jwtToken = call.getString("jwtToken");
|
||||
|
||||
if (apiBaseUrl == null || activeDid == null || jwtSecret == null) {
|
||||
call.reject("Missing required parameters: apiBaseUrl, activeDid, and jwtSecret are required");
|
||||
if (apiBaseUrl == null || activeDid == null || jwtToken == null) {
|
||||
call.reject("Missing required parameters: apiBaseUrl, activeDid, and jwtToken are required");
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -225,7 +231,7 @@ public class DailyNotificationPlugin extends Plugin {
|
||||
"... activeDid: " + activeDid.substring(0, Math.min(30, activeDid.length())) + "...");
|
||||
|
||||
// Call configure on the native fetcher (defaults to no-op if not implemented)
|
||||
fetcher.configure(apiBaseUrl, activeDid, jwtSecret);
|
||||
fetcher.configure(apiBaseUrl, activeDid, jwtToken);
|
||||
|
||||
Log.i(TAG, "SPI: Native fetcher configured successfully");
|
||||
call.resolve();
|
||||
|
||||
@@ -129,12 +129,15 @@ public interface NativeNotificationContentFetcher {
|
||||
* - Production: "https://api.timesafari.com"
|
||||
* @param activeDid Active DID (Decentralized Identifier) for authentication.
|
||||
* Used as the JWT issuer/subject. Format: "did:ethr:0x..."
|
||||
* @param jwtSecret JWT secret key for signing authentication tokens.
|
||||
* Keep this secure - consider using secure storage for production.
|
||||
* @param jwtToken Pre-generated JWT token (ES256K signed) from TypeScript.
|
||||
* This token is generated in the host app using TimeSafari's
|
||||
* {@code createEndorserJwtForKey()} function. The native fetcher
|
||||
* should use this token directly in the Authorization header as
|
||||
* "Bearer {jwtToken}". No JWT generation or signing is needed in Java.
|
||||
*
|
||||
* @see DailyNotificationPlugin#configureNativeFetcher(PluginCall)
|
||||
*/
|
||||
default void configure(String apiBaseUrl, String activeDid, String jwtSecret) {
|
||||
default void configure(String apiBaseUrl, String activeDid, String jwtToken) {
|
||||
// Default no-op implementation - fetchers that need config can override
|
||||
// This allows fetchers that don't need TypeScript-provided configuration
|
||||
// to ignore this method without implementing an empty body.
|
||||
|
||||
@@ -1,45 +1,50 @@
|
||||
#!/bin/bash
|
||||
|
||||
# =============================================================================
|
||||
# FIX SCRIPT: capacitor.build.gradle for Plugin Development Projects
|
||||
# FIX SCRIPT: Capacitor Auto-Generated Files for Plugin Development Projects
|
||||
# =============================================================================
|
||||
#
|
||||
# PURPOSE: This script fixes a common issue in Capacitor plugin development
|
||||
# projects where the auto-generated capacitor.build.gradle file tries to load
|
||||
# a file that doesn't exist, causing build failures.
|
||||
# PURPOSE: This script fixes common issues in Capacitor plugin development
|
||||
# projects where auto-generated files have incorrect paths or references.
|
||||
#
|
||||
# WHEN TO USE:
|
||||
# - After running 'npx cap sync'
|
||||
# - After running 'npx cap update'
|
||||
# - After running 'npx cap add android'
|
||||
# - After any Capacitor CLI command that regenerates integration files
|
||||
# - When you get build errors about missing cordova.variables.gradle
|
||||
# - When you get build errors about missing files or incorrect paths
|
||||
#
|
||||
# WHAT IT DOES:
|
||||
# - Finds the problematic line in capacitor.build.gradle
|
||||
# - Comments it out with an explanatory comment
|
||||
# - Prevents build failures in plugin development projects
|
||||
# 1. Fixes capacitor.build.gradle (missing cordova.variables.gradle)
|
||||
# 2. Fixes capacitor.settings.gradle (incorrect plugin path in test-apps)
|
||||
#
|
||||
# FIX 1: capacitor.build.gradle
|
||||
# THE PROBLEM:
|
||||
# Capacitor generates this line in capacitor.build.gradle:
|
||||
# apply from: "../capacitor-cordova-android-plugins/cordova.variables.gradle"
|
||||
# But plugin development projects don't have this file, causing:
|
||||
# "Could not read script '.../cordova.variables.gradle' as it does not exist"
|
||||
#
|
||||
# THE FIX:
|
||||
# Comments out the problematic line and adds explanation:
|
||||
# // Plugin development project - no Capacitor integration files needed
|
||||
# // apply from: "../capacitor-cordova-android-plugins/cordova.variables.gradle"
|
||||
# FIX 2: capacitor.settings.gradle (Test App)
|
||||
# THE PROBLEM:
|
||||
# Capacitor generates this path in test-apps:
|
||||
# project(':timesafari-daily-notification-plugin').projectDir = new File('../node_modules/@timesafari/daily-notification-plugin/android')
|
||||
# But the plugin module is actually in android/plugin/, not android root, causing:
|
||||
# "No matching variant of project :timesafari-daily-notification-plugin was found"
|
||||
#
|
||||
# =============================================================================
|
||||
|
||||
set -e
|
||||
|
||||
CAPACITOR_BUILD_GRADLE="android/app/capacitor.build.gradle"
|
||||
CAPACITOR_SETTINGS_GRADLE="android/capacitor.settings.gradle"
|
||||
|
||||
echo "🔧 DailyNotification Plugin - Capacitor Build Fix Script"
|
||||
echo "========================================================"
|
||||
|
||||
# =============================================================================
|
||||
# FIX 1: capacitor.build.gradle
|
||||
# =============================================================================
|
||||
if [ -f "$CAPACITOR_BUILD_GRADLE" ]; then
|
||||
echo "📁 Found capacitor.build.gradle at: $CAPACITOR_BUILD_GRADLE"
|
||||
|
||||
@@ -75,6 +80,53 @@ else
|
||||
echo " - The file hasn't been generated yet"
|
||||
fi
|
||||
|
||||
# =============================================================================
|
||||
# FIX 2: capacitor.settings.gradle (Test App Plugin Path)
|
||||
# =============================================================================
|
||||
if [ -f "$CAPACITOR_SETTINGS_GRADLE" ]; then
|
||||
echo ""
|
||||
echo "📁 Checking capacitor.settings.gradle at: $CAPACITOR_SETTINGS_GRADLE"
|
||||
|
||||
# Check if the path points to android instead of android/plugin
|
||||
# Look for the line without the /plugin suffix
|
||||
if grep -q "project(':timesafari-daily-notification-plugin').projectDir = new File('../node_modules/@timesafari/daily-notification-plugin/android')$" "$CAPACITOR_SETTINGS_GRADLE"; then
|
||||
echo "🔧 Applying fix to capacitor.settings.gradle..."
|
||||
echo " Problem: Path points to 'android' but plugin module is in 'android/plugin'"
|
||||
echo " Solution: Updating path to 'android/plugin'"
|
||||
|
||||
# Apply the fix by updating the path - simpler approach
|
||||
# Handle macOS vs Linux sed differences
|
||||
if [[ "$OSTYPE" == "darwin"* ]]; then
|
||||
# macOS sed - replace android')$ with android/plugin')
|
||||
sed -i '' "s|/android')$|/android/plugin')|" "$CAPACITOR_SETTINGS_GRADLE"
|
||||
# Add comment before the line if it doesn't already exist
|
||||
if ! grep -q "NOTE: Plugin module is in android/plugin" "$CAPACITOR_SETTINGS_GRADLE"; then
|
||||
sed -i '' "/^include ':timesafari-daily-notification-plugin'/a\\
|
||||
// NOTE: Plugin module is in android/plugin/ subdirectory, not android root\\
|
||||
// This file is auto-generated by Capacitor, but must be manually corrected for this plugin structure\\
|
||||
" "$CAPACITOR_SETTINGS_GRADLE"
|
||||
fi
|
||||
else
|
||||
# Linux sed - replace android')$ with android/plugin')
|
||||
sed -i "s|/android')$|/android/plugin')|" "$CAPACITOR_SETTINGS_GRADLE"
|
||||
# Add comment before the line if it doesn't already exist
|
||||
if ! grep -q "NOTE: Plugin module is in android/plugin" "$CAPACITOR_SETTINGS_GRADLE"; then
|
||||
sed -i "/^include ':timesafari-daily-notification-plugin'/a\\// NOTE: Plugin module is in android/plugin/ subdirectory, not android root\\n// This file is auto-generated by Capacitor, but must be manually corrected for this plugin structure" "$CAPACITOR_SETTINGS_GRADLE"
|
||||
fi
|
||||
fi
|
||||
|
||||
echo "✅ Fix applied successfully!"
|
||||
echo "💡 The path has been updated to point to android/plugin/"
|
||||
echo "⚠️ Note: This fix will be lost if you run Capacitor CLI commands again"
|
||||
elif grep -q "android/plugin" "$CAPACITOR_SETTINGS_GRADLE"; then
|
||||
echo "ℹ️ capacitor.settings.gradle already has the correct path (android/plugin)"
|
||||
else
|
||||
echo "ℹ️ capacitor.settings.gradle doesn't reference the plugin or uses a different structure"
|
||||
fi
|
||||
else
|
||||
echo "ℹ️ capacitor.settings.gradle not found (may not be a test-app)"
|
||||
fi
|
||||
|
||||
echo ""
|
||||
echo "📚 For more information, see:"
|
||||
echo " - BUILDING.md (troubleshooting section)"
|
||||
|
||||
@@ -365,8 +365,13 @@ export interface DailyNotificationPlugin {
|
||||
* - Production: `"https://api.timesafari.com"`
|
||||
* - `activeDid` (required): Active DID for authentication.
|
||||
* Format: `"did:ethr:0x..."`. Used as JWT issuer/subject.
|
||||
* - `jwtSecret` (required): JWT secret for signing tokens.
|
||||
* **Keep secure in production!** Consider using secure storage.
|
||||
* - `jwtToken` (required): Pre-generated JWT token (ES256K signed).
|
||||
* Generated in TypeScript using TimeSafari's `createEndorserJwtForKey()` function.
|
||||
* **Note**: Token should be ES256K signed (DID-based), not HS256.
|
||||
*
|
||||
* **Architecture Note**: JWT tokens should be generated in TypeScript using TimeSafari's
|
||||
* `createEndorserJwtForKey()` function (which uses DID-based ES256K signing), then passed
|
||||
* to this method. This avoids the complexity of implementing DID-based JWT signing in Java.
|
||||
*
|
||||
* @throws {Error} If configuration fails (missing params, no fetcher registered, etc.)
|
||||
*
|
||||
@@ -376,7 +381,7 @@ export interface DailyNotificationPlugin {
|
||||
configureNativeFetcher(options: {
|
||||
apiBaseUrl: string;
|
||||
activeDid: string;
|
||||
jwtSecret: string;
|
||||
jwtToken: string; // Pre-generated JWT token (ES256K signed) from TypeScript
|
||||
}): Promise<void>;
|
||||
|
||||
// Rolling window management
|
||||
|
||||
141
test-apps/daily-notification-test/IMPLEMENTATION_COMPLETE.md
Normal file
141
test-apps/daily-notification-test/IMPLEMENTATION_COMPLETE.md
Normal file
@@ -0,0 +1,141 @@
|
||||
# Implementation Complete Summary
|
||||
|
||||
**Date**: 2025-10-31
|
||||
**Status**: ✅ **ALL HIGH-PRIORITY ITEMS COMPLETE**
|
||||
|
||||
---
|
||||
|
||||
## ✅ Completed Implementation
|
||||
|
||||
### 1. ES256K JWT Token Generation (Item #1) - **COMPLETE**
|
||||
|
||||
**Implementation**:
|
||||
- Added `did-jwt@^7.0.0` and `ethers@^6.0.0` dependencies
|
||||
- Implemented `generateEndorserJWT()` function in `test-user-zero.ts`
|
||||
- Derives Ethereum private key from seed phrase using `ethers.HDNodeWallet`
|
||||
- Uses `did-jwt.SimpleSigner` for ES256K signing
|
||||
- Creates proper ES256K signed JWTs matching TimeSafari's pattern
|
||||
|
||||
**Files Modified**:
|
||||
- `test-apps/daily-notification-test/src/config/test-user-zero.ts` - Added `generateEndorserJWT()` function
|
||||
- `test-apps/daily-notification-test/src/views/HomeView.vue` - Updated to use `generateEndorserJWT()`
|
||||
- `test-apps/daily-notification-test/package.json` - Added dependencies
|
||||
|
||||
**Architecture**:
|
||||
- JWT generation happens in TypeScript (no Java DID libraries needed)
|
||||
- Native fetcher receives pre-generated tokens
|
||||
- Matches TimeSafari's production pattern
|
||||
|
||||
---
|
||||
|
||||
### 2. Network Security Configuration (Item #15) - **COMPLETE**
|
||||
|
||||
**Implementation**:
|
||||
- Created `network_security_config.xml` allowing cleartext HTTP traffic to `10.0.2.2`, `localhost`, and `127.0.0.1`
|
||||
- Updated `AndroidManifest.xml` to reference the network security config
|
||||
|
||||
**Files Created/Modified**:
|
||||
- `test-apps/daily-notification-test/android/app/src/main/res/xml/network_security_config.xml` (created)
|
||||
- `test-apps/daily-notification-test/android/app/src/main/AndroidManifest.xml` (line 10)
|
||||
|
||||
**Purpose**:
|
||||
- Enables HTTP connections to `http://10.0.2.2:3000` from Android emulator
|
||||
- Required for localhost testing (emulator's special IP for host machine)
|
||||
|
||||
---
|
||||
|
||||
### 3. TypeScript/Java Interface Updates - **COMPLETE**
|
||||
|
||||
**Changes**:
|
||||
- Updated `configureNativeFetcher()` to accept `jwtToken` instead of `jwtSecret`
|
||||
- Updated all TypeScript definitions, Java plugin, and native interface
|
||||
- Removed `as any` type assertion from `HomeView.vue`
|
||||
- Rebuilt plugin to update `dist/definitions.d.ts`
|
||||
|
||||
**Files Modified**:
|
||||
- `src/definitions.ts` - Updated method signature and JSDoc
|
||||
- `android/plugin/.../DailyNotificationPlugin.java` - Updated parameter handling
|
||||
- `android/plugin/.../NativeNotificationContentFetcher.java` - Updated interface
|
||||
- `test-apps/.../TestNativeFetcher.java` - Simplified (removed JWT generation)
|
||||
- `test-apps/.../HomeView.vue` - Updated to use new token-based approach
|
||||
- `dist/definitions.d.ts` - Rebuilt with updated types
|
||||
|
||||
---
|
||||
|
||||
### 4. TestNativeFetcher Simplification - **COMPLETE**
|
||||
|
||||
**Changes**:
|
||||
- Removed entire `generateJWTToken()` method (HMAC-SHA256 implementation)
|
||||
- Removed unused imports (`Mac`, `SecretKeySpec`, `Base64`, `MessageDigest`)
|
||||
- Changed `jwtSecret` field → `jwtToken`
|
||||
- Updated to use pre-generated token directly in Authorization header
|
||||
|
||||
**Files Modified**:
|
||||
- `test-apps/daily-notification-test/android/app/src/main/java/com/timesafari/dailynotification/test/TestNativeFetcher.java`
|
||||
|
||||
---
|
||||
|
||||
## 🚀 Ready for Testing
|
||||
|
||||
### Prerequisites Complete:
|
||||
- ✅ ES256K JWT generation implemented
|
||||
- ✅ Network security config in place
|
||||
- ✅ All interfaces updated
|
||||
- ✅ Plugin rebuilt with correct types
|
||||
|
||||
### Next Steps for Testing:
|
||||
|
||||
1. **Enable Real API Calls**:
|
||||
```typescript
|
||||
// In test-apps/daily-notification-test/src/config/test-user-zero.ts
|
||||
// Change line 28:
|
||||
serverMode: "localhost" as "localhost" | "staging" | "production" | "mock" | "custom",
|
||||
```
|
||||
|
||||
2. **Build and Run Android Test App**:
|
||||
```bash
|
||||
cd test-apps/daily-notification-test
|
||||
npm run build
|
||||
npx cap sync android
|
||||
npx cap run android
|
||||
```
|
||||
|
||||
3. **Verify JWT Generation**:
|
||||
- Check console logs for JWT generation
|
||||
- Verify token is passed to native fetcher
|
||||
- Check logcat for native fetcher configuration
|
||||
|
||||
4. **Test API Calls**:
|
||||
- Schedule a notification
|
||||
- Verify prefetch occurs
|
||||
- Check that API calls succeed with ES256K tokens
|
||||
- Verify notifications appear
|
||||
|
||||
---
|
||||
|
||||
## 📋 Remaining Optional Tasks
|
||||
|
||||
### Medium Priority:
|
||||
- **Item #4**: Add `root.has("data")` validation in API response parser
|
||||
- **Item #5**: Enhance logging with timing and structured tags
|
||||
|
||||
### Low Priority:
|
||||
- Items #7-11: Unit tests, JWT utility extraction, interceptors, caching, metrics
|
||||
|
||||
### Documentation:
|
||||
- Update `docs/NATIVE_FETCHER_CONFIGURATION.md` to reflect token-based approach
|
||||
- Document ES256K JWT generation pattern for TimeSafari integration
|
||||
|
||||
---
|
||||
|
||||
## 🔗 Related Files
|
||||
|
||||
- **TODO Document**: `test-apps/daily-notification-test/TODO_NATIVE_FETCHER.md`
|
||||
- **Investigation Results**: `test-apps/daily-notification-test/INVESTIGATION_JWT_ALGORITHM_RESULTS.md`
|
||||
- **Test Config**: `test-apps/daily-notification-test/src/config/test-user-zero.ts`
|
||||
- **Network Config**: `test-apps/daily-notification-test/android/app/src/main/res/xml/network_security_config.xml`
|
||||
|
||||
---
|
||||
|
||||
**All critical implementation work is complete. Ready for testing!**
|
||||
|
||||
301
test-apps/daily-notification-test/INVESTIGATION_JWT_ALGORITHM.md
Normal file
301
test-apps/daily-notification-test/INVESTIGATION_JWT_ALGORITHM.md
Normal file
@@ -0,0 +1,301 @@
|
||||
# JWT Algorithm Investigation Guide
|
||||
|
||||
**Created**: 2025-10-31
|
||||
**Purpose**: Systematically investigate which JWT signing algorithm endorser-ch and TimeSafari actually use
|
||||
|
||||
---
|
||||
|
||||
## Investigation Checklist
|
||||
|
||||
### Phase 1: TimeSafari Repository (Most Authoritative)
|
||||
|
||||
**Repository**: `ssh://git@173.199.124.46:222/trent_larson/crowd-funder-for-time-pwa.git`
|
||||
|
||||
#### Step 1: Find `createEndorserJwtForDid` Function
|
||||
|
||||
```bash
|
||||
# Clone or navigate to the repository
|
||||
cd /path/to/crowd-funder-for-time-pwa
|
||||
|
||||
# Search for the function
|
||||
grep -r "createEndorserJwtForDid" .
|
||||
grep -r "createEndorserJWT" .
|
||||
grep -r "createEndorser.*JWT" .
|
||||
```
|
||||
|
||||
**What to look for:**
|
||||
- Function implementation showing how JWT is generated
|
||||
- Import statements (e.g., `did-jwt`, `jsonwebtoken`, `jose`, etc.)
|
||||
- Algorithm parameter (e.g., `alg: 'HS256'`, `algorithm: 'HS256'`, etc.)
|
||||
- Key/secret usage (shared secret vs DID private key)
|
||||
|
||||
#### Step 2: Find Endpoint Usage
|
||||
|
||||
```bash
|
||||
# Search for endpoint usage
|
||||
grep -r "plansLastUpdatedBetween" .
|
||||
grep -r "/api/v2/report" .
|
||||
```
|
||||
|
||||
**What to look for:**
|
||||
- How the JWT is attached to requests (Authorization header)
|
||||
- What function generates the JWT for this endpoint
|
||||
- Any configuration or secret management
|
||||
|
||||
#### Step 3: Check JWT Generation Code
|
||||
|
||||
**Files to examine:**
|
||||
- Any files matching `*jwt*.ts`, `*jwt*.js`, `*auth*.ts`, `*auth*.js`
|
||||
- Configuration files (`.env`, `config/*.ts`, `src/config/*.ts`)
|
||||
- API client files that call endorser endpoints
|
||||
|
||||
**Key questions:**
|
||||
1. Does it use a shared secret (string) or DID private key?
|
||||
2. What library is used? (`jsonwebtoken`, `did-jwt`, `jose`, etc.)
|
||||
3. What algorithm is specified in the code?
|
||||
|
||||
---
|
||||
|
||||
### Phase 2: endorser-ch Repository (Server-Side Verification)
|
||||
|
||||
**Repository**: `https://github.com/trentlarson/endorser-ch`
|
||||
|
||||
#### Step 1: Find Endpoint Handler
|
||||
|
||||
```bash
|
||||
# Clone or navigate to the repository
|
||||
cd /path/to/endorser-ch
|
||||
|
||||
# Search for endpoint handler
|
||||
grep -r "plansLastUpdatedBetween" .
|
||||
grep -r "/api/v2/report" .
|
||||
```
|
||||
|
||||
**Files likely to contain:**
|
||||
- `src/server.js` or `server/server.js`
|
||||
- `src/routes.js` or `routes/*.js`
|
||||
- `src/api/` or `src/controllers/`
|
||||
|
||||
#### Step 2: Find Authentication Middleware
|
||||
|
||||
```bash
|
||||
# Search for JWT verification
|
||||
grep -r "verifyJWT" .
|
||||
grep -r "verify.*JWT" .
|
||||
grep -r "Authorization.*Bearer" .
|
||||
grep -r "did-jwt" .
|
||||
grep -r "jsonwebtoken" .
|
||||
```
|
||||
|
||||
**What to look for:**
|
||||
- Middleware that processes `Authorization: Bearer {token}` header
|
||||
- JWT verification logic
|
||||
- Algorithm used for verification
|
||||
- Secret/key used for verification
|
||||
|
||||
#### Step 3: Check Configuration for JWT Secret
|
||||
|
||||
```bash
|
||||
# Search for JWT secret configuration
|
||||
grep -r "JWT_SECRET" .
|
||||
grep -r "API_SECRET" .
|
||||
grep -r "jwtSecret" .
|
||||
grep -r "jwt.*secret" -i .
|
||||
```
|
||||
|
||||
**Files to check:**
|
||||
- `.env` files
|
||||
- `config/*.js` or `conf/*.js`
|
||||
- `package.json` (for dependencies)
|
||||
|
||||
**Key questions:**
|
||||
1. Is there a JWT_SECRET environment variable?
|
||||
2. Does it use `did-jwt.verifyJWT()` with DID resolution?
|
||||
3. Or does it use `jsonwebtoken.verify()` with a shared secret?
|
||||
|
||||
---
|
||||
|
||||
## Expected Findings & Decision Matrix
|
||||
|
||||
### Scenario A: HS256 with Shared Secret ✅ (Most Likely)
|
||||
|
||||
**Indicators:**
|
||||
- ✅ TimeSafari code uses `jsonwebtoken.sign()` or similar with a secret string
|
||||
- ✅ endorser-ch uses `jsonwebtoken.verify()` or similar with a secret
|
||||
- ✅ Environment variable `JWT_SECRET` exists
|
||||
- ✅ Code explicitly sets `algorithm: 'HS256'` or `alg: 'HS256'`
|
||||
|
||||
**Action**: Our current HMAC-SHA256 implementation is **CORRECT**
|
||||
|
||||
### Scenario B: DID-based (ES256K) ⚠️
|
||||
|
||||
**Indicators:**
|
||||
- ⚠️ TimeSafari code uses `did-jwt.createJWT()` or Veramo JWT creation
|
||||
- ⚠️ endorser-ch uses `did-jwt.verifyJWT()` with DID resolver
|
||||
- ⚠️ Code uses Ethereum private keys or DID document keys
|
||||
- ⚠️ No shared secret configuration found
|
||||
|
||||
**Action**: Need to implement DID-based signing with ES256K algorithm
|
||||
|
||||
### Scenario C: Hybrid Approach 🔄
|
||||
|
||||
**Indicators:**
|
||||
- 🔄 Different algorithms for different endpoints
|
||||
- 🔄 API auth uses HS256, but claim verification uses DID-based
|
||||
- 🔄 Multiple authentication methods supported
|
||||
|
||||
**Action**: Use HS256 for `/api/v2/report/plansLastUpdatedBetween` endpoint
|
||||
|
||||
---
|
||||
|
||||
## Investigation Commands Summary
|
||||
|
||||
### TimeSafari Repository Investigation
|
||||
|
||||
```bash
|
||||
cd /path/to/crowd-funder-for-time-pwa
|
||||
|
||||
# Find JWT generation
|
||||
grep -r "createEndorserJwtForDid\|createEndorserJWT" . --include="*.ts" --include="*.js"
|
||||
|
||||
# Find endpoint usage
|
||||
grep -r "plansLastUpdatedBetween" . --include="*.ts" --include="*.js"
|
||||
|
||||
# Check for JWT libraries
|
||||
grep -r "jsonwebtoken\|did-jwt\|jose" package.json
|
||||
grep -r "from.*jsonwebtoken\|from.*did-jwt\|import.*jose" . --include="*.ts" --include="*.js"
|
||||
|
||||
# Check for secret usage
|
||||
grep -r "jwtSecret\|JWT_SECRET\|shared.*secret" . --include="*.ts" --include="*.js" -i
|
||||
```
|
||||
|
||||
### endorser-ch Repository Investigation
|
||||
|
||||
```bash
|
||||
cd /path/to/endorser-ch
|
||||
|
||||
# Find endpoint handler
|
||||
grep -r "plansLastUpdatedBetween" . --include="*.js" --include="*.ts"
|
||||
|
||||
# Find authentication middleware
|
||||
grep -r "Authorization.*Bearer\|verifyJWT\|verify.*JWT" . --include="*.js" --include="*.ts"
|
||||
|
||||
# Check for JWT libraries
|
||||
grep -r "jsonwebtoken\|did-jwt\|jose" package.json
|
||||
|
||||
# Check for secret configuration
|
||||
grep -r "JWT_SECRET\|API_SECRET\|jwt.*secret" . -i --include="*.js" --include="*.env*"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Documentation References to Review
|
||||
|
||||
### TimeSafari Repository
|
||||
|
||||
**Look for:**
|
||||
- API client documentation
|
||||
- Authentication/authorization documentation
|
||||
- JWT generation examples
|
||||
- Configuration documentation
|
||||
|
||||
**Files to check:**
|
||||
- `README.md`
|
||||
- `docs/api.md` or `docs/auth.md`
|
||||
- `src/api/` or `src/services/` directories
|
||||
- Configuration files in `src/config/` or root
|
||||
|
||||
### endorser-ch Repository
|
||||
|
||||
**Look for:**
|
||||
- API documentation
|
||||
- Authentication documentation
|
||||
- README.md (already mentions `did-jwt` in JWT verification section)
|
||||
- Route handler documentation
|
||||
|
||||
**Files to check:**
|
||||
- `README.md` (check JWT verification section mentioned)
|
||||
- `src/` or `server/` directories
|
||||
- Route files
|
||||
- Authentication middleware files
|
||||
|
||||
---
|
||||
|
||||
## Quick Decision Guide
|
||||
|
||||
After investigation, use this decision matrix:
|
||||
|
||||
| Finding | Algorithm | Implementation Status |
|
||||
|---------|-----------|----------------------|
|
||||
| `jsonwebtoken` + `JWT_SECRET` | HS256 | ✅ Already implemented |
|
||||
| `did-jwt` + DID resolution | ES256K | ❌ Need to implement |
|
||||
| Both found (hybrid) | HS256 for API | ✅ Already implemented |
|
||||
| Can't determine | Test both | ⚠️ Implement both options |
|
||||
|
||||
---
|
||||
|
||||
## Next Steps After Investigation
|
||||
|
||||
1. **If HS256 confirmed:**
|
||||
- ✅ Mark TODO item #1 as complete
|
||||
- ✅ Current implementation is correct
|
||||
- ✅ Test against API
|
||||
|
||||
2. **If DID-based confirmed:**
|
||||
- ❌ Need to implement DID-based signing
|
||||
- Add dependency: `did-jwt-java` or `web3j` for Ethereum signing
|
||||
- Update `generateJWTToken()` to use DID private keys
|
||||
- Update TODO with findings
|
||||
|
||||
3. **If hybrid or unclear:**
|
||||
- Document findings
|
||||
- Implement both if needed
|
||||
- Add configuration option to switch between methods
|
||||
|
||||
---
|
||||
|
||||
## Investigation Results Template
|
||||
|
||||
After investigation, fill out:
|
||||
|
||||
```markdown
|
||||
## Investigation Results
|
||||
|
||||
**Date**: YYYY-MM-DD
|
||||
**Investigator**: [Name]
|
||||
|
||||
### TimeSafari Repository Findings
|
||||
|
||||
**createEndorserJwtForDid function:**
|
||||
- Location: `[file path]`
|
||||
- Library used: `[jsonwebtoken/did-jwt/etc]`
|
||||
- Algorithm: `[HS256/ES256K/etc]`
|
||||
- Key type: `[shared secret/DID private key]`
|
||||
- Code snippet: `[paste relevant code]`
|
||||
|
||||
**Endpoint usage:**
|
||||
- Location: `[file path]`
|
||||
- How JWT is generated: `[description]`
|
||||
- Code snippet: `[paste relevant code]`
|
||||
|
||||
### endorser-ch Repository Findings
|
||||
|
||||
**Endpoint handler:**
|
||||
- Location: `[file path]`
|
||||
- Authentication middleware: `[description]`
|
||||
- Verification method: `[jsonwebtoken.verify/did-jwt.verifyJWT/etc]`
|
||||
- Algorithm: `[HS256/ES256K/etc]`
|
||||
- Secret/key source: `[environment variable/DID resolution/etc]`
|
||||
- Code snippet: `[paste relevant code]`
|
||||
|
||||
### Conclusion
|
||||
|
||||
**Algorithm**: [HS256/ES256K/Hybrid/Unclear]
|
||||
**Action Required**: [Update implementation/Mark complete/etc]
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
**Status**: Ready for investigation
|
||||
**Priority**: CRITICAL - Blocks API authentication
|
||||
|
||||
@@ -0,0 +1,220 @@
|
||||
# JWT Algorithm Investigation Results
|
||||
|
||||
**Date**: 2025-10-31
|
||||
**Investigator**: Auto (AI Assistant)
|
||||
**Status**: ✅ **INVESTIGATION COMPLETE**
|
||||
|
||||
---
|
||||
|
||||
## 🔴 CRITICAL FINDING: DID-Based JWT Signing Required (ES256K)
|
||||
|
||||
### Conclusion
|
||||
|
||||
**The endorser-ch API expects DID-based JWTs signed with ES256K (or ES256K-R), NOT HMAC-SHA256 (HS256).**
|
||||
|
||||
The current implementation in `TestNativeFetcher.java` using HMAC-SHA256 is **INCORRECT** and will fail authentication.
|
||||
|
||||
---
|
||||
|
||||
## Investigation Details
|
||||
|
||||
### TimeSafari Repository Findings
|
||||
|
||||
**Location**: `~/projects/timesafari/crowd-funder-for-time-pwa/src/libs/crypto/vc/index.ts`
|
||||
|
||||
**Function**: `createEndorserJwtForKey()`
|
||||
|
||||
**Implementation**:
|
||||
```typescript
|
||||
export async function createEndorserJwtForKey(
|
||||
account: KeyMetaWithPrivate,
|
||||
payload: object,
|
||||
expiresIn?: number,
|
||||
) {
|
||||
if (account?.identity) {
|
||||
const identity: IIdentifier = JSON.parse(account.identity!);
|
||||
const privateKeyHex = identity.keys[0].privateKeyHex;
|
||||
const signer = await SimpleSigner(privateKeyHex as string);
|
||||
const options = {
|
||||
// alg: "ES256K", // "K" is the default, "K-R" is used by the server in tests
|
||||
issuer: account.did,
|
||||
signer: signer,
|
||||
expiresIn: undefined as number | undefined,
|
||||
};
|
||||
if (expiresIn) {
|
||||
options.expiresIn = expiresIn;
|
||||
}
|
||||
return didJwt.createJWT(payload, options);
|
||||
}
|
||||
// ... passkey handling ...
|
||||
}
|
||||
```
|
||||
|
||||
**Key Points**:
|
||||
- Uses `did-jwt.createJWT()` library
|
||||
- Uses `SimpleSigner(privateKeyHex)` - signs with Ethereum private key
|
||||
- Algorithm is **ES256K** (default)
|
||||
- Signs with DID private key, NOT a shared secret
|
||||
|
||||
**Library Used**: `did-jwt` (DID-based JWT library)
|
||||
|
||||
---
|
||||
|
||||
### endorser-ch Repository Findings
|
||||
|
||||
**Location**: `~/projects/timesafari/endorser-ch/src/api/services/vc/index.js`
|
||||
|
||||
**Function**: `decodeAndVerifyJwt()`
|
||||
|
||||
**Implementation**:
|
||||
```javascript
|
||||
export async function decodeAndVerifyJwt(jwt) {
|
||||
const pieces = jwt.split('.')
|
||||
const header = JSON.parse(base64url.decode(pieces[0]))
|
||||
const payload = JSON.parse(base64url.decode(pieces[1]))
|
||||
const issuerDid = payload.iss
|
||||
|
||||
if (issuerDid.startsWith(ETHR_DID_PREFIX)) {
|
||||
try {
|
||||
const verifiedResult = await didJwt.verifyJWT(jwt, {resolver})
|
||||
return verifiedResult
|
||||
} catch (e) {
|
||||
return Promise.reject({
|
||||
clientError: {
|
||||
message: `JWT failed verification: ` + e.toString(),
|
||||
code: JWT_VERIFY_FAILED_CODE
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
// ... other DID methods ...
|
||||
}
|
||||
```
|
||||
|
||||
**Key Points**:
|
||||
- Uses `did-jwt.verifyJWT(jwt, {resolver})` - DID-based verification
|
||||
- Verifies signature using DID resolver (resolves DID to public key)
|
||||
- **NO shared secret used** - uses DID public key from resolver
|
||||
- Algorithm: **ES256K** (implicit from did-jwt library)
|
||||
|
||||
**Middleware Location**: `~/projects/timesafari/endorser-ch/src/common/server.js`
|
||||
|
||||
**Authentication Flow**:
|
||||
```javascript
|
||||
decodeAndVerifyJwt(authorizationJwt)
|
||||
.then((result) => {
|
||||
const { header, issuer, payload, verified } = result
|
||||
if (!verified) {
|
||||
res.status(400).json({
|
||||
error: {
|
||||
message: "Signature failed validation.",
|
||||
code: ERROR_CODES.JWT_VERIFY_FAILED
|
||||
}
|
||||
}).end()
|
||||
} else {
|
||||
res.locals.authTokenIssuer = issuer
|
||||
next()
|
||||
}
|
||||
})
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Impact on Current Implementation
|
||||
|
||||
### Current Implementation (WRONG)
|
||||
|
||||
**File**: `test-apps/daily-notification-test/android/app/src/main/java/com/timesafari/dailynotification/test/TestNativeFetcher.java`
|
||||
|
||||
**Current Approach**: HMAC-SHA256 with shared secret
|
||||
```java
|
||||
Mac hmac = Mac.getInstance("HmacSHA256");
|
||||
SecretKeySpec secretKey = new SecretKeySpec(
|
||||
jwtSecret.getBytes(StandardCharsets.UTF_8),
|
||||
"HmacSHA256"
|
||||
);
|
||||
hmac.init(secretKey);
|
||||
byte[] signatureBytes = hmac.doFinal(unsignedToken.getBytes(StandardCharsets.UTF_8));
|
||||
```
|
||||
|
||||
**Problem**: Server expects ES256K with DID private key, NOT HMAC-SHA256 with shared secret
|
||||
|
||||
---
|
||||
|
||||
## Required Changes
|
||||
|
||||
### Action Required
|
||||
|
||||
1. **Remove HMAC-SHA256 implementation** - This is completely wrong
|
||||
2. **Implement DID-based signing (ES256K)** - Sign with Ethereum private key
|
||||
3. **Access DID private key** - Need to retrieve private key from DID/account storage
|
||||
4. **Use did-jwt-java or web3j** - Java library for DID-based JWT signing
|
||||
|
||||
### Implementation Options
|
||||
|
||||
#### Option 1: Use did-jwt-java Library (Recommended)
|
||||
|
||||
```java
|
||||
// Add dependency to build.gradle
|
||||
implementation 'io.uport:uport-did-jwt:3.1.0'
|
||||
|
||||
// Sign with DID private key
|
||||
import io.uport.sdk.did.jwt.DIDJWT;
|
||||
import io.uport.sdk.did.jwt.SimpleSigner;
|
||||
|
||||
String privateKeyHex = getPrivateKeyForDid(activeDid); // Need to implement
|
||||
SimpleSigner signer = new SimpleSigner(privateKeyHex);
|
||||
String jwt = DIDJWT.createJWT(payload, signer, issuer: activeDid);
|
||||
```
|
||||
|
||||
#### Option 2: Use web3j for Ethereum Signing
|
||||
|
||||
```java
|
||||
// Add dependency
|
||||
implementation 'org.web3j:core:4.9.8'
|
||||
|
||||
import org.web3j.crypto.ECKeyPair;
|
||||
import org.web3j.crypto.Sign;
|
||||
|
||||
ECKeyPair keyPair = getKeyPairForDid(activeDid); // Need to implement
|
||||
Sign.SignatureData signature = Sign.signMessage(
|
||||
unsignedToken.getBytes(StandardCharsets.UTF_8),
|
||||
keyPair
|
||||
);
|
||||
// Then encode signature according to ES256K format
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Next Steps
|
||||
|
||||
1. **Remove `jwtSecret` parameter** - No longer needed (shared secret not used)
|
||||
2. **Add DID private key retrieval** - Need mechanism to get private key for `activeDid`
|
||||
3. **Implement ES256K signing** - Using did-jwt-java or web3j
|
||||
4. **Update `configureNativeFetcher()`** - Remove `jwtSecret`, add private key retrieval mechanism
|
||||
5. **Test with real API** - Verify JWTs are accepted by endorser-ch server
|
||||
|
||||
---
|
||||
|
||||
## References
|
||||
|
||||
- **TimeSafari Implementation**: `~/projects/timesafari/crowd-funder-for-time-pwa/src/libs/crypto/vc/index.ts`
|
||||
- **endorser-ch Verification**: `~/projects/timesafari/endorser-ch/src/api/services/vc/index.js`
|
||||
- **did-jwt Library**: https://github.com/decentralized-identity/did-jwt
|
||||
- **did-jwt-java**: https://github.com/uport-project/uport-did-jwt (if available) or alternative Java DID libraries
|
||||
|
||||
---
|
||||
|
||||
## Evidence Summary
|
||||
|
||||
| Component | Finding | Evidence |
|
||||
|-----------|---------|----------|
|
||||
| **TimeSafari JWT Creation** | ✅ DID-based (ES256K) | Uses `didJwt.createJWT()` with `SimpleSigner(privateKeyHex)` |
|
||||
| **endorser-ch JWT Verification** | ✅ DID-based (ES256K) | Uses `didJwt.verifyJWT(jwt, {resolver})` |
|
||||
| **Current TestNativeFetcher** | ❌ HMAC-SHA256 | Uses `Mac.getInstance("HmacSHA256")` with shared secret |
|
||||
| **Shared Secret Config** | ❌ Not Used | No `JWT_SECRET` found in endorser-ch, no shared secret in TimeSafari |
|
||||
|
||||
---
|
||||
|
||||
**Status**: Investigation complete. Implementation changes required.
|
||||
|
||||
@@ -29,6 +29,26 @@ See [Vite Configuration Reference](https://vite.dev/config/).
|
||||
npm install
|
||||
```
|
||||
|
||||
**Note**: The `postinstall` script automatically fixes Capacitor configuration files after installation.
|
||||
|
||||
### Capacitor Sync (Android)
|
||||
|
||||
**Important**: Use the wrapper script instead of `npx cap sync` directly to automatically fix plugin paths:
|
||||
|
||||
```sh
|
||||
npm run cap:sync
|
||||
```
|
||||
|
||||
This will:
|
||||
1. Run `npx cap sync android`
|
||||
2. Automatically fix `capacitor.settings.gradle` (corrects plugin path from `android/` to `android/plugin/`)
|
||||
3. Ensure `capacitor.plugins.json` has the correct plugin registration
|
||||
|
||||
If you run `npx cap sync android` directly, you can manually fix afterward:
|
||||
```sh
|
||||
node scripts/fix-capacitor-plugins.js
|
||||
```
|
||||
|
||||
### Compile and Hot-Reload for Development
|
||||
|
||||
```sh
|
||||
|
||||
677
test-apps/daily-notification-test/TODO_NATIVE_FETCHER.md
Normal file
677
test-apps/daily-notification-test/TODO_NATIVE_FETCHER.md
Normal file
@@ -0,0 +1,677 @@
|
||||
# TODO: Test App Native Fetcher Improvements
|
||||
|
||||
**Created**: 2025-10-31
|
||||
**Updated**: 2025-10-31 (implementation complete: ES256K JWT generation and network security config)
|
||||
**Context**: Review of `TestNativeFetcher.java` implementation for endorser API integration
|
||||
**Status**: ✅ **IMPLEMENTATION COMPLETE** - All high-priority items complete. Item #1 (ES256K JWT generation) implemented using `did-jwt` and `ethers`. Item #15 (Network Security Config) created. All TypeScript/Java interfaces updated. Plugin rebuilt with updated definitions.
|
||||
|
||||
---
|
||||
|
||||
## 📊 Progress Summary
|
||||
|
||||
### ✅ Completed High-Priority Items
|
||||
- **Item #1**: JWT Algorithm Investigation - ✅ **COMPLETE** (ES256K requirement confirmed, implementation pending)
|
||||
- **Item #2**: SharedPreferences Persistence - ✅ **COMPLETE**
|
||||
- **Item #3**: Error Handling and Retry Logic - ✅ **COMPLETE**
|
||||
- **Item #6**: Context Management - ✅ **COMPLETE**
|
||||
|
||||
### ✅ Critical Implementation Complete (Item #1)
|
||||
- **ES256K JWT Token Generation** - ✅ **COMPLETE**: Implemented `generateEndorserJWT()` function using `did-jwt` and `ethers` libraries. Generates proper ES256K signed JWTs from test user zero's seed phrase. Native fetcher now receives pre-generated tokens, avoiding Java DID library complexity.
|
||||
|
||||
### 🟡 Medium Priority Status
|
||||
- **Item #4**: API Response Structure Validation - ✅ **VERIFIED** (structure verified, validation improvements needed - see details below)
|
||||
- **Item #5**: Comprehensive Logging - Partial (basic logging exists, more comprehensive needed)
|
||||
|
||||
### 🟢 Low Priority
|
||||
- Items #7-11: All pending
|
||||
|
||||
### 📝 Configuration Tasks
|
||||
- **Item #13**: Real API Calls - Ready (can now enable localhost mode, ES256K JWT generation complete)
|
||||
- **Item #14**: TypeScript Types - ✅ **COMPLETE** (plugin rebuilt, types updated, `as any` removed)
|
||||
- **Item #15**: Network Security Config - ✅ **COMPLETE** (config created, AndroidManifest updated)
|
||||
|
||||
---
|
||||
|
||||
## 🔴 High Priority
|
||||
|
||||
### 1. ⚠️ VERIFY: Determine Correct JWT Signing Algorithm (CRITICAL)
|
||||
|
||||
- [x] **Status**: ✅ **INVESTIGATION COMPLETE** - See `INVESTIGATION_JWT_ALGORITHM_RESULTS.md`
|
||||
|
||||
**CRITICAL FINDING**: Endorser-ch API expects **DID-based JWTs (ES256K)**, NOT HMAC-SHA256. Current implementation is WRONG and must be replaced.
|
||||
|
||||
**Current State**: Uses SHA-256 hash with `jwtSecret:unsignedToken` format, header claims `"alg": "HS256"`
|
||||
|
||||
**Location**: `TestNativeFetcher.java` → `generateJWTToken()`
|
||||
|
||||
**Investigation Results** (See `INVESTIGATION_JWT_ALGORITHM_RESULTS.md` for full details):
|
||||
|
||||
**✅ CONFIRMED**: Endorser-ch API uses **DID-based JWTs (ES256K)**, NOT HMAC-SHA256.
|
||||
|
||||
**TimeSafari Implementation** (`~/projects/timesafari/crowd-funder-for-time-pwa/src/libs/crypto/vc/index.ts`):
|
||||
- Uses `did-jwt.createJWT()` with `SimpleSigner(privateKeyHex)`
|
||||
- Signs with Ethereum private key from DID identity
|
||||
- Algorithm: **ES256K** (default for did-jwt library)
|
||||
|
||||
**endorser-ch Verification** (`~/projects/timesafari/endorser-ch/src/api/services/vc/index.js`):
|
||||
- Uses `did-jwt.verifyJWT(jwt, {resolver})` for verification
|
||||
- Verifies signature using DID resolver (resolves DID to public key)
|
||||
- **NO shared secret used** - authentication is DID-based
|
||||
|
||||
**Current Implementation Problem**:
|
||||
- Currently uses HMAC-SHA256 with shared secret (`jwtSecret`)
|
||||
- Server will **reject** these tokens as invalid
|
||||
- Must be replaced with DID-based ES256K signing
|
||||
|
||||
**Required Implementation Changes:**
|
||||
|
||||
1. **Remove HMAC-SHA256 implementation** - Completely incorrect approach
|
||||
2. **Remove `jwtSecret` parameter** - No longer needed (shared secret not used)
|
||||
3. **Implement DID-based ES256K signing** - Sign with Ethereum private key
|
||||
4. **Add DID private key retrieval** - Need mechanism to get private key for `activeDid`
|
||||
5. **Use Java DID library** - did-jwt-java or web3j for ES256K signing
|
||||
|
||||
**Implementation Options:**
|
||||
|
||||
**Option 1: Use did-jwt-java Library (Recommended)**
|
||||
```java
|
||||
// Add dependency to build.gradle
|
||||
implementation 'io.uport:uport-did-jwt:3.1.0' // or equivalent
|
||||
|
||||
// Sign with DID private key
|
||||
import io.uport.sdk.did.jwt.DIDJWT;
|
||||
import io.uport.sdk.did.jwt.SimpleSigner;
|
||||
|
||||
String privateKeyHex = getPrivateKeyForDid(activeDid); // Need to implement
|
||||
SimpleSigner signer = new SimpleSigner(privateKeyHex);
|
||||
String jwt = DIDJWT.createJWT(payload, signer, issuer: activeDid);
|
||||
```
|
||||
|
||||
**Option 2: Use web3j for Ethereum Signing**
|
||||
```java
|
||||
// Add dependency
|
||||
implementation 'org.web3j:core:4.9.8'
|
||||
|
||||
import org.web3j.crypto.ECKeyPair;
|
||||
import org.web3j.crypto.Sign;
|
||||
|
||||
ECKeyPair keyPair = getKeyPairForDid(activeDid); // Need to implement
|
||||
Sign.SignatureData signature = Sign.signMessage(
|
||||
unsignedToken.getBytes(StandardCharsets.UTF_8),
|
||||
keyPair
|
||||
);
|
||||
// Then encode signature according to ES256K format for JWT
|
||||
```
|
||||
|
||||
**References:**
|
||||
- **Investigation Results**: See `INVESTIGATION_JWT_ALGORITHM_RESULTS.md` for complete findings
|
||||
- **TimeSafari Implementation**: `~/projects/timesafari/crowd-funder-for-time-pwa/src/libs/crypto/vc/index.ts` - `createEndorserJwtForKey()` function
|
||||
- **endorser-ch Verification**: `~/projects/timesafari/endorser-ch/src/api/services/vc/index.js` - `decodeAndVerifyJwt()` function
|
||||
- [did-jwt library](https://github.com/decentralized-identity/did-jwt) - DID-based JWT library (JavaScript reference)
|
||||
- [did-jwt-java](https://github.com/uport-project/uport-did-jwt) - Java implementation (if available) or alternative Java DID libraries
|
||||
|
||||
**Impact**: **CRITICAL** - Current HMAC-SHA256 implementation will be rejected by API server. All authentication will fail until DID-based ES256K signing is implemented.
|
||||
|
||||
**✅ ARCHITECTURAL DECISION**: Generate JWT in TypeScript (host app), pass token to native fetcher
|
||||
|
||||
**Rationale**:
|
||||
- TimeSafari already has `did-jwt` library available in TypeScript
|
||||
- TimeSafari can access DID private keys via Capacitor SQLite (`retrieveFullyDecryptedAccount()`)
|
||||
- Plugin SPI pattern is designed for host app to provide implementations
|
||||
- Avoids complexity of finding/implementing Java DID libraries
|
||||
- Keeps cryptography in TypeScript where libraries are mature
|
||||
|
||||
**Updated Implementation Approach**:
|
||||
|
||||
**Option A: Generate JWT in TypeScript, Pass Token (RECOMMENDED)**
|
||||
```typescript
|
||||
// In HomeView.vue or TimeSafari app
|
||||
import { createEndorserJwtForDid } from '@/libs/endorserServer';
|
||||
|
||||
// Generate JWT in TypeScript using existing TimeSafari infrastructure
|
||||
const account = await retrieveFullyDecryptedAccount(activeDid);
|
||||
const jwtToken = await createEndorserJwtForDid(activeDid, {
|
||||
exp: Math.floor(Date.now() / 1000) + 60,
|
||||
iat: Math.floor(Date.now() / 1000),
|
||||
iss: activeDid
|
||||
});
|
||||
|
||||
// Pass JWT token to native fetcher (no private key needed)
|
||||
await DailyNotification.configureNativeFetcher({
|
||||
apiBaseUrl: 'http://10.0.2.2:3000',
|
||||
activeDid: activeDid,
|
||||
jwtToken: jwtToken // Pre-generated token, not secret
|
||||
});
|
||||
```
|
||||
|
||||
```java
|
||||
// In TestNativeFetcher.java - Just use the token, don't generate it
|
||||
private volatile String jwtToken; // Pre-generated token from TypeScript
|
||||
|
||||
public void configure(String apiBaseUrl, String activeDid, String jwtToken) {
|
||||
this.apiBaseUrl = apiBaseUrl;
|
||||
this.activeDid = activeDid;
|
||||
this.jwtToken = jwtToken; // Use pre-generated token
|
||||
}
|
||||
|
||||
private String getAuthorizationHeader() {
|
||||
return "Bearer " + jwtToken;
|
||||
}
|
||||
```
|
||||
|
||||
**Benefits**:
|
||||
- ✅ No Java DID library needed
|
||||
- ✅ Uses existing TimeSafari infrastructure (`did-jwt`, `retrieveFullyDecryptedAccount`)
|
||||
- ✅ Keeps cryptography in TypeScript (where it's proven to work)
|
||||
- ✅ Native fetcher just uses token (simple, no crypto complexity)
|
||||
- ✅ Token can be refreshed in TypeScript when needed
|
||||
|
||||
**Next Actions**:
|
||||
- [ ] Update `configureNativeFetcher()` TypeScript signature to accept `jwtToken` instead of `jwtSecret`
|
||||
- [ ] Update Java `configureNativeFetcher()` to accept `jwtToken` parameter
|
||||
- [ ] Remove JWT generation code from `TestNativeFetcher.generateJWTToken()`
|
||||
- [ ] Use pre-generated token in `getAuthorizationHeader()` method
|
||||
- [ ] Update `HomeView.vue` to generate JWT using TimeSafari's `createEndorserJwtForDid()`
|
||||
- [ ] For TimeSafari integration: Generate JWT in host app using account data from SQLite
|
||||
- [ ] Test with real endorser-ch API to verify tokens are accepted
|
||||
|
||||
**Testing**:
|
||||
- [x] ✅ Verified algorithm with endorser-ch source code - ES256K confirmed
|
||||
- [x] ✅ Generate JWT in TypeScript using `did-jwt` library (ES256K signing)
|
||||
- [x] ✅ Pass pre-generated JWT token to native fetcher via `configureNativeFetcher()`
|
||||
- [ ] ⚠️ Verify tokens are accepted by the endorser API endpoint (pending real API testing)
|
||||
- [ ] Implement token refresh mechanism in TypeScript when tokens expire (60 seconds)
|
||||
|
||||
**Implementation Checklist**:
|
||||
|
||||
**1. Update TypeScript Interface (`src/definitions.ts`)**:
|
||||
- [x] ✅ Change `jwtSecret: string` → `jwtToken: string` in `configureNativeFetcher()` signature
|
||||
- [x] ✅ Update JSDoc to reflect that token is pre-generated, not a secret
|
||||
- [x] ✅ Rebuild plugin to update `dist/definitions.d.ts`
|
||||
|
||||
**2. Update Java Plugin (`android/plugin/.../DailyNotificationPlugin.java`)**:
|
||||
- [x] ✅ Change `String jwtSecret = call.getString("jwtSecret")` → `String jwtToken = call.getString("jwtToken")`
|
||||
- [x] ✅ Update validation message: "Missing required parameters: apiBaseUrl, activeDid, and jwtToken are required"
|
||||
- [x] ✅ Update `fetcher.configure(apiBaseUrl, activeDid, jwtToken)` call
|
||||
- [x] ✅ Update Javadoc to reflect token-based approach
|
||||
|
||||
**3. Update Native Interface (`android/plugin/.../NativeNotificationContentFetcher.java`)**:
|
||||
- [x] ✅ Change `default void configure(String apiBaseUrl, String activeDid, String jwtSecret)`
|
||||
→ `default void configure(String apiBaseUrl, String activeDid, String jwtToken)`
|
||||
- [x] ✅ Update Javadoc to explain token is pre-generated by TypeScript
|
||||
|
||||
**4. Update TestNativeFetcher (`test-apps/.../TestNativeFetcher.java`)**:
|
||||
- [x] ✅ Change `private volatile String jwtSecret;` → `private volatile String jwtToken;`
|
||||
- [x] ✅ Update `configure()` method signature and implementation
|
||||
- [x] ✅ Remove entire `generateJWTToken()` method (HMAC-SHA256 implementation removed)
|
||||
- [x] ✅ Update `fetchContentWithRetry()` to use `jwtToken` directly
|
||||
- [x] ✅ Remove JWT-related constants (`JWT_EXPIRATION_MINUTES`) - no longer needed
|
||||
- [x] ✅ Remove HMAC-SHA256 imports (`Mac`, `SecretKeySpec`, `Base64`, `MessageDigest`) - not used elsewhere
|
||||
|
||||
**5. Update HomeView.vue (`test-apps/.../HomeView.vue`)**:
|
||||
- [x] ✅ Implement ES256K JWT generation using `did-jwt` and `ethers` libraries
|
||||
- [x] ✅ Generate JWT before calling `configureNativeFetcher()` using `generateEndorserJWT()`
|
||||
- [x] ✅ Update `configureNativeFetcher()` call to use `jwtToken` instead of `jwtSecret`
|
||||
- [x] ✅ Remove `as any` type assertion after plugin rebuild
|
||||
|
||||
**6. ES256K JWT Generation Implementation (`test-apps/.../test-user-zero.ts`)**:
|
||||
- [x] ✅ Add `did-jwt@^7.0.0` and `ethers@^6.0.0` dependencies
|
||||
- [x] ✅ Implement `generateEndorserJWT()` function:
|
||||
- Derives Ethereum private key from seed phrase using `ethers.HDNodeWallet`
|
||||
- Uses `did-jwt.SimpleSigner` for ES256K signing
|
||||
- Creates proper ES256K signed JWTs matching TimeSafari's pattern
|
||||
- [x] ✅ Export `generateEndorserJWT` for use in HomeView.vue
|
||||
|
||||
**6. For TimeSafari Production Integration**:
|
||||
- [ ] In TimeSafari app, generate JWT using `createEndorserJwtForKey()` with account from SQLite
|
||||
- [ ] Call `DailyNotification.configureNativeFetcher()` with generated token
|
||||
- [ ] Implement token refresh logic when tokens expire (60 seconds)
|
||||
- [ ] Handle token expiration errors and regenerate tokens
|
||||
|
||||
**7. Documentation Updates**:
|
||||
- [ ] Update `docs/NATIVE_FETCHER_CONFIGURATION.md` to reflect token-based approach
|
||||
- [ ] Document how to generate tokens in TypeScript using TimeSafari infrastructure
|
||||
- [ ] Remove references to `jwtSecret` and HMAC-SHA256
|
||||
|
||||
---
|
||||
|
||||
### 2. Implement SharedPreferences Persistence for Configuration
|
||||
|
||||
- [x] **Status**: ✅ **COMPLETE**
|
||||
|
||||
**Current State**: ✅ Implemented - `getStarredPlanIds()` and `getLastAcknowledgedJwtId()` use SharedPreferences
|
||||
|
||||
**Location**: `TestNativeFetcher.java` → `getStarredPlanIds()`, `getLastAcknowledgedJwtId()`
|
||||
|
||||
**Implementation Status**: ✅ **COMPLETE**
|
||||
- ✅ SharedPreferences initialized in constructor with `PREFS_NAME = "DailyNotificationPrefs"`
|
||||
- ✅ `getStarredPlanIds()` - Loads JSON array from SharedPreferences
|
||||
- ✅ `getLastAcknowledgedJwtId()` - Loads JWT ID from SharedPreferences
|
||||
- ✅ `updateStarredPlanIds()` - Saves plan IDs to SharedPreferences
|
||||
- ✅ `updateLastAckedJwtId()` - Saves JWT ID to SharedPreferences
|
||||
- ✅ Context passed to constructor and stored as `appContext`
|
||||
|
||||
**Impact**: ✅ Proper state management and pagination support enabled
|
||||
|
||||
---
|
||||
|
||||
### 3. Add Error Handling and Retry Logic
|
||||
|
||||
- [x] **Status**: ✅ **COMPLETE**
|
||||
|
||||
**Current State**: ✅ Implemented - `fetchContentWithRetry()` with exponential backoff
|
||||
|
||||
**Location**: `TestNativeFetcher.java` → `fetchContentWithRetry()`
|
||||
|
||||
**Implementation Status**:
|
||||
- [x] ✅ Retry logic for network failures (transient errors) - `fetchContentWithRetry()` with `MAX_RETRIES = 3`
|
||||
- [x] ✅ Distinguish retryable vs non-retryable errors - `shouldRetry()` method checks response codes and retry count
|
||||
- [x] ✅ Log error details for debugging - Detailed logging with retry counts and error messages
|
||||
- [x] ✅ Exponential backoff for retries - `RETRY_DELAY_MS * (1 << retryCount)` implemented
|
||||
|
||||
**Suggested Implementation**:
|
||||
```java
|
||||
private static final int MAX_RETRIES = 3;
|
||||
private static final int RETRY_DELAY_MS = 1000;
|
||||
|
||||
private CompletableFuture<List<NotificationContent>> fetchContentWithRetry(
|
||||
FetchContext context, int retryCount) {
|
||||
// ... existing fetch logic ...
|
||||
|
||||
if (responseCode == 401 || responseCode == 403) {
|
||||
// Non-retryable - authentication issue
|
||||
Log.e(TAG, "Authentication failed - check credentials");
|
||||
return CompletableFuture.completedFuture(Collections.emptyList());
|
||||
}
|
||||
|
||||
if (responseCode >= 500 && retryCount < MAX_RETRIES) {
|
||||
// Retryable - server error
|
||||
Log.w(TAG, "Server error " + responseCode + ", retrying... (" + retryCount + "/" + MAX_RETRIES + ")");
|
||||
Thread.sleep(RETRY_DELAY_MS * (1 << retryCount)); // Exponential backoff
|
||||
return fetchContentWithRetry(context, retryCount + 1);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Impact**: Improves reliability for transient network issues
|
||||
|
||||
---
|
||||
|
||||
## 🟡 Medium Priority
|
||||
|
||||
### 4. Validate API Response Structure (Need to Verify Actual Structure)
|
||||
|
||||
- [x] **Status**: ✅ **VERIFIED** - Endpoint structure verified, parser validation needs improvement
|
||||
|
||||
**Current State**: Parser handles both flat and nested structures, but could add more validation
|
||||
|
||||
**Location**: `TestNativeFetcher.java` → `parseApiResponse()`
|
||||
|
||||
**✅ Endpoint Structure Verified:**
|
||||
|
||||
- [x] ✅ **Check endorser-ch endpoint implementation:**
|
||||
- [x] ✅ Found `/api/v2/report/plansLastUpdatedBetween` handler in endorser-ch source: `~/projects/timesafari/endorser-ch/src/api/controllers/report-router.js`
|
||||
- [x] ✅ Verified actual response structure - Returns `{ data: [...], hitLimit: boolean }` where data items have `plan` and `wrappedClaimBefore` properties
|
||||
- [x] ✅ Response matches `PlanSummaryAndPreviousClaim` structure - Items have `plan.handleId`, `plan.jwtId`, and `wrappedClaimBefore` properties
|
||||
|
||||
- [x] ✅ **Current parser handles:**
|
||||
- [x] ✅ Both flat structure (`jwtId`, `planId`) and nested structure (`plan.jwtId`, `plan.handleId`)
|
||||
- [ ] ⚠️ **Missing**: Does NOT extract `wrappedClaimBefore` from response (exists in API response but not used in parser)
|
||||
|
||||
**✅ Verification Results:**
|
||||
|
||||
**Null Checks Status:**
|
||||
- [x] ✅ Uses `.has()` checks before accessing fields (planId, jwtId) - Lines 502, 511
|
||||
- [x] ✅ Checks `item.has("plan")` before accessing plan object - Lines 504, 513
|
||||
- [x] ✅ Checks `dataArray != null` after getting it - Line 491
|
||||
- [x] ✅ Handles null values safely (planId and jwtId can be null) - Lines 499-518
|
||||
- [x] ✅ Wrapped in try-catch for error handling - Line 485, 570
|
||||
- [ ] ⚠️ **Missing**: No check for `root.has("data")` before calling `getAsJsonArray("data")` (line 490) - Could throw `IllegalStateException` if "data" field missing or wrong type
|
||||
- [ ] ⚠️ **Potential Issue**: `getAsJsonObject("plan")` (lines 505, 514) could throw if "plan" exists but is not a JsonObject (currently caught by try-catch)
|
||||
|
||||
**Structure Validation Status:**
|
||||
- [ ] ⚠️ **Missing**: No validation that root has "data" field before accessing (line 490)
|
||||
- [ ] ⚠️ **Missing**: No warning logged if "data" field is missing or wrong type (would be caught by try-catch, but no specific warning)
|
||||
- [x] ✅ Handles missing/null data gracefully (creates default notification if contents empty) - Line 554-568
|
||||
- [x] ✅ Handles parse errors gracefully (returns empty list on exception) - Line 570-573
|
||||
|
||||
**Nested Structure Handling:**
|
||||
- [x] ✅ Handle nested structures correctly - Parser handles both `item.plan.handleId` and `item.planId` formats
|
||||
- [x] ✅ Handles both flat (`jwtId`, `planId`) and nested (`plan.jwtId`, `plan.handleId`) structures
|
||||
|
||||
**Remaining Work:**
|
||||
- [ ] Add `root.has("data")` check before accessing data array (line 490 - currently could throw if "data" missing)
|
||||
- [ ] Add null check for `plan` object before accessing nested fields (line 505, 514 - currently relies on try-catch)
|
||||
- [ ] Log warnings for unexpected response formats (missing "data" field, wrong types)
|
||||
- [ ] Extract `wrappedClaimBefore` from response items (API provides this but parser doesn't use it)
|
||||
|
||||
**Suggested Improvements**:
|
||||
```java
|
||||
private List<NotificationContent> parseApiResponse(String responseBody, FetchContext context) {
|
||||
try {
|
||||
JsonParser parser = new JsonParser();
|
||||
JsonObject root = parser.parse(responseBody).getAsJsonObject();
|
||||
|
||||
// Validate response structure
|
||||
if (!root.has("data")) {
|
||||
Log.w(TAG, "API response missing 'data' field");
|
||||
return Collections.emptyList();
|
||||
}
|
||||
|
||||
JsonArray dataArray = root.getAsJsonArray("data");
|
||||
if (dataArray == null) {
|
||||
Log.w(TAG, "API response 'data' field is not an array");
|
||||
return Collections.emptyList();
|
||||
}
|
||||
|
||||
// ... rest of parsing with null checks ...
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Impact**: Prevents crashes on unexpected API responses
|
||||
|
||||
---
|
||||
|
||||
### 5. Add Comprehensive Logging for Debugging
|
||||
|
||||
- [ ] **Status**: Partial - Basic logging exists, more comprehensive logging needed
|
||||
|
||||
**Current State**: Basic logging present (Log.d/i/e/w), but could be more detailed and structured
|
||||
|
||||
**Location**: Throughout `TestNativeFetcher.java`
|
||||
|
||||
**Current Implementation**:
|
||||
- ✅ Uses Log.d/i/e/w for different severity levels
|
||||
- ✅ Some structured tags (e.g., "TestNativeFetcher")
|
||||
- ✅ Logs error details and retry information
|
||||
|
||||
**Required Changes**:
|
||||
- [ ] Add more comprehensive log levels (DEBUG, INFO, WARN, ERROR) - Basic levels exist but could be more systematic
|
||||
- [ ] Log request/response details (truncated for sensitive data) - Some logging exists, could be more comprehensive
|
||||
- [ ] Log timing information for performance monitoring - Not currently implemented (timing would be helpful)
|
||||
- [ ] Add structured logging tags for filtering - Basic tags exist, could be more structured with consistent prefixes
|
||||
|
||||
**Suggested Logging Points**:
|
||||
- Configuration received
|
||||
- Fetch start/end with timing
|
||||
- HTTP request details (URL, method, headers - sanitized)
|
||||
- Response code and size
|
||||
- Parse success/failure
|
||||
- Number of notifications created
|
||||
|
||||
**Example**:
|
||||
```java
|
||||
Log.d(TAG, String.format("FETCH_START trigger=%s scheduledTime=%d fetchTime=%d",
|
||||
context.trigger, context.scheduledTime, context.fetchTime));
|
||||
|
||||
long startTime = System.currentTimeMillis();
|
||||
// ... radius logic ...
|
||||
long duration = System.currentTimeMillis() - startTime;
|
||||
|
||||
Log.i(TAG, String.format("FETCH_COMPLETE duration=%dms items=%d",
|
||||
duration, contents.size()));
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 6. Implement Proper Context Management
|
||||
|
||||
- [x] **Status**: ✅ **COMPLETE**
|
||||
|
||||
**Current State**: ✅ Context passed to constructor and stored
|
||||
|
||||
**Location**: `TestNativeFetcher.java` constructor, `TestApplication.java`
|
||||
|
||||
**Implementation Status**:
|
||||
- [x] ✅ Pass `Application` context to `TestNativeFetcher` constructor - Done in `TestApplication.onCreate()`
|
||||
- [x] ✅ Store context for SharedPreferences access - Stored as `appContext` and used for SharedPreferences
|
||||
- [x] ✅ Using application context (no leaks) - `context.getApplicationContext()` used
|
||||
|
||||
**Example**:
|
||||
```java
|
||||
// In TestNativeFetcher:
|
||||
private final Context appContext;
|
||||
|
||||
public TestNativeFetcher(Context context) {
|
||||
this.appContext = context.getApplicationContext(); // Use app context
|
||||
// ... initialize SharedPreferences ...
|
||||
}
|
||||
|
||||
// In TestApplication.onCreate():
|
||||
Context context = getApplicationContext();
|
||||
TestNativeFetcher fetcher = new TestNativeFetcher(context);
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 🟢 Low Priority / Nice to Have
|
||||
|
||||
### 7. Add Unit Tests
|
||||
|
||||
- [ ] **Status**: Not started
|
||||
|
||||
**Location**: Create `TestNativeFetcher/CONTENT_FETCHERTest.java`
|
||||
|
||||
**Coverage Needed**:
|
||||
- [ ] JWT token generation (verify signature format)
|
||||
- [ ] API response parsing (valid and invalid responses)
|
||||
- [ ] Error handling (network failures, invalid responses)
|
||||
- [ ] Configuration updates
|
||||
- [ ] SharedPreferences integration
|
||||
|
||||
**Test Framework**: JUnit 4/5 + Mockito
|
||||
|
||||
---
|
||||
|
||||
### 8. Extract JWT Generation to Utility Class
|
||||
|
||||
- [ ] **Status**: Not started
|
||||
|
||||
**Current State**: JWT generation code embedded in fetcher
|
||||
|
||||
**Benefit**: Reusable across other parts of the app, easier to test
|
||||
|
||||
**Location**: Create `test-apps/daily-notification-test/android/app/src/main/java/com/timesafari/dailynotification/test/JwtUtils.java`
|
||||
|
||||
---
|
||||
|
||||
### 9. Add Request/Response Interceptors for Development
|
||||
|
||||
- [ ] **Status**: Not started
|
||||
|
||||
**Purpose**: Log full request/response for API debugging
|
||||
|
||||
**Implementation**:
|
||||
- [ ] Add methods to log full HTTP requests/responses (with data sanitization)
|
||||
|
||||
**Note**: Only enable in debug builds for security
|
||||
|
||||
---
|
||||
|
||||
### 10. Support API Response Caching
|
||||
|
||||
- [ ] **Status**: Not started
|
||||
|
||||
**Current State**: Always fetches from API
|
||||
|
||||
**Enhancement**:
|
||||
- [ ] Cache successful responses with TTL
|
||||
- [ ] Use cached data if available and fresh
|
||||
|
||||
**Use Case**: Reduce API calls when multiple notifications scheduled close together
|
||||
|
||||
---
|
||||
|
||||
### 11. Add Metrics/Telemetry
|
||||
|
||||
- [ ] **Status**: Not started
|
||||
|
||||
**Purpose**: Track fetch success rates, latency, error types
|
||||
|
||||
**Implementation**:
|
||||
- [ ] Record metrics to SharedPreferences or analytics service
|
||||
- [ ] Track: success/failure rate, average response time, error distribution
|
||||
- [ ] Log summary periodically
|
||||
|
||||
---
|
||||
|
||||
### 12. ✅ MOVED TO HIGH PRIORITY: Verify API Response Structure
|
||||
|
||||
**This item has been moved to High Priority item #4** - Validate API Response Structure
|
||||
|
||||
**Action Required** (covered in item #4 above):
|
||||
- [x] ✅ Check endorser-ch source code for actual endpoint response structure
|
||||
- [x] ✅ Compare with `PlansLastUpdatedResponse` and `PlanSummaryWithPreviousClaim` interfaces in codebase
|
||||
- [x] ✅ Update parser if structure differs - Parser handles both flat and nested structures
|
||||
- [ ] Add validation for required fields - Additional validation still needed
|
||||
- [ ] Test with real API responses from endorser-ch server - Pending real API testing
|
||||
|
||||
**Location**:
|
||||
- Check endorser-ch source: `src/` or `server/` directories for endpoint handler
|
||||
- Check `src/definitions.ts` in this codebase for expected interface structure
|
||||
- Test with real responses from running endorser-ch server
|
||||
|
||||
---
|
||||
|
||||
## 📝 Configuration & Setup Tasks
|
||||
|
||||
### 13. Enable Real API Calls in Test Config
|
||||
|
||||
- [ ] **Status**: Ready to enable - ES256K JWT generation complete, network config ready
|
||||
|
||||
**Current State**: `TEST_USER_ZERO_CONFIG.api.serverMode = "mock"`
|
||||
|
||||
**Ready to Enable**: ES256K JWT generation is complete, network security config is in place
|
||||
|
||||
**Action**:
|
||||
- [ ] Change `serverMode` to `"localhost"` in `test-user-zero.ts` (line 28) for emulator testing
|
||||
- [ ] Test API calls to `http://10.0.2.2:3000` with real ES256K signed tokens
|
||||
- [ ] Verify tokens are accepted by endorser-ch API
|
||||
- [ ] Update to `"staging"` or `"production"` for real environment testing when ready
|
||||
|
||||
**Location**: `test-apps/daily-notification-test/src/config/test-user-zero.ts` (line 28)
|
||||
|
||||
**Note**: JWT generation now uses `generateEndorserJWT()` which creates proper ES256K signed tokens from the seed phrase.
|
||||
|
||||
---
|
||||
|
||||
### 14. Update TypeScript Type Definitions
|
||||
|
||||
- [x] **Status**: ✅ **VERIFIED** - Type definitions exist and are being used, may need updates after ES256K changes
|
||||
|
||||
**Current State**: Using `as any` type assertion for `configureNativeFetcher` in HomeView.vue
|
||||
|
||||
**Verification Results:**
|
||||
- [x] ✅ `configureNativeFetcher()` method added to plugin interface (`src/definitions.ts`) - Verified: Line 376
|
||||
- [x] ✅ Method signature exists with `apiBaseUrl`, `activeDid`, `jwtSecret` parameters
|
||||
- [x] ✅ Method is being called in `HomeView.vue` - Line 474 (`onMounted` hook)
|
||||
- [x] ✅ Currently uses `(DailyNotification as any).configureNativeFetcher` type assertion - Line 454
|
||||
|
||||
**Remaining Work:**
|
||||
- [ ] Rebuild plugin package to ensure TypeScript definitions are up to date in `dist/`
|
||||
- [ ] Remove `as any` type assertion in `HomeView.vue` (after confirming types are available)
|
||||
- [ ] ⚠️ **CRITICAL**: Update method signature after ES256K implementation (remove `jwtSecret`, add private key mechanism)
|
||||
|
||||
**Location**: `test-apps/daily-notification-test/src/views/HomeView.vue` - Currently uses `(DailyNotification as any).configureNativeFetcher`
|
||||
|
||||
---
|
||||
|
||||
### 15. Add Network Security Configuration
|
||||
|
||||
- [x] **Status**: ✅ **COMPLETE** - Network security config created and AndroidManifest.xml updated
|
||||
|
||||
**Purpose**: Allow HTTP connections in Android emulator (for localhost:3000)
|
||||
|
||||
**Verification Results:**
|
||||
- [x] ✅ Checked: `network_security_config.xml` does NOT exist in `android/app/src/main/res/xml/`
|
||||
- [x] ✅ Checked: No `networkSecurityConfig` attribute in `AndroidManifest.xml`
|
||||
- [x] ✅ Checked: No `usesCleartextTraffic` attribute in `AndroidManifest.xml`
|
||||
|
||||
**Implementation Status**:
|
||||
- [x] ✅ Created `android/app/src/main/res/xml/network_security_config.xml` allowing cleartext traffic for `10.0.2.2`, `localhost`, and `127.0.0.1`
|
||||
- [x] ✅ Added `android:networkSecurityConfig="@xml/network_security_config"` to `<application>` tag in `AndroidManifest.xml`
|
||||
- [x] ✅ Documented that this is safe for development (emulator-only access)
|
||||
|
||||
**Files Modified**:
|
||||
- `test-apps/daily-notification-test/android/app/src/main/res/xml/network_security_config.xml` (created)
|
||||
- `test-apps/daily-notification-test/android/app/src/main/AndroidManifest.xml` (updated line 10)
|
||||
|
||||
**Note**: Required for HTTP connections (not HTTPS) to `http://10.0.2.2:3000` from Android emulator
|
||||
|
||||
---
|
||||
|
||||
## 🧪 Testing Checklist
|
||||
|
||||
Before considering this implementation complete:
|
||||
|
||||
- [ ] ⚠️ **CRITICAL**: JWT tokens are accepted by endorser API (pending ES256K implementation - current HMAC-SHA256 will fail)
|
||||
- [ ] API responses are correctly parsed into notifications
|
||||
- [ ] Notifications appear at scheduled times
|
||||
- [ ] Prefetch occurs 5 minutes before notification
|
||||
- [x] ✅ Error handling works for network failures - Retry logic implemented
|
||||
- [x] ✅ SharedPreferences persists starred plan IDs - Implementation complete
|
||||
- [ ] Pagination works with `afterId` parameter (SharedPreferences ready, needs API testing)
|
||||
- [ ] Multiple notifications can be scheduled and fetched
|
||||
- [ ] Background fetches work when app is closed
|
||||
- [x] ✅ App recovers gracefully from API errors - Error handling and retry logic implemented
|
||||
|
||||
---
|
||||
|
||||
## 📚 Related Documentation
|
||||
|
||||
- `docs/NATIVE_FETCHER_CONFIGURATION.md` - Native fetcher configuration guide
|
||||
- `src/types/content-fetcher.ts` - TypeScript type definitions
|
||||
- `src/definitions.ts` - `PlansLastUpdatedResponse` and `PlanSummaryWithPreviousClaim` interfaces
|
||||
- `examples/native-fetcher-android.kt` - Example Android implementation
|
||||
- **Investigation Results**: `INVESTIGATION_JWT_ALGORITHM_RESULTS.md` - ✅ Complete investigation findings
|
||||
- [endorser-ch Repository](https://github.com/trentlarson/endorser-ch/tree/master) - **CONFIRMED**: Uses `did-jwt.verifyJWT()` with DID resolver for ES256K verification
|
||||
- JWT verification: `~/projects/timesafari/endorser-ch/src/api/services/vc/index.js` - `decodeAndVerifyJwt()`
|
||||
- `/api/v2/report/plansLastUpdatedBetween` endpoint handler: `~/projects/timesafari/endorser-ch/src/api/controllers/report-router.js`
|
||||
- [did-jwt Library](https://github.com/decentralized-identity/did-jwt) - DID-based JWT implementation (JavaScript reference)
|
||||
- **TimeSafari Repository**: `~/projects/timesafari/crowd-funder-for-time-pwa/src/libs/crypto/vc/index.ts`
|
||||
- **CONFIRMED**: Uses `createEndorserJwtForKey()` with `did-jwt.createJWT()` and ES256K signing
|
||||
- Signs with Ethereum private key from DID identity
|
||||
- Reference implementation for production JWT generation
|
||||
|
||||
---
|
||||
|
||||
## Notes
|
||||
|
||||
- **JWT Implementation**: ✅ **CONFIRMED** - TimeSafari repository investigation complete. Endorser-ch API uses **DID-based JWTs (ES256K)** for API authentication. Current HMAC-SHA256 implementation must be replaced. See `INVESTIGATION_JWT_ALGORITHM_RESULTS.md` for complete findings.
|
||||
- **Context Dependency**: Consider whether `TestNativeFetcher` should be a singleton or if context should be injected differently
|
||||
- **Error Strategy**: Decide on error handling strategy - should fetches fail silently (current), or propagate errors for user notification?
|
||||
- **State Management**: Consider whether starred plan IDs should be managed by the app or fetched from API user profile
|
||||
- **Investigation Status**: ✅ Item #1 (JWT algorithm) investigation **COMPLETE**. Confirmed ES256K requirement. Implementation changes required before proceeding with other items.
|
||||
|
||||
---
|
||||
|
||||
## ✅ Verification Summary (2025-10-31)
|
||||
|
||||
**Items Verified Through Code Inspection:**
|
||||
|
||||
1. **Item #4 (API Response Structure)**:
|
||||
- ✅ Endpoint structure confirmed in endorser-ch source
|
||||
- ✅ Parser handles both flat and nested structures
|
||||
- ✅ Null checks exist for most fields (uses `.has()` checks)
|
||||
- ⚠️ Missing: `root.has("data")` check before accessing data array
|
||||
- ⚠️ Missing: `wrappedClaimBefore` extraction from response
|
||||
|
||||
2. **Item #14 (TypeScript Types)**:
|
||||
- ✅ Method exists in `src/definitions.ts` (Line 376)
|
||||
- ✅ Method signature includes `apiBaseUrl`, `activeDid`, `jwtSecret`
|
||||
- ✅ Method being called in `HomeView.vue` (Line 474, `onMounted` hook)
|
||||
- ⚠️ Currently uses `as any` type assertion (needs plugin rebuild for proper types)
|
||||
|
||||
3. **Item #15 (Network Security Config)**:
|
||||
- ✅ Confirmed: `network_security_config.xml` does NOT exist
|
||||
- ✅ Confirmed: No `networkSecurityConfig` attribute in AndroidManifest.xml
|
||||
- ✅ Confirmed: No `usesCleartextTraffic` attribute in AndroidManifest.xml
|
||||
- Needs to be created for HTTP connections to `http://10.0.2.2:3000`
|
||||
|
||||
---
|
||||
|
||||
**Last Updated**: 2025-10-31 (Checkboxes updated - Items 4, 14, 15 verified through code inspection)
|
||||
**Next Review**: After ES256K JWT signing implementation is complete
|
||||
|
||||
|
||||
@@ -7,6 +7,7 @@
|
||||
android:icon="@mipmap/ic_launcher"
|
||||
android:label="@string/app_name"
|
||||
android:roundIcon="@mipmap/ic_launcher_round"
|
||||
android:networkSecurityConfig="@xml/network_security_config"
|
||||
android:supportsRtl="true"
|
||||
android:theme="@style/AppTheme">
|
||||
|
||||
|
||||
@@ -11,6 +11,7 @@
|
||||
package com.timesafari.dailynotification.test;
|
||||
|
||||
import android.app.Application;
|
||||
import android.content.Context;
|
||||
import android.util.Log;
|
||||
import com.timesafari.dailynotification.DailyNotificationPlugin;
|
||||
import com.timesafari.dailynotification.NativeNotificationContentFetcher;
|
||||
@@ -28,9 +29,10 @@ public class TestApplication extends Application {
|
||||
|
||||
Log.i(TAG, "Initializing Daily Notification Plugin test app");
|
||||
|
||||
// Register test native fetcher
|
||||
// Register test native fetcher with application context
|
||||
Context context = getApplicationContext();
|
||||
NativeNotificationContentFetcher testFetcher =
|
||||
new com.timesafari.dailynotification.test.TestNativeFetcher();
|
||||
new com.timesafari.dailynotification.test.TestNativeFetcher(context);
|
||||
DailyNotificationPlugin.setNativeFetcher(testFetcher);
|
||||
|
||||
Log.i(TAG, "Test native fetcher registered: " + testFetcher.getClass().getName());
|
||||
|
||||
@@ -10,6 +10,8 @@
|
||||
|
||||
package com.timesafari.dailynotification.test;
|
||||
|
||||
import android.content.Context;
|
||||
import android.content.SharedPreferences;
|
||||
import android.util.Log;
|
||||
import androidx.annotation.NonNull;
|
||||
import com.timesafari.dailynotification.FetchContext;
|
||||
@@ -25,9 +27,7 @@ import java.io.OutputStream;
|
||||
import java.net.HttpURLConnection;
|
||||
import java.net.URL;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.security.MessageDigest;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Base64;
|
||||
import java.util.Collections;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
@@ -43,6 +43,59 @@ import java.util.concurrent.CompletableFuture;
|
||||
public class TestNativeFetcher implements NativeNotificationContentFetcher {
|
||||
|
||||
private static final String TAG = "TestNativeFetcher";
|
||||
private static final String ENDORSER_ENDPOINT = "/api/v2/report/plansLastUpdatedBetween";
|
||||
private static final int CONNECT_TIMEOUT_MS = 10000; // 10 seconds
|
||||
private static final int READ_TIMEOUT_MS = 15000; // 15 seconds
|
||||
private static final int MAX_RETRIES = 3; // Maximum number of retry attempts
|
||||
private static final int RETRY_DELAY_MS = 1000; // Base delay for exponential backoff
|
||||
|
||||
// SharedPreferences constants
|
||||
private static final String PREFS_NAME = "DailyNotificationPrefs";
|
||||
private static final String KEY_STARRED_PLAN_IDS = "starred_plan_ids";
|
||||
private static final String KEY_LAST_ACKED_JWT_ID = "last_acked_jwt_id";
|
||||
|
||||
private final Gson gson = new Gson();
|
||||
private final Context appContext;
|
||||
private SharedPreferences prefs;
|
||||
|
||||
// Volatile fields for configuration, set via configure() method
|
||||
private volatile String apiBaseUrl;
|
||||
private volatile String activeDid;
|
||||
private volatile String jwtToken; // Pre-generated JWT token from TypeScript (ES256K signed)
|
||||
|
||||
/**
|
||||
* Constructor
|
||||
*
|
||||
* @param context Application context for SharedPreferences access
|
||||
*/
|
||||
public TestNativeFetcher(Context context) {
|
||||
this.appContext = context.getApplicationContext();
|
||||
this.prefs = appContext.getSharedPreferences(PREFS_NAME, Context.MODE_PRIVATE);
|
||||
Log.d(TAG, "TestNativeFetcher: Initialized with context");
|
||||
}
|
||||
|
||||
/**
|
||||
* Configure the native fetcher with API credentials
|
||||
*
|
||||
* Called by the plugin when configureNativeFetcher() is invoked from TypeScript.
|
||||
* This method stores the configuration for use in background fetches.
|
||||
*
|
||||
* <p><b>Architecture Note:</b> The JWT token is pre-generated in TypeScript using
|
||||
* TimeSafari's {@code createEndorserJwtForKey()} function (ES256K DID-based signing).
|
||||
* The native fetcher just uses the token directly - no JWT generation needed.</p>
|
||||
*
|
||||
* @param apiBaseUrl Base URL for API server (e.g., "http://10.0.2.2:3000")
|
||||
* @param activeDid Active DID for authentication (e.g., "did:ethr:0x...")
|
||||
* @param jwtToken Pre-generated JWT token (ES256K signed) from TypeScript
|
||||
*/
|
||||
@Override
|
||||
public void configure(String apiBaseUrl, String activeDid, String jwtToken) {
|
||||
this.apiBaseUrl = apiBaseUrl;
|
||||
this.activeDid = activeDid;
|
||||
this.jwtToken = jwtToken;
|
||||
Log.i(TAG, "TestNativeFetcher: Configured with API: " + apiBaseUrl +
|
||||
", ActiveDID: " + (activeDid != null ? activeDid.substring(0, Math.min(20, activeDid.length())) + "..." : "null"));
|
||||
}
|
||||
|
||||
@Override
|
||||
@NonNull
|
||||
@@ -52,19 +105,30 @@ public class TestNativeFetcher implements NativeNotificationContentFetcher {
|
||||
Log.d(TAG, "TestNativeFetcher: Fetch triggered - trigger=" + context.trigger +
|
||||
", scheduledTime=" + context.scheduledTime + ", fetchTime=" + context.fetchTime);
|
||||
|
||||
// Start with retry count 0
|
||||
return fetchContentWithRetry(context, 0);
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetch content with retry logic for transient errors
|
||||
*
|
||||
* @param context Fetch context
|
||||
* @param retryCount Current retry attempt (0 for first attempt)
|
||||
* @return Future with notification contents or empty list on failure
|
||||
*/
|
||||
private CompletableFuture<List<NotificationContent>> fetchContentWithRetry(
|
||||
@NonNull FetchContext context, int retryCount) {
|
||||
|
||||
return CompletableFuture.supplyAsync(() -> {
|
||||
try {
|
||||
// Check if configured
|
||||
if (apiBaseUrl == null || activeDid == null || jwtSecret == null) {
|
||||
if (apiBaseUrl == null || activeDid == null || jwtToken == null) {
|
||||
Log.e(TAG, "TestNativeFetcher: Not configured. Call configureNativeFetcher() from TypeScript first.");
|
||||
return Collections.emptyList();
|
||||
}
|
||||
|
||||
Log.i(TAG, "TestNativeFetcher: Starting fetch from " + apiBaseUrl + ENDORSER_ENDPOINT);
|
||||
|
||||
// Generate JWT token for authentication
|
||||
String jwtToken = generateJWTToken();
|
||||
|
||||
// Build request URL
|
||||
String urlString = apiBaseUrl + ENDORSER_ENDPOINT;
|
||||
URL url = new URL(urlString);
|
||||
@@ -113,6 +177,12 @@ public class TestNativeFetcher implements NativeNotificationContentFetcher {
|
||||
// Parse response and convert to NotificationContent
|
||||
List<NotificationContent> contents = parseApiResponse(responseBody, context);
|
||||
|
||||
// Update last acknowledged JWT ID from the response (for pagination)
|
||||
if (!contents.isEmpty()) {
|
||||
// Get the last JWT ID from the parsed response (stored during parsing)
|
||||
updateLastAckedJwtIdFromResponse(contents, responseBody);
|
||||
}
|
||||
|
||||
Log.i(TAG, "TestNativeFetcher: Successfully fetched " + contents.size() +
|
||||
" notification(s)");
|
||||
|
||||
@@ -120,100 +190,227 @@ public class TestNativeFetcher implements NativeNotificationContentFetcher {
|
||||
|
||||
} else {
|
||||
// Read error response
|
||||
BufferedReader reader = new BufferedReader(
|
||||
new InputStreamReader(connection.getErrorStream(), StandardCharsets.UTF_8));
|
||||
StringBuilder errorResponse = new StringBuilder();
|
||||
String line;
|
||||
while ((line = reader.readLine()) != null) {
|
||||
errorResponse.append(line);
|
||||
String errorMessage = "Unknown error";
|
||||
try {
|
||||
BufferedReader reader = new BufferedReader(
|
||||
new InputStreamReader(connection.getErrorStream(), StandardCharsets.UTF_8));
|
||||
StringBuilder errorResponse = new StringBuilder();
|
||||
String line;
|
||||
while ((line = reader.readLine()) != null) {
|
||||
errorResponse.append(line);
|
||||
}
|
||||
reader.close();
|
||||
errorMessage = errorResponse.toString();
|
||||
} catch (Exception e) {
|
||||
Log.w(TAG, "TestNativeFetcher: Could not read error stream", e);
|
||||
}
|
||||
reader.close();
|
||||
|
||||
Log.e(TAG, "TestNativeFetcher: API error " + responseCode +
|
||||
": " + errorResponse.toString());
|
||||
Log.e(TAG, "TestNativeFetcher: API error " + responseCode + ": " + errorMessage);
|
||||
|
||||
// Handle retryable errors (5xx server errors, network timeouts)
|
||||
if (shouldRetry(responseCode, retryCount)) {
|
||||
long delayMs = RETRY_DELAY_MS * (1 << retryCount); // Exponential backoff
|
||||
Log.w(TAG, "TestNativeFetcher: Retryable error, retrying in " + delayMs + "ms " +
|
||||
"(" + (retryCount + 1) + "/" + MAX_RETRIES + ")");
|
||||
|
||||
try {
|
||||
Thread.sleep(delayMs);
|
||||
} catch (InterruptedException e) {
|
||||
Thread.currentThread().interrupt();
|
||||
Log.e(TAG, "TestNativeFetcher: Retry delay interrupted", e);
|
||||
return Collections.emptyList();
|
||||
}
|
||||
|
||||
// Recursive retry
|
||||
return fetchContentWithRetry(context, retryCount + 1).join();
|
||||
}
|
||||
|
||||
// Non-retryable errors (4xx client errors, max retries reached)
|
||||
if (responseCode >= 400 && responseCode < 500) {
|
||||
Log.e(TAG, "TestNativeFetcher: Non-retryable client error " + responseCode);
|
||||
} else if (retryCount >= MAX_RETRIES) {
|
||||
Log.e(TAG, "TestNativeFetcher: Max retries (" + MAX_RETRIES + ") reached");
|
||||
}
|
||||
|
||||
// Return empty list on error (fallback will be handled by worker)
|
||||
return Collections.emptyList();
|
||||
}
|
||||
|
||||
} catch (java.net.SocketTimeoutException | java.net.UnknownHostException e) {
|
||||
// Network errors are retryable
|
||||
Log.w(TAG, "TestNativeFetcher: Network error during fetch", e);
|
||||
|
||||
if (shouldRetry(0, retryCount)) { // Use 0 as response code for network errors
|
||||
long delayMs = RETRY_DELAY_MS * (1 << retryCount);
|
||||
Log.w(TAG, "TestNativeFetcher: Retrying after network error in " + delayMs + "ms " +
|
||||
"(" + (retryCount + 1) + "/" + MAX_RETRIES + ")");
|
||||
|
||||
try {
|
||||
Thread.sleep(delayMs);
|
||||
} catch (InterruptedException ie) {
|
||||
Thread.currentThread().interrupt();
|
||||
Log.e(TAG, "TestNativeFetcher: Retry delay interrupted", ie);
|
||||
return Collections.emptyList();
|
||||
}
|
||||
|
||||
return fetchContentWithRetry(context, retryCount + 1).join();
|
||||
}
|
||||
|
||||
Log.e(TAG, "TestNativeFetcher: Max retries reached for network error");
|
||||
return Collections.emptyList();
|
||||
|
||||
} catch (Exception e) {
|
||||
Log.e(TAG, "TestNativeFetcher: Error during fetch", e);
|
||||
// Return empty list on error (fallback will be handled by worker)
|
||||
// Non-retryable errors (parsing, configuration, etc.)
|
||||
return Collections.emptyList();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate JWT token for API authentication
|
||||
* Simplified implementation matching test-user-zero config
|
||||
* Determine if an error should be retried
|
||||
*
|
||||
* @param responseCode HTTP response code (0 for network errors)
|
||||
* @param retryCount Current retry attempt count
|
||||
* @return true if error is retryable and retry count not exceeded
|
||||
*/
|
||||
private String generateJWTToken() {
|
||||
private boolean shouldRetry(int responseCode, int retryCount) {
|
||||
if (retryCount >= MAX_RETRIES) {
|
||||
return false; // Max retries exceeded
|
||||
}
|
||||
|
||||
// Retry on network errors (responseCode 0) or server errors (5xx)
|
||||
// Don't retry on client errors (4xx) as they indicate permanent issues
|
||||
if (responseCode == 0) {
|
||||
return true; // Network error (timeout, unknown host, etc.)
|
||||
}
|
||||
|
||||
if (responseCode >= 500 && responseCode < 600) {
|
||||
return true; // Server error (retryable)
|
||||
}
|
||||
|
||||
// Some 4xx errors might be retryable (e.g., 429 Too Many Requests)
|
||||
if (responseCode == 429) {
|
||||
return true; // Rate limit - retry with backoff
|
||||
}
|
||||
|
||||
return false; // Other client errors (401, 403, 404, etc.) are not retryable
|
||||
}
|
||||
|
||||
/**
|
||||
* Get starred plan IDs from SharedPreferences
|
||||
*
|
||||
* @return List of starred plan IDs, empty list if none stored
|
||||
*/
|
||||
private List<String> getStarredPlanIds() {
|
||||
try {
|
||||
long nowEpoch = System.currentTimeMillis() / 1000;
|
||||
long expEpoch = nowEpoch + (JWT_EXPIRATION_MINUTES * 60);
|
||||
String idsJson = prefs.getString(KEY_STARRED_PLAN_IDS, "[]");
|
||||
if (idsJson == null || idsJson.isEmpty() || idsJson.equals("[]")) {
|
||||
return new ArrayList<>();
|
||||
}
|
||||
|
||||
// Create JWT header
|
||||
Map<String, Object> header = new HashMap<>();
|
||||
header.put("alg", "HS256");
|
||||
header.put("typ", "JWT");
|
||||
// Parse JSON array
|
||||
JsonParser parser = new JsonParser();
|
||||
JsonArray jsonArray = parser.parse(idsJson).getAsJsonArray();
|
||||
List<String> planIds = new ArrayList<>();
|
||||
|
||||
// Create JWT payload
|
||||
Map<String, Object> payload = new HashMap<>();
|
||||
payload.put("exp", expEpoch);
|
||||
payload.put("iat", nowEpoch);
|
||||
payload.put("iss", activeDid);
|
||||
payload.put("aud", "timesafari.notifications");
|
||||
payload.put("sub", activeDid);
|
||||
for (int i = 0; i < jsonArray.size(); i++) {
|
||||
planIds.add(jsonArray.get(i).getAsString());
|
||||
}
|
||||
|
||||
// Encode header and payload
|
||||
String headerJson = gson.toJson(header);
|
||||
String payloadJson = gson.toJson(payload);
|
||||
|
||||
String headerB64 = base64UrlEncode(headerJson.getBytes(StandardCharsets.UTF_8));
|
||||
String payloadB64 = base64UrlEncode(payloadJson.getBytes(StandardCharsets.UTF_8));
|
||||
|
||||
// Create signature
|
||||
String unsignedToken = headerB64 + "." + payloadB64;
|
||||
MessageDigest digest = MessageDigest.getInstance("SHA-256");
|
||||
byte[] hash = digest.digest((unsignedToken + ":" + activeDid).getBytes(StandardCharsets.UTF_8));
|
||||
String signature = base64UrlEncode(hash);
|
||||
|
||||
String jwt = unsignedToken + "." + signature;
|
||||
Log.d(TAG, "TestNativeFetcher: Generated JWT token");
|
||||
|
||||
return jwt;
|
||||
Log.d(TAG, "TestNativeFetcher: Loaded " + planIds.size() + " starred plan IDs");
|
||||
return planIds;
|
||||
|
||||
} catch (Exception e) {
|
||||
Log.e(TAG, "TestNativeFetcher: Error generating JWT", e);
|
||||
throw new RuntimeException("Failed to generate JWT", e);
|
||||
Log.e(TAG, "TestNativeFetcher: Error loading starred plan IDs", e);
|
||||
return new ArrayList<>();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Base64 URL encoding (without padding)
|
||||
*/
|
||||
private String base64UrlEncode(byte[] data) {
|
||||
String encoded = Base64.getEncoder().encodeToString(data);
|
||||
return encoded.replace("+", "-").replace("/", "_").replace("=", "");
|
||||
}
|
||||
|
||||
/**
|
||||
* Get starred plan IDs (from test-user-zero config or SharedPreferences)
|
||||
*/
|
||||
private List<String> getStarredPlanIds() {
|
||||
// TODO: Load from SharedPreferences or config
|
||||
// For now, return empty list (API will return all relevant plans)
|
||||
return new ArrayList<>();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get last acknowledged JWT ID (for pagination)
|
||||
* Get last acknowledged JWT ID from SharedPreferences (for pagination)
|
||||
*
|
||||
* @return Last acknowledged JWT ID, or null if none stored
|
||||
*/
|
||||
private String getLastAcknowledgedJwtId() {
|
||||
// TODO: Load from SharedPreferences
|
||||
// For now, return null (fetch all updates)
|
||||
return null;
|
||||
try {
|
||||
String jwtId = prefs.getString(KEY_LAST_ACKED_JWT_ID, null);
|
||||
if (jwtId != null) {
|
||||
Log.d(TAG, "TestNativeFetcher: Loaded last acknowledged JWT ID");
|
||||
}
|
||||
return jwtId;
|
||||
} catch (Exception e) {
|
||||
Log.e(TAG, "TestNativeFetcher: Error loading last acknowledged JWT ID", e);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Update starred plan IDs in SharedPreferences
|
||||
*
|
||||
* @param planIds List of plan IDs to store
|
||||
*/
|
||||
public void updateStarredPlanIds(List<String> planIds) {
|
||||
try {
|
||||
String idsJson = gson.toJson(planIds);
|
||||
prefs.edit().putString(KEY_STARRED_PLAN_IDS, idsJson).apply();
|
||||
Log.i(TAG, "TestNativeFetcher: Updated starred plan IDs: " + planIds.size() + " plans");
|
||||
} catch (Exception e) {
|
||||
Log.e(TAG, "TestNativeFetcher: Error updating starred plan IDs", e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Update last acknowledged JWT ID in SharedPreferences
|
||||
*
|
||||
* @param jwtId JWT ID to store as last acknowledged
|
||||
*/
|
||||
public void updateLastAckedJwtId(String jwtId) {
|
||||
try {
|
||||
prefs.edit().putString(KEY_LAST_ACKED_JWT_ID, jwtId).apply();
|
||||
Log.d(TAG, "TestNativeFetcher: Updated last acknowledged JWT ID");
|
||||
} catch (Exception e) {
|
||||
Log.e(TAG, "TestNativeFetcher: Error updating last acknowledged JWT ID", e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Update last acknowledged JWT ID from the API response
|
||||
* Uses the last JWT ID from the data array for pagination
|
||||
*
|
||||
* @param contents Parsed notification contents (may contain JWT IDs)
|
||||
* @param responseBody Original response body for parsing
|
||||
*/
|
||||
private void updateLastAckedJwtIdFromResponse(List<NotificationContent> contents, String responseBody) {
|
||||
try {
|
||||
JsonParser parser = new JsonParser();
|
||||
JsonObject root = parser.parse(responseBody).getAsJsonObject();
|
||||
JsonArray dataArray = root.getAsJsonArray("data");
|
||||
|
||||
if (dataArray != null && dataArray.size() > 0) {
|
||||
// Get the last item's JWT ID (most recent)
|
||||
JsonObject lastItem = dataArray.get(dataArray.size() - 1).getAsJsonObject();
|
||||
|
||||
// Try to get JWT ID from different possible locations in response structure
|
||||
String jwtId = null;
|
||||
if (lastItem.has("jwtId")) {
|
||||
jwtId = lastItem.get("jwtId").getAsString();
|
||||
} else if (lastItem.has("plan")) {
|
||||
JsonObject plan = lastItem.getAsJsonObject("plan");
|
||||
if (plan.has("jwtId")) {
|
||||
jwtId = plan.get("jwtId").getAsString();
|
||||
}
|
||||
}
|
||||
|
||||
if (jwtId != null && !jwtId.isEmpty()) {
|
||||
updateLastAckedJwtId(jwtId);
|
||||
Log.d(TAG, "TestNativeFetcher: Updated last acknowledged JWT ID: " +
|
||||
jwtId.substring(0, Math.min(20, jwtId.length())) + "...");
|
||||
}
|
||||
}
|
||||
} catch (Exception e) {
|
||||
Log.w(TAG, "TestNativeFetcher: Could not extract JWT ID from response for pagination", e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -235,8 +432,27 @@ public class TestNativeFetcher implements NativeNotificationContentFetcher {
|
||||
NotificationContent content = new NotificationContent();
|
||||
|
||||
// Extract data from API response
|
||||
String planId = item.has("planId") ? item.get("planId").getAsString() : null;
|
||||
String jwtId = item.has("jwtId") ? item.get("jwtId").getAsString() : null;
|
||||
// Support both flat structure (jwtId, planId) and nested (plan.jwtId, plan.handleId)
|
||||
String planId = null;
|
||||
String jwtId = null;
|
||||
|
||||
if (item.has("planId")) {
|
||||
planId = item.get("planId").getAsString();
|
||||
} else if (item.has("plan")) {
|
||||
JsonObject plan = item.getAsJsonObject("plan");
|
||||
if (plan.has("handleId")) {
|
||||
planId = plan.get("handleId").getAsString();
|
||||
}
|
||||
}
|
||||
|
||||
if (item.has("jwtId")) {
|
||||
jwtId = item.get("jwtId").getAsString();
|
||||
} else if (item.has("plan")) {
|
||||
JsonObject plan = item.getAsJsonObject("plan");
|
||||
if (plan.has("jwtId")) {
|
||||
jwtId = plan.get("jwtId").getAsString();
|
||||
}
|
||||
}
|
||||
|
||||
// Create notification ID
|
||||
String notificationId = "endorser_" + (jwtId != null ? jwtId :
|
||||
|
||||
@@ -0,0 +1,33 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!--
|
||||
Network Security Configuration for Daily Notification Test App
|
||||
|
||||
Allows cleartext HTTP traffic to localhost (10.0.2.2) for Android emulator testing.
|
||||
This is required for connecting to http://10.0.2.2:3000 from the Android emulator
|
||||
(which maps to host machine's localhost:3000).
|
||||
|
||||
Author: Matthew Raymer
|
||||
Date: 2025-10-31
|
||||
-->
|
||||
<network-security-config>
|
||||
<!--
|
||||
Allow cleartext traffic to Android emulator's localhost alias (10.0.2.2).
|
||||
This is safe because:
|
||||
1. Only accessible from Android emulator
|
||||
2. Only used for development/testing
|
||||
3. Does not affect production builds (production should use HTTPS)
|
||||
-->
|
||||
<domain-config cleartextTrafficPermitted="true">
|
||||
<!-- Android emulator's special IP for host machine's localhost -->
|
||||
<domain includeSubdomains="false">10.0.2.2</domain>
|
||||
<!-- Also allow direct localhost access (for some emulator configurations) -->
|
||||
<domain includeSubdomains="false">localhost</domain>
|
||||
<domain includeSubdomains="false">127.0.0.1</domain>
|
||||
</domain-config>
|
||||
|
||||
<!--
|
||||
Production domains should use HTTPS only (default behavior).
|
||||
This configuration only adds exceptions for localhost development.
|
||||
-->
|
||||
</network-security-config>
|
||||
|
||||
@@ -3,4 +3,6 @@ include ':capacitor-android'
|
||||
project(':capacitor-android').projectDir = new File('../node_modules/@capacitor/android/capacitor')
|
||||
|
||||
include ':timesafari-daily-notification-plugin'
|
||||
// NOTE: Plugin module is in android/plugin/ subdirectory, not android root
|
||||
// This file is auto-generated by Capacitor, but must be manually corrected for this plugin structure
|
||||
project(':timesafari-daily-notification-plugin').projectDir = new File('../node_modules/@timesafari/daily-notification-plugin/android/plugin')
|
||||
|
||||
@@ -9,26 +9,26 @@
|
||||
# 1. Build web assets
|
||||
npm run build
|
||||
|
||||
# 2. Sync with native projects
|
||||
npx cap sync android
|
||||
# 2. Sync with native projects (automatically fixes plugin paths)
|
||||
npm run cap:sync
|
||||
|
||||
# 3. 🔧 FIX PLUGIN REGISTRY (REQUIRED!)
|
||||
node scripts/fix-capacitor-plugins.js
|
||||
|
||||
# 4. Build and deploy Android
|
||||
# 3. Build and deploy Android
|
||||
cd android
|
||||
./gradlew :app:assembleDebug
|
||||
adb install -r app/build/outputs/apk/debug/app-debug.apk
|
||||
adb shell am start -n com.timesafari.dailynotification.test/.MainActivity
|
||||
```
|
||||
|
||||
## ⚠️ Why Step 3 is Critical
|
||||
## ⚠️ Why `npm run cap:sync` is Important
|
||||
|
||||
**Problem**: `npx cap sync` overwrites `capacitor.plugins.json` and removes custom plugins.
|
||||
**Problem**: `npx cap sync` overwrites `capacitor.plugins.json` and `capacitor.settings.gradle` with incorrect paths.
|
||||
|
||||
**Solution**: The fix script restores the DailyNotification plugin entry.
|
||||
**Solution**: The `cap:sync` script automatically:
|
||||
1. Runs `npx cap sync android`
|
||||
2. Fixes `capacitor.settings.gradle` (corrects plugin path from `android/` to `android/plugin/`)
|
||||
3. Restores the DailyNotification plugin entry in `capacitor.plugins.json`
|
||||
|
||||
**Without Step 3**: Plugin detection fails, "simplified dialog" appears.
|
||||
**Without the fix**: Plugin detection fails, build errors occur, "simplified dialog" appears.
|
||||
|
||||
## 🔍 Verification Checklist
|
||||
|
||||
@@ -52,10 +52,8 @@ echo "🔨 Building web assets..."
|
||||
npm run build
|
||||
|
||||
echo "🔄 Syncing with native projects..."
|
||||
npx cap sync android
|
||||
|
||||
echo "🔧 Fixing plugin registry..."
|
||||
node scripts/fix-capacitor-plugins.js
|
||||
npm run cap:sync
|
||||
# This automatically syncs and fixes plugin paths
|
||||
|
||||
echo "🏗️ Building Android app..."
|
||||
cd android
|
||||
@@ -72,7 +70,7 @@ echo "✅ Build and deploy complete!"
|
||||
|
||||
| Issue | Symptom | Solution |
|
||||
|-------|---------|----------|
|
||||
| Empty plugin registry | `capacitor.plugins.json` is `[]` | Run `node scripts/fix-capacitor-plugins.js` |
|
||||
| Empty plugin registry | `capacitor.plugins.json` is `[]` | Run `npm run cap:sync` or `node scripts/fix-capacitor-plugins.js` |
|
||||
| Plugin not detected | "Plugin: Not Available" (red) | Check plugin registry, rebuild |
|
||||
| Click events not working | Buttons don't respond | Check Vue 3 compatibility, router config |
|
||||
| Inconsistent status | Different status in different cards | Use consistent detection logic |
|
||||
@@ -92,4 +90,4 @@ adb shell pm list packages | grep dailynotification
|
||||
|
||||
---
|
||||
|
||||
**Remember**: Always run the fix script after `npx cap sync`!
|
||||
**Remember**: Use `npm run cap:sync` instead of `npx cap sync android` directly - it automatically fixes the configuration files!
|
||||
|
||||
@@ -150,6 +150,8 @@ node scripts/fix-capacitor-plugins.js
|
||||
|
||||
**Solution**: Always run fix script after sync
|
||||
```bash
|
||||
npm run cap:sync
|
||||
# Or manually:
|
||||
npx cap sync android && node scripts/fix-capacitor-plugins.js
|
||||
```
|
||||
|
||||
@@ -159,9 +161,8 @@ npx cap sync android && node scripts/fix-capacitor-plugins.js
|
||||
|
||||
1. **Make Changes**: Edit Vue components, native code, or plugin logic
|
||||
2. **Build Web**: `npm run build`
|
||||
3. **Sync Native**: `npx cap sync android`
|
||||
4. **Fix Plugins**: `node scripts/fix-capacitor-plugins.js`
|
||||
5. **Build Android**: `cd android && ./gradlew :app:assembleDebug`
|
||||
3. **Sync Native**: `npm run cap:sync` (automatically fixes plugin paths)
|
||||
4. **Build Android**: `cd android && ./gradlew :app:assembleDebug`
|
||||
6. **Deploy**: `adb install -r app/build/outputs/apk/debug/app-debug.apk`
|
||||
7. **Test**: Launch app and verify plugin detection
|
||||
|
||||
|
||||
220
test-apps/daily-notification-test/package-lock.json
generated
220
test-apps/daily-notification-test/package-lock.json
generated
@@ -13,6 +13,8 @@
|
||||
"@capacitor/core": "^6.2.1",
|
||||
"@timesafari/daily-notification-plugin": "file:../../",
|
||||
"date-fns": "^4.1.0",
|
||||
"did-jwt": "^7.4.7",
|
||||
"ethers": "^6.15.0",
|
||||
"pinia": "^3.0.3",
|
||||
"vue": "^3.5.22",
|
||||
"vue-facing-decorator": "^3.0.4",
|
||||
@@ -77,6 +79,12 @@
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@adraffy/ens-normalize": {
|
||||
"version": "1.10.1",
|
||||
"resolved": "https://registry.npmjs.org/@adraffy/ens-normalize/-/ens-normalize-1.10.1.tgz",
|
||||
"integrity": "sha512-96Z2IP3mYmF1Xg2cDm8f1gWGf/HUVedQ3FMifV4kG/PQ4yEP51xDtRAEfhVNt5f/uzpNkZHwWQuUcu6D6K+Ekw==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@babel/code-frame": {
|
||||
"version": "7.27.1",
|
||||
"resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.27.1.tgz",
|
||||
@@ -1530,6 +1538,48 @@
|
||||
"@jridgewell/sourcemap-codec": "^1.4.14"
|
||||
}
|
||||
},
|
||||
"node_modules/@multiformats/base-x": {
|
||||
"version": "4.0.1",
|
||||
"resolved": "https://registry.npmjs.org/@multiformats/base-x/-/base-x-4.0.1.tgz",
|
||||
"integrity": "sha512-eMk0b9ReBbV23xXU693TAIrLyeO5iTgBZGSJfpqriG8UkYvr/hC9u9pyMlAakDNHWmbhMZCDs6KQO0jzKD8OTw==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@noble/ciphers": {
|
||||
"version": "0.4.1",
|
||||
"resolved": "https://registry.npmjs.org/@noble/ciphers/-/ciphers-0.4.1.tgz",
|
||||
"integrity": "sha512-QCOA9cgf3Rc33owG0AYBB9wszz+Ul2kramWN8tXG44Gyciud/tbkEqvxRF/IpqQaBpRBNi9f4jdNxqB2CQCIXg==",
|
||||
"license": "MIT",
|
||||
"funding": {
|
||||
"url": "https://paulmillr.com/funding/"
|
||||
}
|
||||
},
|
||||
"node_modules/@noble/curves": {
|
||||
"version": "1.9.7",
|
||||
"resolved": "https://registry.npmjs.org/@noble/curves/-/curves-1.9.7.tgz",
|
||||
"integrity": "sha512-gbKGcRUYIjA3/zCCNaWDciTMFI0dCkvou3TL8Zmy5Nc7sJ47a0jtOeZoTaMxkuqRo9cRhjOdZJXegxYE5FN/xw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@noble/hashes": "1.8.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^14.21.3 || >=16"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://paulmillr.com/funding/"
|
||||
}
|
||||
},
|
||||
"node_modules/@noble/hashes": {
|
||||
"version": "1.8.0",
|
||||
"resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.8.0.tgz",
|
||||
"integrity": "sha512-jCs9ldd7NwzpgXDIf6P3+NrHh9/sD6CQdxHyjQI+h/6rDNo88ypBxxz45UDuZHz9r3tNz7N/VInSVoVdtXEI4A==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": "^14.21.3 || >=16"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://paulmillr.com/funding/"
|
||||
}
|
||||
},
|
||||
"node_modules/@nodelib/fs.scandir": {
|
||||
"version": "2.1.5",
|
||||
"resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz",
|
||||
@@ -1890,6 +1940,15 @@
|
||||
"win32"
|
||||
]
|
||||
},
|
||||
"node_modules/@scure/base": {
|
||||
"version": "1.2.6",
|
||||
"resolved": "https://registry.npmjs.org/@scure/base/-/base-1.2.6.tgz",
|
||||
"integrity": "sha512-g/nm5FgUa//MCj1gV09zTJTaM6KBAHqLN907YVQqf7zC49+DcO4B1so4ZX07Ef10Twr6nuqYEH9GEggFXA4Fmg==",
|
||||
"license": "MIT",
|
||||
"funding": {
|
||||
"url": "https://paulmillr.com/funding/"
|
||||
}
|
||||
},
|
||||
"node_modules/@sec-ant/readable-stream": {
|
||||
"version": "0.4.1",
|
||||
"resolved": "https://registry.npmjs.org/@sec-ant/readable-stream/-/readable-stream-0.4.1.tgz",
|
||||
@@ -2620,6 +2679,12 @@
|
||||
"acorn": "^6.0.0 || ^7.0.0 || ^8.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/aes-js": {
|
||||
"version": "4.0.0-beta.5",
|
||||
"resolved": "https://registry.npmjs.org/aes-js/-/aes-js-4.0.0-beta.5.tgz",
|
||||
"integrity": "sha512-G965FqalsNyrPqgEGON7nIx1e/OVENSgiEIzyC63haUMuvNnwIgIjMs52hlTCKhkBny7A2ORNlfY9Zu+jmGk1Q==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/ajv": {
|
||||
"version": "6.12.6",
|
||||
"resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz",
|
||||
@@ -2915,6 +2980,15 @@
|
||||
],
|
||||
"license": "CC-BY-4.0"
|
||||
},
|
||||
"node_modules/canonicalize": {
|
||||
"version": "2.1.0",
|
||||
"resolved": "https://registry.npmjs.org/canonicalize/-/canonicalize-2.1.0.tgz",
|
||||
"integrity": "sha512-F705O3xrsUtgt98j7leetNhTWPe+5S72rlL5O4jA1pKqBVQ/dT1O1D6PFxmSXvc0SUOinWS57DKx0I3CHrXJHQ==",
|
||||
"license": "Apache-2.0",
|
||||
"bin": {
|
||||
"canonicalize": "bin/canonicalize.js"
|
||||
}
|
||||
},
|
||||
"node_modules/chalk": {
|
||||
"version": "4.1.2",
|
||||
"resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz",
|
||||
@@ -3148,6 +3222,29 @@
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/did-jwt": {
|
||||
"version": "7.4.7",
|
||||
"resolved": "https://registry.npmjs.org/did-jwt/-/did-jwt-7.4.7.tgz",
|
||||
"integrity": "sha512-Apz7nIfIHSKWIMaEP5L/K8xkwByvjezjTG0xiqwKdnNj1x8M0+Yasury5Dm/KPltxi2PlGfRPf3IejRKZrT8mQ==",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"@noble/ciphers": "^0.4.0",
|
||||
"@noble/curves": "^1.0.0",
|
||||
"@noble/hashes": "^1.3.0",
|
||||
"@scure/base": "^1.1.3",
|
||||
"canonicalize": "^2.0.0",
|
||||
"did-resolver": "^4.1.0",
|
||||
"multibase": "^4.0.6",
|
||||
"multiformats": "^9.6.2",
|
||||
"uint8arrays": "3.1.1"
|
||||
}
|
||||
},
|
||||
"node_modules/did-resolver": {
|
||||
"version": "4.1.0",
|
||||
"resolved": "https://registry.npmjs.org/did-resolver/-/did-resolver-4.1.0.tgz",
|
||||
"integrity": "sha512-S6fWHvCXkZg2IhS4RcVHxwuyVejPR7c+a4Go0xbQ9ps5kILa8viiYQgrM4gfTyeTjJ0ekgJH9gk/BawTpmkbZA==",
|
||||
"license": "Apache-2.0"
|
||||
},
|
||||
"node_modules/electron-to-chromium": {
|
||||
"version": "1.5.237",
|
||||
"resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.237.tgz",
|
||||
@@ -3508,6 +3605,79 @@
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/ethers": {
|
||||
"version": "6.15.0",
|
||||
"resolved": "https://registry.npmjs.org/ethers/-/ethers-6.15.0.tgz",
|
||||
"integrity": "sha512-Kf/3ZW54L4UT0pZtsY/rf+EkBU7Qi5nnhonjUb8yTXcxH3cdcWrV2cRyk0Xk/4jK6OoHhxxZHriyhje20If2hQ==",
|
||||
"funding": [
|
||||
{
|
||||
"type": "individual",
|
||||
"url": "https://github.com/sponsors/ethers-io/"
|
||||
},
|
||||
{
|
||||
"type": "individual",
|
||||
"url": "https://www.buymeacoffee.com/ricmoo"
|
||||
}
|
||||
],
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@adraffy/ens-normalize": "1.10.1",
|
||||
"@noble/curves": "1.2.0",
|
||||
"@noble/hashes": "1.3.2",
|
||||
"@types/node": "22.7.5",
|
||||
"aes-js": "4.0.0-beta.5",
|
||||
"tslib": "2.7.0",
|
||||
"ws": "8.17.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=14.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/ethers/node_modules/@noble/curves": {
|
||||
"version": "1.2.0",
|
||||
"resolved": "https://registry.npmjs.org/@noble/curves/-/curves-1.2.0.tgz",
|
||||
"integrity": "sha512-oYclrNgRaM9SsBUBVbb8M6DTV7ZHRTKugureoYEncY5c65HOmRzvSiTE3y5CYaPYJA/GVkrhXEoF0M3Ya9PMnw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@noble/hashes": "1.3.2"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://paulmillr.com/funding/"
|
||||
}
|
||||
},
|
||||
"node_modules/ethers/node_modules/@noble/hashes": {
|
||||
"version": "1.3.2",
|
||||
"resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.3.2.tgz",
|
||||
"integrity": "sha512-MVC8EAQp7MvEcm30KWENFjgR+Mkmf+D189XJTkFIlwohU5hcBbn1ZkKq7KVTi2Hme3PMGF390DaL52beVrIihQ==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 16"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://paulmillr.com/funding/"
|
||||
}
|
||||
},
|
||||
"node_modules/ethers/node_modules/@types/node": {
|
||||
"version": "22.7.5",
|
||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-22.7.5.tgz",
|
||||
"integrity": "sha512-jML7s2NAzMWc//QSJ1a3prpk78cOPchGvXJsC3C6R6PSMoooztvRVQEz89gmBTBY1SPMaqo5teB4uNHPdetShQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"undici-types": "~6.19.2"
|
||||
}
|
||||
},
|
||||
"node_modules/ethers/node_modules/tslib": {
|
||||
"version": "2.7.0",
|
||||
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.7.0.tgz",
|
||||
"integrity": "sha512-gLXCKdN1/j47AiHiOkJN69hJmcbGTHI0ImLmbYLHykhgeN0jVGola9yVjFgzCUklsZQMW55o+dW7IXv3RCXDzA==",
|
||||
"license": "0BSD"
|
||||
},
|
||||
"node_modules/ethers/node_modules/undici-types": {
|
||||
"version": "6.19.8",
|
||||
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.19.8.tgz",
|
||||
"integrity": "sha512-ve2KP6f/JnbPBFyobGHuerC9g1FYGn/F8n1LWTwNxCEzd6IfqTwUQcNXgEtmmQ6DlRrC1hrSrBnCZPokRrDHjw==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/execa": {
|
||||
"version": "9.6.0",
|
||||
"resolved": "https://registry.npmjs.org/execa/-/execa-9.6.0.tgz",
|
||||
@@ -4467,6 +4637,26 @@
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/multibase": {
|
||||
"version": "4.0.6",
|
||||
"resolved": "https://registry.npmjs.org/multibase/-/multibase-4.0.6.tgz",
|
||||
"integrity": "sha512-x23pDe5+svdLz/k5JPGCVdfn7Q5mZVMBETiC+ORfO+sor9Sgs0smJzAjfTbM5tckeCqnaUuMYoz+k3RXMmJClQ==",
|
||||
"deprecated": "This module has been superseded by the multiformats module",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@multiformats/base-x": "^4.0.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=12.0.0",
|
||||
"npm": ">=6.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/multiformats": {
|
||||
"version": "9.9.0",
|
||||
"resolved": "https://registry.npmjs.org/multiformats/-/multiformats-9.9.0.tgz",
|
||||
"integrity": "sha512-HoMUjhH9T8DDBNT+6xzkrd9ga/XiBI4xLr58LJACwK6G3HTOPeMz4nB4KJs33L2BelrIJa7P0VuNaVF3hMYfjg==",
|
||||
"license": "(Apache-2.0 AND MIT)"
|
||||
},
|
||||
"node_modules/nanoid": {
|
||||
"version": "3.3.11",
|
||||
"resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz",
|
||||
@@ -5646,6 +5836,15 @@
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/uint8arrays": {
|
||||
"version": "3.1.1",
|
||||
"resolved": "https://registry.npmjs.org/uint8arrays/-/uint8arrays-3.1.1.tgz",
|
||||
"integrity": "sha512-+QJa8QRnbdXVpHYjLoTpJIdCTiw9Ir62nocClWuXIq2JIh4Uta0cQsTSpFL678p2CN8B+XSApwcU+pQEqVpKWg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"multiformats": "^9.4.2"
|
||||
}
|
||||
},
|
||||
"node_modules/undici-types": {
|
||||
"version": "6.21.0",
|
||||
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz",
|
||||
@@ -6269,6 +6468,27 @@
|
||||
"url": "https://github.com/chalk/wrap-ansi?sponsor=1"
|
||||
}
|
||||
},
|
||||
"node_modules/ws": {
|
||||
"version": "8.17.1",
|
||||
"resolved": "https://registry.npmjs.org/ws/-/ws-8.17.1.tgz",
|
||||
"integrity": "sha512-6XQFvXTkbfUOZOKKILFG1PDK2NDQs4azKQl26T0YS5CxqWLgXajbPZ+h4gZekJyRqFU8pvnbAbbs/3TgRPy+GQ==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=10.0.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"bufferutil": "^4.0.1",
|
||||
"utf-8-validate": ">=5.0.2"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"bufferutil": {
|
||||
"optional": true
|
||||
},
|
||||
"utf-8-validate": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/wsl-utils": {
|
||||
"version": "0.1.0",
|
||||
"resolved": "https://registry.npmjs.org/wsl-utils/-/wsl-utils-0.1.0.tgz",
|
||||
|
||||
@@ -12,18 +12,22 @@
|
||||
"preview": "vite preview",
|
||||
"build-only": "vite build",
|
||||
"type-check": "vue-tsc --build",
|
||||
"lint": "eslint . --fix"
|
||||
"lint": "eslint . --fix",
|
||||
"cap:sync": "npx cap sync android && node scripts/fix-capacitor-plugins.js",
|
||||
"postinstall": "node scripts/fix-capacitor-plugins.js"
|
||||
},
|
||||
"dependencies": {
|
||||
"@capacitor/android": "^6.2.1",
|
||||
"@capacitor/cli": "^6.2.1",
|
||||
"@capacitor/core": "^6.2.1",
|
||||
"@timesafari/daily-notification-plugin": "file:../../",
|
||||
"date-fns": "^4.1.0",
|
||||
"did-jwt": "^7.4.7",
|
||||
"ethers": "^6.15.0",
|
||||
"pinia": "^3.0.3",
|
||||
"vue": "^3.5.22",
|
||||
"vue-facing-decorator": "^3.0.4",
|
||||
"vue-router": "^4.5.1",
|
||||
"@timesafari/daily-notification-plugin": "file:../../"
|
||||
"vue-router": "^4.5.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@tsconfig/node22": "^22.0.2",
|
||||
|
||||
@@ -1,9 +1,14 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
/**
|
||||
* Post-sync script to fix capacitor.plugins.json
|
||||
* This ensures the DailyNotification plugin is always registered
|
||||
* even after npx cap sync overwrites the file
|
||||
* Post-sync script to fix Capacitor auto-generated files
|
||||
*
|
||||
* Fixes:
|
||||
* 1. capacitor.plugins.json - Ensures DailyNotification plugin is registered
|
||||
* 2. capacitor.settings.gradle - Corrects plugin path from android/ to android/plugin/
|
||||
*
|
||||
* This script should run automatically after 'npx cap sync android'
|
||||
* to fix issues with Capacitor's auto-generated files.
|
||||
*
|
||||
* @author Matthew Raymer
|
||||
*/
|
||||
@@ -16,12 +21,16 @@ const __filename = fileURLToPath(import.meta.url);
|
||||
const __dirname = path.dirname(__filename);
|
||||
|
||||
const PLUGINS_JSON_PATH = path.join(__dirname, '../android/app/src/main/assets/capacitor.plugins.json');
|
||||
const SETTINGS_GRADLE_PATH = path.join(__dirname, '../android/capacitor.settings.gradle');
|
||||
|
||||
const PLUGIN_ENTRY = {
|
||||
name: "DailyNotification",
|
||||
classpath: "com.timesafari.dailynotification.DailyNotificationPlugin"
|
||||
};
|
||||
|
||||
/**
|
||||
* Fix capacitor.plugins.json to ensure DailyNotification is registered
|
||||
*/
|
||||
function fixCapacitorPlugins() {
|
||||
console.log('🔧 Fixing capacitor.plugins.json...');
|
||||
|
||||
@@ -50,9 +59,66 @@ function fixCapacitorPlugins() {
|
||||
}
|
||||
}
|
||||
|
||||
// Run if called directly
|
||||
if (import.meta.url === `file://${process.argv[1]}`) {
|
||||
fixCapacitorPlugins();
|
||||
/**
|
||||
* Fix capacitor.settings.gradle to point to android/plugin/ instead of android/
|
||||
*/
|
||||
function fixCapacitorSettingsGradle() {
|
||||
console.log('🔧 Fixing capacitor.settings.gradle...');
|
||||
|
||||
if (!fs.existsSync(SETTINGS_GRADLE_PATH)) {
|
||||
console.log('ℹ️ capacitor.settings.gradle not found (may not be a test-app)');
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
let content = fs.readFileSync(SETTINGS_GRADLE_PATH, 'utf8');
|
||||
const originalContent = content;
|
||||
|
||||
// Check if the path already points to android/plugin
|
||||
if (content.includes('android/plugin')) {
|
||||
console.log('✅ capacitor.settings.gradle already has correct path (android/plugin)');
|
||||
return;
|
||||
}
|
||||
|
||||
// Check if we need to fix the path (points to android but should be android/plugin)
|
||||
if (content.includes("project(':timesafari-daily-notification-plugin').projectDir = new File('../node_modules/@timesafari/daily-notification-plugin/android')")) {
|
||||
// Replace the path
|
||||
content = content.replace(
|
||||
"project(':timesafari-daily-notification-plugin').projectDir = new File('../node_modules/@timesafari/daily-notification-plugin/android')",
|
||||
`// NOTE: Plugin module is in android/plugin/ subdirectory, not android root
|
||||
// This file is auto-generated by Capacitor, but must be manually corrected for this plugin structure
|
||||
project(':timesafari-daily-notification-plugin').projectDir = new File('../node_modules/@timesafari/daily-notification-plugin/android/plugin')`
|
||||
);
|
||||
|
||||
fs.writeFileSync(SETTINGS_GRADLE_PATH, content);
|
||||
console.log('✅ Fixed plugin path in capacitor.settings.gradle (android -> android/plugin)');
|
||||
} else {
|
||||
console.log('ℹ️ capacitor.settings.gradle doesn\'t reference the plugin or uses a different structure');
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
console.error('❌ Error fixing capacitor.settings.gradle:', error.message);
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
export { fixCapacitorPlugins };
|
||||
/**
|
||||
* Run all fixes
|
||||
*/
|
||||
function fixAll() {
|
||||
console.log('🔧 DailyNotification Plugin - Post-Sync Fix Script');
|
||||
console.log('================================================\n');
|
||||
|
||||
fixCapacitorPlugins();
|
||||
fixCapacitorSettingsGradle();
|
||||
|
||||
console.log('\n✅ All fixes applied successfully!');
|
||||
console.log('💡 These fixes will persist until the next "npx cap sync android"');
|
||||
}
|
||||
|
||||
// Run if called directly
|
||||
if (import.meta.url === `file://${process.argv[1]}`) {
|
||||
fixAll();
|
||||
}
|
||||
|
||||
export { fixCapacitorPlugins, fixCapacitorSettingsGradle, fixAll };
|
||||
|
||||
@@ -213,8 +213,61 @@ export const MOCK_STARRED_PROJECTS_RESPONSE = {
|
||||
} as const;
|
||||
|
||||
/**
|
||||
* Generate test JWT token for User Zero
|
||||
* Mimics the crowd-master createEndorserJwtForDid function
|
||||
* Generate ES256K signed JWT token for User Zero using DID-based signing
|
||||
*
|
||||
* This function mimics TimeSafari's createEndorserJwtForKey() function,
|
||||
* using did-jwt library with ES256K algorithm (DID-based signing).
|
||||
*
|
||||
* @returns Promise<string> ES256K signed JWT token
|
||||
*/
|
||||
export async function generateEndorserJWT(): Promise<string> {
|
||||
try {
|
||||
// Dynamic import to avoid loading did-jwt in environments where it's not needed
|
||||
const { createJWT, SimpleSigner } = await import('did-jwt');
|
||||
const { HDNodeWallet, Mnemonic } = await import('ethers');
|
||||
|
||||
// Derive Ethereum private key from seed phrase
|
||||
// Using the same derivation as TimeSafari (m/44'/60'/0'/0/0 - Ethereum standard)
|
||||
const mnemonic = Mnemonic.fromPhrase(TEST_USER_ZERO_CONFIG.identity.seedPhrase);
|
||||
const wallet = HDNodeWallet.fromMnemonic(mnemonic);
|
||||
const privateKeyHex = wallet.privateKey.slice(2); // Remove '0x' prefix
|
||||
|
||||
// Create signer for ES256K (Ethereum secp256k1 curve)
|
||||
const signer = SimpleSigner(privateKeyHex);
|
||||
|
||||
// Create JWT payload with standard claims
|
||||
const nowEpoch = Math.floor(Date.now() / 1000);
|
||||
const expiresIn = TEST_USER_ZERO_CONFIG.api.jwtExpirationMinutes * 60;
|
||||
|
||||
const payload = {
|
||||
// Standard JWT claims
|
||||
iat: nowEpoch,
|
||||
exp: nowEpoch + expiresIn,
|
||||
iss: TEST_USER_ZERO_CONFIG.identity.did,
|
||||
sub: TEST_USER_ZERO_CONFIG.identity.did,
|
||||
// Additional claims that endorser-ch might expect
|
||||
aud: "endorser-ch"
|
||||
};
|
||||
|
||||
// Create ES256K signed JWT (ES256K is the default algorithm for did-jwt)
|
||||
const jwt = await createJWT(payload, {
|
||||
issuer: TEST_USER_ZERO_CONFIG.identity.did,
|
||||
signer: signer,
|
||||
expiresIn: expiresIn
|
||||
});
|
||||
|
||||
return jwt;
|
||||
} catch (error) {
|
||||
console.error('Failed to generate ES256K JWT:', error);
|
||||
throw new Error(`JWT generation failed: ${error instanceof Error ? error.message : String(error)}`);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate test JWT token for User Zero (DEPRECATED - use generateEndorserJWT instead)
|
||||
*
|
||||
* @deprecated This function generates an unsigned test token. Use generateEndorserJWT()
|
||||
* for real ES256K signed tokens that work with the endorser-ch API.
|
||||
*/
|
||||
export function generateTestJWT(): string {
|
||||
const nowEpoch = Math.floor(Date.now() / 1000);
|
||||
@@ -260,7 +313,7 @@ export class TestUserZeroAPI {
|
||||
*/
|
||||
setBaseUrl(url: string): void {
|
||||
this.baseUrl = url;
|
||||
// eslint-disable-next-line no-console
|
||||
|
||||
console.log("🔧 API base URL updated to:", url);
|
||||
}
|
||||
|
||||
@@ -278,7 +331,7 @@ export class TestUserZeroAPI {
|
||||
|
||||
if (useMock) {
|
||||
// Return mock data for offline testing
|
||||
// eslint-disable-next-line no-console
|
||||
|
||||
console.log("🧪 Using mock starred projects response");
|
||||
return MOCK_STARRED_PROJECTS_RESPONSE;
|
||||
}
|
||||
@@ -297,9 +350,9 @@ export class TestUserZeroAPI {
|
||||
afterId: afterId || TEST_USER_ZERO_CONFIG.starredProjects.lastAckedJwtId
|
||||
};
|
||||
|
||||
// eslint-disable-next-line no-console
|
||||
|
||||
console.log("🌐 Making real API call to:", url);
|
||||
// eslint-disable-next-line no-console
|
||||
|
||||
console.log("📦 Request body:", requestBody);
|
||||
|
||||
const response = await fetch(url, {
|
||||
@@ -320,7 +373,7 @@ export class TestUserZeroAPI {
|
||||
*/
|
||||
refreshToken(): void {
|
||||
this.jwt = generateTestJWT();
|
||||
// eslint-disable-next-line no-console
|
||||
|
||||
console.log("🔄 JWT token refreshed");
|
||||
}
|
||||
|
||||
|
||||
@@ -105,7 +105,7 @@ router.beforeEach((to, from, next) => {
|
||||
}
|
||||
|
||||
// Add loading state
|
||||
// eslint-disable-next-line no-console
|
||||
|
||||
console.log(`🔄 Navigating from ${String(from.name) || 'unknown'} to ${String(to.name) || 'unknown'}`)
|
||||
|
||||
next()
|
||||
@@ -113,7 +113,7 @@ router.beforeEach((to, from, next) => {
|
||||
|
||||
router.afterEach((to) => {
|
||||
// Clear any previous errors on successful navigation
|
||||
// eslint-disable-next-line no-console
|
||||
|
||||
console.log(`✅ Navigation completed: ${String(to.name) || 'unknown'}`)
|
||||
})
|
||||
|
||||
|
||||
@@ -115,6 +115,7 @@ import { useRouter } from 'vue-router'
|
||||
import { useAppStore } from '@/stores/app'
|
||||
import ActionCard from '@/components/cards/ActionCard.vue'
|
||||
import StatusCard from '@/components/cards/StatusCard.vue'
|
||||
import { TEST_USER_ZERO_CONFIG, generateEndorserJWT } from '@/config/test-user-zero'
|
||||
|
||||
const router = useRouter()
|
||||
const appStore = useAppStore()
|
||||
@@ -429,9 +430,54 @@ const openConsole = (): void => {
|
||||
alert('📖 Console Logs\n\nOpen your browser\'s Developer Tools (F12) and check the Console tab for detailed diagnostic information.')
|
||||
}
|
||||
|
||||
// Configure native fetcher with test user zero credentials
|
||||
const configureNativeFetcher = async (): Promise<void> => {
|
||||
try {
|
||||
const { Capacitor } = await import('@capacitor/core')
|
||||
const isNative = Capacitor.isNativePlatform()
|
||||
|
||||
if (isNative) {
|
||||
const { DailyNotification } = await import('@timesafari/daily-notification-plugin')
|
||||
|
||||
// Get API server URL based on mode and platform
|
||||
const apiBaseUrl = TEST_USER_ZERO_CONFIG.getApiServerUrl()
|
||||
|
||||
// Only configure if not in mock mode
|
||||
if (TEST_USER_ZERO_CONFIG.api.serverMode !== 'mock' && apiBaseUrl !== 'mock://localhost') {
|
||||
console.log('🔧 Configuring native fetcher with:', {
|
||||
apiBaseUrl,
|
||||
activeDid: TEST_USER_ZERO_CONFIG.identity.did.substring(0, 30) + '...',
|
||||
serverMode: TEST_USER_ZERO_CONFIG.api.serverMode
|
||||
})
|
||||
|
||||
// Generate ES256K JWT token using did-jwt library
|
||||
// This mimics TimeSafari's createEndorserJwtForKey() function
|
||||
// In production TimeSafari app, this would use:
|
||||
// const account = await retrieveFullyDecryptedAccount(activeDid);
|
||||
// const jwtToken = await createEndorserJwtForKey(account, {...});
|
||||
const jwtToken = await generateEndorserJWT()
|
||||
|
||||
await DailyNotification.configureNativeFetcher({
|
||||
apiBaseUrl: apiBaseUrl,
|
||||
activeDid: TEST_USER_ZERO_CONFIG.identity.did,
|
||||
jwtToken: jwtToken // Pre-generated token (ES256K signed in production)
|
||||
})
|
||||
|
||||
console.log('✅ Native fetcher configured successfully')
|
||||
} else {
|
||||
console.log('⏭️ Skipping native fetcher configuration (mock mode enabled)')
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('❌ Failed to configure native fetcher:', error)
|
||||
// Don't block app initialization if configuration fails
|
||||
}
|
||||
}
|
||||
|
||||
// Initialize system status when component mounts
|
||||
onMounted(async () => {
|
||||
console.log('🏠 HomeView mounted - checking initial system status...')
|
||||
await configureNativeFetcher()
|
||||
await checkSystemStatus()
|
||||
})
|
||||
</script>
|
||||
|
||||
@@ -19,7 +19,7 @@
|
||||
</div>
|
||||
<div class="config-item">
|
||||
<label>API Server:</label>
|
||||
<span class="config-value">{{ config.api.server }}</span>
|
||||
<span class="config-value">{{ config.getApiServerUrl() }} (mode: {{ config.api.serverMode }})</span>
|
||||
</div>
|
||||
<div class="config-item">
|
||||
<label>JWT Expiration:</label>
|
||||
|
||||
Reference in New Issue
Block a user