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:
Jose Olarte III
2025-12-30 12:35:10 +08:00
parent 36e15633be
commit 9565191101
9 changed files with 280 additions and 82 deletions

View File

@@ -361,12 +361,16 @@ npm install
# Build Vue 3 app
npm run build
# Add Capacitor
npm install @capacitor/android
# Add Capacitor platforms
npm install @capacitor/android @capacitor/ios
# Sync with Capacitor
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
npx cap run android
@@ -374,6 +378,149 @@ npx cap run android
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:**
- Interactive plugin testing interface
@@ -390,8 +537,13 @@ test-apps/daily-notification-test/
│ ├── components/ # Reusable UI components
│ └── stores/ # Pinia state management
├── android/ # Android Capacitor app
├── ios/ # iOS Capacitor app
│ └── App/
│ ├── Podfile # CocoaPods dependencies
│ └── App.xcworkspace # Xcode workspace
├── docs/ # Test app documentation
└── scripts/ # Test app build scripts
│ └── fix-capacitor-plugins.js # Auto-fixes Podfile
```
#### Android Test Apps

View File

@@ -263,12 +263,12 @@ class DailyNotificationDatabase {
return
}
sqlite3_bind_text(stmt, 1, (content.id as NSString).utf8String, -1, SQLITE_TRANSIENT)
sqlite3_bind_text(stmt, 2, (json 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, nil)
sqlite3_bind_int64(stmt, 3, sqlite3_int64(content.fetchedAt))
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 {
sqlite3_bind_null(stmt, 4)
}
@@ -310,7 +310,7 @@ class DailyNotificationDatabase {
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 {
print("\(Self.TAG): deleteNotificationContent step failed: \(String(cString: sqlite3_errmsg(db)))")

View File

@@ -261,17 +261,8 @@ class PersistenceController {
description?.shouldMigrateStoreAutomatically = 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
tempContainer?.loadPersistentStores { description, error in
tempContainer?.loadPersistentStores { storeDescription, error in
if let error = error as NSError? {
loadError = error
print("DNP-PLUGIN: CoreData store load error: \(error.localizedDescription)")
@@ -281,7 +272,19 @@ class PersistenceController {
}
} else {
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
}
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
if currentVersion != expectedVersion {
@@ -381,9 +390,13 @@ class PersistenceController {
print("DNP-PLUGIN: CoreData auto-migration will handle schema changes")
// Update metadata for future reference (does not trigger migration)
var metadata = store.metadata
metadata["schema_version"] = expectedVersion
// Use the coordinator to set metadata
if let coordinator = container?.persistentStoreCoordinator {
var newMetadata = metadata
newMetadata["schema_version"] = expectedVersion
coordinator.setMetadata(newMetadata, for: store)
// Note: Metadata persists on next store save
}
} else {
print("DNP-PLUGIN: Schema version verified: \(currentVersion)")
}

View File

@@ -419,6 +419,9 @@ public class DailyNotificationPlugin: CAPPlugin {
// Phase 3: Check for JWT-signed fetcher configuration
// If native fetcher is configured, use it; otherwise fall back to dummy content
let nativeFetcherConfig = UserDefaults.standard.string(forKey: "native_fetcher_config")
// Save content to storage via state actor (thread-safe)
Task {
let content: NotificationContent
if let configJson = nativeFetcherConfig,
@@ -468,9 +471,6 @@ public class DailyNotificationPlugin: CAPPlugin {
etag: nil
)
}
// Save content to storage via state actor (thread-safe)
Task {
do {
// Use the content (either from JWT fetcher or dummy)
if #available(iOS 13.0, *) {
@@ -513,12 +513,17 @@ public class DailyNotificationPlugin: CAPPlugin {
// Phase 3.3: Schedule next background task
// Calculate next fetch time based on notification schedule
if let nextScheduledTime = self.getNextScheduledNotificationTime() {
self.scheduleBackgroundFetch(scheduledTime: nextScheduledTime)
if let scheduler = self.scheduler {
let nextScheduledTime = await scheduler.getNextNotificationTime()
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 {
print("DNP-FETCH: Scheduler not available, skipping next task schedule")
}
guard !taskCompleted else { return }
task.setTaskCompleted(success: true)
@@ -575,10 +580,13 @@ public class DailyNotificationPlugin: CAPPlugin {
// Phase 3.3: Schedule next background task if needed
// For notify task, schedule next occurrence if applicable
if let nextScheduledTime = self.getNextScheduledNotificationTime() {
if let scheduler = self.scheduler {
let nextScheduledTime = await scheduler.getNextNotificationTime()
if let nextTime = 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 \(nextScheduledTime)")
print("DNP-NOTIFY: Next notification scheduled at \(nextTime)")
}
}
guard !taskCompleted else { return }
@@ -1091,7 +1099,7 @@ public class DailyNotificationPlugin: CAPPlugin {
scheduler: scheduler,
storage: self.storage,
stateActor: await self.stateActor,
scheduleBackgroundFetch: { [weak self] scheduledTime in
scheduleBackgroundFetch: { [weak self] (scheduledTime: Int64) -> Void in
self?.scheduleBackgroundFetch(scheduledTime: scheduledTime)
}
)
@@ -1532,8 +1540,8 @@ public class DailyNotificationPlugin: CAPPlugin {
// User must check in Settings app
// Delegate storage access to storage service
let lastFetchExecution = storage?.getLastSuccessfulRun() ?? NSNull()
let lastNotifyExecution = storage?.getLastNotifyExecution() ?? NSNull()
let lastFetchExecution: Any = storage?.getLastSuccessfulRun() ?? NSNull()
let lastNotifyExecution: Any = storage?.getLastNotifyExecution() ?? NSNull()
let result: [String: Any] = [
"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)
// iOS doesn't have per-channel control, so check app-wide notification authorization
let channelId = call.getString("channelId") ?? "default"
Task {
// Delegate to scheduler for permission status check
let status = await scheduler.checkPermissionStatus()
@@ -1677,9 +1686,6 @@ public class DailyNotificationPlugin: CAPPlugin {
}
}
}
call.reject("Invalid settings URL")
}
}
/**
* Update notification settings

View File

@@ -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'
use_frameworks!
@@ -9,8 +9,8 @@ use_frameworks!
install! 'cocoapods', :disable_input_output_paths => true
def capacitor_pods
pod 'Capacitor', :path => '../../../../node_modules/@capacitor/ios'
pod 'CapacitorCordova', :path => '../../../../node_modules/@capacitor/ios'
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

View File

@@ -12,6 +12,7 @@
"@capacitor/android": "^6.2.1",
"@capacitor/cli": "^6.2.1",
"@capacitor/core": "^6.2.1",
"@capacitor/ios": "^6.2.1",
"@timesafari/daily-notification-plugin": "file:../../",
"date-fns": "^4.1.0",
"did-jwt": "^7.4.7",
@@ -117,6 +118,7 @@
"integrity": "sha512-2BCOP7TN8M+gVDj7/ht3hsaO/B/n5oDbiAyyvnRlNOs+u1o+JWNYTQrmpuNp1/Wq2gcFrI01JAW+paEKDMx/CA==",
"dev": true,
"license": "MIT",
"peer": true,
"dependencies": {
"@babel/code-frame": "^7.27.1",
"@babel/generator": "^7.28.3",
@@ -416,6 +418,7 @@
"resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.28.4.tgz",
"integrity": "sha512-yZbBqeM6TkpP9du/I2pUZnJsRMGGvOuIrhjzC1AwHwW+6he4mni6Bp/m8ijn0iOuZuPI2BfkCoSRunpyjnrQKg==",
"license": "MIT",
"peer": true,
"dependencies": {
"@babel/types": "^7.28.4"
},
@@ -634,10 +637,20 @@
"resolved": "https://registry.npmjs.org/@capacitor/core/-/core-6.2.1.tgz",
"integrity": "sha512-urZwxa7hVE/BnA18oCFAdizXPse6fCKanQyEqpmz6cBJ2vObwMpyJDG5jBeoSsgocS9+Ax+9vb4ducWJn0y2qQ==",
"license": "MIT",
"peer": true,
"dependencies": {
"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": {
"version": "0.25.11",
"resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.11.tgz",
@@ -2065,6 +2078,7 @@
"integrity": "sha512-6JSSaBZmsKvEkbRUkf7Zj7dru/8ZCrJxAqArcLaVMee5907JdtEbKGsZ7zNiIm/UAkpGUkaSMZEXShnN2D1HZA==",
"dev": true,
"license": "MIT",
"peer": true,
"dependencies": {
"@typescript-eslint/scope-manager": "8.46.1",
"@typescript-eslint/types": "8.46.1",
@@ -2663,6 +2677,7 @@
"integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==",
"dev": true,
"license": "MIT",
"peer": true,
"bin": {
"acorn": "bin/acorn"
},
@@ -2911,6 +2926,7 @@
}
],
"license": "MIT",
"peer": true,
"dependencies": {
"baseline-browser-mapping": "^2.8.9",
"caniuse-lite": "^1.0.30001746",
@@ -3373,6 +3389,7 @@
"integrity": "sha512-XyLmROnACWqSxiGYArdef1fItQd47weqB7iwtfr9JHwRrqIXZdcFMvvEcL9xHCmL0SNsOvF0c42lWyM1U5dgig==",
"dev": true,
"license": "MIT",
"peer": true,
"dependencies": {
"@eslint-community/eslint-utils": "^4.8.0",
"@eslint-community/regexpp": "^4.12.1",
@@ -3434,6 +3451,7 @@
"integrity": "sha512-K6tP0dW8FJVZLQxa2S7LcE1lLw3X8VvB3t887Q6CLrFVxHYBXGANbXvwNzYIu6Ughx1bSJ5BDT0YB3ybPT39lw==",
"dev": true,
"license": "MIT",
"peer": true,
"dependencies": {
"@eslint-community/eslint-utils": "^4.4.0",
"natural-compare": "^1.4.0",
@@ -4293,6 +4311,7 @@
"integrity": "sha512-ekilCSN1jwRvIbgeg/57YFh8qQDNbwDb9xT/qu2DAHbFFZUicIl4ygVaAvzveMhMVr3LnpSKTNnwt8PoOfmKhQ==",
"dev": true,
"license": "MIT",
"peer": true,
"bin": {
"jiti": "lib/jiti-cli.mjs"
}
@@ -5721,6 +5740,7 @@
"integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==",
"dev": true,
"license": "MIT",
"peer": true,
"engines": {
"node": ">=12"
},
@@ -5798,6 +5818,7 @@
"integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==",
"devOptional": true,
"license": "Apache-2.0",
"peer": true,
"bin": {
"tsc": "bin/tsc",
"tsserver": "bin/tsserver"
@@ -6031,6 +6052,7 @@
"integrity": "sha512-CmuvUBzVJ/e3HGxhg6cYk88NGgTnBoOo7ogtfJJ0fefUWAxN/WDSUa50o+oVBxuIhO8FoEZW0j2eW7sfjs5EtA==",
"dev": true,
"license": "MIT",
"peer": true,
"dependencies": {
"esbuild": "^0.25.0",
"fdir": "^6.5.0",
@@ -6301,6 +6323,7 @@
"integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==",
"dev": true,
"license": "MIT",
"peer": true,
"engines": {
"node": ">=12"
},
@@ -6320,6 +6343,7 @@
"resolved": "https://registry.npmjs.org/vue/-/vue-3.5.22.tgz",
"integrity": "sha512-toaZjQ3a/G/mYaLSbV+QsQhIdMo9x5rrqIpYRObsJ6T/J+RyCSFwN2LHNVH9v8uIcljDNa3QzPVdv3Y6b9hAJQ==",
"license": "MIT",
"peer": true,
"dependencies": {
"@vue/compiler-dom": "3.5.22",
"@vue/compiler-sfc": "3.5.22",

View File

@@ -15,14 +15,14 @@
"lint": "eslint . --fix",
"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: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"
},
"dependencies": {
"@capacitor/android": "^6.2.1",
"@capacitor/ios": "^6.2.1",
"@capacitor/cli": "^6.2.1",
"@capacitor/core": "^6.2.1",
"@capacitor/ios": "^6.2.1",
"@timesafari/daily-notification-plugin": "file:../../",
"date-fns": "^4.1.0",
"did-jwt": "^7.4.7",

View File

@@ -40,6 +40,9 @@ export interface ScheduleResponse {
export interface PermissionStatus {
notifications: 'granted' | 'denied'
notificationsEnabled: boolean
exactAlarmEnabled?: boolean
wakeLockEnabled?: boolean
allPermissionsGranted?: boolean
}
export interface NotificationStatus {

View File

@@ -402,7 +402,7 @@ const checkAndRequestPermissions = async (): Promise<void> => {
// Request permissions - this will show the iOS system dialog
// Try requestNotificationPermissions first (iOS), fallback to requestPermissions
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') {
await (plugin as { requestPermissions: () => Promise<any> }).requestPermissions()
} else {