Use getHeaders(activeDid) for POST /notifications/register and
/notifications/refresh so requests include Authorization: Bearer tokens
like the rest of the app. Add notificationApiAuth helper for shared header
resolution, auth logging, and graceful handling when identity or token
is missing or the server returns 401/403.
Introduce NotificationDebugConfig so register/refresh use getNotificationApiBaseUrl()
(APP_SERVER by default, optional LAN/ngrok override) and configurable testMode without rebuilds.
If randomUUID is unavailable (older WebViews), generate a one-time ID
with Date.now + random segment, log a single DeviceId warning, and
persist it as before so registration still works.
Add getOrCreateDeviceId() backed by Capacitor Preferences so one UUID
survives app restarts and token refreshes. Include deviceId in POST
/notifications/register alongside fcmToken, platform, and testMode.
Add @capacitor/preferences and lightweight DeviceId logs (no token/ID values).
Expose wall-clock fire targets from the iOS NotificationInspector
(scheduled_time userInfo and predictive_<epochMs> ids) so the debug
panel is not misleading when nextTriggerDate resamples for interval
triggers. Extend TS types and show the scheduled target in the UI,
with a note when iOS nextTriggerDate diverges.
Make refreshPending a plain fetch so mock refresh, wakeup ping, flood
test, and clear notifications can refresh the pending list while an
outer withBusy guard is already active.
Add includeDevToolkitRoutes (vite dev or MODE !== production) and use it
from the router, AccountViewView, and NotificationDebugView so the debug
screen matches dev-notifications registration after vite build.
Update the gated banner copy to refer to production Vite builds.
Register the dev-notifications route whenever the bundle is non-production
(DEV or Vite MODE !== production), matching the account screen so RouterLink
to Notification Debug does not throw after vite build.
Align AccountViewView isDev with that rule and document the coupling.
Add NotificationInspectorPlugin.swift to the App target compile sources so
AppDelegate can register the plugin.
Vue’s template compiler treats bindings as non-module JS, so
`import.meta.env.DEV` in `v-if` broke the Capacitor/Vite build.
Expose a readonly `isDev` from the script instead.
- Report UNIMPLEMENTED from Android NotificationInspector instead of empty pending
- Surface iOS-only inspector message in NotificationDebugPanel without noisy errors
- Gate Account debug link with import.meta.env.DEV and document intent
- Add architecture comments on NotificationDebugService, inspector plugin, and native exports
Add a dev-only Notification Debug Panel at /dev/notifications for testing
predictive refresh and WAKEUP_PING without a backend.
- Gate route and Advanced Settings entry on import.meta.env.DEV
- NotificationDebugService drives mock refresh, flood test, clear, and
wake simulation via existing handleCapacitorPushNotificationReceived and
applyNotificationRefreshPayload (shared with refreshNotifications)
- Add NotificationInspector Capacitor plugin: iOS lists pending
UNNotificationRequest identifiers and next trigger; Android stub returns
empty pending for safe registration
Stop converting backend timestamps to HH:mm/recurring schedules and remove
createSchedule/updateSchedule reconciliation. After a successful refresh payload,
clear existing notifications and schedule exact timestamps via the plugin
scheduleNotifications API (with back-compat clear fallback) to prevent drift.
Trigger refreshNotifications on composable mount and document resume, using a
debounced/in-flight guarded wrapper to avoid rapid duplicate refresh calls.
Expose the debounced refresh function from useNotifications.
Cancel all native notifications before applying the backend-provided schedule so
refreshNotifications always performs a full replacement and never leaves stale
entries behind.
Update refreshNotifications to POST /notifications/refresh and map returned
nextNotifications timestamps to clockTime schedules, upserting them via the
DailyNotification schedule APIs (with deterministic IDs) after refreshing native
fetcher credentials.
Add refreshNotifications (configureNativeFetcherIfReady) and
handleCapacitorPushNotificationReceived for data.type WAKEUP_PING; invoke from
Capacitor pushNotificationReceived without UI.
Add registerToken POST to /notifications/register (platform, testMode).
Call it from Capacitor registration and Firebase getToken with deduped
registerRetrievedToken; expose registerToken via barrel and useNotifications
as registerFcmToken.
Add firebaseMessagingClient to ensure the Firebase app is created from VITE_FIREBASE_*,
wire PushNotifications (listeners, requestPermissions, register) before token work,
and call getMessaging/getToken/onMessage when firebase/messaging is supported. Hook
startup from main.capacitor and set PushNotifications presentationOptions in
capacitor.config. Depend on firebase and @capacitor/push-notifications.
Restructure the quick start with Web, Android, and iOS subheadings; put
each npm command in its own code block; fold the test-page step into the
Web section. Document Android (build:android:test:run + ADB, link to
BUILDING.md) and iOS (build:ios:studio + Xcode prerequisites).
Use generic/platform=iOS Simulator instead of a fixed device name so CLI builds
do not fail when that simulator is not installed (e.g. newer Xcode runtimes).
Pass -quiet to xcodebuild and enable SWIFT_SUPPRESS_WARNINGS plus
GCC_WARN_INHIBIT_ALL_WARNINGS for scripted builds and IPA archive/export so
terminal output stays smaller; full diagnostics remain available in Xcode.
Add TimeSafariNativeFetcher (plansLastUpdatedBetween parity with Android) and
call DailyNotificationPlugin.registerNativeFetcher from AppDelegate before JS
configureNativeFetcher; broaden DailyNotificationDelivered scheduled_time types
in willPresent. Wire the new file into the App target; normalize PBX object IDs
to 24-char hex.
Document plugin ≥3 handoff (consuming-app-handoff-ios-native-fetcher-chained-dual),
refresh iOS/Android parity and notification-from-api-call file tables.
Add doc/new-activity-notifications-ios-android-parity.md covering dual-schedule
and Endorser API parity, plugin vs app work, Android dual-path notes, prefetch
vs notify ordering on iOS (§3.3), and clarified Phase B JWT pool status on
both platforms. Link the guide from doc/notification-from-api-call.md under the
iOS checklist.
Aggregate API rows into one notification with Starred Project Update(s) titles,
plan names in typographic quotes, and "+ N more have been updated." for multiples.
Stop emitting the empty-data "No Project Updates" fallback. Sync internal docs.
Add syncStarredPlansToNativePlugin() and call it from AccountViewView
(schedule + initializeState) and ProjectViewView.toggleStar when New
Activity is enabled so Android prefetch uses the current starred list.
Update notification-from-api-call.md with the helper and file references.
- Mint BACKGROUND_JWT_POOL_SIZE (90 + 10) distinct background JWTs with
unique jti; pass jwtTokens from nativeFetcherConfig into configureNativeFetcher.
- Android TimeSafariNativeFetcher: overload configure with jwtTokenPool;
select bearer via epoch day mod pool size; fall back to primary jwtToken.
- Log truncated plansLastUpdatedBetween response at DEBUG for prefetch debugging.
Add BACKGROUND_JWT_EXPIRY_DAYS/SECONDS and accessTokenForBackgroundNotifications
via createEndorserJwtForDid; configureNativeFetcher uses it instead of getHeaders
so WorkManager prefetch is not stuck with a 60s access token. Interactive API
calls unchanged.
Add plan-background-jwt-pool-and-expiry.md (Phase A/B, expiryDays + buffer sizing,
pool size 100). Add plugin-feedback-daily-notification-configureNativeFetcher-jwt-pool.md
for daily-notification-plugin: optional jwtTokens on configureNativeFetcher. Link plan
to plugin doc and endorser-jwt-background-prefetch-options.md.
Call configureNativeFetcherIfReady when the app becomes active so getHeaders can
supply a new JWT before the next background prefetch when users return from
background.
In TimeSafariNativeFetcher, read HttpURLConnection#getErrorStream for non-200
responses and log a capped body snippet (including on retryable errors) to
diagnose JWT_VERIFY_FAILED and other API failures.
`npx cap run android` runs `sync` by default, which regenerated
`capacitor.plugins.json` and removed SafeArea and SharedImage entries
after `restore-local-plugins.js` had already run. Use `--no-sync` in
`build-android.sh` (auto-run) and `auto-run.sh` so the launch step does
not overwrite the restored plugin list.
Document how the daily-notification-plugin dual path uses FetchWorker mock/URL
fetch instead of NativeNotificationContentFetcher, schedules fetch immediately
rather than at contentFetch cron, and why DualScheduleHelper shows useCache=false.
Includes acceptance criteria and file pointers for maintainers fixing the plugin.
Log configure-time starred plan count, fetchContent entry (trigger, scheduledTime,
thread), worker start, POST summary (plan count, truncated afterId), and HTTP
status at INFO so logcat shows clearly when the native fetcher runs versus
plugin-only DNP-FETCH paths.
Add an Android-focused procedure for verifying API-driven copy when a
starred plan has updates via plansLastUpdatedBetween, including expected
notification text, prefetch timing, repeatability, and logcat. Clarify
that iOS parity is documented separately and that the native fetcher uses
Account API Server URL (test Endorser is valid), not the Partner API URL.
- Treat updateStarredPlans as optional: catch UNIMPLEMENTED and continue to
scheduleDualNotification so missing native method no longer blocks scheduling.
- Show specific toast when BGTaskSchedulerErrorDomain error 1 occurs (e.g.
Simulator): explain that a real device and Background App Refresh are required.
- Add PluginHeaders diagnostic in AccountViewView and main.capacitor.ts to debug
UNIMPLEMENTED (log DNP methods at call time and at launch).
- Fix main.capacitor.ts build: use CapacitorWindow type and safe cap assignment
so vite build --mode capacitor succeeds.
- Docs: add UNIMPLEMENTED troubleshooting and updateStarredPlans note in
plugin-feedback-ios-scheduleDualNotification.md; add section 8.3 in
notification-new-activity-lay-of-the-land.md.
- Lockfile updates (package-lock.json, Podfile.lock).
Add plugin-feedback-ios-scheduleDualNotification.md for the
daily-notification-plugin repo: config shape from the app, expected
behavior, and acceptance criteria so iOS can implement or fix
scheduleDualNotification (currently returns UNIMPLEMENTED).
- PushNotificationPermission: show "Turn on New Activity Notifications"
when enabling New Activity; use NOTIFY_PUSH_SUCCESS_NEW_ACTIVITY for
success toast so copy says "New Activity notifications are now enabled."
- App.vue: on native, turnOffNotifications invokes the modal's callback
only (fixes turn-off not updating state); add comment that callback is
per notification type.
- AccountViewView: handle plugin UNIMPLEMENTED for scheduleDualNotification
on iOS with friendlier message; add New Activity time block and "Edit
New Activity Notification…"; rename Daily Reminder button to "Edit Daily
Reminder…".
- Constants: add NOTIFY_PUSH_SUCCESS_NEW_ACTIVITY. Reminder IDs and
Option A (skip single reminder for New Activity) from earlier commit.
- PushNotificationPermission: on native, when enabling New Activity
(DAILY_CHECK_TITLE), skip scheduleDailyNotification so only
AccountViewView's scheduleNewActivityDualNotification runs (dual
schedule only). Daily Reminder still uses single reminder path.
- Add reminderIds.ts with REMINDER_ID_DAILY_REMINDER and
REMINDER_ID_NEW_ACTIVITY; NativeNotificationService uses the former.
- Export reminder IDs from notifications index. Fixes "always fires /
can't turn off" by avoiding a second, uncancellable single reminder
for New Activity.
Document how daily-notification-plugin aligns with app usage (APIs,
dual vs single schedules, native fetcher, exact alarm). Note attention
items: cancelDailyReminder argument shape, INTEGRATION_GUIDE scope, and
iOS use of app-provided id for scheduleDailyNotification.
Expand notification-new-activity-lay-of-the-land.md with section 7 on
testing New Activity on real iOS/Android devices (prerequisites, enable/
disable flows, what to verify before and after fix). Update Android
device notes to state this app has exact alarm disabled (no
SCHEDULE_EXACT_ALARM) and that delivery may be inexact or batched.
Add section 6 with Option A (skip single reminder when dialog is for New
Activity), Option B (cancel single reminder on disable, with caveats),
and optional cleanup notes. For team discussion and implementation.
- Move NOTIFICATION_TROUBLESHOOTING content into HelpNotificationsView with prose styling
- Remove exact-alarms section from doc and view (feature removed from app/plugin)
- Add collapsible iOS/Android sections (open by default for current platform)
- Add rotating carets on collapsible summaries
- Add bullet for 10-minute rollover option in "If it still doesn't work"
- Add @tailwindcss/typography plugin for prose classes
- Reset #app scroll on route change so Help Notifications opens at top
Allow scheduling to continue when exact alarm is not granted instead of
opening Settings and rejecting. Consumer apps can inform users about
exact alarms in their own UI.
- Replace "npm:@jsr/nostr__tools" with "nostr-tools" to fix npm 404
- Update imports in NewEditProjectView and Vite configs
- Remove Vite aliases so resolution uses package exports
- Rename "Reminder Notification(s)" to "Daily Reminder" in Account and Help views
- Update NOTIFY_PUSH_SUCCESS title/message ("Notifications On", "Daily Reminder notifications are now enabled.")
- Align plugin spec doc with "Notifications" section naming
Document two rollover-interval bugs for daily-notification-plugin with
logcat evidence and required fixes. Both issues have been fixed on the
plugin side; rollovers now chain every N minutes across reboots without
opening the app.
- Add dev-only "Use 10-minute rollover (testing)" toggle in Reminder
Notifications (Account view). Visible only when not on prod API server
(isNotProdServer). Toggle persists and reschedules reminder with
rolloverIntervalMinutes when changed.
- Extend daily notification flow to pass optional rolloverIntervalMinutes
to the plugin: NotificationService/NativeNotificationService options,
PushNotificationPermission dialog options, first-time and edit flows.
- Add settings: reminderFastRolloverForTesting (Settings, AccountSettings,
PlatformServiceMixin boolean mapping, migration 007).
- Centralize isNotProdServer(apiServer) in constants/app.ts; use in
AccountViewView (toggle visibility), ImportAccountView, and TestView.
- Add docs/plugin-spec-rollover-interval-minutes.md for the plugin repo
(configurable rollover interval and persistence after reboot).
Note: Daily notification plugin dependency is currently pointed at the
"rollover-interval" branch for testing this feature.
Add doc/plugin-feedback-android-6-api23-zoneid-fix.md for the
daily-notification-plugin repo: replace java.time.ZoneId with
TimeZone.getDefault().id so the plugin runs on API 23 without
affecting behavior on API 26+.
- Fix typo in Android Chrome "Clear browsing data" step
- Use TimeSafari in Settings steps and clarify PWA vs native install
- Add Battery & background subsection (iOS and Android)
- Add Focus / Do Not Disturb note under Check App Permissions
- Add quick checklist at top before Full Test
- Replace hardcoded app names with AppString.APP_NAME and APP_NAME_NO_SPACES
Document that boot recovery can skip rescheduling after device restart
(duplicate PendingIntent check), causing the next daily notification to
fail to fire on devices that clear AlarmManager alarms on reboot. Include
Scenario 1 logcat (notification fired when alarm survived), two-scenario
distinction, recommended fix (skipPendingIntentIdempotence in boot path),
duplicate-alarm clarification, and Cursor-ready implementation section
for the daily-notification-plugin repo.
Document bug where daily reminder shows fallback text after device restart
(Intent extras not surviving reboot). Includes root cause, recommended
plugin fixes (persist/restore title/body, use DB in recovery), and
verification steps for the daily-notification-plugin repo.
Add docs/plugin-feedback-android-duplicate-reminder-notification.md
describing the duplicate notification on first-time reminder setup (one
correct + one fallback). Root cause: ScheduleHelper schedules the
static reminder alarm and also enqueues the prefetch worker, which
on fallback schedules a second alarm via DailyNotificationScheduler.
Suggested fix: for static-reminder schedules, do not enqueue prefetch
(or have prefetch skip scheduleNotificationIfNeeded). The suggested
plugin changes were applied and fixed the issue.
Exclude electron/src/rt/electron-plugins.js from clean in build-electron.sh
so the hand-maintained plugin list is not deleted. Update Podfile.lock
(TimesafariDailyNotificationPlugin 1.1.0 → 1.1.6) and electron
package-lock.json.
- Toast/notify: Keep dialog open until schedule flow finishes so success/error
$notify runs while component is mounted (fixes missing toast on Android).
Add success/error notify in edit-reminder callback (AccountViewView).
- Boot recovery: Split BootReceiver intent-filter so BOOT_COMPLETED and
LOCKED_BOOT_COMPLETED use a filter without <data>; use a separate filter
with <data scheme="package"/> for MY_PACKAGE_REPLACED/PACKAGE_REPLACED.
Boot broadcasts have no Uri and were not matching the previous filter,
so daily reminder now reschedules after device restart.
Resolves long-standing issue where the second scheduled time (after editing
the reminder) did not fire on Android.
- PushNotificationPermission: add open(..., options?: { skipSchedule }).
When skipSchedule is true (edit flow), dialog only invokes callback with
time/message; parent is sole scheduler so the plugin is not called twice.
- AccountViewView: pass { skipSchedule: true } when opening the dialog for
edit; keep cancel (iOS only) + single scheduleDailyNotification in callback.
- NativeNotificationService: serialize scheduleDailyNotification so only one
schedule runs at a time (scheduleLock + doScheduleDailyNotification).
- AccountViewView: guard edit-reminder callback with editReminderScheduleInProgress
so one schedule per user action.
- Gate pre-cancel on Android in edit flow (CONSUMING_APP brief): skip
cancelDailyNotification before schedule on Android; plugin cancels internally.
- Use single stable reminder id and always pass id on both platforms (plugin 1.1.2+).
- Add doc/plugin-android-edit-reschedule-alarm-not-firing.md for plugin repo
(cancel-before-reschedule may cancel the PendingIntent used for setAlarmClock).
- NativeNotificationService: use single stable reminder id on iOS and Android,
always pass id in scheduleOptions (plugin v1.1.2+ optional cleanup).
- Add doc/plugin-fix-scheduleExactNotification-calls.md for plugin repo
(fix Java call sites for scheduleExactNotification 8th parameter).
- package-lock.json: update lockfile.
Android second-schedule issue still present; to be revisited.
- NativeNotificationService: platform-specific schedule/cancel
- iOS: pass id "daily_timesafari_reminder", call cancelDailyReminder before schedule
- Android: no id (plugin uses "daily_notification"), skip pre-cancel to match test app
- Verification: return true when schedule succeeds but reminder not found (avoids error dialog)
- doc: android-daily-notification-second-schedule-issue.md
- Symptom, timing (re-schedule-too-soon), test app vs TimeSafari
- Plugin-side section: entry point, files, likely cause, suggested fixes, repro steps
- For use in plugin repo (e.g. Cursor) to fix second-schedule
Re-scheduled notifications on Android still fail to fire; fix expected in plugin (see doc).
Add native Android components for daily notification plugin integration:
- TimeSafariApplication: Custom Application class to register native fetcher
- TimeSafariNativeFetcher: Implements NativeNotificationContentFetcher interface
- network_security_config.xml: Allow cleartext for local development
Configuration updates:
- AndroidManifest.xml: Link custom Application class, add required permissions
- build.gradle: Add Java 17 compile options and required dependencies
- capacitor.config.ts: Add DailyNotification plugin configuration
- NativeNotificationService.ts: Use "daily_" prefixed ID for schedule rollover
Note: Subsequent notification scheduling after first fire still has issues
that require further investigation.
Co-authored-by: Cursor <cursoragent@cursor.com>
Add comprehensive guide for building and testing TimeSafari on physical
Android devices, covering device setup, USB debugging, network
configuration for local development servers, and troubleshooting.
- Create doc/android-physical-device-guide.md with full instructions
- Update BUILDING.md to reference the new guide in two places
- Add "Android Emulator Without Android Studio" section under Android Build
- Document env setup, SDK components, and API 36 system images
- Include Mac Silicon (arm64-v8a) and Intel (x86_64) AVD instructions
- Add steps: start emulator, build, install APK, and launch app
- Document one-shot build-and-run (debug:run, test:run)
- Update Prerequisites to mention command-line-only option and link to section
- Link to doc/android-emulator-deployment-guide.md for troubleshooting
--sync was calling npx cap sync ios directly, so pod install failed
on Xcode 26 (objectVersion 70). Define run_cap_sync_with_workaround
once before sync-only and use it for both --sync and the full build;
remove the duplicate definition at Step 6.6.
Plugin fix is in @timesafari/daily-notification-plugin (explicit
PendingIntent component + id in Intent). Document resolution date,
summary of the fix, and follow-up steps (npm install, cap sync,
restore-local-plugins, test on device).
PushNotificationPermission:
- Swap hour/minute number inputs and AM/PM toggle for native <input type="time">
- Add timeValue computed to keep existing hour/minute/AM-PM state in sync
- Remove unused checkHourInput and checkMinuteInput
- Tighten copy and layout: headings, labels, char count, button spacing
AccountViewView:
- Show reminder time and message in a bordered box with Time/Message labels
- Adjust spacing in notifications section
- iOS: set UNUserNotificationCenter delegate and implement willPresent
so notifications show in foreground and DailyNotificationDelivered is
posted for rollover; implement didReceive for tap handling; re-set
delegate in applicationDidBecomeActive
- Android: move DailyNotificationReceiver and BootReceiver inside
<application>; add NotifyReceiver; extend BootReceiver with
LOCKED_BOOT_COMPLETED, MY_PACKAGE_REPLACED, directBootAware
- main.capacitor: import daily-notification-plugin at startup so
plugin (and recovery) load on launch
- doc: add daily-notification-alignment-outline.md
Fixes foreground notifications not showing and rollover recovery;
Android receivers were previously declared outside <application>.
Add com.timesafari.dailynotification.fetch and com.timesafari.dailynotification.notify
to BGTaskSchedulerPermittedIdentifiers in Info.plist to resolve registration
rejection errors. The plugin was attempting to register these identifiers but
they were not declared in the app's Info.plist, causing iOS to reject the
background task registrations.
Fixes Xcode log errors:
- Registration rejected; com.timesafari.dailynotification.fetch is not advertised
- Registration rejected; com.timesafari.dailynotification.notify is not advertised
fix(notifications): Reminder Notification toggle not turning off on native platforms
The toggle wasn't turning off on iOS/Android because:
1. Checkbox was being toggled directly before dialog confirmation
2. turnOffNotifications only handles web push, not native notifications
Fixed by:
- Preventing direct checkbox toggling with readonly and event handlers
- Adding native platform handling to cancel notifications via
NotificationService.cancelDailyNotification() directly
- Bypassing web push unsubscribe flow on native platforms
Move the Daily Notification Plugin podspec fix script to execute before
the first pod install operation. Previously, the fix script ran after
pod install but before Capacitor sync, causing "No podspec found for
TimesafariDailyNotificationPlugin" errors when pod install tried to
resolve dependencies.
The Podfile expects TimesafariDailyNotificationPlugin.podspec, but the
plugin package only includes CapacitorDailyNotification.podspec. The
fix script creates the expected podspec file, but it must run before
CocoaPods attempts to resolve dependencies.
Fixes build failures on fresh installations where the podspec file
doesn't exist yet.
- Fix permission request to use correct iOS method (requestNotificationPermissions)
- Add robust handling for varying permission result formats
- Fix cancelDailyReminder to pass object parameter matching Swift plugin expectation
- Add notification cancellation before rescheduling to prevent duplicates
- Add verification after scheduling to ensure notification was actually scheduled
- Fix getStatus to handle both array and object response formats
- Enable notifications section in AccountView for native platforms (iOS/Android)
- Add edit button to allow users to modify existing notification time and message
- Add editReminderNotification method with form pre-population
- Add parseTimeTo24Hour helper for time format conversion
Fixes issues where:
- Notifications were stored but not actually scheduled with UNUserNotificationCenter
- cancelDailyReminder failed due to parameter type mismatch
- Notification time updates didn't properly cancel old notifications
- Users couldn't easily edit existing notification settings
The notification section is now visible on native platforms and includes an edit
button that opens the notification dialog with current values pre-populated.
Integrate DailyNotificationPlugin with notification UI to enable native
notifications on iOS/Android while maintaining web push for web/PWA.
- Add platform detection to PushNotificationPermission component
- Implement native notification flow via NotificationService
- Hide push server setting on native platforms (not needed)
- Add time conversion (AM/PM to 24-hour) for native plugin
- Add comprehensive documentation
Breaking Changes: None (backward compatible)
- Add podspec file for daily notification plugin with correct name
- Create TimesafariDailyNotificationPlugin.podspec to match Capacitor's
expected naming convention
- Podspec name must match Podfile reference for CocoaPods compatibility
- Update Podfile to reference TimesafariDailyNotificationPlugin
- Add automated fix script for podspec creation
- scripts/fix-daily-notification-podspec.sh creates podspec with correct
name before Capacitor sync
- Integrated into build-ios.sh build process
- Fix typo in package.json: change "pina" to "pinia" (^2.1.7)
Fixes:
- Vite build error: "Failed to resolve import 'pinia'"
- CocoaPods error: "No podspec found for 'TimesafariDailyNotificationPlugin'"
- CocoaPods error: "The name of the given podspec doesn't match the expected one"
The podspec file is created automatically during the build process to ensure
Capacitor sync can find the plugin with the expected name, while maintaining
compatibility with the actual podspec file name in the plugin package.
- Add @timesafari/daily-notification-plugin dependency from private Gitea repo
- Configure .npmrc to be ignored by git to protect authentication tokens
- Remove .npmrc from version control (contains sensitive Gitea token)
- Update package-lock.json with new dependency
The plugin is installed via git URL and automatically builds during npm install
thanks to the prepare script in the plugin repository.
Installation requires Gitea personal access token configured in local .npmrc file.
Remove JPEG conversion and preserve original image formats and filenames
to test support for all image file types. Refactor image processing logic
for clarity and maintainability.
Changes:
- Remove forced JPEG conversion (was using 0.9 compression quality)
- Preserve original filename extensions instead of forcing .jpg
- Refactor from 3 branches to 2 (removed unlikely UIImage branch)
- Replace if statement with guard for non-image attachment filtering
- Simplify error check from optional binding to nil check
- Extract filename extension helper method to reduce duplication
- Update default filename from "shared-image.jpg" to "shared-image"
The only conversion that may occur is to PNG (lossless) when raw data
cannot be read from a file URL, preserving image quality while ensuring
compatibility.
- Add SafeArea and SharedImage plugins to capacitor.plugins.json
- Create restore-local-plugins.js to persist plugins after cap sync
- Integrate plugin restoration into build scripts
- Improve Android share intent timing with retry logic
- Clean up debug logging while keeping essential error handling
- Add documentation for local plugin management
Fixes issue where SharedImage plugin wasn't recognized, causing
"UNIMPLEMENTED" errors. Image sharing now works correctly on Android.
- Fix large images not loading from share sheet (exceeded UserDefaults 4MB limit)
- Store all shared images as files in App Group container instead of base64 in UserDefaults
- Fix image refresh when sharing new image while already on SharedPhotoView
- Add route query watcher to detect _refresh parameter changes
- Remove debug logging and redundant UIImage check
The previous implementation tried to store base64-encoded images in UserDefaults,
which failed for large images (HEIF images were often 6MB+ when base64 encoded). Now all images
are stored as files in the App Group container, with only the file path stored
in UserDefaults. This supports images of any size and provides consistent
behavior regardless of format.
Also fixes an issue where sharing a new image while already viewing
SharedPhotoView wouldn't refresh the displayed image.
Replace the buggy temp file polling mechanism with direct native-to-JS
communication via custom Capacitor plugins for iOS and Android. This
eliminates race conditions, file management complexity, and polling overhead.
BREAKING CHANGE: Removes backward compatibility with temp file approach.
Android minSdkVersion upgraded from 22 to 23.
Changes:
- iOS: Created SharedImagePlugin (CAPBridgedPlugin) and SharedImageUtility
- Uses App Group UserDefaults for data sharing between extension and app
- Implements getSharedImage() and hasSharedImage() methods
- Removes temp file writing/reading logic from AppDelegate
- Android: Created SharedImagePlugin with @CapacitorPlugin annotation
- Uses SharedPreferences for data storage instead of temp files
- Implements same interface as iOS plugin
- Removes temp file handling from MainActivity
- TypeScript: Added plugin definitions and registration
- Created SharedImagePlugin interface and web fallback
- Updated main.capacitor.ts to use plugin instead of Filesystem API
- Removed pollForFileExistence() and related file I/O code
- Android: Upgraded minSdkVersion from 22 to 23
- Required for SharedPreferences improvements and better API support
Benefits:
- Eliminates race conditions from file polling
- Removes file cleanup complexity
- Direct native-to-JS communication (no file I/O)
- Better performance (no polling overhead)
- More reliable data transfer between share extension and app
- Cleaner architecture with proper plugin abstraction
Downgrade @fortawesome/free-brands-svg-icons from ^7.1.0 to ^6.5.1 to match
the version of @fortawesome/fontawesome-svg-core and other FontAwesome
packages. This resolves a TypeScript type error where IconDefinition from
the brands package was incompatible with the library.add() function due to
version mismatch.
Fix deprecation warnings and ensure future compatibility by updating
getParcelableExtra() calls to use the new API introduced in Android 13
(API 33), while maintaining backward compatibility with older versions.
Changes:
- Update getParcelableExtra() to use typed version (Uri.class) on API 33+
- Update getParcelableArrayListExtra() to use typed version on API 33+
- Add version checks with Build.VERSION.SDK_INT >= TIRAMISU
- Maintain backward compatibility for API 22-32 using deprecated API
with @SuppressWarnings("deprecation")
- No functional changes - share feature works identically across versions
The new API (getParcelableExtra(key, Class)) was introduced in API 33
to provide type safety. The old API is deprecated but still functional
on older versions, so we use runtime checks to support both.
This ensures the app builds cleanly with targetSdkVersion 36 and remains
compatible with minSdkVersion 22.
Fix share extension not appearing in share sheet on iOS 18.6 and earlier
versions by updating deployment target and resolving API availability issues.
Changes:
- Update share extension deployment target from 26.1 to 14.0
- iOS 18.6 cannot use extensions requiring iOS 26.1
- UTType API requires iOS 14.0+ (used in ShareViewController)
- Update share extension display name from "TimeSafariShareExtension" to
"TimeSafari" for cleaner appearance in share sheet
- Fix compiler warning: replace unused error binding with boolean check
(if let error = error → if error != nil)
The share extension now works on iOS 14.0+ (matching UTType requirements)
and displays properly in the share sheet on older iOS versions.
Remove unused Capacitor plugin implementation and simplify shared image
handling to use only the temp file approach, which is already working
reliably for both iOS and Android.
Changes:
- Delete ios/App/App/ShareImagePlugin.swift (unused, never registered)
- Delete ios/App/App/ShareImageBridge.swift (only used by plugin)
- Remove ~80 lines of plugin fallback code from src/main.capacitor.ts
- Simplify error handling to single code path
- Add documentation comment explaining temp file approach and future
plugin implementation possibility
Benefits:
- ~154 lines of code removed
- Single, consistent code path for both platforms
- Easier to maintain and debug
- No functional changes - temp file approach already working
The temp file approach uses Capacitor's Filesystem plugin to read shared
image data written by native code (AppDelegate on iOS, MainActivity on
Android). This is simpler and more reliable than the plugin approach,
which required registration and bridge setup.
Future improvement: Consider implementing Capacitor plugins for direct
native-to-JS communication if lower latency becomes a priority, though
the current approach performs well in production.
Implement native Android share functionality to allow users to share
images from other apps directly to TimeSafari. This mirrors the iOS
share extension functionality and provides a seamless cross-platform
experience.
Changes:
- Add ACTION_SEND and ACTION_SEND_MULTIPLE intent filters to
AndroidManifest.xml to register the app as a share target for images
- Implement share intent handling in MainActivity.java:
- Process incoming share intents in onCreate() and onNewIntent()
- Read shared image data from content URI using ContentResolver
- Convert image to Base64 encoding
- Write image data to temporary JSON file in app's internal storage
- Use getFilesDir() which maps to Capacitor's Directory.Data
- Update src/main.capacitor.ts to support Android platform:
- Extend checkAndStoreNativeSharedImage() to check for Android platform
- Use Directory.Data for Android file operations (vs Directory.Documents for iOS)
- Add Android to initial load and app state change listeners
- Ensure shared image detection works on app launch and activation
The implementation follows the same pattern as iOS:
- Native layer (MainActivity) writes shared image to temp file
- JavaScript layer polls for file existence with exponential backoff
- Image is stored in temp database and user is navigated to SharedPhotoView
This enables users to share images from Gallery, Photos, and other apps
directly to TimeSafari on Android devices.
Replace hardcoded timeout with reliable file existence polling and extract
duplicate image storage code into reusable function.
File Polling Improvements:
- Replace hardcoded 300ms timeout with pollForFileExistence() function
- Use Filesystem.stat() to check file existence instead of guessing timing
- Implement exponential backoff (100ms, 200ms, 400ms, 800ms, 1600ms)
- Maximum 5 retries with configurable parameters
- More reliable: actually checks if file exists rather than fixed wait time
Code Deduplication:
- Extract storeSharedImageInTempDB() function to eliminate code duplication
- Consolidates clearing old data, base64-to-dataURL conversion, and DB storage
- Used in both temp file and plugin fallback code paths
- Improves maintainability and reduces risk of inconsistencies
This makes the code more robust and maintainable while ensuring the file
is actually available before attempting to read it.
Improve iOS share extension implementation to skip interstitial UI and
fix issues with subsequent image shares not updating the view.
iOS Share Extension Improvements:
- Replace SLComposeServiceViewController with custom UIViewController
to eliminate interstitial "Post" button UI
- Use minimal URL (timesafari://) instead of deep link for app launch
- Implement app lifecycle detection via Capacitor appStateChange listener
instead of relying solely on deep links
Deep Link and Navigation Fixes:
- Remove "shared-photo" from deep link schemas (no longer needed)
- Add empty path URL handling for share extension launches
- Implement processing lock to prevent duplicate image processing
- Add retry mechanism (300ms delay) to handle race conditions with
AppDelegate writing temp files
- Use router.replace() when already on /shared-photo route to force refresh
- Clear old images from temp DB before storing new ones
- Delete temp file immediately after reading to prevent stale data
SharedPhotoView Component:
- Add route watcher (@Watch) to reload image when fileName query
parameter changes
- Extract image loading logic into reusable loadSharedImage() method
- Improve error handling to clear image state on failures
This fixes the issue where sharing a second image while already on
SharedPhotoView would display the previous image instead of the new one.
Implement iOS Share Extension to enable native image sharing from Photos
and other apps directly into TimeSafari. Users can now share images from
the iOS share sheet, which will open in SharedPhotoView for use as gifts
or profile pictures.
iOS Native Implementation:
- Add TimeSafariShareExtension target with ShareViewController
- Configure App Groups for data sharing between extension and main app
- Implement ShareViewController to process shared images and convert to base64
- Store shared image data in App Group UserDefaults
- Add ShareImageBridge utility to read shared data from App Group
- Update AppDelegate to handle shared-photo deep link and bridge data to JS
JavaScript Integration:
- Add checkAndStoreNativeSharedImage() in main.capacitor.ts to read shared
images from native layer via temporary file bridge
- Convert base64 data to data URL format for compatibility with base64ToBlob
- Integrate with existing SharedPhotoView component
- Add "shared-photo" to deep link validation schema
Build System:
- Integrate Xcode 26 / CocoaPods compatibility workaround into build-ios.sh
- Add run_pod_install_with_workaround() for explicit pod install
- Add run_cap_sync_with_workaround() for Capacitor sync (which runs pod
install internally)
- Automatically detect project format version 70 and apply workaround
- Remove standalone pod-install-workaround.sh script
Code Cleanup:
- Remove verbose debug logs from ShareViewController, AppDelegate, and
main.capacitor.ts
- Retain essential logger calls for production debugging
Documentation:
- Add ios-share-extension-setup.md with manual Xcode setup instructions
- Add ios-share-extension-git-commit-guide.md for version control best practices
- Add ios-share-implementation-status.md tracking implementation progress
- Add native-share-target-implementation.md with overall architecture
- Add xcode-26-cocoapods-workaround.md documenting the compatibility issue
The implementation uses a temporary file bridge (AppDelegate writes to Documents
directory, JS reads via Capacitor Filesystem plugin) as a workaround for
Capacitor plugin auto-discovery issues. This can be improved in the future by
properly registering ShareImagePlugin in Capacitor's plugin registry.