Fix iOS build errors and test app setup
- Fix async/await usage in background fetch handler - Fix Core Data metadata access errors - Replace SQLITE_TRANSIENT with nil for Swift compatibility - Fix PermissionStatus interface and type casts in test app - Add iOS setup documentation to BUILDING.md - Update iOS sync workflow to handle Podfile regeneration Resolves all iOS compilation errors and improves test app setup process.
This commit is contained in:
156
BUILDING.md
156
BUILDING.md
@@ -361,12 +361,16 @@ npm install
|
|||||||
# Build Vue 3 app
|
# Build Vue 3 app
|
||||||
npm run build
|
npm run build
|
||||||
|
|
||||||
# Add Capacitor
|
# Add Capacitor platforms
|
||||||
npm install @capacitor/android
|
npm install @capacitor/android @capacitor/ios
|
||||||
|
|
||||||
# Sync with Capacitor
|
# Sync with Capacitor
|
||||||
npx cap sync android
|
npx cap sync android
|
||||||
|
|
||||||
|
# For iOS: Use the npm script (handles Podfile fixes automatically)
|
||||||
|
npm run cap:sync:ios
|
||||||
|
# This runs: cap copy ios + fix Podfile + pod install
|
||||||
|
|
||||||
# Run on Android device/emulator
|
# Run on Android device/emulator
|
||||||
npx cap run android
|
npx cap run android
|
||||||
|
|
||||||
@@ -374,6 +378,149 @@ npx cap run android
|
|||||||
npx cap run ios
|
npx cap run ios
|
||||||
```
|
```
|
||||||
|
|
||||||
|
**iOS Setup (Vue 3 Test App)**
|
||||||
|
|
||||||
|
The iOS setup requires additional steps to configure the plugin correctly:
|
||||||
|
|
||||||
|
**1. Install Dependencies**
|
||||||
|
```bash
|
||||||
|
cd test-apps/daily-notification-test
|
||||||
|
npm install
|
||||||
|
```
|
||||||
|
|
||||||
|
**2. Build Vue App**
|
||||||
|
```bash
|
||||||
|
npm run build
|
||||||
|
```
|
||||||
|
|
||||||
|
**3. Add iOS Platform (if not already added)**
|
||||||
|
```bash
|
||||||
|
npx cap add ios
|
||||||
|
```
|
||||||
|
|
||||||
|
**4. Fix Podfile Configuration**
|
||||||
|
|
||||||
|
**Critical**: Capacitor's `npx cap sync ios` regenerates the Podfile with incorrect plugin references (`TimesafariDailyNotificationPlugin` instead of `DailyNotificationPlugin`).
|
||||||
|
|
||||||
|
**Solution**: Use the npm script `npm run cap:sync:ios` which:
|
||||||
|
1. Copies assets without running pod install (`npx cap copy ios`)
|
||||||
|
2. Automatically fixes the Podfile
|
||||||
|
3. Then runs `pod install` with the corrected Podfile
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Use the npm script (recommended)
|
||||||
|
npm run cap:sync:ios
|
||||||
|
|
||||||
|
# Or manually fix after copy
|
||||||
|
npx cap copy ios
|
||||||
|
node scripts/fix-capacitor-plugins.js
|
||||||
|
cd ios/App && pod install && cd ../..
|
||||||
|
```
|
||||||
|
|
||||||
|
The fix script will:
|
||||||
|
- Change `TimesafariDailyNotificationPlugin` → `DailyNotificationPlugin`
|
||||||
|
- Fix the path from `'../../../..'` → `'../../node_modules/@timesafari/daily-notification-plugin/ios'`
|
||||||
|
|
||||||
|
**5. Install CocoaPods Dependencies**
|
||||||
|
|
||||||
|
After the Podfile is fixed, install the iOS dependencies:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd ios/App
|
||||||
|
pod install
|
||||||
|
cd ../..
|
||||||
|
```
|
||||||
|
|
||||||
|
**Expected Podfile Configuration:**
|
||||||
|
|
||||||
|
The Podfile should reference the plugin like this:
|
||||||
|
|
||||||
|
```ruby
|
||||||
|
def capacitor_pods
|
||||||
|
pod 'Capacitor', :path => '../../node_modules/@capacitor/ios'
|
||||||
|
pod 'CapacitorCordova', :path => '../../node_modules/@capacitor/ios'
|
||||||
|
pod 'DailyNotificationPlugin', :path => '../../node_modules/@timesafari/daily-notification-plugin/ios'
|
||||||
|
end
|
||||||
|
```
|
||||||
|
|
||||||
|
**Important Notes:**
|
||||||
|
- The pod name must be `DailyNotificationPlugin` (not `TimesafariDailyNotificationPlugin`)
|
||||||
|
- The path must point to `../../node_modules/@timesafari/daily-notification-plugin/ios`
|
||||||
|
- The plugin must be installed in `node_modules` via `npm install` (it's installed as a local file dependency)
|
||||||
|
|
||||||
|
**6. Sync and Build**
|
||||||
|
|
||||||
|
**Important**: `npx cap sync ios` tries to run `pod install` automatically, but it will fail because the Podfile has incorrect plugin references. Use the npm script instead:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Option 1: Use the npm script (recommended - handles everything)
|
||||||
|
npm run cap:sync:ios
|
||||||
|
|
||||||
|
# This script:
|
||||||
|
# 1. Copies web assets (npx cap copy ios)
|
||||||
|
# 2. Fixes the Podfile (node scripts/fix-capacitor-plugins.js)
|
||||||
|
# 3. Installs pods (cd ios/App && pod install)
|
||||||
|
|
||||||
|
# Option 2: Manual steps (if you need more control)
|
||||||
|
npx cap copy ios # Copy assets without pod install
|
||||||
|
node scripts/fix-capacitor-plugins.js # Fix Podfile
|
||||||
|
cd ios/App && pod install && cd ../.. # Install pods
|
||||||
|
|
||||||
|
# Open in Xcode
|
||||||
|
npx cap open ios
|
||||||
|
```
|
||||||
|
|
||||||
|
**Why this approach?**
|
||||||
|
- `npx cap sync ios` regenerates the Podfile with wrong references, then tries to run `pod install` which fails
|
||||||
|
- `npx cap copy ios` only copies files, allowing us to fix the Podfile before `pod install`
|
||||||
|
- The npm script automates the entire workflow correctly
|
||||||
|
|
||||||
|
**Troubleshooting iOS Setup:**
|
||||||
|
|
||||||
|
**Error: `[!] No podspec found for 'TimesafariDailyNotificationPlugin'`**
|
||||||
|
|
||||||
|
This means the Podfile has the wrong pod name or path. Solutions:
|
||||||
|
|
||||||
|
1. **Run the fix script:**
|
||||||
|
```bash
|
||||||
|
node scripts/fix-capacitor-plugins.js
|
||||||
|
```
|
||||||
|
|
||||||
|
2. **Manually fix the Podfile:**
|
||||||
|
- Open `ios/App/Podfile`
|
||||||
|
- Change `TimesafariDailyNotificationPlugin` to `DailyNotificationPlugin`
|
||||||
|
- Change path from `'../../../..'` to `'../../node_modules/@timesafari/daily-notification-plugin/ios'`
|
||||||
|
|
||||||
|
3. **Verify plugin is installed:**
|
||||||
|
```bash
|
||||||
|
ls -la node_modules/@timesafari/daily-notification-plugin/ios/DailyNotificationPlugin.podspec
|
||||||
|
```
|
||||||
|
|
||||||
|
4. **Reinstall dependencies if needed:**
|
||||||
|
```bash
|
||||||
|
rm -rf node_modules package-lock.json
|
||||||
|
npm install
|
||||||
|
```
|
||||||
|
|
||||||
|
**Error: `pod install` fails**
|
||||||
|
|
||||||
|
1. **Update CocoaPods:**
|
||||||
|
```bash
|
||||||
|
sudo gem install cocoapods
|
||||||
|
```
|
||||||
|
|
||||||
|
2. **Clean CocoaPods cache:**
|
||||||
|
```bash
|
||||||
|
cd ios/App
|
||||||
|
rm -rf Pods Podfile.lock
|
||||||
|
pod install --repo-update
|
||||||
|
```
|
||||||
|
|
||||||
|
3. **Verify Xcode Command Line Tools:**
|
||||||
|
```bash
|
||||||
|
xcode-select --install
|
||||||
|
```
|
||||||
|
|
||||||
**Test App Features:**
|
**Test App Features:**
|
||||||
|
|
||||||
- Interactive plugin testing interface
|
- Interactive plugin testing interface
|
||||||
@@ -390,8 +537,13 @@ test-apps/daily-notification-test/
|
|||||||
│ ├── components/ # Reusable UI components
|
│ ├── components/ # Reusable UI components
|
||||||
│ └── stores/ # Pinia state management
|
│ └── stores/ # Pinia state management
|
||||||
├── android/ # Android Capacitor app
|
├── android/ # Android Capacitor app
|
||||||
|
├── ios/ # iOS Capacitor app
|
||||||
|
│ └── App/
|
||||||
|
│ ├── Podfile # CocoaPods dependencies
|
||||||
|
│ └── App.xcworkspace # Xcode workspace
|
||||||
├── docs/ # Test app documentation
|
├── docs/ # Test app documentation
|
||||||
└── scripts/ # Test app build scripts
|
└── scripts/ # Test app build scripts
|
||||||
|
│ └── fix-capacitor-plugins.js # Auto-fixes Podfile
|
||||||
```
|
```
|
||||||
|
|
||||||
#### Android Test Apps
|
#### Android Test Apps
|
||||||
|
|||||||
@@ -263,12 +263,12 @@ class DailyNotificationDatabase {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
sqlite3_bind_text(stmt, 1, (content.id as NSString).utf8String, -1, SQLITE_TRANSIENT)
|
sqlite3_bind_text(stmt, 1, (content.id as NSString).utf8String, -1, nil)
|
||||||
sqlite3_bind_text(stmt, 2, (json as NSString).utf8String, -1, SQLITE_TRANSIENT)
|
sqlite3_bind_text(stmt, 2, (json as NSString).utf8String, -1, nil)
|
||||||
sqlite3_bind_int64(stmt, 3, sqlite3_int64(content.fetchedAt))
|
sqlite3_bind_int64(stmt, 3, sqlite3_int64(content.fetchedAt))
|
||||||
|
|
||||||
if let etag = content.etag {
|
if let etag = content.etag {
|
||||||
sqlite3_bind_text(stmt, 4, (etag as NSString).utf8String, -1, SQLITE_TRANSIENT)
|
sqlite3_bind_text(stmt, 4, (etag as NSString).utf8String, -1, nil)
|
||||||
} else {
|
} else {
|
||||||
sqlite3_bind_null(stmt, 4)
|
sqlite3_bind_null(stmt, 4)
|
||||||
}
|
}
|
||||||
@@ -310,7 +310,7 @@ class DailyNotificationDatabase {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
sqlite3_bind_text(stmt, 1, (id as NSString).utf8String, -1, SQLITE_TRANSIENT)
|
sqlite3_bind_text(stmt, 1, (id as NSString).utf8String, -1, nil)
|
||||||
|
|
||||||
if sqlite3_step(stmt) != SQLITE_DONE {
|
if sqlite3_step(stmt) != SQLITE_DONE {
|
||||||
print("\(Self.TAG): deleteNotificationContent step failed: \(String(cString: sqlite3_errmsg(db)))")
|
print("\(Self.TAG): deleteNotificationContent step failed: \(String(cString: sqlite3_errmsg(db)))")
|
||||||
|
|||||||
@@ -261,17 +261,8 @@ class PersistenceController {
|
|||||||
description?.shouldMigrateStoreAutomatically = true
|
description?.shouldMigrateStoreAutomatically = true
|
||||||
description?.shouldInferMappingModelAutomatically = true
|
description?.shouldInferMappingModelAutomatically = true
|
||||||
|
|
||||||
// Set initial schema version metadata (for new stores)
|
|
||||||
if !inMemory {
|
|
||||||
var metadata = description?.metadata ?? [:]
|
|
||||||
if metadata["schema_version"] == nil {
|
|
||||||
metadata["schema_version"] = PersistenceController.SCHEMA_VERSION
|
|
||||||
description?.metadata = metadata
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
var loadError: Error? = nil
|
var loadError: Error? = nil
|
||||||
tempContainer?.loadPersistentStores { description, error in
|
tempContainer?.loadPersistentStores { storeDescription, error in
|
||||||
if let error = error as NSError? {
|
if let error = error as NSError? {
|
||||||
loadError = error
|
loadError = error
|
||||||
print("DNP-PLUGIN: CoreData store load error: \(error.localizedDescription)")
|
print("DNP-PLUGIN: CoreData store load error: \(error.localizedDescription)")
|
||||||
@@ -281,7 +272,19 @@ class PersistenceController {
|
|||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
print("DNP-PLUGIN: CoreData store loaded successfully")
|
print("DNP-PLUGIN: CoreData store loaded successfully")
|
||||||
print("DNP-PLUGIN: Store URL: \(description.url?.absoluteString ?? "unknown")")
|
print("DNP-PLUGIN: Store URL: \(storeDescription.url?.absoluteString ?? "unknown")")
|
||||||
|
|
||||||
|
// Set initial schema version metadata (for new stores)
|
||||||
|
// Metadata must be set using the coordinator after the store is loaded
|
||||||
|
if !inMemory,
|
||||||
|
let coordinator = tempContainer?.persistentStoreCoordinator,
|
||||||
|
let store = coordinator.persistentStores.first,
|
||||||
|
let metadata = store.metadata,
|
||||||
|
metadata["schema_version"] == nil {
|
||||||
|
var newMetadata = metadata
|
||||||
|
newMetadata["schema_version"] = PersistenceController.SCHEMA_VERSION
|
||||||
|
coordinator.setMetadata(newMetadata, for: store)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -373,7 +376,13 @@ class PersistenceController {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
let currentVersion = store.metadata["schema_version"] as? Int ?? 1
|
// store.metadata is optional, so we need to unwrap it
|
||||||
|
guard let metadata = store.metadata else {
|
||||||
|
print("DNP-PLUGIN: Store metadata is nil, using default schema version")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
let currentVersion = metadata["schema_version"] as? Int ?? 1
|
||||||
let expectedVersion = PersistenceController.SCHEMA_VERSION
|
let expectedVersion = PersistenceController.SCHEMA_VERSION
|
||||||
|
|
||||||
if currentVersion != expectedVersion {
|
if currentVersion != expectedVersion {
|
||||||
@@ -381,9 +390,13 @@ class PersistenceController {
|
|||||||
print("DNP-PLUGIN: CoreData auto-migration will handle schema changes")
|
print("DNP-PLUGIN: CoreData auto-migration will handle schema changes")
|
||||||
|
|
||||||
// Update metadata for future reference (does not trigger migration)
|
// Update metadata for future reference (does not trigger migration)
|
||||||
var metadata = store.metadata
|
// Use the coordinator to set metadata
|
||||||
metadata["schema_version"] = expectedVersion
|
if let coordinator = container?.persistentStoreCoordinator {
|
||||||
// Note: Metadata persists on next store save
|
var newMetadata = metadata
|
||||||
|
newMetadata["schema_version"] = expectedVersion
|
||||||
|
coordinator.setMetadata(newMetadata, for: store)
|
||||||
|
// Note: Metadata persists on next store save
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
print("DNP-PLUGIN: Schema version verified: \(currentVersion)")
|
print("DNP-PLUGIN: Schema version verified: \(currentVersion)")
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -419,58 +419,58 @@ public class DailyNotificationPlugin: CAPPlugin {
|
|||||||
// Phase 3: Check for JWT-signed fetcher configuration
|
// Phase 3: Check for JWT-signed fetcher configuration
|
||||||
// If native fetcher is configured, use it; otherwise fall back to dummy content
|
// If native fetcher is configured, use it; otherwise fall back to dummy content
|
||||||
let nativeFetcherConfig = UserDefaults.standard.string(forKey: "native_fetcher_config")
|
let nativeFetcherConfig = UserDefaults.standard.string(forKey: "native_fetcher_config")
|
||||||
let content: NotificationContent
|
|
||||||
|
|
||||||
if let configJson = nativeFetcherConfig,
|
// Save content to storage via state actor (thread-safe)
|
||||||
let configData = configJson.data(using: .utf8),
|
Task {
|
||||||
let config = try? JSONSerialization.jsonObject(with: configData) as? [String: Any],
|
let content: NotificationContent
|
||||||
let apiBaseUrl = config["apiBaseUrl"] as? String,
|
|
||||||
let activeDid = config["activeDid"] as? String,
|
|
||||||
let jwtToken = config["jwtToken"] as? String {
|
|
||||||
// Phase 3: JWT-signed fetcher is configured - attempt HTTP fetch
|
|
||||||
print("DNP-FETCH: Using JWT-signed fetcher (apiBaseUrl=\(apiBaseUrl), activeDid=\(activeDid.prefix(30))...)")
|
|
||||||
|
|
||||||
// Attempt to fetch content from TimeSafari API
|
if let configJson = nativeFetcherConfig,
|
||||||
// Note: This is a minimal implementation - can be extended with full API client
|
let configData = configJson.data(using: .utf8),
|
||||||
do {
|
let config = try? JSONSerialization.jsonObject(with: configData) as? [String: Any],
|
||||||
let fetchedContent = try await fetchContentFromAPI(
|
let apiBaseUrl = config["apiBaseUrl"] as? String,
|
||||||
apiBaseUrl: apiBaseUrl,
|
let activeDid = config["activeDid"] as? String,
|
||||||
activeDid: activeDid,
|
let jwtToken = config["jwtToken"] as? String {
|
||||||
jwtToken: jwtToken
|
// Phase 3: JWT-signed fetcher is configured - attempt HTTP fetch
|
||||||
)
|
print("DNP-FETCH: Using JWT-signed fetcher (apiBaseUrl=\(apiBaseUrl), activeDid=\(activeDid.prefix(30))...)")
|
||||||
content = fetchedContent
|
|
||||||
print("DNP-FETCH: Successfully fetched content from API")
|
// Attempt to fetch content from TimeSafari API
|
||||||
} catch {
|
// Note: This is a minimal implementation - can be extended with full API client
|
||||||
// Fallback to dummy content on fetch failure
|
do {
|
||||||
print("DNP-FETCH: API fetch failed (\(error.localizedDescription)), using fallback content")
|
let fetchedContent = try await fetchContentFromAPI(
|
||||||
|
apiBaseUrl: apiBaseUrl,
|
||||||
|
activeDid: activeDid,
|
||||||
|
jwtToken: jwtToken
|
||||||
|
)
|
||||||
|
content = fetchedContent
|
||||||
|
print("DNP-FETCH: Successfully fetched content from API")
|
||||||
|
} catch {
|
||||||
|
// Fallback to dummy content on fetch failure
|
||||||
|
print("DNP-FETCH: API fetch failed (\(error.localizedDescription)), using fallback content")
|
||||||
|
content = NotificationContent(
|
||||||
|
id: "fallback_\(Date().timeIntervalSince1970)",
|
||||||
|
title: "Daily Update",
|
||||||
|
body: "Your daily notification is ready",
|
||||||
|
scheduledTime: Int64(Date().timeIntervalSince1970 * 1000) + (5 * 60 * 1000),
|
||||||
|
fetchedAt: Int64(Date().timeIntervalSince1970 * 1000),
|
||||||
|
url: nil,
|
||||||
|
payload: ["fetchError": error.localizedDescription],
|
||||||
|
etag: nil
|
||||||
|
)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Fallback: Dummy content fetch (no network)
|
||||||
|
print("DNP-FETCH: Using dummy content (native fetcher not configured)")
|
||||||
content = NotificationContent(
|
content = NotificationContent(
|
||||||
id: "fallback_\(Date().timeIntervalSince1970)",
|
id: "dummy_\(Date().timeIntervalSince1970)",
|
||||||
title: "Daily Update",
|
title: "Daily Update",
|
||||||
body: "Your daily notification is ready",
|
body: "Your daily notification is ready",
|
||||||
scheduledTime: Int64(Date().timeIntervalSince1970 * 1000) + (5 * 60 * 1000),
|
scheduledTime: Int64(Date().timeIntervalSince1970 * 1000) + (5 * 60 * 1000),
|
||||||
fetchedAt: Int64(Date().timeIntervalSince1970 * 1000),
|
fetchedAt: Int64(Date().timeIntervalSince1970 * 1000),
|
||||||
url: nil,
|
url: nil,
|
||||||
payload: ["fetchError": error.localizedDescription],
|
payload: nil,
|
||||||
etag: nil
|
etag: nil
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
} else {
|
|
||||||
// Fallback: Dummy content fetch (no network)
|
|
||||||
print("DNP-FETCH: Using dummy content (native fetcher not configured)")
|
|
||||||
content = NotificationContent(
|
|
||||||
id: "dummy_\(Date().timeIntervalSince1970)",
|
|
||||||
title: "Daily Update",
|
|
||||||
body: "Your daily notification is ready",
|
|
||||||
scheduledTime: Int64(Date().timeIntervalSince1970 * 1000) + (5 * 60 * 1000),
|
|
||||||
fetchedAt: Int64(Date().timeIntervalSince1970 * 1000),
|
|
||||||
url: nil,
|
|
||||||
payload: nil,
|
|
||||||
etag: nil
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Save content to storage via state actor (thread-safe)
|
|
||||||
Task {
|
|
||||||
do {
|
do {
|
||||||
// Use the content (either from JWT fetcher or dummy)
|
// Use the content (either from JWT fetcher or dummy)
|
||||||
if #available(iOS 13.0, *) {
|
if #available(iOS 13.0, *) {
|
||||||
@@ -513,11 +513,16 @@ public class DailyNotificationPlugin: CAPPlugin {
|
|||||||
|
|
||||||
// Phase 3.3: Schedule next background task
|
// Phase 3.3: Schedule next background task
|
||||||
// Calculate next fetch time based on notification schedule
|
// Calculate next fetch time based on notification schedule
|
||||||
if let nextScheduledTime = self.getNextScheduledNotificationTime() {
|
if let scheduler = self.scheduler {
|
||||||
self.scheduleBackgroundFetch(scheduledTime: nextScheduledTime)
|
let nextScheduledTime = await scheduler.getNextNotificationTime()
|
||||||
print("DNP-FETCH: Next background fetch scheduled")
|
if let nextTime = nextScheduledTime {
|
||||||
|
self.scheduleBackgroundFetch(scheduledTime: nextTime)
|
||||||
|
print("DNP-FETCH: Next background fetch scheduled")
|
||||||
|
} else {
|
||||||
|
print("DNP-FETCH: No future notifications found, skipping next task schedule")
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
print("DNP-FETCH: No future notifications found, skipping next task schedule")
|
print("DNP-FETCH: Scheduler not available, skipping next task schedule")
|
||||||
}
|
}
|
||||||
|
|
||||||
guard !taskCompleted else { return }
|
guard !taskCompleted else { return }
|
||||||
@@ -575,10 +580,13 @@ public class DailyNotificationPlugin: CAPPlugin {
|
|||||||
|
|
||||||
// Phase 3.3: Schedule next background task if needed
|
// Phase 3.3: Schedule next background task if needed
|
||||||
// For notify task, schedule next occurrence if applicable
|
// For notify task, schedule next occurrence if applicable
|
||||||
if let nextScheduledTime = self.getNextScheduledNotificationTime() {
|
if let scheduler = self.scheduler {
|
||||||
// Calculate next notify task time (if applicable)
|
let nextScheduledTime = await scheduler.getNextNotificationTime()
|
||||||
// Note: Notify tasks are typically scheduled less frequently than fetch tasks
|
if let nextTime = nextScheduledTime {
|
||||||
print("DNP-NOTIFY: Next notification scheduled at \(nextScheduledTime)")
|
// Calculate next notify task time (if applicable)
|
||||||
|
// Note: Notify tasks are typically scheduled less frequently than fetch tasks
|
||||||
|
print("DNP-NOTIFY: Next notification scheduled at \(nextTime)")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
guard !taskCompleted else { return }
|
guard !taskCompleted else { return }
|
||||||
@@ -1091,7 +1099,7 @@ public class DailyNotificationPlugin: CAPPlugin {
|
|||||||
scheduler: scheduler,
|
scheduler: scheduler,
|
||||||
storage: self.storage,
|
storage: self.storage,
|
||||||
stateActor: await self.stateActor,
|
stateActor: await self.stateActor,
|
||||||
scheduleBackgroundFetch: { [weak self] scheduledTime in
|
scheduleBackgroundFetch: { [weak self] (scheduledTime: Int64) -> Void in
|
||||||
self?.scheduleBackgroundFetch(scheduledTime: scheduledTime)
|
self?.scheduleBackgroundFetch(scheduledTime: scheduledTime)
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
@@ -1532,8 +1540,8 @@ public class DailyNotificationPlugin: CAPPlugin {
|
|||||||
// User must check in Settings app
|
// User must check in Settings app
|
||||||
|
|
||||||
// Delegate storage access to storage service
|
// Delegate storage access to storage service
|
||||||
let lastFetchExecution = storage?.getLastSuccessfulRun() ?? NSNull()
|
let lastFetchExecution: Any = storage?.getLastSuccessfulRun() ?? NSNull()
|
||||||
let lastNotifyExecution = storage?.getLastNotifyExecution() ?? NSNull()
|
let lastNotifyExecution: Any = storage?.getLastNotifyExecution() ?? NSNull()
|
||||||
|
|
||||||
let result: [String: Any] = [
|
let result: [String: Any] = [
|
||||||
"fetchTaskRegistered": true, // Assumed registered if setupBackgroundTasks() was called
|
"fetchTaskRegistered": true, // Assumed registered if setupBackgroundTasks() was called
|
||||||
@@ -1630,6 +1638,7 @@ public class DailyNotificationPlugin: CAPPlugin {
|
|||||||
|
|
||||||
// Get channelId from call (optional, for API parity with Android)
|
// Get channelId from call (optional, for API parity with Android)
|
||||||
// iOS doesn't have per-channel control, so check app-wide notification authorization
|
// iOS doesn't have per-channel control, so check app-wide notification authorization
|
||||||
|
let channelId = call.getString("channelId") ?? "default"
|
||||||
Task {
|
Task {
|
||||||
// Delegate to scheduler for permission status check
|
// Delegate to scheduler for permission status check
|
||||||
let status = await scheduler.checkPermissionStatus()
|
let status = await scheduler.checkPermissionStatus()
|
||||||
@@ -1677,9 +1686,6 @@ public class DailyNotificationPlugin: CAPPlugin {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
call.reject("Invalid settings URL")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Update notification settings
|
* Update notification settings
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
require_relative '../../../../node_modules/@capacitor/ios/scripts/pods_helpers'
|
require_relative '../../node_modules/@capacitor/ios/scripts/pods_helpers'
|
||||||
|
|
||||||
platform :ios, '13.0'
|
platform :ios, '13.0'
|
||||||
use_frameworks!
|
use_frameworks!
|
||||||
@@ -9,8 +9,8 @@ use_frameworks!
|
|||||||
install! 'cocoapods', :disable_input_output_paths => true
|
install! 'cocoapods', :disable_input_output_paths => true
|
||||||
|
|
||||||
def capacitor_pods
|
def capacitor_pods
|
||||||
pod 'Capacitor', :path => '../../../../node_modules/@capacitor/ios'
|
pod 'Capacitor', :path => '../../node_modules/@capacitor/ios'
|
||||||
pod 'CapacitorCordova', :path => '../../../../node_modules/@capacitor/ios'
|
pod 'CapacitorCordova', :path => '../../node_modules/@capacitor/ios'
|
||||||
pod 'DailyNotificationPlugin', :path => '../../node_modules/@timesafari/daily-notification-plugin/ios'
|
pod 'DailyNotificationPlugin', :path => '../../node_modules/@timesafari/daily-notification-plugin/ios'
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|||||||
24
test-apps/daily-notification-test/package-lock.json
generated
24
test-apps/daily-notification-test/package-lock.json
generated
@@ -12,6 +12,7 @@
|
|||||||
"@capacitor/android": "^6.2.1",
|
"@capacitor/android": "^6.2.1",
|
||||||
"@capacitor/cli": "^6.2.1",
|
"@capacitor/cli": "^6.2.1",
|
||||||
"@capacitor/core": "^6.2.1",
|
"@capacitor/core": "^6.2.1",
|
||||||
|
"@capacitor/ios": "^6.2.1",
|
||||||
"@timesafari/daily-notification-plugin": "file:../../",
|
"@timesafari/daily-notification-plugin": "file:../../",
|
||||||
"date-fns": "^4.1.0",
|
"date-fns": "^4.1.0",
|
||||||
"did-jwt": "^7.4.7",
|
"did-jwt": "^7.4.7",
|
||||||
@@ -117,6 +118,7 @@
|
|||||||
"integrity": "sha512-2BCOP7TN8M+gVDj7/ht3hsaO/B/n5oDbiAyyvnRlNOs+u1o+JWNYTQrmpuNp1/Wq2gcFrI01JAW+paEKDMx/CA==",
|
"integrity": "sha512-2BCOP7TN8M+gVDj7/ht3hsaO/B/n5oDbiAyyvnRlNOs+u1o+JWNYTQrmpuNp1/Wq2gcFrI01JAW+paEKDMx/CA==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
|
"peer": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@babel/code-frame": "^7.27.1",
|
"@babel/code-frame": "^7.27.1",
|
||||||
"@babel/generator": "^7.28.3",
|
"@babel/generator": "^7.28.3",
|
||||||
@@ -416,6 +418,7 @@
|
|||||||
"resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.28.4.tgz",
|
"resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.28.4.tgz",
|
||||||
"integrity": "sha512-yZbBqeM6TkpP9du/I2pUZnJsRMGGvOuIrhjzC1AwHwW+6he4mni6Bp/m8ijn0iOuZuPI2BfkCoSRunpyjnrQKg==",
|
"integrity": "sha512-yZbBqeM6TkpP9du/I2pUZnJsRMGGvOuIrhjzC1AwHwW+6he4mni6Bp/m8ijn0iOuZuPI2BfkCoSRunpyjnrQKg==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
|
"peer": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@babel/types": "^7.28.4"
|
"@babel/types": "^7.28.4"
|
||||||
},
|
},
|
||||||
@@ -634,10 +637,20 @@
|
|||||||
"resolved": "https://registry.npmjs.org/@capacitor/core/-/core-6.2.1.tgz",
|
"resolved": "https://registry.npmjs.org/@capacitor/core/-/core-6.2.1.tgz",
|
||||||
"integrity": "sha512-urZwxa7hVE/BnA18oCFAdizXPse6fCKanQyEqpmz6cBJ2vObwMpyJDG5jBeoSsgocS9+Ax+9vb4ducWJn0y2qQ==",
|
"integrity": "sha512-urZwxa7hVE/BnA18oCFAdizXPse6fCKanQyEqpmz6cBJ2vObwMpyJDG5jBeoSsgocS9+Ax+9vb4ducWJn0y2qQ==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
|
"peer": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"tslib": "^2.1.0"
|
"tslib": "^2.1.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@capacitor/ios": {
|
||||||
|
"version": "6.2.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/@capacitor/ios/-/ios-6.2.1.tgz",
|
||||||
|
"integrity": "sha512-tbMlQdQjxe1wyaBvYVU1yTojKJjgluZQsJkALuJxv/6F8QTw5b6vd7X785O/O7cMpIAZfUWo/vtAHzFkRV+kXw==",
|
||||||
|
"license": "MIT",
|
||||||
|
"peerDependencies": {
|
||||||
|
"@capacitor/core": "^6.2.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/@esbuild/aix-ppc64": {
|
"node_modules/@esbuild/aix-ppc64": {
|
||||||
"version": "0.25.11",
|
"version": "0.25.11",
|
||||||
"resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.11.tgz",
|
"resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.11.tgz",
|
||||||
@@ -2065,6 +2078,7 @@
|
|||||||
"integrity": "sha512-6JSSaBZmsKvEkbRUkf7Zj7dru/8ZCrJxAqArcLaVMee5907JdtEbKGsZ7zNiIm/UAkpGUkaSMZEXShnN2D1HZA==",
|
"integrity": "sha512-6JSSaBZmsKvEkbRUkf7Zj7dru/8ZCrJxAqArcLaVMee5907JdtEbKGsZ7zNiIm/UAkpGUkaSMZEXShnN2D1HZA==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
|
"peer": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@typescript-eslint/scope-manager": "8.46.1",
|
"@typescript-eslint/scope-manager": "8.46.1",
|
||||||
"@typescript-eslint/types": "8.46.1",
|
"@typescript-eslint/types": "8.46.1",
|
||||||
@@ -2663,6 +2677,7 @@
|
|||||||
"integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==",
|
"integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
|
"peer": true,
|
||||||
"bin": {
|
"bin": {
|
||||||
"acorn": "bin/acorn"
|
"acorn": "bin/acorn"
|
||||||
},
|
},
|
||||||
@@ -2911,6 +2926,7 @@
|
|||||||
}
|
}
|
||||||
],
|
],
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
|
"peer": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"baseline-browser-mapping": "^2.8.9",
|
"baseline-browser-mapping": "^2.8.9",
|
||||||
"caniuse-lite": "^1.0.30001746",
|
"caniuse-lite": "^1.0.30001746",
|
||||||
@@ -3373,6 +3389,7 @@
|
|||||||
"integrity": "sha512-XyLmROnACWqSxiGYArdef1fItQd47weqB7iwtfr9JHwRrqIXZdcFMvvEcL9xHCmL0SNsOvF0c42lWyM1U5dgig==",
|
"integrity": "sha512-XyLmROnACWqSxiGYArdef1fItQd47weqB7iwtfr9JHwRrqIXZdcFMvvEcL9xHCmL0SNsOvF0c42lWyM1U5dgig==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
|
"peer": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@eslint-community/eslint-utils": "^4.8.0",
|
"@eslint-community/eslint-utils": "^4.8.0",
|
||||||
"@eslint-community/regexpp": "^4.12.1",
|
"@eslint-community/regexpp": "^4.12.1",
|
||||||
@@ -3434,6 +3451,7 @@
|
|||||||
"integrity": "sha512-K6tP0dW8FJVZLQxa2S7LcE1lLw3X8VvB3t887Q6CLrFVxHYBXGANbXvwNzYIu6Ughx1bSJ5BDT0YB3ybPT39lw==",
|
"integrity": "sha512-K6tP0dW8FJVZLQxa2S7LcE1lLw3X8VvB3t887Q6CLrFVxHYBXGANbXvwNzYIu6Ughx1bSJ5BDT0YB3ybPT39lw==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
|
"peer": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@eslint-community/eslint-utils": "^4.4.0",
|
"@eslint-community/eslint-utils": "^4.4.0",
|
||||||
"natural-compare": "^1.4.0",
|
"natural-compare": "^1.4.0",
|
||||||
@@ -4293,6 +4311,7 @@
|
|||||||
"integrity": "sha512-ekilCSN1jwRvIbgeg/57YFh8qQDNbwDb9xT/qu2DAHbFFZUicIl4ygVaAvzveMhMVr3LnpSKTNnwt8PoOfmKhQ==",
|
"integrity": "sha512-ekilCSN1jwRvIbgeg/57YFh8qQDNbwDb9xT/qu2DAHbFFZUicIl4ygVaAvzveMhMVr3LnpSKTNnwt8PoOfmKhQ==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
|
"peer": true,
|
||||||
"bin": {
|
"bin": {
|
||||||
"jiti": "lib/jiti-cli.mjs"
|
"jiti": "lib/jiti-cli.mjs"
|
||||||
}
|
}
|
||||||
@@ -5721,6 +5740,7 @@
|
|||||||
"integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==",
|
"integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
|
"peer": true,
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=12"
|
"node": ">=12"
|
||||||
},
|
},
|
||||||
@@ -5798,6 +5818,7 @@
|
|||||||
"integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==",
|
"integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==",
|
||||||
"devOptional": true,
|
"devOptional": true,
|
||||||
"license": "Apache-2.0",
|
"license": "Apache-2.0",
|
||||||
|
"peer": true,
|
||||||
"bin": {
|
"bin": {
|
||||||
"tsc": "bin/tsc",
|
"tsc": "bin/tsc",
|
||||||
"tsserver": "bin/tsserver"
|
"tsserver": "bin/tsserver"
|
||||||
@@ -6031,6 +6052,7 @@
|
|||||||
"integrity": "sha512-CmuvUBzVJ/e3HGxhg6cYk88NGgTnBoOo7ogtfJJ0fefUWAxN/WDSUa50o+oVBxuIhO8FoEZW0j2eW7sfjs5EtA==",
|
"integrity": "sha512-CmuvUBzVJ/e3HGxhg6cYk88NGgTnBoOo7ogtfJJ0fefUWAxN/WDSUa50o+oVBxuIhO8FoEZW0j2eW7sfjs5EtA==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
|
"peer": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"esbuild": "^0.25.0",
|
"esbuild": "^0.25.0",
|
||||||
"fdir": "^6.5.0",
|
"fdir": "^6.5.0",
|
||||||
@@ -6301,6 +6323,7 @@
|
|||||||
"integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==",
|
"integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
|
"peer": true,
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=12"
|
"node": ">=12"
|
||||||
},
|
},
|
||||||
@@ -6320,6 +6343,7 @@
|
|||||||
"resolved": "https://registry.npmjs.org/vue/-/vue-3.5.22.tgz",
|
"resolved": "https://registry.npmjs.org/vue/-/vue-3.5.22.tgz",
|
||||||
"integrity": "sha512-toaZjQ3a/G/mYaLSbV+QsQhIdMo9x5rrqIpYRObsJ6T/J+RyCSFwN2LHNVH9v8uIcljDNa3QzPVdv3Y6b9hAJQ==",
|
"integrity": "sha512-toaZjQ3a/G/mYaLSbV+QsQhIdMo9x5rrqIpYRObsJ6T/J+RyCSFwN2LHNVH9v8uIcljDNa3QzPVdv3Y6b9hAJQ==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
|
"peer": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@vue/compiler-dom": "3.5.22",
|
"@vue/compiler-dom": "3.5.22",
|
||||||
"@vue/compiler-sfc": "3.5.22",
|
"@vue/compiler-sfc": "3.5.22",
|
||||||
|
|||||||
@@ -15,14 +15,14 @@
|
|||||||
"lint": "eslint . --fix",
|
"lint": "eslint . --fix",
|
||||||
"cap:sync": "npx cap sync && node scripts/fix-capacitor-plugins.js",
|
"cap:sync": "npx cap sync && node scripts/fix-capacitor-plugins.js",
|
||||||
"cap:sync:android": "npx cap sync android && node scripts/fix-capacitor-plugins.js",
|
"cap:sync:android": "npx cap sync android && node scripts/fix-capacitor-plugins.js",
|
||||||
"cap:sync:ios": "npx cap sync ios",
|
"cap:sync:ios": "npx cap copy ios && node scripts/fix-capacitor-plugins.js && cd ios/App && pod install && cd ../..",
|
||||||
"postinstall": "node scripts/fix-capacitor-plugins.js"
|
"postinstall": "node scripts/fix-capacitor-plugins.js"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@capacitor/android": "^6.2.1",
|
"@capacitor/android": "^6.2.1",
|
||||||
"@capacitor/ios": "^6.2.1",
|
|
||||||
"@capacitor/cli": "^6.2.1",
|
"@capacitor/cli": "^6.2.1",
|
||||||
"@capacitor/core": "^6.2.1",
|
"@capacitor/core": "^6.2.1",
|
||||||
|
"@capacitor/ios": "^6.2.1",
|
||||||
"@timesafari/daily-notification-plugin": "file:../../",
|
"@timesafari/daily-notification-plugin": "file:../../",
|
||||||
"date-fns": "^4.1.0",
|
"date-fns": "^4.1.0",
|
||||||
"did-jwt": "^7.4.7",
|
"did-jwt": "^7.4.7",
|
||||||
|
|||||||
@@ -40,6 +40,9 @@ export interface ScheduleResponse {
|
|||||||
export interface PermissionStatus {
|
export interface PermissionStatus {
|
||||||
notifications: 'granted' | 'denied'
|
notifications: 'granted' | 'denied'
|
||||||
notificationsEnabled: boolean
|
notificationsEnabled: boolean
|
||||||
|
exactAlarmEnabled?: boolean
|
||||||
|
wakeLockEnabled?: boolean
|
||||||
|
allPermissionsGranted?: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface NotificationStatus {
|
export interface NotificationStatus {
|
||||||
|
|||||||
@@ -402,7 +402,7 @@ const checkAndRequestPermissions = async (): Promise<void> => {
|
|||||||
// Request permissions - this will show the iOS system dialog
|
// Request permissions - this will show the iOS system dialog
|
||||||
// Try requestNotificationPermissions first (iOS), fallback to requestPermissions
|
// Try requestNotificationPermissions first (iOS), fallback to requestPermissions
|
||||||
if (typeof (plugin as any).requestNotificationPermissions === 'function') {
|
if (typeof (plugin as any).requestNotificationPermissions === 'function') {
|
||||||
await (plugin as { requestNotificationPermissions: () => Promise<void> }).requestNotificationPermissions()
|
await (plugin as { requestNotificationPermissions: () => Promise<any> }).requestNotificationPermissions()
|
||||||
} else if (typeof (plugin as any).requestPermissions === 'function') {
|
} else if (typeof (plugin as any).requestPermissions === 'function') {
|
||||||
await (plugin as { requestPermissions: () => Promise<any> }).requestPermissions()
|
await (plugin as { requestPermissions: () => Promise<any> }).requestPermissions()
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
Reference in New Issue
Block a user