Compare commits
10
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a03ed849c4 | ||
|
|
6183afc29c | ||
|
|
1afe5461b2 | ||
|
|
4bb0e4d4f9 | ||
|
|
2fd5549098 | ||
|
|
28ee81b9d3 | ||
|
|
38af28154e | ||
|
|
70372a113a | ||
|
|
9e88cf056c | ||
|
|
db13332dd6 |
@@ -13,7 +13,7 @@ This guard is in `android/app/build.gradle`. Do NOT change this conditional to a
|
|||||||
|
|
||||||
`google-services.json` is intentionally excluded from git (`android/.gitignore`). Never commit it.
|
`google-services.json` is intentionally excluded from git (`android/.gitignore`). Never commit it.
|
||||||
|
|
||||||
Full details, incident history, and F-Droid notes: `doc/development/android-firebase-gms.md`
|
Full details, incident history, and F-Droid notes: `doc/android-firebase-gms.md`
|
||||||
|
|
||||||
## Android Build — MLKit Barcode Scanner
|
## Android Build — MLKit Barcode Scanner
|
||||||
|
|
||||||
|
|||||||
+44
-22
@@ -1084,6 +1084,11 @@ npm run build:ios:assets # Generate assets only
|
|||||||
npm run build:ios:deploy # Build and deploy to connected device
|
npm run build:ios:deploy # Build and deploy to connected device
|
||||||
```
|
```
|
||||||
|
|
||||||
|
The iOS asset command reads source artwork from `resources/` explicitly. It
|
||||||
|
generates the default 1024×1024 App Store icon, from which Xcode derives the
|
||||||
|
required device sizes, and installs any available appearance variants under
|
||||||
|
`ios/App/App/Assets.xcassets/AppIcon.appiconset/`.
|
||||||
|
|
||||||
#### Automated Build Script
|
#### Automated Build Script
|
||||||
|
|
||||||
The recommended way to build for iOS is using the automated build script:
|
The recommended way to build for iOS is using the automated build script:
|
||||||
@@ -1127,7 +1132,7 @@ If you need to build manually or want to understand the individual steps:
|
|||||||
|
|
||||||
##### 0. First time (or if dependencies change)
|
##### 0. First time (or if dependencies change)
|
||||||
|
|
||||||
- `pkgx +rubygems.org +pod zsh`
|
- `pkgx +rubygems.org +pod /bin/zsh`
|
||||||
|
|
||||||
- ... and you may have to fix these, especially with pkgx:
|
- ... and you may have to fix these, especially with pkgx:
|
||||||
|
|
||||||
@@ -1141,7 +1146,7 @@ export GEM_PATH=$shortened_path
|
|||||||
##### 1. Bump the version in package.json & CHANGELOG.md for `MARKETING_VERSION`, then `grep CURRENT_PROJECT_VERSION ios/App/App.xcodeproj/project.pbxproj` and add 1 for the numbered version here:
|
##### 1. Bump the version in package.json & CHANGELOG.md for `MARKETING_VERSION`, then `grep CURRENT_PROJECT_VERSION ios/App/App.xcodeproj/project.pbxproj` and add 1 for the numbered version here:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
cd ios/App && xcrun agvtool new-version 70 && perl -p -i -e "s/MARKETING_VERSION = .*;/MARKETING_VERSION = 1.4.4;/g" App.xcodeproj/project.pbxproj && cd -
|
cd ios/App && xcrun agvtool new-version 72 && perl -p -i -e "s/MARKETING_VERSION = .*;/MARKETING_VERSION = 1.4.6;/g" App.xcodeproj/project.pbxproj && cd -
|
||||||
# Unfortunately this edits Info.plist directly.
|
# Unfortunately this edits Info.plist directly.
|
||||||
#xcrun agvtool new-marketing-version 0.4.5
|
#xcrun agvtool new-marketing-version 0.4.5
|
||||||
```
|
```
|
||||||
@@ -1162,14 +1167,20 @@ npm run build:ios:prod
|
|||||||
|
|
||||||
3.2. Use Xcode to release.
|
3.2. Use Xcode to release.
|
||||||
|
|
||||||
- Someday: Under "General" we want to rename a bunch of things to "Time Safari"
|
- Someday: Under "General" we want to rename a bunch of things to "Giftopia"
|
||||||
- Choose Product -> Destination -> Any iOS Device
|
- Choose Product -> Destination -> Any iOS Device
|
||||||
- Choose Product -> Archive
|
- Choose Product -> Archive
|
||||||
- This will trigger a build and take time, needing user's "login" keychain
|
- This will trigger a build and take time, needing user's "login" keychain
|
||||||
password (user's login password), repeatedly.
|
password (user's login password), repeatedly.
|
||||||
|
- Required password entry 26 times.
|
||||||
|
- It can get stuck on uploading SPI; try canceling and restarting.
|
||||||
- If it fails with `building for 'iOS', but linking in dylib
|
- If it fails with `building for 'iOS', but linking in dylib
|
||||||
(.../.pkgx/zlib.net/v1.3.0/lib/libz.1.3.dylib) built for 'macOS'` then run
|
(.../.pkgx/zlib.net/v1.3.0/lib/libz.1.3.dylib) built for 'macOS'` then run
|
||||||
XCode outside that terminal (ie. not with `npx cap open ios`).
|
XCode outside that terminal (ie. not with `npx cap open ios`).
|
||||||
|
- Before distributing, confirm the archive carries production server
|
||||||
|
defaults. Xcode packages whatever web bundle is in `ios/App/App/public`,
|
||||||
|
even one from a `--test` build:
|
||||||
|
`./scripts/check-prod-bundle.sh ios/App/App/public`
|
||||||
- Click Distribute -> App Store Connect
|
- Click Distribute -> App Store Connect
|
||||||
- In AppStoreConnect, add the build to the distribution. You may have to remove
|
- In AppStoreConnect, add the build to the distribution. You may have to remove
|
||||||
the current build with the "-" when you hover over it, then "Add Build" with the
|
the current build with the "-" when you hover over it, then "Add Build" with the
|
||||||
@@ -1319,6 +1330,7 @@ npm run build:android:prod # Production build (builds for production e
|
|||||||
# Auto-run builds
|
# Auto-run builds
|
||||||
npm run build:android:test:run # Test build with auto-run (builds then runs on emulator)
|
npm run build:android:test:run # Test build with auto-run (builds then runs on emulator)
|
||||||
npm run build:android:prod:run # Production build with auto-run (builds then runs on emulator)
|
npm run build:android:prod:run # Production build with auto-run (builds then runs on emulator)
|
||||||
|
# Auto-run targets the one connected device; with none or several it prompts, or exits 9 without a terminal
|
||||||
|
|
||||||
# Debug and release builds
|
# Debug and release builds
|
||||||
npm run build:android:debug # Debug build (builds debug APK)
|
npm run build:android:debug # Debug build (builds debug APK)
|
||||||
@@ -1422,8 +1434,8 @@ The recommended way to build for Android is using the automated build script:
|
|||||||
##### 1. Bump the version in package.json, then update these versions & run:
|
##### 1. Bump the version in package.json, then update these versions & run:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
perl -p -i -e 's/versionCode .*/versionCode 70/g' android/app/build.gradle
|
perl -p -i -e 's/versionCode .*/versionCode 72/g' android/app/build.gradle
|
||||||
perl -p -i -e 's/versionName .*/versionName "1.4.4"/g' android/app/build.gradle
|
perl -p -i -e 's/versionName .*/versionName "1.4.6"/g' android/app/build.gradle
|
||||||
```
|
```
|
||||||
|
|
||||||
##### 2. Build
|
##### 2. Build
|
||||||
@@ -1442,35 +1454,38 @@ npx cap open android
|
|||||||
|
|
||||||
##### 4. Use Android Studio to build and run on emulator or device
|
##### 4. Use Android Studio to build and run on emulator or device
|
||||||
|
|
||||||
## Android Build from the console
|
#### Android Build from the console
|
||||||
|
|
||||||
```bash
|
`cd android`
|
||||||
cd android
|
|
||||||
./gradlew clean
|
|
||||||
./gradlew build -Dlint.baselines.continue=true
|
|
||||||
cd -
|
|
||||||
```
|
|
||||||
|
|
||||||
... or, to create the `aab` file, `bundle` instead of `build`:
|
* Note that this currently fails: `./gradlew build`
|
||||||
|
|
||||||
|
To create the `aab` file, `bundle` instead of `build` (but not signed and not usable in Play Store):
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
./gradlew bundleDebug -Dlint.baselines.continue=true
|
./gradlew bundleDebug -Dlint.baselines.continue=true
|
||||||
```
|
```
|
||||||
|
|
||||||
... or, to create a signed release:
|
So, to create a signed `aab` release:
|
||||||
|
|
||||||
- Setup by adding the app/gradle.properties.secrets file (see properties at top
|
- Setup by adding the app/gradle.properties.secrets file (see properties at top
|
||||||
of app/build.gradle) and the app/time-safari-upload-key-pkcs12.jks file
|
of app/build.gradle) and the app/time-safari-upload-key-pkcs12.jks file
|
||||||
- Then `bundleRelease`:
|
- Then `bundleRelease` or `bundleDebug`:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
cd android
|
# if you use pkgx: pkgx +openjdk.org /bin/zsh
|
||||||
./gradlew bundleRelease -Dlint.baselines.continue=true -PfirebaseEnabled
|
./gradlew bundleRelease -Dlint.baselines.continue=true -PfirebaseEnabled
|
||||||
cd -
|
|
||||||
```
|
```
|
||||||
|
|
||||||
... and find your `aab` file at app/build/outputs/bundle/release
|
... and find your `aab` file at app/build/outputs/bundle/release
|
||||||
|
|
||||||
|
* Gradle packages whatever web bundle was last synced into
|
||||||
|
`android/app/src/main/assets/public`, even one from a `--test` build. Before
|
||||||
|
uploading, confirm it has production server defaults:
|
||||||
|
`./scripts/check-prod-bundle.sh android/app/build/outputs/bundle/release/app-release.aab`
|
||||||
|
* `build-android.sh` runs `./gradlew clean`, which deletes any earlier `aab`, so
|
||||||
|
run `bundleRelease` after the build script finishes.
|
||||||
|
|
||||||
* Note that F-Droid builds should omit `-PfirebaseEnabled`.
|
* Note that F-Droid builds should omit `-PfirebaseEnabled`.
|
||||||
|
|
||||||
At play.google.com/console:
|
At play.google.com/console:
|
||||||
@@ -1607,10 +1622,13 @@ Use the commands above to check and fix code quality issues.
|
|||||||
4. **Native Build**: Platform-specific compilation
|
4. **Native Build**: Platform-specific compilation
|
||||||
5. **Package Creation**: APK/IPA generation
|
5. **Package Creation**: APK/IPA generation
|
||||||
|
|
||||||
`resources/` is the canonical source for app icons and splash screens.
|
`resources/` is the canonical source for app icons and splash screens. Build
|
||||||
Do not keep a legacy top-level `assets/` directory unless it is intentionally
|
scripts select it explicitly with `--assetPath resources`. Use the same option
|
||||||
used: `@capacitor/assets` prioritizes `assets/` over `resources/`, which can
|
when invoking the generator directly:
|
||||||
prevent the canonical assets from being discovered.
|
|
||||||
|
```bash
|
||||||
|
npx capacitor-assets generate --assetPath resources
|
||||||
|
```
|
||||||
|
|
||||||
## Architecture Environment Configuration
|
## Architecture Environment Configuration
|
||||||
|
|
||||||
@@ -1822,7 +1840,7 @@ npm run build:android:assets
|
|||||||
- Configure signing keys
|
- Configure signing keys
|
||||||
- Check device/emulator setup
|
- Check device/emulator setup
|
||||||
|
|
||||||
## Additional Resources
|
## Additional Documentation
|
||||||
|
|
||||||
- [Electron Build Patterns](doc/electron-build-patterns.md)
|
- [Electron Build Patterns](doc/electron-build-patterns.md)
|
||||||
- [iOS Build Scripts](doc/ios-build-scripts.md)
|
- [iOS Build Scripts](doc/ios-build-scripts.md)
|
||||||
@@ -2212,6 +2230,8 @@ command chaining, following DRY principles.
|
|||||||
- `7` - Asset generation failed
|
- `7` - Asset generation failed
|
||||||
- `8` - Android Studio launch failed
|
- `8` - Android Studio launch failed
|
||||||
- `9` - Resource check failed
|
- `9` - Resource check failed
|
||||||
|
- `10` - Production bundle has test/dev server defaults (`--prod` only; see
|
||||||
|
`scripts/check-prod-bundle.sh`)
|
||||||
|
|
||||||
### A.4 build-ios.sh
|
### A.4 build-ios.sh
|
||||||
|
|
||||||
@@ -2272,6 +2292,8 @@ command chaining, following DRY principles.
|
|||||||
- **Clean Build**: Removes Xcode build artifacts and DerivedData
|
- **Clean Build**: Removes Xcode build artifacts and DerivedData
|
||||||
- **Asset Generation**: Creates platform-specific assets
|
- **Asset Generation**: Creates platform-specific assets
|
||||||
- **Simulator Support**: Launches iOS Simulator for testing
|
- **Simulator Support**: Launches iOS Simulator for testing
|
||||||
|
- **Production Bundle Check**: With `--prod`, exits with code `10` if the synced
|
||||||
|
web bundle has test/dev server defaults (`scripts/check-prod-bundle.sh`)
|
||||||
|
|
||||||
### A.5 common.sh
|
### A.5 common.sh
|
||||||
|
|
||||||
|
|||||||
+7
-2
@@ -6,13 +6,18 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
|
|||||||
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
||||||
|
|
||||||
|
|
||||||
## [?] - 2026
|
## [1.4.6] - 2026.09.27
|
||||||
|
### Fixed
|
||||||
|
- Incorrect 'test' server URLs on production.
|
||||||
|
|
||||||
|
|
||||||
|
## [1.4.5] - 2026.09.24
|
||||||
### Added
|
### Added
|
||||||
- Full flow for setting up SMS notifications: phone registration and code
|
- Full flow for setting up SMS notifications: phone registration and code
|
||||||
verification, delegated alertSearch JWT batches with a send hour, and
|
verification, delegated alertSearch JWT batches with a send hour, and
|
||||||
revocation to stop the texts
|
revocation to stop the texts
|
||||||
- Notification debug panel action to mint and upload a push-channel alertSearch
|
- Notification debug panel action to mint and upload a push-channel alertSearch
|
||||||
authorization, built on the same batch minter as the SMS channel
|
authorization, built on the same batch minter as the SMS channel (hidden by dev flag)
|
||||||
### Fixed
|
### Fixed
|
||||||
- Native build scripts set NODE_ENV, so `import.meta.env.DEV` matches the build
|
- Native build scripts set NODE_ENV, so `import.meta.env.DEV` matches the build
|
||||||
mode instead of always reporting a production build
|
mode instead of always reporting a production build
|
||||||
|
|||||||
@@ -121,7 +121,7 @@ See [Logging Configuration Guide](doc/logging-configuration.md) for complete det
|
|||||||
|
|
||||||
## Notification Debug Panel (dev builds)
|
## Notification Debug Panel (dev builds)
|
||||||
|
|
||||||
In non-production bundles (for example `vite dev` or a Vite build whose mode is not `production`), the **Notification Debug Panel** at `/dev/notifications` helps you test FCM token registration, AlertSearch authorization upload, FCM wakeup delivery (`/debug/send-wakeup`), and local schedule inspection on native builds. The app no longer consumes `WAKEUP_PING` to call `/notifications/refresh` or schedule `api_*` notifications.
|
In non-production bundles (for example `vite dev` or a Vite build whose mode is not `production`), the **Notification Debug Panel** at `/dev/notifications` helps you test notification registration, backend refresh, WAKEUP_PING handling, and local schedule inspection on native builds.
|
||||||
|
|
||||||
**Access:** **Account** → enable **Show All General Advanced Functions** → **Notification Debug Panel**.
|
**Access:** **Account** → enable **Show All General Advanced Functions** → **Notification Debug Panel**.
|
||||||
|
|
||||||
|
|||||||
@@ -37,8 +37,8 @@ android {
|
|||||||
applicationId "app.timesafari.app"
|
applicationId "app.timesafari.app"
|
||||||
minSdkVersion rootProject.ext.minSdkVersion
|
minSdkVersion rootProject.ext.minSdkVersion
|
||||||
targetSdkVersion rootProject.ext.targetSdkVersion
|
targetSdkVersion rootProject.ext.targetSdkVersion
|
||||||
versionCode 70
|
versionCode 72
|
||||||
versionName "1.4.4"
|
versionName "1.4.6"
|
||||||
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
|
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
|
||||||
aaptOptions {
|
aaptOptions {
|
||||||
// Files and dirs to omit from the packaged assets dir, modified to accommodate modern web apps.
|
// Files and dirs to omit from the packaged assets dir, modified to accommodate modern web apps.
|
||||||
|
|||||||
@@ -54,5 +54,9 @@
|
|||||||
{
|
{
|
||||||
"pkg": "SharedImage",
|
"pkg": "SharedImage",
|
||||||
"classpath": "app.timesafari.sharedimage.SharedImagePlugin"
|
"classpath": "app.timesafari.sharedimage.SharedImagePlugin"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"pkg": "NotificationInspector",
|
||||||
|
"classpath": "app.timesafari.notifications.NotificationInspectorPlugin"
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
|||||||
+17
-1
@@ -1,5 +1,6 @@
|
|||||||
package app.timesafari.notifications;
|
package app.timesafari.notifications;
|
||||||
|
|
||||||
|
import com.getcapacitor.JSObject;
|
||||||
import com.getcapacitor.Plugin;
|
import com.getcapacitor.Plugin;
|
||||||
import com.getcapacitor.PluginCall;
|
import com.getcapacitor.PluginCall;
|
||||||
import com.getcapacitor.PluginMethod;
|
import com.getcapacitor.PluginMethod;
|
||||||
@@ -12,5 +13,20 @@ public class NotificationInspectorPlugin extends Plugin {
|
|||||||
call.unimplemented(
|
call.unimplemented(
|
||||||
"Pending notification inspection is currently implemented on iOS only");
|
"Pending notification inspection is currently implemented on iOS only");
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Whether this build carries a Firebase config, i.e. was built with
|
||||||
|
* -PfirebaseEnabled. Without it, PushNotifications.register() throws on the
|
||||||
|
* Capacitor plugin thread and kills the app, so JS must check this first.
|
||||||
|
* The google-services Gradle plugin generates the google_app_id resource,
|
||||||
|
* which is what Firebase's own auto-initialization looks for.
|
||||||
|
*/
|
||||||
|
@PluginMethod
|
||||||
|
public void isFirebaseConfigured(PluginCall call) {
|
||||||
|
int resId = getContext().getResources().getIdentifier(
|
||||||
|
"google_app_id", "string", getContext().getPackageName());
|
||||||
|
JSObject ret = new JSObject();
|
||||||
|
ret.put("configured", resId != 0);
|
||||||
|
call.resolve(ret);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -42,6 +42,16 @@ This means a single Play Store AAB (`bundleRelease -PfirebaseEnabled`) covers bo
|
|||||||
|
|
||||||
**F-Droid** is stricter: their build policy rejects any APK with GMS dependencies at the binary level, even with graceful degradation. F-Droid submission would require a separate `assembleRelease` build (no flag) and a dedicated F-Droid listing.
|
**F-Droid** is stricter: their build policy rejects any APK with GMS dependencies at the binary level, even with graceful degradation. F-Droid submission would require a separate `assembleRelease` build (no flag) and a dedicated F-Droid listing.
|
||||||
|
|
||||||
|
## Push registration in builds without Firebase
|
||||||
|
|
||||||
|
A build without `-PfirebaseEnabled` contains the `@capacitor/push-notifications` plugin, but no Firebase config. In that build, `PushNotifications.register()` throws `Default FirebaseApp is not initialized` on the Capacitor plugin thread, which kills the app; a JS `try/catch` cannot intercept it.
|
||||||
|
|
||||||
|
The JS therefore asks native code first. `NotificationInspector.isFirebaseConfigured()` (`android/app/src/main/java/app/timesafari/notifications/NotificationInspectorPlugin.java`) reports whether the `google_app_id` string resource exists; the google-services Gradle plugin generates that resource only when Firebase is enabled, and it is the same signal Firebase's auto-initialization uses. `firebaseMessagingClient.ts` skips `register()` on Android when it is absent (or when the check fails), logging `No Firebase config in this Android build; skipping push registration`. Listener setup and the notification permission request run either way, since local daily notifications need that permission.
|
||||||
|
|
||||||
|
`NotificationInspector` must be listed in `scripts/restore-local-plugins.js`: `MainActivity` registers plugins after the Capacitor bridge is built, so a plugin missing from `capacitor.plugins.json` reports "not implemented on android" and the check falls back to skipping push.
|
||||||
|
|
||||||
|
Any code that calls `PushNotifications.register()` or other `FirebaseMessaging` APIs on Android must go through the same check.
|
||||||
|
|
||||||
## The `google-services.json` file
|
## The `google-services.json` file
|
||||||
|
|
||||||
- Gitignored (`android/.gitignore` line 80) — never commit it
|
- Gitignored (`android/.gitignore` line 80) — never commit it
|
||||||
@@ -64,3 +74,9 @@ This is an accepted trade-off. Removing it would require either forking the plug
|
|||||||
Jose Olarte III's `notify-api` branch placed a production `google-services.json` in `android/` to test Firebase Cloud Messaging. The branch was never merged to `master`, but because the file is gitignored it persisted on disk after switching branches. At the time, the Gradle conditional activated Firebase based on file presence alone (no opt-in flag), so all subsequent local builds embedded Firebase and required Google Play Services. This silently broke APK/Aurora/Zapstore distribution.
|
Jose Olarte III's `notify-api` branch placed a production `google-services.json` in `android/` to test Firebase Cloud Messaging. The branch was never merged to `master`, but because the file is gitignored it persisted on disk after switching branches. At the time, the Gradle conditional activated Firebase based on file presence alone (no opt-in flag), so all subsequent local builds embedded Firebase and required Google Play Services. This silently broke APK/Aurora/Zapstore distribution.
|
||||||
|
|
||||||
**Fix applied:** deleted `google-services.json` from disk, changed the Gradle conditional to require `-PfirebaseEnabled`, and documented the rule in `AGENTS.md`.
|
**Fix applied:** deleted `google-services.json` from disk, changed the Gradle conditional to require `-PfirebaseEnabled`, and documented the rule in `AGENTS.md`.
|
||||||
|
|
||||||
|
## Incident: September 2026
|
||||||
|
|
||||||
|
After the June fix, local builds without `-PfirebaseEnabled` (including every `scripts/build-android.sh` build and the FOSS APKs from v1.4.4 on) crashed at launch as soon as notification permission was granted, because startup code called `PushNotifications.register()` unconditionally. Found while running `npm run build:android:prod:run` on an emulator.
|
||||||
|
|
||||||
|
**Fix applied:** the `isFirebaseConfigured()` gate described in "Push registration in builds without Firebase", plus adding `NotificationInspector` to `scripts/restore-local-plugins.js` so the check is reachable on Android.
|
||||||
|
|||||||
@@ -2,10 +2,10 @@
|
|||||||
|
|
||||||
**Created:** 2026-06-02
|
**Created:** 2026-06-02
|
||||||
**Source document:** [local-ios-testing-ngrok.md](./local-ios-testing-ngrok.md)
|
**Source document:** [local-ios-testing-ngrok.md](./local-ios-testing-ngrok.md)
|
||||||
**Status:** Planning snapshot from 2026-06-02. Several iOS-guide assumptions (app **Refresh Notifications**, Simulate WAKEUP, `applyNotificationRefreshPayload`, WAKEUP_PING → `/notifications/refresh` → `api_*`) are **retired**. For current Android procedure see [local-android-testing-ngrok.md](./local-android-testing-ngrok.md). Do not treat the reuse tables below as the live app path.
|
|
||||||
|
|
||||||
**Purpose:** Plan a future **Android** counterpart guide by mapping what can be reused from the iOS ngrok workflow and what must be written for Android-specific push, permissions, and OS behavior.
|
**Purpose:** Plan a future **Android** counterpart guide by mapping what can be reused from the iOS ngrok workflow and what must be written for Android-specific push, permissions, and OS behavior.
|
||||||
|
|
||||||
|
**Status:** Planning only — does not replace or modify the iOS guide.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Executive summary
|
## Executive summary
|
||||||
|
|||||||
@@ -1,10 +1,8 @@
|
|||||||
# Local Android Testing with ngrok (notification-wakeup-service)
|
# Local Android Testing with ngrok (notification-wakeup-service)
|
||||||
|
|
||||||
**Last updated:** 2026-09-24 (retired WAKEUP_PING → refresh → `api_*` consumption)
|
**Last updated:** 2026-06-02 (verification checklist, end-to-end test)
|
||||||
**Audience:** Developers on **crowd-funder-for-time-pwa**, **daily-notification-plugin**, and **notification-wakeup-service**
|
**Audience:** Developers on **crowd-funder-for-time-pwa**, **daily-notification-plugin**, and **notification-wakeup-service**
|
||||||
**Goal:** Exercise FCM token registration and FCM wakeup **delivery** (`WAKEUP_PING` via `/debug/send-wakeup`) against a Mac-hosted backend reachable from a physical Android device.
|
**Goal:** Exercise FCM wakeup (`WAKEUP_PING`), FCM token registration, and notification refresh against a Mac-hosted backend reachable from a physical Android device.
|
||||||
|
|
||||||
> **Retired (do not expect this in the app):** `WAKEUP_PING` → `POST /notifications/refresh` → `applyNotificationRefreshPayload()` → `api_*` local schedules. The app logs ignored push types and does not refresh or schedule from wakeup. `/debug/send-wakeup` remains an FCM diagnostic. Mac `curl` of `/notifications/refresh` only tests the backend. Local schedules come from Static Daily Reminder, New Activity / dual, and the native fetcher.
|
|
||||||
|
|
||||||
**See also:** [local-ios-testing-ngrok.md](./local-ios-testing-ngrok.md) for the iOS workflow (APNs, Xcode). [android-physical-device-guide.md](./android-physical-device-guide.md) for USB debugging and build/run commands.
|
**See also:** [local-ios-testing-ngrok.md](./local-ios-testing-ngrok.md) for the iOS workflow (APNs, Xcode). [android-physical-device-guide.md](./android-physical-device-guide.md) for USB debugging and build/run commands.
|
||||||
|
|
||||||
@@ -12,7 +10,7 @@
|
|||||||
|
|
||||||
## Architecture overview
|
## Architecture overview
|
||||||
|
|
||||||
End-to-end flow when testing FCM registration and wakeup **delivery** on a physical Android phone:
|
End-to-end flow when testing New Activity / silent wake on a physical Android phone:
|
||||||
|
|
||||||
```text
|
```text
|
||||||
┌─────────────────────┐ HTTPS ┌──────────────────────┐
|
┌─────────────────────┐ HTTPS ┌──────────────────────┐
|
||||||
@@ -21,7 +19,7 @@ End-to-end flow when testing FCM registration and wakeup **delivery** on a physi
|
|||||||
│ wakeup-service │ └──────────┬───────────┘
|
│ wakeup-service │ └──────────┬───────────┘
|
||||||
└──────────┬──────────┘ │
|
└──────────┬──────────┘ │
|
||||||
│ │ fetch
|
│ │ fetch
|
||||||
│ │ POST /notifications/register
|
│ POST /notifications/refresh │ POST /notifications/register
|
||||||
│ ▼
|
│ ▼
|
||||||
│ ┌──────────────────────┐
|
│ ┌──────────────────────┐
|
||||||
│ │ crowd-funder-for- │
|
│ │ crowd-funder-for- │
|
||||||
@@ -30,7 +28,7 @@ End-to-end flow when testing FCM registration and wakeup **delivery** on a physi
|
|||||||
│ └──────────┬───────────┘
|
│ └──────────┬───────────┘
|
||||||
│ │
|
│ │
|
||||||
│ FCM data message (WAKEUP_PING) │ daily-notification-plugin
|
│ FCM data message (WAKEUP_PING) │ daily-notification-plugin
|
||||||
▼ ▼ (Daily Reminder / dual / fetcher)
|
▼ ▼ (local schedule replace)
|
||||||
┌─────────────────────┐ ┌──────────────────────┐
|
┌─────────────────────┐ ┌──────────────────────┐
|
||||||
│ Firebase Cloud │ ──FCM────────► │ Android device │
|
│ Firebase Cloud │ ──FCM────────► │ Android device │
|
||||||
│ Messaging │ direct │ app.timesafari.app │
|
│ Messaging │ direct │ app.timesafari.app │
|
||||||
@@ -43,16 +41,18 @@ Unlike iOS, Android does **not** use APNs. FCM delivers directly to the app via
|
|||||||
|
|
||||||
| Repo | Role |
|
| Repo | Role |
|
||||||
|------|------|
|
|------|------|
|
||||||
| **notification-wakeup-service** | HTTP API: device registration, health, debug wakeup send; may still expose `/notifications/refresh` for the backend |
|
| **notification-wakeup-service** | HTTP API: device registration, refresh payload (`nextNotifications`), health, debug wakeup send |
|
||||||
| **crowd-funder-for-time-pwa** | Capacitor app: FCM token, `POST /notifications/register`; logs `WAKEUP_PING` without refresh/`api_*` scheduling |
|
| **crowd-funder-for-time-pwa** | Capacitor app: FCM token, `POST /notifications/register` & `/refresh`, handles `WAKEUP_PING` push |
|
||||||
| **daily-notification-plugin** | Native Android: Daily Reminder, New Activity / dual, native fetcher; Phase 4 `clearApiNotifications()` |
|
| **daily-notification-plugin** | Native Android: clear + reschedule local notifications from refresh timestamps |
|
||||||
|
|
||||||
### Android wakeup flow (current)
|
### Android wakeup flow (production path)
|
||||||
|
|
||||||
1. **notification-wakeup-service** `/debug/send-wakeup` sends an FCM **data** message with `data.type = "WAKEUP_PING"`.
|
1. **notification-wakeup-service** (or `/debug/send-wakeup`) sends an FCM **data** message with `data.type = "WAKEUP_PING"`.
|
||||||
2. FCM delivers to the device (best-effort; see [Android Platform Notes](#8-android-platform-notes) and [Battery Optimization Caveats](#9-battery-optimization-caveats)).
|
2. FCM delivers to the device (best-effort; see [Android Platform Notes](#8-android-platform-notes) and [Battery Optimization Caveats](#9-battery-optimization-caveats)).
|
||||||
3. Capacitor `pushNotificationReceived` fires → `handleCapacitorPushNotificationReceived()` in `NativeNotificationService.ts`.
|
3. Capacitor `pushNotificationReceived` fires → `handleCapacitorPushNotificationReceived()` in `NativeNotificationService.ts`.
|
||||||
4. Handler logs `[Notifications] push handler ignored type=WAKEUP_PING`. It does **not** POST `/notifications/refresh` or schedule `api_*` notifications.
|
4. Handler calls `refreshNotificationsWithDiagnostics({ source: "WAKEUP_PING" })`, which `POST`s `{backend}/notifications/refresh` with `testMode` from the debug config.
|
||||||
|
5. Backend returns `nextNotifications: [{ timestamp }, ...]`.
|
||||||
|
6. App calls `applyNotificationRefreshPayload()` → **daily-notification-plugin** clears existing local alarms and schedules new ones.
|
||||||
|
|
||||||
Console and debug panel lines are prefixed with **`[Notifications]`** (see `NotificationDebugEvents.ts`).
|
Console and debug panel lines are prefixed with **`[Notifications]`** (see `NotificationDebugEvents.ts`).
|
||||||
|
|
||||||
@@ -290,11 +290,12 @@ For a full panel reference (configuration, URL resolution order, authentication,
|
|||||||
| Control | Purpose |
|
| Control | Purpose |
|
||||||
|---------|---------|
|
|---------|---------|
|
||||||
| **Notification Backend URL** | Paste ngrok HTTPS URL → **Save Backend URL** (changes target server only) |
|
| **Notification Backend URL** | Paste ngrok HTTPS URL → **Save Backend URL** (changes target server only) |
|
||||||
| **Test Mode** | Sends `testMode: true/false` in register / send-wakeup JSON bodies (default on when unset in storage) |
|
| **Test Mode** | Sends `testMode: true/false` in register/refresh JSON bodies (default on when unset in storage) |
|
||||||
| **Skip JWT Authentication (Local Development Only)** | When on, omits `Authorization` headers for local servers that accept unauthenticated requests (default **off**) |
|
| **Skip JWT Authentication (Local Development Only)** | When on, omits `Authorization` headers for local servers that accept unauthenticated requests (default **off**) |
|
||||||
| **Register Token Now** | `POST /notifications/register` with current FCM token and `platform: "android"` |
|
| **Register Token Now** | `POST /notifications/register` with current FCM token and `platform: "android"` |
|
||||||
| **Upload AlertSearch Authorization** | Uploads AlertSearch delegated JWTs (`/notifications/alert-authorization`) |
|
| **Refresh Notifications** | `POST /notifications/refresh` (same as post-wakeup flow) |
|
||||||
| **Send Real WAKEUP_PING** | `POST /debug/send-wakeup`; FCM delivery diagnostic only (no app-side refresh/`api_*` scheduling) |
|
| **Simulate WAKEUP_PING (Local)** | Calls refresh API directly (no FCM) — quick backend + ngrok test |
|
||||||
|
| **Send Real WAKEUP_PING** | `POST /debug/send-wakeup` on the backend override URL; server sends a real FCM `WAKEUP_PING` to the panel’s current FCM token (see below) |
|
||||||
| **Event Log** | Shared `[Notifications]` panel log (100 entries) |
|
| **Event Log** | Shared `[Notifications]` panel log (100 entries) |
|
||||||
|
|
||||||
Persistence: `localStorage` keys `notificationDebug.backendBaseUrl`, `notificationDebug.testMode`, and `notificationDebug.bypassAuth` (`NotificationDebugConfig.ts`).
|
Persistence: `localStorage` keys `notificationDebug.backendBaseUrl`, `notificationDebug.testMode`, and `notificationDebug.bypassAuth` (`NotificationDebugConfig.ts`).
|
||||||
@@ -313,13 +314,19 @@ For **local ngrok / localhost**: set the backend URL to your tunnel or `http://1
|
|||||||
|
|
||||||
### testMode
|
### testMode
|
||||||
|
|
||||||
When **Test Mode** is on (default if never saved), register and send-wakeup requests include `"testMode": true`. The backend can route dev traffic separately from production. Turn it off in the panel only if you intentionally want production-mode API behavior against your tunnel.
|
When **Test Mode** is on (default if never saved), register and refresh requests include `"testMode": true`. The backend can route dev traffic separately from production. Turn it off in the panel only if you intentionally want production-mode API behavior against your tunnel.
|
||||||
|
|
||||||
### WAKEUP_PING debug controls
|
### WAKEUP_PING debug controls
|
||||||
|
|
||||||
**Send Real WAKEUP_PING** is the remaining panel control for wakeup. Mock refresh, Simulate WAKEUP_PING, Wakeup Ping Simulator, and Flood Test were removed with the retired refresh pipeline.
|
Three panel actions exercise different segments of the wakeup pipeline. Use them to bisect failures (see [Troubleshooting §14](#fcm-message-not-received)).
|
||||||
|
|
||||||
Use **Send Real WAKEUP_PING** to confirm backend → FCM → Capacitor listener delivery. Do not expect `/notifications/refresh` or `api_*` rescheduling.
|
| Button | Behavior |
|
||||||
|
|--------|----------|
|
||||||
|
| **Backend Testing → Simulate WAKEUP_PING (Local)** | Skips FCM; calls refresh API only (ngrok path test) |
|
||||||
|
| **Backend Testing → Send Real WAKEUP_PING** | Full pipeline: backend `/debug/send-wakeup` → FCM → Capacitor listener → refresh (see below) |
|
||||||
|
| **Wakeup Ping Simulator** (lower on panel) | Runs production handler with synthetic `WAKEUP_PING` payload (no FCM, no backend wakeup call) |
|
||||||
|
|
||||||
|
Use **Simulate WAKEUP_PING (Local)** to verify ngrok + refresh; use **Wakeup Ping Simulator** to verify handler + refresh chaining without FCM; use **Send Real WAKEUP_PING** for end-to-end FCM delivery on device.
|
||||||
|
|
||||||
### Send Real WAKEUP_PING
|
### Send Real WAKEUP_PING
|
||||||
|
|
||||||
@@ -330,9 +337,9 @@ Use **Send Real WAKEUP_PING** to confirm backend → FCM → Capacitor listener
|
|||||||
1. Reads the **Current FCM Token** shown in the panel (same token used by **Register Token Now**). If no token is available, the action fails with a panel status message.
|
1. Reads the **Current FCM Token** shown in the panel (same token used by **Register Token Now**). If no token is available, the action fails with a panel status message.
|
||||||
2. `POST`s to `{backend override}/debug/send-wakeup` with `deviceId`, `fcmToken`, `platform: "android"`, and `testMode` from the panel config (`NotificationDebugService.sendRealWakeupPing()`).
|
2. `POST`s to `{backend override}/debug/send-wakeup` with `deviceId`, `fcmToken`, `platform: "android"`, and `testMode` from the panel config (`NotificationDebugService.sendRealWakeupPing()`).
|
||||||
3. On HTTP success, the **notification-wakeup-service** enqueues a real FCM **data** message with `data.type = "WAKEUP_PING"` to that device.
|
3. On HTTP success, the **notification-wakeup-service** enqueues a real FCM **data** message with `data.type = "WAKEUP_PING"` to that device.
|
||||||
4. When FCM delivers the message to the app, Capacitor fires `pushNotificationReceived` → `handleCapacitorPushNotificationReceived()`, which logs `push handler ignored type=WAKEUP_PING`. There is no `refreshNotificationsWithDiagnostics` and no `applyNotificationRefreshPayload`.
|
4. When FCM delivers the message to the app, Capacitor fires `pushNotificationReceived` → `handleCapacitorPushNotificationReceived()` → `refreshNotificationsWithDiagnostics({ source: "WAKEUP_PING" })` → `POST /notifications/refresh` → `applyNotificationRefreshPayload()` (clear + reschedule via **daily-notification-plugin**).
|
||||||
|
|
||||||
That exercises **backend → FCM → Capacitor push listener** on Android without manual `curl` on the Mac. It does **not** reschedule local notifications.
|
That exercises the full **backend → FCM → Capacitor push listener → refresh request → notification rescheduling** path on Android without manual `curl` on the Mac.
|
||||||
|
|
||||||
**Prerequisites:** ngrok backend override saved, **Test Mode** as needed, notification permission granted, **Register Token Now** succeeded, app **backgrounded** (Home — not force-stop) before expecting FCM delivery ([§8](#8-android-platform-notes)).
|
**Prerequisites:** ngrok backend override saved, **Test Mode** as needed, notification permission granted, **Register Token Now** succeeded, app **backgrounded** (Home — not force-stop) before expecting FCM delivery ([§8](#8-android-platform-notes)).
|
||||||
|
|
||||||
@@ -341,24 +348,27 @@ That exercises **backend → FCM → Capacitor push listener** on Android withou
|
|||||||
Filter logcat (prefix is always `[Notifications]`):
|
Filter logcat (prefix is always `[Notifications]`):
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
adb logcat | grep -E '\[Notifications\].*(Real WAKEUP_PING|push handler ignored)'
|
adb logcat | grep -E '\[Notifications\].*(Real WAKEUP_PING|pushNotificationReceived|WAKEUP_PING|Refresh started|Refresh completed)'
|
||||||
```
|
```
|
||||||
|
|
||||||
On a **successful FCM delivery** run (HTTP success from the panel, then FCM delivery within ~30–120s), expect these key lines in order:
|
On a **successful end-to-end** run (HTTP success from the panel, then FCM delivery within ~30–120s), expect these key lines in order:
|
||||||
|
|
||||||
```
|
```
|
||||||
[Notifications] Real WAKEUP_PING requested
|
[Notifications] Real WAKEUP_PING requested
|
||||||
[Notifications] Real WAKEUP_PING success
|
[Notifications] Real WAKEUP_PING success
|
||||||
[Notifications] push handler ignored type=WAKEUP_PING
|
[Notifications] pushNotificationReceived type=WAKEUP_PING
|
||||||
|
[Notifications] WAKEUP_PING handler — invoking refresh
|
||||||
|
[Notifications] Refresh started (WAKEUP_PING)
|
||||||
|
[Notifications] Refresh completed (WAKEUP_PING) in …ms (scheduled N)
|
||||||
```
|
```
|
||||||
|
|
||||||
Auth bypass messages when **Skip JWT Authentication** is on are normal. ngrok should **not** show a new `POST /notifications/refresh` from the app after the push.
|
Intermediate lines (e.g. `WAKEUP_PING received — will trigger refresh`, `Schedule replacement: …`, `Using authenticated notification request` or auth bypass messages when **Skip JWT Authentication** is on) are normal. ngrok should also show a new `POST /notifications/refresh` after the push is handled.
|
||||||
|
|
||||||
#### Send Real WAKEUP_PING vs delivery success
|
#### Send Real WAKEUP_PING vs end-to-end success
|
||||||
|
|
||||||
The panel status **“Real WAKEUP_PING sent via backend.”** and the Event Log line **`Real WAKEUP_PING success`** only confirm that the backend **accepted the request and attempted FCM delivery**. They do **not** prove the device received the push.
|
The panel status **“Real WAKEUP_PING sent via backend.”** and the Event Log line **`Real WAKEUP_PING success`** only confirm that the backend **accepted the request and attempted FCM delivery**. They do **not** prove the device received the push or ran refresh.
|
||||||
|
|
||||||
**Successful delivery** is confirmed when **`push handler ignored type=WAKEUP_PING`** appears in logcat (or Event Log) within the delivery window. If you see `Real WAKEUP_PING success` but not that line, treat it as an FCM delivery problem ([FCM message not received](#fcm-message-not-received)), not a backend enqueue failure.
|
**Successful end-to-end delivery** is confirmed only when the subsequent **`pushNotificationReceived`**, **`WAKEUP_PING handler`**, and **`Refresh started (WAKEUP_PING)`** / **`Refresh completed (WAKEUP_PING)`** lines appear in logcat (or Event Log) within the delivery window. If you see `Real WAKEUP_PING success` but not those lines, treat it as an FCM delivery problem ([FCM message not received](#fcm-message-not-received)), not a backend enqueue failure.
|
||||||
|
|
||||||
### Programmatic override (optional)
|
### Programmatic override (optional)
|
||||||
|
|
||||||
@@ -445,11 +455,15 @@ On **API 33+**, `POST_NOTIFICATIONS` is a runtime permission ([section 7](#7-and
|
|||||||
|
|
||||||
### Network connectivity
|
### Network connectivity
|
||||||
|
|
||||||
FCM needs network to Google’s endpoints. Registration needs the ngrok HTTPS URL for `POST /notifications/register`. Airplane mode, captive portals, VPNs, or flaky Wi‑Fi cause “server sent wakeup but app never logged ignored type” symptoms even when FCM eventually arrives.
|
FCM and your ngrok-backed refresh both need network:
|
||||||
|
|
||||||
|
- Device must reach **Google’s FCM endpoints** (mobile data or Wi‑Fi).
|
||||||
|
- After wake, the app must reach the **ngrok HTTPS URL** for `POST /notifications/refresh`.
|
||||||
|
- Airplane mode, captive portals, VPNs, or flaky Wi‑Fi cause “server sent wakeup but app never refreshed” symptoms even when FCM eventually arrives.
|
||||||
|
|
||||||
### Physical device vs emulator
|
### Physical device vs emulator
|
||||||
|
|
||||||
Doze, App Standby, and OEM battery menus are weak or absent on many emulators. Use a **physical device** for wakeup SLA testing; emulators are fine for panel-only register smoke tests.
|
Doze, App Standby, and OEM battery menus are weak or absent on many emulators. Use a **physical device** for wakeup SLA testing; emulators are fine for panel-only register/refresh smoke tests.
|
||||||
|
|
||||||
### Logcat
|
### Logcat
|
||||||
|
|
||||||
@@ -457,7 +471,7 @@ Doze, App Standby, and OEM battery menus are weak or absent on many emulators. U
|
|||||||
adb logcat | grep -E '\[Notifications\]|\[FirebaseMessaging\]|\[NativeNotificationService\]'
|
adb logcat | grep -E '\[Notifications\]|\[FirebaseMessaging\]|\[NativeNotificationService\]'
|
||||||
```
|
```
|
||||||
|
|
||||||
Expect `push handler ignored type=WAKEUP_PING` after a successful wake. For a panel-driven test, use **Send Real WAKEUP_PING** ([§6](#send-real-wakeup_ping)) instead of manual `curl`.
|
Expect `pushNotificationReceived type=WAKEUP_PING` and `WAKEUP_PING handler — invoking refresh` after a successful wake. For a panel-driven test, use **Send Real WAKEUP_PING** ([§6](#send-real-wakeup_ping)) instead of manual `curl`.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -485,7 +499,7 @@ Apps used infrequently move to **standby** buckets with reduced background netwo
|
|||||||
|
|
||||||
### Manufacturer-specific restrictions
|
### Manufacturer-specific restrictions
|
||||||
|
|
||||||
OEMs add layers on top of AOSP. Aggressive battery management can delay **WAKEUP_PING** delivery.
|
OEMs add layers on top of AOSP. Aggressive battery management can delay **WAKEUP_PING** delivery or prevent the refresh `fetch` from running promptly.
|
||||||
|
|
||||||
| Vendor | Where to look (names vary by OS version) |
|
| Vendor | Where to look (names vary by OS version) |
|
||||||
|--------|------------------------------------------|
|
|--------|------------------------------------------|
|
||||||
@@ -501,8 +515,8 @@ If wakeup works on a **Pixel** but fails on an OEM phone, assume battery policy
|
|||||||
|
|
||||||
1. Server accepts `/debug/send-wakeup` → FCM enqueue succeeds.
|
1. Server accepts `/debug/send-wakeup` → FCM enqueue succeeds.
|
||||||
2. Device may **hold** the message until Doze maintenance or OEM policy allows delivery.
|
2. Device may **hold** the message until Doze maintenance or OEM policy allows delivery.
|
||||||
3. `pushNotificationReceived` runs → `push handler ignored type=WAKEUP_PING`.
|
3. `pushNotificationReceived` runs → `refreshNotificationsWithDiagnostics()` → ngrok `POST /notifications/refresh`.
|
||||||
4. Any step can lag under battery savers; use **Send Real WAKEUP_PING** for the FCM path ([§6](#send-real-wakeup_ping)).
|
4. Any step can lag under battery savers; use **Simulate WAKEUP_PING (Local)** in the debug panel to separate FCM delay from refresh/API issues; use **Send Real WAKEUP_PING** for the full FCM path ([§6](#send-real-wakeup_ping)).
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -514,10 +528,12 @@ If wakeup works on a **Pixel** but fails on an OEM phone, assume battery policy
|
|||||||
4. Grant notification permission when prompted (or enable in Settings).
|
4. Grant notification permission when prompted (or enable in Settings).
|
||||||
5. Open **Notification Debug Panel** → paste ngrok URL → **Save Backend URL**; confirm **Test Mode** is on; enable **Skip JWT Authentication** only if your local backend accepts unauthenticated requests.
|
5. Open **Notification Debug Panel** → paste ngrok URL → **Save Backend URL**; confirm **Test Mode** is on; enable **Skip JWT Authentication** only if your local backend accepts unauthenticated requests.
|
||||||
6. Tap **Register Token Now** → confirm ngrok `POST /notifications/register` and `[Notifications] Token registration success` in Event Log / logcat.
|
6. Tap **Register Token Now** → confirm ngrok `POST /notifications/register` and `[Notifications] Token registration success` in Event Log / logcat.
|
||||||
7. Background the app (Home), then tap **Send Real WAKEUP_PING** (or from the Mac, call **`/debug/send-wakeup`** — see [curl examples](#13-sample-curl-commands)) with the registered `deviceId` / token as required by **notification-wakeup-service**.
|
7. Tap **Refresh Notifications** → confirm `POST /notifications/refresh` and `Refresh completed in Nms (scheduled X)` in Event Log.
|
||||||
8. Watch logcat for `push handler ignored type=WAKEUP_PING` ([expected output](#expected-logcat-output)).
|
8. Optional: tap **Simulate WAKEUP_PING (Local)** to verify ngrok + refresh without FCM.
|
||||||
9. Open **ngrok inspect UI** (`http://127.0.0.1:4040`) to correlate HTTP traffic (register and send-wakeup; not app-initiated refresh).
|
9. Background the app (Home), then tap **Send Real WAKEUP_PING** (or from the Mac, call **`/debug/send-wakeup`** — see [curl examples](#13-sample-curl-commands)) with the registered `deviceId` / token as required by **notification-wakeup-service**.
|
||||||
10. Use **Pending Notification Inspector** for Daily Reminder / New Activity / dual schedules — not for retired `api_*` refresh.
|
10. Watch logcat for `WAKEUP_PING` and `Refresh completed (WAKEUP_PING)` lines ([expected output](#expected-logcat-output)).
|
||||||
|
11. Open **ngrok inspect UI** (`http://127.0.0.1:4040`) to correlate HTTP traffic.
|
||||||
|
12. Use **Pending Notification Inspector** on the panel to confirm locally scheduled fires after refresh.
|
||||||
|
|
||||||
For a formal pass/fail sequence, use the [Verification Checklist](#11-verification-checklist) below.
|
For a formal pass/fail sequence, use the [Verification Checklist](#11-verification-checklist) below.
|
||||||
|
|
||||||
@@ -533,12 +549,12 @@ Use this checklist during development or QA sign-off. Each step lists **actions*
|
|||||||
| 2 | Device → backend override | §2 |
|
| 2 | Device → backend override | §2 |
|
||||||
| 3 | FCM token registered | §3 |
|
| 3 | FCM token registered | §3 |
|
||||||
| 4 | Device record in wakeup service | §4 |
|
| 4 | Device record in wakeup service | §4 |
|
||||||
| 5 | Optional backend `/notifications/refresh` curl | §5 (backend only; app does not call this) |
|
| 5 | Refresh returns schedule data | §5 |
|
||||||
| 6 | Locals scheduled | §6 (Daily Reminder / dual / fetcher — not refresh) |
|
| 6 | Locals scheduled | §6 |
|
||||||
| 7 | Manual wakeup sends FCM | §7 |
|
| 7 | Manual wakeup sends FCM | §7 |
|
||||||
| 8 | WAKEUP_PING delivered | §8 |
|
| 8 | WAKEUP_PING → refresh | §8 |
|
||||||
| 9 | _(retired)_ Replace after refresh | skipped |
|
| 9 | Replace after refresh | §9 |
|
||||||
| 10 | _(retired)_ Test mode frequent refreshes | skipped |
|
| 10 | Test mode frequent refreshes | §10 |
|
||||||
|
|
||||||
### 1. Backend reachable through ngrok
|
### 1. Backend reachable through ngrok
|
||||||
|
|
||||||
@@ -562,7 +578,7 @@ curl -sS -w "\nHTTP %{http_code}\n" "$BASE/health"
|
|||||||
3. Confirm **Backend Status → URL** matches the saved ngrok host.
|
3. Confirm **Backend Status → URL** matches the saved ngrok host.
|
||||||
4. Enable **Test Mode** if using dev backend behavior.
|
4. Enable **Test Mode** if using dev backend behavior.
|
||||||
|
|
||||||
**Expected outcome:** **Active** URL in the panel equals your ngrok `https://…` host. Subsequent app requests use that base (not the default `DEFAULT_NOTIFY_API_SERVER`) for `/notifications/register` and `/debug/send-wakeup`.
|
**Expected outcome:** **Active** URL in the panel equals your ngrok `https://…` host. Subsequent app requests use that base (not the default `DEFAULT_NOTIFY_API_SERVER`) for `/notifications/register` and `/notifications/refresh`.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -595,11 +611,12 @@ curl -sS -w "\nHTTP %{http_code}\n" "$BASE/health"
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
### 5. Refresh endpoint (backend only; app does not call this)
|
### 5. Refresh endpoint returns schedule data
|
||||||
|
|
||||||
**Actions:**
|
**Actions:**
|
||||||
|
|
||||||
Mac-side curl against **notification-wakeup-service** if you need to confirm the backend still answers. The app has no **Refresh Notifications** button and does not schedule from this payload.
|
1. Tap **Refresh Notifications** in the panel (or curl below).
|
||||||
|
2. Inspect ngrok response body.
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
curl -sS -X POST "$BASE/notifications/refresh" \
|
curl -sS -X POST "$BASE/notifications/refresh" \
|
||||||
@@ -607,18 +624,18 @@ curl -sS -X POST "$BASE/notifications/refresh" \
|
|||||||
-d '{"platform":"android","testMode":true}'
|
-d '{"platform":"android","testMode":true}'
|
||||||
```
|
```
|
||||||
|
|
||||||
**Expected outcome:** HTTP **200** from the **service** if that route is still deployed. Do **not** expect Event Log `Refresh completed` or `api_*` schedules in the app.
|
**Expected outcome:** HTTP **200**; JSON includes `nextNotifications` array with at least one `{ "timestamp": <number> }` in the **future** (epoch ms). Event Log: `Refresh completed in …ms (scheduled N)` with **N ≥ 1**. If `scheduled 0` or empty array, fix backend auth, DID/native fetcher, or `testMode` handling before scheduling tests.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
### 6. Local notifications are scheduled (not from refresh)
|
### 6. Local notifications are scheduled
|
||||||
|
|
||||||
**Actions:**
|
**Actions:**
|
||||||
|
|
||||||
1. Configure Static Daily Reminder and/or New Activity (dual) in the app, or rely on the native fetcher.
|
1. After a successful refresh (step 5), open **Pending Notification Inspector** → **Refresh** (list button).
|
||||||
2. Open **Pending Notification Inspector** → **Refresh** (list button).
|
2. Optionally cross-check logcat for `Schedule replacement applied (N timestamp(s))`.
|
||||||
|
|
||||||
**Expected outcome:** Inspector lists pending Daily Reminder / dual / fetcher items. Do not expect `api_*` identifiers after Phase 4 startup cleanup. There is no `Schedule replacement applied` from WAKEUP_PING.
|
**Expected outcome:** Inspector lists **N** pending item(s) matching refresh count; each row has a **future** `nextTriggerDate` / wall-clock time. No `Schedule replacement aborted` or `skipped (no valid timestamps)` in Event Log.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -639,11 +656,11 @@ curl -sS -X POST "$BASE/debug/send-wakeup" \
|
|||||||
|
|
||||||
3. Check **notification-wakeup-service** logs for FCM send success (no Admin SDK / token errors).
|
3. Check **notification-wakeup-service** logs for FCM send success (no Admin SDK / token errors).
|
||||||
|
|
||||||
**Expected outcome:** HTTP **200** (or documented success code) from `/debug/send-wakeup`; Event Log / logcat: `Real WAKEUP_PING success` when using the panel button; server logs indicate FCM message enqueued/sent with `data.type = "WAKEUP_PING"`. This step alone does not prove device delivery (see step 8 and [Send Real WAKEUP_PING vs delivery success](#send-real-wakeup_ping-vs-delivery-success)).
|
**Expected outcome:** HTTP **200** (or documented success code) from `/debug/send-wakeup`; Event Log / logcat: `Real WAKEUP_PING success` when using the panel button; server logs indicate FCM message enqueued/sent with `data.type = "WAKEUP_PING"`. This step alone does not prove device delivery (see step 8 and [Send Real WAKEUP_PING vs end-to-end success](#send-real-wakeup_ping-vs-end-to-end-success)).
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
### 8. WAKEUP_PING is delivered (no refresh)
|
### 8. WAKEUP_PING triggers refreshNotifications()
|
||||||
|
|
||||||
**Actions:**
|
**Actions:**
|
||||||
|
|
||||||
@@ -652,28 +669,42 @@ curl -sS -X POST "$BASE/debug/send-wakeup" \
|
|||||||
3. Filter logcat:
|
3. Filter logcat:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
adb logcat | grep -E 'WAKEUP_PING|push handler ignored'
|
adb logcat | grep -E 'WAKEUP_PING|pushNotificationReceived|Refresh completed'
|
||||||
```
|
```
|
||||||
|
|
||||||
**Expected outcome (within ~30–120s, longer under Doze/OEM):**
|
**Expected outcome (within ~30–120s, longer under Doze/OEM):**
|
||||||
|
|
||||||
1. `Real WAKEUP_PING success` (panel path)
|
1. `pushNotificationReceived type=WAKEUP_PING` / `WAKEUP_PING received`
|
||||||
2. `push handler ignored type=WAKEUP_PING`
|
2. `WAKEUP_PING handler — invoking refresh`
|
||||||
3. ngrok: **no** app-initiated `POST /notifications/refresh`
|
3. `Refresh started (WAKEUP_PING)`
|
||||||
|
4. ngrok: new `POST /notifications/refresh`
|
||||||
|
5. `Refresh completed (WAKEUP_PING) in …ms (scheduled N)`
|
||||||
|
|
||||||
Full expected logcat for **Send Real WAKEUP_PING**: [§6](#expected-logcat-output).
|
**Isolation:** **Wakeup Ping Simulator** on the panel should produce lines 2–5 without FCM. **Simulate WAKEUP_PING (Local)** proves refresh only (no handler, source `WAKEUP_PING simulation`). Full expected logcat for **Send Real WAKEUP_PING**: [§6](#expected-logcat-output).
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
### 9. Existing notifications are replaced after refresh
|
### 9. Existing notifications are replaced after refresh
|
||||||
|
|
||||||
**Retired.** The app no longer replaces local schedules from `/notifications/refresh`. Pending list changes come from Daily Reminder / dual / native fetcher, or from Phase 4 `clearApiNotifications()`.
|
**Actions:**
|
||||||
|
|
||||||
|
1. Note pending count and identifiers in **Pending Notification Inspector**.
|
||||||
|
2. Tap **Refresh Notifications** again (or trigger step 8).
|
||||||
|
3. Refresh inspector; compare IDs/times to step 1.
|
||||||
|
|
||||||
|
**Expected outcome:** Event Log shows `clearAllNotifications` or `cancelAllNotifications` then `Schedule replacement applied`; pending list reflects **new** timestamps (old alarms not accumulated). Total pending count should match latest refresh `N`, not double from duplicate refreshes unless backend returned more slots.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
### 10. Test mode produces frequent notification refreshes
|
### 10. Test mode produces frequent notification refreshes
|
||||||
|
|
||||||
**Retired.** `testMode` still goes on register and send-wakeup bodies. It does not drive app-side `api_*` refresh cadences.
|
**Actions:**
|
||||||
|
|
||||||
|
1. Confirm **Test Mode** checked; **Backend Status → testMode: true**.
|
||||||
|
2. Call refresh twice (panel or curl) with `testMode: true`.
|
||||||
|
3. Compare `nextNotifications` timestamps in ngrok responses.
|
||||||
|
|
||||||
|
**Expected outcome:** With **testMode: true**, **notification-wakeup-service** returns **dev-friendly** schedule data—typically **sooner** fire times than production mode (shorter horizons). Pending Inspector updates to nearer triggers after each refresh. With Test Mode **off**, timestamps should be farther out (production-like); use that contrast to confirm the flag is wired end-to-end.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -708,23 +739,31 @@ npm run dev
|
|||||||
|
|
||||||
8. Paste `BASE` → **Save Backend URL**; enable **Test Mode**.
|
8. Paste `BASE` → **Save Backend URL**; enable **Test Mode**.
|
||||||
|
|
||||||
### Phase C — Register
|
### Phase C — Register and schedule
|
||||||
|
|
||||||
9. **Register Token Now** → Event Log success; ngrok `POST /notifications/register` **200**; copy `deviceId` from ngrok request body.
|
9. **Register Token Now** → Event Log success; ngrok `POST /notifications/register` **200**; copy `deviceId` from ngrok request body.
|
||||||
|
|
||||||
10. Confirm device in **notification-wakeup-service** (logs/DB per that repo).
|
10. Confirm device in **notification-wakeup-service** (logs/DB per that repo).
|
||||||
|
|
||||||
### Phase D — FCM wakeup delivery
|
11. **Refresh Notifications** → Event Log `scheduled N`, N ≥ 1; ngrok refresh **200** with `nextNotifications`.
|
||||||
|
|
||||||
11. Background app (Home).
|
12. **Pending Notification Inspector** → **Refresh** → future alarm(s) listed.
|
||||||
|
|
||||||
12. Tap **Send Real WAKEUP_PING** in the panel (or Mac: `curl -X POST "$BASE/debug/send-wakeup" -H "Content-Type: application/json" -d '{"deviceId":"…","testMode":true}'`) → panel `Real WAKEUP_PING success` and server FCM enqueue success.
|
### Phase D — FCM wakeup path
|
||||||
|
|
||||||
13. Within 30–120s (longer if unplugged/Doze): logcat shows `push handler ignored type=WAKEUP_PING`; ngrok does **not** show an app `POST /notifications/refresh`.
|
13. Background app (Home).
|
||||||
|
|
||||||
### Phase E — Optional local notification proof
|
14. Tap **Send Real WAKEUP_PING** in the panel (or Mac: `curl -X POST "$BASE/debug/send-wakeup" -H "Content-Type: application/json" -d '{"deviceId":"…","testMode":true}'`) → panel `Real WAKEUP_PING success` and server FCM enqueue success.
|
||||||
|
|
||||||
14. Enable Daily Reminder or New Activity; wait for wall-clock fire with app backgrounded; confirm the OS notification is **not** the retired generic `api_*` “Reminder” copy.
|
15. Within 30–120s (longer if unplugged/Doze): logcat shows `pushNotificationReceived` → `Refresh completed (WAKEUP_PING)`; ngrok shows second `POST /notifications/refresh`.
|
||||||
|
|
||||||
|
16. Pending Inspector **Refresh** → timestamps updated (replacement, not duplicate stack).
|
||||||
|
|
||||||
|
### Phase E — Optional delivery proof
|
||||||
|
|
||||||
|
17. If `testMode` returned a trigger within a few minutes, wait for wall-clock fire with app backgrounded; confirm notification appears (permission + channel + exact alarm rules).
|
||||||
|
|
||||||
|
18. If FCM step 15 failed but step 11 passed: run **Simulate WAKEUP_PING (Local)** then **Wakeup Ping Simulator** to bisect FCM vs handler; see [Troubleshooting](#14-troubleshooting) and [Send Real WAKEUP_PING vs end-to-end success](#send-real-wakeup_ping-vs-end-to-end-success).
|
||||||
|
|
||||||
### End-to-end pass criteria
|
### End-to-end pass criteria
|
||||||
|
|
||||||
@@ -732,9 +771,9 @@ npm run dev
|
|||||||
|-------|------|
|
|-------|------|
|
||||||
| A | Health OK local + ngrok |
|
| A | Health OK local + ngrok |
|
||||||
| B | Override URL + testMode active |
|
| B | Override URL + testMode active |
|
||||||
| C | Register succeeded |
|
| C | Register + refresh + pending list populated |
|
||||||
| D | Wakeup curl/panel OK → logcat ignored-type line |
|
| D | Wakeup curl OK → logcat refresh chain → pending updated |
|
||||||
| E | Optional visible Daily Reminder / New Activity notification |
|
| E | Optional visible notification at scheduled time |
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -765,7 +804,7 @@ curl -sS -X POST "$BASE/notifications/register" \
|
|||||||
}'
|
}'
|
||||||
```
|
```
|
||||||
|
|
||||||
### Refresh (backend only; app does not consume)
|
### Refresh (mirror app payload)
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
curl -sS -X POST "$BASE/notifications/refresh" \
|
curl -sS -X POST "$BASE/notifications/refresh" \
|
||||||
@@ -776,7 +815,19 @@ curl -sS -X POST "$BASE/notifications/refresh" \
|
|||||||
}'
|
}'
|
||||||
```
|
```
|
||||||
|
|
||||||
This exercises **notification-wakeup-service** only. The app does **not** call `applyNotificationRefreshPayload` or schedule `api_*` from the response.
|
Example success body shape (actual fields may vary by service version):
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"shouldNotify": true,
|
||||||
|
"nextNotifications": [
|
||||||
|
{ "timestamp": 1710000000000 },
|
||||||
|
{ "timestamp": 1710003600000 }
|
||||||
|
]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
The app schedules those timestamps via **daily-notification-plugin** (`applyNotificationRefreshPayload` in `NativeNotificationService.ts`).
|
||||||
|
|
||||||
### Send wakeup push (debug)
|
### Send wakeup push (debug)
|
||||||
|
|
||||||
@@ -791,7 +842,7 @@ curl -sS -X POST "$BASE/debug/send-wakeup" \
|
|||||||
}'
|
}'
|
||||||
```
|
```
|
||||||
|
|
||||||
Confirm parameters (token vs `deviceId`, auth headers) in that repo’s README or OpenAPI spec. The server may still send FCM data including `type: "WAKEUP_PING"`; the app logs it as ignored and does not refresh.
|
Confirm parameters (token vs `deviceId`, auth headers) in that repo’s README or OpenAPI spec. The server must send FCM data including `type: "WAKEUP_PING"` to match `handleCapacitorPushNotificationReceived`.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -848,28 +899,35 @@ Compare with panel **Backend Status** and Event Log error text.
|
|||||||
|
|
||||||
### Refresh endpoint failures
|
### Refresh endpoint failures
|
||||||
|
|
||||||
**Symptoms:** Mac `curl` of `/notifications/refresh` fails; ngrok missing that path. This is a **backend** issue. The app does not POST refresh.
|
**Symptoms:** **Refresh Notifications** fails; Event Log HTTP error; no `scheduled X` line; ngrok missing `POST /notifications/refresh`.
|
||||||
|
|
||||||
**Verification:** `curl` health and refresh from the Mac; confirm the service still ships that route.
|
**Likely causes:** Stale ngrok URL; backend down; 404/wrong path; JWT/native fetcher not configured; refresh auth failure.
|
||||||
|
|
||||||
**Fixes:** Fix backend URL and health. App scheduling uses Daily Reminder / dual / native fetcher, not this payload.
|
**Verification:**
|
||||||
|
|
||||||
|
1. **Simulate WAKEUP_PING (Local)** — if this fails, problem is ngrok/refresh API, not FCM.
|
||||||
|
2. ngrok inspect for refresh status code and response body.
|
||||||
|
3. Logcat: `refreshNotifications failed` or JWT errors from `configureNativeFetcherIfReady()`.
|
||||||
|
|
||||||
|
**Fixes:** Fix backend URL and health; ensure active DID and endorser settings ([notification-from-api-call.md](./notification-from-api-call.md)); confirm `testMode` if backend requires it.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
### FCM message not received
|
### FCM message not received
|
||||||
|
|
||||||
**Symptoms:** `/debug/send-wakeup` or panel **Send Real WAKEUP_PING** shows success (`Real WAKEUP_PING success` in Event Log); no `push handler ignored type=WAKEUP_PING` in logcat within 2 minutes.
|
**Symptoms:** `/debug/send-wakeup` or panel **Send Real WAKEUP_PING** shows success (`Real WAKEUP_PING success` in Event Log); no `pushNotificationReceived` / `WAKEUP_PING` in logcat within 2 minutes; refresh never triggered.
|
||||||
|
|
||||||
**Likely causes:** Force-stopped app; wrong FCM token on server; Doze/OEM delay; no Google Play services; payload missing `data.type = "WAKEUP_PING"`; device offline.
|
**Likely causes:** Force-stopped app; wrong FCM token on server; Doze/OEM delay; no Google Play services; payload missing `data.type = "WAKEUP_PING"`; device offline.
|
||||||
|
|
||||||
**Note:** `Real WAKEUP_PING success` only means the backend accepted and sent the FCM request. Missing the ignored-type log indicates a **delivery** failure, not a failed wakeup API call ([§6](#send-real-wakeup_ping-vs-delivery-success)).
|
**Note:** `Real WAKEUP_PING success` only means the backend accepted and sent the FCM request. Missing downstream refresh logs indicates a **delivery** failure, not a failed wakeup API call ([§6](#send-real-wakeup_ping-vs-end-to-end-success)).
|
||||||
|
|
||||||
**Verification:**
|
**Verification:**
|
||||||
|
|
||||||
1. App **backgrounded** (Home), not force-stopped.
|
1. App **backgrounded** (Home), not force-stopped.
|
||||||
2. Panel FCM token matches token used by server/register.
|
2. Panel FCM token matches token used by server/register.
|
||||||
3. Wait 30–120s (longer on Doze/OEM).
|
3. **Simulate WAKEUP_PING (Local)** works → isolates FCM path from refresh/API.
|
||||||
4. Battery **Unrestricted** and OEM autostart enabled for test device.
|
4. Wait 30–120s (longer on Doze/OEM).
|
||||||
|
5. Battery **Unrestricted** and OEM autostart enabled for test device.
|
||||||
|
|
||||||
**Fixes:** Re-register token; relaunch app; relax battery settings ([section 9](#9-battery-optimization-caveats)); confirm **notification-wakeup-service** message format; test on Pixel vs suspect OEM policy.
|
**Fixes:** Re-register token; relaunch app; relax battery settings ([section 9](#9-battery-optimization-caveats)); confirm **notification-wakeup-service** message format; test on Pixel vs suspect OEM policy.
|
||||||
|
|
||||||
@@ -889,28 +947,30 @@ Compare with panel **Backend Status** and Event Log error text.
|
|||||||
|
|
||||||
### Notifications not appearing
|
### Notifications not appearing
|
||||||
|
|
||||||
**Symptoms:** Daily Reminder or New Activity configured but no visible notification at fire time; Pending Inspector empty or stale.
|
**Symptoms:** Refresh succeeds (`scheduled X` in log) but no visible notification at fire time; Pending Inspector empty or stale.
|
||||||
|
|
||||||
**Likely causes:** Permission denied (display blocked); exact alarm permission on Android 12+; timestamps in past; plugin schedule error; DND/channel settings.
|
**Likely causes:** Permission denied (display blocked); exact alarm permission on Android 12+; timestamps in past; plugin schedule error; DND/channel settings.
|
||||||
|
|
||||||
**Verification:**
|
**Verification:**
|
||||||
|
|
||||||
1. Permission granted ([section 7](#7-android-notification-permissions)).
|
1. Permission granted ([section 7](#7-android-notification-permissions)).
|
||||||
2. Pending Notification Inspector for Daily Reminder / dual identifiers (not `api_*` after Phase 4 cleanup).
|
2. Pending Notification Inspector after refresh.
|
||||||
|
3. Logcat: `Schedule replacement applied` vs aborted messages.
|
||||||
|
4. Confirm `nextNotifications` timestamps are in the future (curl refresh response).
|
||||||
|
|
||||||
**Fixes:** Grant `POST_NOTIFICATIONS`; check `SCHEDULE_EXACT_ALARM` / alarm permission per plugin docs. Do not expect wakeup refresh payloads to populate the inspector.
|
**Fixes:** Grant `POST_NOTIFICATIONS`; check `SCHEDULE_EXACT_ALARM` / alarm permission per plugin docs; fix refresh payload; test with nearer timestamps via backend `testMode`.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
### Duplicate notifications
|
### Duplicate notifications
|
||||||
|
|
||||||
**Symptoms:** Multiple identical local notifications.
|
**Symptoms:** Multiple identical local notifications; Event Log shows repeated refresh lines.
|
||||||
|
|
||||||
**Likely causes:** Separate Daily Reminder vs New Activity schedules; duplicate dual config; leftover `api_*` before Phase 4 cleanup.
|
**Likely causes:** Multiple `WAKEUP_PING` deliveries; repeated manual **Refresh**; flood test; separate Daily Reminder vs New Activity schedules.
|
||||||
|
|
||||||
**Verification:** Pending Inspector identifiers; see [notification-new-activity-lay-of-the-land.md](./notification-new-activity-lay-of-the-land.md) for product-level double-schedule issues.
|
**Verification:** Event Log count of refresh completions; ngrok inspect for duplicate `POST /notifications/refresh`.
|
||||||
|
|
||||||
**Fixes:** Confirm Daily Reminder vs dual settings. WAKEUP_PING no longer stacks `api_*` refreshes.
|
**Fixes:** Each refresh should **replace** schedule (clear + schedule)—if duplicates persist, check plugin logs; see [notification-new-activity-lay-of-the-land.md](./notification-new-activity-lay-of-the-land.md) for product-level double-schedule issues.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -946,7 +1006,7 @@ Compare with panel **Backend Status** and Event Log error text.
|
|||||||
| `src/services/notifications/NotificationDebugEvents.ts` | Panel event log + `logNotification()` |
|
| `src/services/notifications/NotificationDebugEvents.ts` | Panel event log + `logNotification()` |
|
||||||
| `src/services/notifications/notificationLog.ts` | Structured log helpers |
|
| `src/services/notifications/notificationLog.ts` | Structured log helpers |
|
||||||
| `src/services/notifications/NotificationService.ts` | `POST /notifications/register` |
|
| `src/services/notifications/NotificationService.ts` | `POST /notifications/register` |
|
||||||
| `src/services/notifications/NativeNotificationService.ts` | Push delivery hook (logs ignored types; no refresh) |
|
| `src/services/notifications/NativeNotificationService.ts` | `refreshNotifications`, `WAKEUP_PING`, `applyNotificationRefreshPayload` |
|
||||||
| `src/services/notifications/firebaseMessagingClient.ts` | Capacitor push listeners, permission, token registration |
|
| `src/services/notifications/firebaseMessagingClient.ts` | Capacitor push listeners, permission, token registration |
|
||||||
| `src/components/dev/NotificationDebugPanel.vue` | Dev UI |
|
| `src/components/dev/NotificationDebugPanel.vue` | Dev UI |
|
||||||
| `src/main.capacitor.ts` | Native push init at startup |
|
| `src/main.capacitor.ts` | Native push init at startup |
|
||||||
|
|||||||
@@ -1,16 +1,14 @@
|
|||||||
# Local iOS Testing with ngrok (notification-wakeup-service)
|
# Local iOS Testing with ngrok (notification-wakeup-service)
|
||||||
|
|
||||||
**Last updated:** 2026-09-24 (retired WAKEUP_PING → refresh → `api_*` consumption)
|
**Last updated:** 2026-05-18
|
||||||
**Audience:** Developers on **crowd-funder-for-time-pwa**, **daily-notification-plugin**, and **notification-wakeup-service**
|
**Audience:** Developers on **crowd-funder-for-time-pwa**, **daily-notification-plugin**, and **notification-wakeup-service**
|
||||||
**Goal:** Exercise FCM token registration and silent-push **delivery** (`WAKEUP_PING` via `/debug/send-wakeup`) against a Mac-hosted backend reachable from a physical iPhone.
|
**Goal:** Exercise silent push wake (`WAKEUP_PING`), FCM token registration, and notification refresh against a Mac-hosted backend reachable from a physical iPhone.
|
||||||
|
|
||||||
> **Retired (do not expect this in the app):** `WAKEUP_PING` → `POST /notifications/refresh` → `applyNotificationRefreshPayload()` → `api_*` local schedules. The app logs ignored push types and does not refresh or schedule from wakeup. `/debug/send-wakeup` remains an FCM/APNs diagnostic. Mac `curl` of `/notifications/refresh` only tests the backend.
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Architecture overview
|
## Architecture overview
|
||||||
|
|
||||||
End-to-end flow when testing FCM registration and wakeup **delivery** on a physical iPhone:
|
End-to-end flow when testing New Activity / silent wake on a physical iPhone:
|
||||||
|
|
||||||
```text
|
```text
|
||||||
┌─────────────────────┐ HTTPS ┌──────────────────────┐
|
┌─────────────────────┐ HTTPS ┌──────────────────────┐
|
||||||
@@ -19,7 +17,7 @@ End-to-end flow when testing FCM registration and wakeup **delivery** on a physi
|
|||||||
│ wakeup-service │ └──────────┬───────────┘
|
│ wakeup-service │ └──────────┬───────────┘
|
||||||
└──────────┬──────────┘ │
|
└──────────┬──────────┘ │
|
||||||
│ │ fetch
|
│ │ fetch
|
||||||
│ │ POST /notifications/register
|
│ POST /notifications/refresh │ POST /notifications/register
|
||||||
│ ▼
|
│ ▼
|
||||||
│ ┌──────────────────────┐
|
│ ┌──────────────────────┐
|
||||||
│ │ crowd-funder-for- │
|
│ │ crowd-funder-for- │
|
||||||
@@ -28,7 +26,7 @@ End-to-end flow when testing FCM registration and wakeup **delivery** on a physi
|
|||||||
│ └──────────┬───────────┘
|
│ └──────────┬───────────┘
|
||||||
│ │
|
│ │
|
||||||
│ FCM data message (WAKEUP_PING) │ daily-notification-plugin
|
│ FCM data message (WAKEUP_PING) │ daily-notification-plugin
|
||||||
▼ ▼ (Daily Reminder / dual / fetcher)
|
▼ ▼ (local schedule replace)
|
||||||
┌─────────────────────┐ ┌──────────────────────┐
|
┌─────────────────────┐ ┌──────────────────────┐
|
||||||
│ Firebase Cloud │ ──APNs──────► │ iPhone (physical) │
|
│ Firebase Cloud │ ──APNs──────► │ iPhone (physical) │
|
||||||
│ Messaging │ silent push │ app.timesafari │
|
│ Messaging │ silent push │ app.timesafari │
|
||||||
@@ -39,16 +37,18 @@ End-to-end flow when testing FCM registration and wakeup **delivery** on a physi
|
|||||||
|
|
||||||
| Repo | Role |
|
| Repo | Role |
|
||||||
|------|------|
|
|------|------|
|
||||||
| **notification-wakeup-service** | HTTP API: device registration, health, debug wakeup send; may still expose `/notifications/refresh` |
|
| **notification-wakeup-service** | HTTP API: device registration, refresh payload (`nextNotifications`), health, debug wakeup send |
|
||||||
| **crowd-funder-for-time-pwa** | Capacitor app: FCM token, `POST /notifications/register`; logs `WAKEUP_PING` without refresh/`api_*` scheduling |
|
| **crowd-funder-for-time-pwa** | Capacitor app: FCM token, `POST /notifications/register` & `/refresh`, handles `WAKEUP_PING` push |
|
||||||
| **daily-notification-plugin** | Native iOS/Android: Daily Reminder, New Activity / dual, native fetcher; Phase 4 `clearApiNotifications()` |
|
| **daily-notification-plugin** | Native iOS/Android: clear + reschedule local notifications from refresh timestamps |
|
||||||
|
|
||||||
### Silent wake sequence (current)
|
### Silent wake sequence (production path)
|
||||||
|
|
||||||
1. Backend `/debug/send-wakeup` sends an FCM **data** message with `data.type = "WAKEUP_PING"`.
|
1. Backend (or `/debug/send-wakeup`) sends an FCM **data** message with `data.type = "WAKEUP_PING"`.
|
||||||
2. APNs delivers to the device (best-effort; see iOS caveats below).
|
2. APNs delivers to the device (best-effort; see iOS caveats below).
|
||||||
3. Capacitor `pushNotificationReceived` fires → `handleCapacitorPushNotificationReceived()`.
|
3. Capacitor `pushNotificationReceived` fires → `handleCapacitorPushNotificationReceived()`.
|
||||||
4. Handler logs `[Notifications] push handler ignored type=WAKEUP_PING`. It does **not** POST `/notifications/refresh` or schedule `api_*` notifications.
|
4. App calls `POST {backend}/notifications/refresh` with `testMode` (from debug config).
|
||||||
|
5. Backend returns `nextNotifications: [{ timestamp }, ...]`.
|
||||||
|
6. App calls `applyNotificationRefreshPayload()` → plugin clears and schedules new local alarms.
|
||||||
|
|
||||||
Console and debug panel lines are prefixed with **`[Notifications]`** (see `NotificationDebugEvents.ts`).
|
Console and debug panel lines are prefixed with **`[Notifications]`** (see `NotificationDebugEvents.ts`).
|
||||||
|
|
||||||
@@ -297,11 +297,12 @@ For a full panel reference (configuration, URL resolution order, authentication,
|
|||||||
| Control | Purpose |
|
| Control | Purpose |
|
||||||
|---------|---------|
|
|---------|---------|
|
||||||
| **Notification Backend URL** | Paste ngrok HTTPS URL → **Save Backend URL** (changes target server only) |
|
| **Notification Backend URL** | Paste ngrok HTTPS URL → **Save Backend URL** (changes target server only) |
|
||||||
| **Test Mode** | Sends `testMode: true/false` in register / send-wakeup JSON bodies (default on when unset in storage) |
|
| **Test Mode** | Sends `testMode: true/false` in register/refresh JSON bodies (default on when unset in storage) |
|
||||||
| **Skip JWT Authentication (Local Development Only)** | When on, omits `Authorization` headers for local servers that accept unauthenticated requests (default **off**) |
|
| **Skip JWT Authentication (Local Development Only)** | When on, omits `Authorization` headers for local servers that accept unauthenticated requests (default **off**) |
|
||||||
| **Register Token Now** | `POST /notifications/register` with current FCM token |
|
| **Register Token Now** | `POST /notifications/register` with current FCM token |
|
||||||
| **Upload AlertSearch Authorization** | Uploads AlertSearch delegated JWTs |
|
| **Refresh Notifications** | `POST /notifications/refresh` (same as post-wakeup flow) |
|
||||||
| **Send Real WAKEUP_PING** | `POST /debug/send-wakeup`; FCM/APNs delivery diagnostic only |
|
| **Simulate WAKEUP_PING (Local)** | Calls refresh API directly (no FCM) — quick backend test |
|
||||||
|
| **Send Real WAKEUP_PING** | `POST /debug/send-wakeup`; server sends real FCM `WAKEUP_PING` (Android doc has full flow) |
|
||||||
| **Event Log** | Shared `[Notifications]` panel log (100 entries) |
|
| **Event Log** | Shared `[Notifications]` panel log (100 entries) |
|
||||||
|
|
||||||
Persistence: `localStorage` keys `notificationDebug.backendBaseUrl`, `notificationDebug.testMode`, and `notificationDebug.bypassAuth` (`NotificationDebugConfig.ts`).
|
Persistence: `localStorage` keys `notificationDebug.backendBaseUrl`, `notificationDebug.testMode`, and `notificationDebug.bypassAuth` (`NotificationDebugConfig.ts`).
|
||||||
@@ -351,7 +352,7 @@ This section is a quick verification checklist for the detailed Firebase/APNs se
|
|||||||
| **GoogleService-Info.plist** | Present in the iOS target if using Firebase iOS SDK paths in your build |
|
| **GoogleService-Info.plist** | Present in the iOS target if using Firebase iOS SDK paths in your build |
|
||||||
| **FCM token** | Confirm **Register Token Now** succeeds in the debug panel and ngrok shows `POST /notifications/register` |
|
| **FCM token** | Confirm **Register Token Now** succeeds in the debug panel and ngrok shows `POST /notifications/register` |
|
||||||
|
|
||||||
Silent/data pushes used for wake typically use a **content-available** style payload; confirm **notification-wakeup-service** and Firebase message format. The app logs `WAKEUP_PING` as ignored and does not refresh.
|
Silent/data pushes used for wake typically use a **content-available** style payload; confirm **notification-wakeup-service** and Firebase message format match what `handleCapacitorPushNotificationReceived` expects (`data.type === "WAKEUP_PING"`).
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -377,9 +378,14 @@ Silent/data pushes used for wake typically use a **content-available** style pay
|
|||||||
- **Low Power Mode** can reduce background execution.
|
- **Low Power Mode** can reduce background execution.
|
||||||
- **Focus / Do Not Disturb** may affect notification presentation (separate from silent data wake, but confusing during tests).
|
- **Focus / Do Not Disturb** may affect notification presentation (separate from silent data wake, but confusing during tests).
|
||||||
|
|
||||||
### Send Real WAKEUP_PING (FCM/APNs diagnostic)
|
### Two “Simulate WAKEUP_PING” buttons
|
||||||
|
|
||||||
**Send Real WAKEUP_PING** posts `/debug/send-wakeup`. Delivery is confirmed by `push handler ignored type=WAKEUP_PING`. Mock refresh, Simulate WAKEUP_PING, and Wakeup Ping Simulator were removed.
|
| Button | Behavior |
|
||||||
|
|--------|----------|
|
||||||
|
| **Backend Testing → Simulate WAKEUP_PING** | Skips FCM; calls refresh API only (ngrok path test) |
|
||||||
|
| **Wakeup Ping Simulator** (lower on panel) | Runs production handler with synthetic `WAKEUP_PING` payload |
|
||||||
|
|
||||||
|
Use the backend button to verify ngrok + refresh; use the simulator to verify handler + refresh chaining.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -389,10 +395,11 @@ Silent/data pushes used for wake typically use a **content-available** style pay
|
|||||||
2. Start **ngrok** and copy the HTTPS URL.
|
2. Start **ngrok** and copy the HTTPS URL.
|
||||||
3. Set URL + **Test Mode** in the Notification Debug Panel; confirm **Backend Status**.
|
3. Set URL + **Test Mode** in the Notification Debug Panel; confirm **Backend Status**.
|
||||||
4. Tap **Register Token Now** → confirm ngrok request and `[Notifications] Token registration success`.
|
4. Tap **Register Token Now** → confirm ngrok request and `[Notifications] Token registration success`.
|
||||||
5. From the backend, call **`/debug/send-wakeup`** (see curl below) with the registered `deviceId` / FCM token as required by that service, or tap **Send Real WAKEUP_PING**.
|
5. Tap **Refresh Notifications** → confirm `Refresh completed in Nms (scheduled X)` in Event Log and ngrok `POST /notifications/refresh`.
|
||||||
6. Watch **Xcode console** for `[Notifications] push handler ignored type=WAKEUP_PING`.
|
6. From the backend, call **`/debug/send-wakeup`** (see curl below) with the registered `deviceId` / FCM token as required by that service.
|
||||||
7. Open **ngrok inspect UI** (`http://127.0.0.1:4040`) to correlate requests (register and send-wakeup; not app-initiated refresh).
|
7. Watch **Xcode console** for `[Notifications] pushNotificationReceived type=WAKEUP_PING` and refresh timing lines.
|
||||||
8. Use **Pending Notification Inspector** for Daily Reminder / dual / fetcher schedules.
|
8. Open **ngrok inspect UI** (`http://127.0.0.1:4040`) to correlate requests.
|
||||||
|
9. Use **Pending Notification Inspector** on the panel to see locally scheduled fires after refresh.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -423,7 +430,7 @@ curl -sS -X POST "$BASE/notifications/register" \
|
|||||||
}'
|
}'
|
||||||
```
|
```
|
||||||
|
|
||||||
### Refresh (backend only; app does not consume)
|
### Refresh (mirror app payload)
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
curl -sS -X POST "$BASE/notifications/refresh" \
|
curl -sS -X POST "$BASE/notifications/refresh" \
|
||||||
@@ -434,7 +441,19 @@ curl -sS -X POST "$BASE/notifications/refresh" \
|
|||||||
}'
|
}'
|
||||||
```
|
```
|
||||||
|
|
||||||
This exercises **notification-wakeup-service** only. The app does **not** call `applyNotificationRefreshPayload` or schedule `api_*` from the response.
|
Example success body shape (actual fields may vary by service version):
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"shouldNotify": true,
|
||||||
|
"nextNotifications": [
|
||||||
|
{ "timestamp": 1710000000000 },
|
||||||
|
{ "timestamp": 1710003600000 }
|
||||||
|
]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
The app schedules those timestamps via **daily-notification-plugin** (`applyNotificationRefreshPayload` in `NativeNotificationService.ts`).
|
||||||
|
|
||||||
### Send wakeup push (debug)
|
### Send wakeup push (debug)
|
||||||
|
|
||||||
@@ -459,8 +478,8 @@ Confirm parameters (token vs deviceId, auth headers) in that repo’s README or
|
|||||||
|
|
||||||
| Symptom | Checks |
|
| Symptom | Checks |
|
||||||
|---------|--------|
|
|---------|--------|
|
||||||
| Mac `curl` of `/notifications/refresh` fails | ngrok running? URL saved without typo/trailing slash? |
|
| Network error in Event Log | ngrok running? URL saved without typo/trailing slash? |
|
||||||
| HTTP 404 | Tunnel port matches backend `PORT`; path is `/notifications/refresh` (backend route; the app does not call it) |
|
| HTTP 404 | Tunnel port matches backend `PORT`; path is `/notifications/refresh` |
|
||||||
| CORS (web only) | Native Capacitor fetch usually avoids browser CORS; if testing in Safari PWA, configure CORS on the service |
|
| CORS (web only) | Native Capacitor fetch usually avoids browser CORS; if testing in Safari PWA, configure CORS on the service |
|
||||||
| ngrok browser warning | Free tier may show an interstitial for browser clients; native `fetch` from the app is usually unaffected |
|
| ngrok browser warning | Free tier may show an interstitial for browser clients; native `fetch` from the app is usually unaffected |
|
||||||
|
|
||||||
@@ -476,15 +495,15 @@ Confirm parameters (token vs deviceId, auth headers) in that repo’s README or
|
|||||||
- App **backgrounded**, not force-quit
|
- App **backgrounded**, not force-quit
|
||||||
- Physical device, correct provisioning profile
|
- Physical device, correct provisioning profile
|
||||||
- APNs key uploaded to Firebase; bundle ID matches
|
- APNs key uploaded to Firebase; bundle ID matches
|
||||||
- FCM message includes `data.type = "WAKEUP_PING"` (logged as ignored in `NativeNotificationService.ts`)
|
- FCM message includes `data.type = "WAKEUP_PING"` (see `NativeNotificationService.ts`)
|
||||||
- Server actually sent to the **same** FCM token shown in the debug panel
|
- Server actually sent to the **same** FCM token shown in the debug panel
|
||||||
- Wait 30–120s — delivery is not instant
|
- Wait 30–120s — delivery is not instant
|
||||||
- Confirm Xcode shows `push handler ignored type=WAKEUP_PING` (there is no Simulate WAKEUP_PING / refresh API in the app)
|
- Try **Simulate WAKEUP_PING** (refresh API) to isolate app/plugin from FCM/APNs
|
||||||
|
|
||||||
### Notifications duplicating
|
### Notifications duplicating
|
||||||
|
|
||||||
- Daily Reminder vs New Activity both scheduling — see `doc/notification-new-activity-lay-of-the-land.md`
|
- Multiple refresh calls (flood test, repeated wakeups) each **replace** schedule via clear + schedule — check Event Log for repeated refreshes
|
||||||
- Leftover `api_*` before Phase 4 cleanup (startup `clearApiNotifications()`)
|
- Separate issue: Daily Reminder vs New Activity both scheduling — see `doc/notification-new-activity-lay-of-the-land.md`
|
||||||
|
|
||||||
### Stale ngrok URL
|
### Stale ngrok URL
|
||||||
|
|
||||||
@@ -506,7 +525,7 @@ Confirm parameters (token vs deviceId, auth headers) in that repo’s README or
|
|||||||
| `src/services/notifications/NotificationDebugEvents.ts` | Panel event log + `logNotification()` |
|
| `src/services/notifications/NotificationDebugEvents.ts` | Panel event log + `logNotification()` |
|
||||||
| `src/services/notifications/notificationLog.ts` | Structured log helpers |
|
| `src/services/notifications/notificationLog.ts` | Structured log helpers |
|
||||||
| `src/services/notifications/NotificationService.ts` | `POST /notifications/register` |
|
| `src/services/notifications/NotificationService.ts` | `POST /notifications/register` |
|
||||||
| `src/services/notifications/NativeNotificationService.ts` | Push delivery hook (logs ignored types; no refresh) |
|
| `src/services/notifications/NativeNotificationService.ts` | Refresh, `WAKEUP_PING`, schedule replace |
|
||||||
| `src/services/notifications/firebaseMessagingClient.ts` | Capacitor push listeners |
|
| `src/services/notifications/firebaseMessagingClient.ts` | Capacitor push listeners |
|
||||||
| `src/components/dev/NotificationDebugPanel.vue` | Dev UI |
|
| `src/components/dev/NotificationDebugPanel.vue` | Dev UI |
|
||||||
| `src/main.capacitor.ts` | Native push init at startup |
|
| `src/main.capacitor.ts` | Native push init at startup |
|
||||||
|
|||||||
@@ -1,16 +1,16 @@
|
|||||||
# Notification Debug Panel
|
# Notification Debug Panel
|
||||||
|
|
||||||
**Created:** 2026-07-07
|
**Created:** 2026-07-07
|
||||||
**Updated:** 2026-09-24
|
**Updated:** 2026-07-22
|
||||||
**Audience:** Developers testing notification registration, AlertSearch authorization upload, and FCM delivery diagnostics on native (iOS/Android) dev builds.
|
**Audience:** Developers testing notification registration, refresh, and WAKEUP_PING flows on native (iOS/Android) dev builds.
|
||||||
|
|
||||||
The **Notification Debug Panel** is a dev-only UI for FCM token registration, AlertSearch authorization upload, FCM wakeup delivery diagnostics (`/debug/send-wakeup`), and local schedule inspection. It does not schedule notifications from `WAKEUP_PING`. The production `WAKEUP_PING` → `/notifications/refresh` → `api_*` path was retired; leftover `api_*` schedules are cleared once at startup (Phase 4).
|
The **Notification Debug Panel** is a dev-only UI for exercising the same notification orchestration paths the production app uses: FCM token registration, backend refresh, wakeup handling, and local schedule inspection. It does not duplicate scheduling logic.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Notification API base URL
|
## Notification API base URL
|
||||||
|
|
||||||
Notification HTTP calls (`/notifications/register`, `/notifications/alert-authorization`, `/debug/send-wakeup`, etc.) do **not** use `APP_SERVER`. They use a dedicated Notification API host, resolved at runtime by `getNotificationApiBaseUrl()` in `NotificationDebugConfig.ts`. The app does not call `/notifications/refresh`.
|
Notification HTTP calls (`/notifications/register`, `/notifications/refresh`, `/debug/send-wakeup`, etc.) do **not** use `APP_SERVER`. They use a dedicated Notification API host, resolved at runtime by `getNotificationApiBaseUrl()` in `NotificationDebugConfig.ts`.
|
||||||
|
|
||||||
### Configuration constants
|
### Configuration constants
|
||||||
|
|
||||||
@@ -65,7 +65,7 @@ Settings persist in `localStorage` via `NotificationDebugConfig.ts`:
|
|||||||
| `notificationDebug.testMode` | `true` | Sent in JSON request bodies (`testMode: true/false`) |
|
| `notificationDebug.testMode` | `true` | Sent in JSON request bodies (`testMode: true/false`) |
|
||||||
| `notificationDebug.bypassAuth` | `false` | When `true`, omit JWT `Authorization` headers on notification API calls |
|
| `notificationDebug.bypassAuth` | `false` | When `true`, omit JWT `Authorization` headers on notification API calls |
|
||||||
|
|
||||||
All notification API requests (`/notifications/register`, `/notifications/alert-authorization`, `/debug/send-wakeup`, etc.) obtain headers through `getNotificationApiHeaders()` in `notificationApiAuth.ts`.
|
All notification API requests (`/notifications/register`, `/notifications/refresh`, `/debug/send-wakeup`, etc.) obtain headers through `getNotificationApiHeaders()` in `notificationApiAuth.ts`.
|
||||||
|
|
||||||
### Notification Backend URL
|
### Notification Backend URL
|
||||||
|
|
||||||
@@ -75,7 +75,7 @@ Leave empty to use the configured build default (`DEFAULT_NOTIFY_API_SERVER`, fr
|
|||||||
|
|
||||||
### Test Mode
|
### Test Mode
|
||||||
|
|
||||||
When enabled (default if never saved), register and send-wakeup requests include `"testMode": true` in the JSON body. The backend can use this to route test traffic separately from production.
|
When enabled (default if never saved), register and refresh requests include `"testMode": true` in the JSON body. The backend can use this to return dev-friendly schedules or route test traffic separately from production.
|
||||||
|
|
||||||
Test Mode is **independent of authentication**. It does not control whether `Authorization` headers are sent.
|
Test Mode is **independent of authentication**. It does not control whether `Authorization` headers are sent.
|
||||||
|
|
||||||
@@ -124,8 +124,9 @@ Example: `https://abc123.ngrok-free.app` or `http://127.0.0.1:3000`
|
|||||||
| Action | What it does |
|
| Action | What it does |
|
||||||
|--------|----------------|
|
|--------|----------------|
|
||||||
| **Register Token Now** | `POST {backend}/notifications/register` with current FCM token, `deviceId`, `platform`, and `testMode`. Forces re-registration (bypasses duplicate-token skip). |
|
| **Register Token Now** | `POST {backend}/notifications/register` with current FCM token, `deviceId`, `platform`, and `testMode`. Forces re-registration (bypasses duplicate-token skip). |
|
||||||
| **Upload AlertSearch Authorization** | Mints and uploads delegated AlertSearch JWTs (`POST /notifications/alert-authorization`). Requires an active `did:ethr` identity; Test Mode is not used. |
|
| **Refresh Notifications** | `POST {backend}/notifications/refresh` — same path used after a real WAKEUP_PING. Applies returned schedule to the native plugin. |
|
||||||
| **Send Real WAKEUP_PING** | `POST {backend}/debug/send-wakeup`; server sends a real FCM data message with `data.type = "WAKEUP_PING"`. FCM delivery diagnostic only — the app logs `push handler ignored type=…` and does **not** call `/notifications/refresh` or schedule `api_*` notifications. Background the app before expecting delivery. |
|
| **Simulate WAKEUP_PING (Local)** | Calls the refresh API directly (no FCM). Quick test of backend URL + auth + refresh parsing without push delivery. |
|
||||||
|
| **Send Real WAKEUP_PING** | `POST {backend}/debug/send-wakeup`; server sends a real FCM data message with `data.type = "WAKEUP_PING"`. Exercises backend → FCM → Capacitor listener → refresh → reschedule. Background the app before expecting delivery. |
|
||||||
|
|
||||||
**Current FCM Token** displays the last token from Capacitor/Firebase registration. **Event Log** shows the last 100 `[Notifications]` messages (also visible in logcat / Xcode console on native).
|
**Current FCM Token** displays the last token from Capacitor/Firebase registration. **Event Log** shows the last 100 `[Notifications]` messages (also visible in logcat / Xcode console on native).
|
||||||
|
|
||||||
@@ -135,8 +136,12 @@ Example: `https://abc123.ngrok-free.app` or `http://127.0.0.1:3000`
|
|||||||
|
|
||||||
| Section | Purpose |
|
| Section | Purpose |
|
||||||
|---------|---------|
|
|---------|---------|
|
||||||
| **Pending Notification Inspector** | Lists locally scheduled notifications (Daily Reminder, New Activity / dual, and any leftover `api_*` until Phase 4 cleanup). |
|
| **Mock Timing Presets** | Interval for mock refresh timestamps (30 sec – 10 min). |
|
||||||
| **Clear Notifications** | Clears/cancels plugin-scheduled notifications on native. Does not replace the one-time `api_*` startup cleanup. |
|
| **Trigger Mock Refresh** | Applies synthetic future timestamps locally — no backend call. |
|
||||||
|
| **Wakeup Ping Simulator** | Runs the production push handler with a synthetic `WAKEUP_PING` payload (no FCM, no backend). |
|
||||||
|
| **Flood Test** | Runs 20 sequential mock refreshes (stress test). |
|
||||||
|
| **Pending Notification Inspector** | Lists locally scheduled notifications (iOS; Android may show unavailable). |
|
||||||
|
| **Clear Notifications** | Clears/cancels all plugin-scheduled notifications on native. |
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -193,11 +198,11 @@ The app deferred registration because JWT could not be built (no active DID or e
|
|||||||
|
|
||||||
Same as above. Confirm the **Active** URL in the panel matches your running tunnel or local server port.
|
Same as above. Confirm the **Active** URL in the panel matches your running tunnel or local server port.
|
||||||
|
|
||||||
### Register succeeds but Send Real WAKEUP_PING does not show delivery
|
### Register succeeds but Send Real WAKEUP_PING does not trigger refresh
|
||||||
|
|
||||||
**Real WAKEUP_PING success** only means the backend accepted the wakeup request and attempted FCM delivery. It does **not** schedule local `api_*` notifications. Delivery is confirmed when logcat / Event Log shows `push handler ignored type=WAKEUP_PING` (the retired refresh chain is gone).
|
**Real WAKEUP_PING success** only means the backend accepted the wakeup request and attempted FCM delivery. Missing `pushNotificationReceived` / `Refresh completed (WAKEUP_PING)` indicates an FCM delivery or background execution issue — not necessarily a bad wakeup API call.
|
||||||
|
|
||||||
**Checks:** App backgrounded (not force-stopped); FCM token matches registration; Firebase / Play services available.
|
**Checks:** App backgrounded (not force-stopped); FCM token matches registration; **Simulate WAKEUP_PING (Local)** works (isolates FCM from refresh API).
|
||||||
|
|
||||||
See platform-specific guides for extended ngrok and FCM workflows:
|
See platform-specific guides for extended ngrok and FCM workflows:
|
||||||
|
|
||||||
@@ -217,7 +222,7 @@ See platform-specific guides for extended ngrok and FCM workflows:
|
|||||||
| `src/services/notifications/notificationApiDebugMode.ts` | Auth bypass gate |
|
| `src/services/notifications/notificationApiDebugMode.ts` | Auth bypass gate |
|
||||||
| `src/services/notifications/NotificationDebugService.ts` | Panel action handlers |
|
| `src/services/notifications/NotificationDebugService.ts` | Panel action handlers |
|
||||||
| `src/services/notifications/NotificationService.ts` | `POST /notifications/register` |
|
| `src/services/notifications/NotificationService.ts` | `POST /notifications/register` |
|
||||||
| `src/services/notifications/NativeNotificationService.ts` | Push delivery hook (logs ignored types; no refresh) |
|
| `src/services/notifications/NativeNotificationService.ts` | `POST /notifications/refresh`, WAKEUP_PING handler |
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|||||||
@@ -72,7 +72,7 @@
|
|||||||
/* End PBXFileReference section */
|
/* End PBXFileReference section */
|
||||||
|
|
||||||
/* Begin PBXFileSystemSynchronizedBuildFileExceptionSet section */
|
/* Begin PBXFileSystemSynchronizedBuildFileExceptionSet section */
|
||||||
C86585E32ED456DE00824752 /* PBXFileSystemSynchronizedBuildFileExceptionSet */ = {
|
C86585E32ED456DE00824752 /* Exceptions for "TimeSafariShareExtension" folder in "TimeSafariShareExtension" target */ = {
|
||||||
isa = PBXFileSystemSynchronizedBuildFileExceptionSet;
|
isa = PBXFileSystemSynchronizedBuildFileExceptionSet;
|
||||||
membershipExceptions = (
|
membershipExceptions = (
|
||||||
Info.plist,
|
Info.plist,
|
||||||
@@ -82,7 +82,18 @@
|
|||||||
/* End PBXFileSystemSynchronizedBuildFileExceptionSet section */
|
/* End PBXFileSystemSynchronizedBuildFileExceptionSet section */
|
||||||
|
|
||||||
/* Begin PBXFileSystemSynchronizedRootGroup section */
|
/* Begin PBXFileSystemSynchronizedRootGroup section */
|
||||||
C86585D62ED456DE00824752 /* TimeSafariShareExtension */ = {isa = PBXFileSystemSynchronizedRootGroup; exceptions = (C86585E32ED456DE00824752 /* PBXFileSystemSynchronizedBuildFileExceptionSet */, ); explicitFileTypes = {}; explicitFolders = (); path = TimeSafariShareExtension; sourceTree = "<group>"; };
|
C86585D62ED456DE00824752 /* TimeSafariShareExtension */ = {
|
||||||
|
isa = PBXFileSystemSynchronizedRootGroup;
|
||||||
|
exceptions = (
|
||||||
|
C86585E32ED456DE00824752 /* Exceptions for "TimeSafariShareExtension" folder in "TimeSafariShareExtension" target */,
|
||||||
|
);
|
||||||
|
explicitFileTypes = {
|
||||||
|
};
|
||||||
|
explicitFolders = (
|
||||||
|
);
|
||||||
|
path = TimeSafariShareExtension;
|
||||||
|
sourceTree = "<group>";
|
||||||
|
};
|
||||||
/* End PBXFileSystemSynchronizedRootGroup section */
|
/* End PBXFileSystemSynchronizedRootGroup section */
|
||||||
|
|
||||||
/* Begin PBXFrameworksBuildPhase section */
|
/* Begin PBXFrameworksBuildPhase section */
|
||||||
@@ -177,7 +188,7 @@
|
|||||||
012076E8FFE4BF260A79B034 /* Fix Privacy Manifest */,
|
012076E8FFE4BF260A79B034 /* Fix Privacy Manifest */,
|
||||||
96A7EF592DF3366D00084D51 /* Fix Privacy Manifest */,
|
96A7EF592DF3366D00084D51 /* Fix Privacy Manifest */,
|
||||||
C86585E02ED456DE00824752 /* Embed Foundation Extensions */,
|
C86585E02ED456DE00824752 /* Embed Foundation Extensions */,
|
||||||
2B3F98670AF3508A35AC3248 /* [CP] Embed Pods Frameworks */,
|
04A558C9E2DEC92B5B2AFBE4 /* [CP] Embed Pods Frameworks */,
|
||||||
);
|
);
|
||||||
buildRules = (
|
buildRules = (
|
||||||
);
|
);
|
||||||
@@ -294,7 +305,7 @@
|
|||||||
shellScript = "\"${PROJECT_DIR}/app_privacy_manifest_fixer/fixer.sh\" \n";
|
shellScript = "\"${PROJECT_DIR}/app_privacy_manifest_fixer/fixer.sh\" \n";
|
||||||
showEnvVarsInLog = 0;
|
showEnvVarsInLog = 0;
|
||||||
};
|
};
|
||||||
2B3F98670AF3508A35AC3248 /* [CP] Embed Pods Frameworks */ = {
|
04A558C9E2DEC92B5B2AFBE4 /* [CP] Embed Pods Frameworks */ = {
|
||||||
isa = PBXShellScriptBuildPhase;
|
isa = PBXShellScriptBuildPhase;
|
||||||
buildActionMask = 2147483647;
|
buildActionMask = 2147483647;
|
||||||
files = (
|
files = (
|
||||||
@@ -455,7 +466,7 @@
|
|||||||
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
|
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
|
||||||
GCC_WARN_UNUSED_FUNCTION = YES;
|
GCC_WARN_UNUSED_FUNCTION = YES;
|
||||||
GCC_WARN_UNUSED_VARIABLE = YES;
|
GCC_WARN_UNUSED_VARIABLE = YES;
|
||||||
IPHONEOS_DEPLOYMENT_TARGET = 15.5;
|
IPHONEOS_DEPLOYMENT_TARGET = 14.0;
|
||||||
MTL_ENABLE_DEBUG_INFO = YES;
|
MTL_ENABLE_DEBUG_INFO = YES;
|
||||||
ONLY_ACTIVE_ARCH = YES;
|
ONLY_ACTIVE_ARCH = YES;
|
||||||
SDKROOT = iphoneos;
|
SDKROOT = iphoneos;
|
||||||
@@ -512,7 +523,7 @@
|
|||||||
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
|
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
|
||||||
GCC_WARN_UNUSED_FUNCTION = YES;
|
GCC_WARN_UNUSED_FUNCTION = YES;
|
||||||
GCC_WARN_UNUSED_VARIABLE = YES;
|
GCC_WARN_UNUSED_VARIABLE = YES;
|
||||||
IPHONEOS_DEPLOYMENT_TARGET = 15.5;
|
IPHONEOS_DEPLOYMENT_TARGET = 14.0;
|
||||||
MTL_ENABLE_DEBUG_INFO = NO;
|
MTL_ENABLE_DEBUG_INFO = NO;
|
||||||
SDKROOT = iphoneos;
|
SDKROOT = iphoneos;
|
||||||
STRING_CATALOG_GENERATE_SYMBOLS = YES;
|
STRING_CATALOG_GENERATE_SYMBOLS = YES;
|
||||||
@@ -530,7 +541,7 @@
|
|||||||
CLANG_ENABLE_MODULES = YES;
|
CLANG_ENABLE_MODULES = YES;
|
||||||
CODE_SIGN_ENTITLEMENTS = App/AppDebug.entitlements;
|
CODE_SIGN_ENTITLEMENTS = App/AppDebug.entitlements;
|
||||||
CODE_SIGN_STYLE = Automatic;
|
CODE_SIGN_STYLE = Automatic;
|
||||||
CURRENT_PROJECT_VERSION = 70;
|
CURRENT_PROJECT_VERSION = 72;
|
||||||
DEVELOPMENT_TEAM = GM3FS5JQPH;
|
DEVELOPMENT_TEAM = GM3FS5JQPH;
|
||||||
ENABLE_APP_SANDBOX = NO;
|
ENABLE_APP_SANDBOX = NO;
|
||||||
ENABLE_USER_SCRIPT_SANDBOXING = NO;
|
ENABLE_USER_SCRIPT_SANDBOXING = NO;
|
||||||
@@ -541,7 +552,7 @@
|
|||||||
"$(inherited)",
|
"$(inherited)",
|
||||||
"@executable_path/Frameworks",
|
"@executable_path/Frameworks",
|
||||||
);
|
);
|
||||||
MARKETING_VERSION = 1.4.4;
|
MARKETING_VERSION = 1.4.6;
|
||||||
OTHER_SWIFT_FLAGS = "$(inherited) \"-D\" \"COCOAPODS\" \"-DDEBUG\"";
|
OTHER_SWIFT_FLAGS = "$(inherited) \"-D\" \"COCOAPODS\" \"-DDEBUG\"";
|
||||||
PRODUCT_BUNDLE_IDENTIFIER = app.timesafari;
|
PRODUCT_BUNDLE_IDENTIFIER = app.timesafari;
|
||||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||||
@@ -560,7 +571,7 @@
|
|||||||
CLANG_ENABLE_MODULES = YES;
|
CLANG_ENABLE_MODULES = YES;
|
||||||
CODE_SIGN_ENTITLEMENTS = App/App.entitlements;
|
CODE_SIGN_ENTITLEMENTS = App/App.entitlements;
|
||||||
CODE_SIGN_STYLE = Automatic;
|
CODE_SIGN_STYLE = Automatic;
|
||||||
CURRENT_PROJECT_VERSION = 70;
|
CURRENT_PROJECT_VERSION = 72;
|
||||||
DEVELOPMENT_TEAM = GM3FS5JQPH;
|
DEVELOPMENT_TEAM = GM3FS5JQPH;
|
||||||
ENABLE_APP_SANDBOX = NO;
|
ENABLE_APP_SANDBOX = NO;
|
||||||
ENABLE_USER_SCRIPT_SANDBOXING = NO;
|
ENABLE_USER_SCRIPT_SANDBOXING = NO;
|
||||||
@@ -571,7 +582,7 @@
|
|||||||
"$(inherited)",
|
"$(inherited)",
|
||||||
"@executable_path/Frameworks",
|
"@executable_path/Frameworks",
|
||||||
);
|
);
|
||||||
MARKETING_VERSION = 1.4.4;
|
MARKETING_VERSION = 1.4.6;
|
||||||
PRODUCT_BUNDLE_IDENTIFIER = app.timesafari;
|
PRODUCT_BUNDLE_IDENTIFIER = app.timesafari;
|
||||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||||
SWIFT_ACTIVE_COMPILATION_CONDITIONS = "";
|
SWIFT_ACTIVE_COMPILATION_CONDITIONS = "";
|
||||||
@@ -589,21 +600,21 @@
|
|||||||
CLANG_ENABLE_OBJC_WEAK = YES;
|
CLANG_ENABLE_OBJC_WEAK = YES;
|
||||||
CODE_SIGN_ENTITLEMENTS = TimeSafariShareExtension/TimeSafariShareExtension.entitlements;
|
CODE_SIGN_ENTITLEMENTS = TimeSafariShareExtension/TimeSafariShareExtension.entitlements;
|
||||||
CODE_SIGN_STYLE = Automatic;
|
CODE_SIGN_STYLE = Automatic;
|
||||||
CURRENT_PROJECT_VERSION = 70;
|
CURRENT_PROJECT_VERSION = 72;
|
||||||
DEVELOPMENT_TEAM = GM3FS5JQPH;
|
DEVELOPMENT_TEAM = GM3FS5JQPH;
|
||||||
GCC_C_LANGUAGE_STANDARD = gnu17;
|
GCC_C_LANGUAGE_STANDARD = gnu17;
|
||||||
GENERATE_INFOPLIST_FILE = YES;
|
GENERATE_INFOPLIST_FILE = YES;
|
||||||
INFOPLIST_FILE = TimeSafariShareExtension/Info.plist;
|
INFOPLIST_FILE = TimeSafariShareExtension/Info.plist;
|
||||||
INFOPLIST_KEY_CFBundleDisplayName = Giftopia;
|
INFOPLIST_KEY_CFBundleDisplayName = Giftopia;
|
||||||
INFOPLIST_KEY_NSHumanReadableCopyright = "";
|
INFOPLIST_KEY_NSHumanReadableCopyright = "";
|
||||||
IPHONEOS_DEPLOYMENT_TARGET = 15.5;
|
IPHONEOS_DEPLOYMENT_TARGET = 14.0;
|
||||||
LD_RUNPATH_SEARCH_PATHS = (
|
LD_RUNPATH_SEARCH_PATHS = (
|
||||||
"$(inherited)",
|
"$(inherited)",
|
||||||
"@executable_path/Frameworks",
|
"@executable_path/Frameworks",
|
||||||
"@executable_path/../../Frameworks",
|
"@executable_path/../../Frameworks",
|
||||||
);
|
);
|
||||||
LOCALIZATION_PREFERS_STRING_CATALOGS = YES;
|
LOCALIZATION_PREFERS_STRING_CATALOGS = YES;
|
||||||
MARKETING_VERSION = 1.4.4;
|
MARKETING_VERSION = 1.4.6;
|
||||||
MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE;
|
MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE;
|
||||||
MTL_FAST_MATH = YES;
|
MTL_FAST_MATH = YES;
|
||||||
PRODUCT_BUNDLE_IDENTIFIER = app.timesafari.TimeSafariShareExtension;
|
PRODUCT_BUNDLE_IDENTIFIER = app.timesafari.TimeSafariShareExtension;
|
||||||
@@ -627,21 +638,21 @@
|
|||||||
CLANG_ENABLE_OBJC_WEAK = YES;
|
CLANG_ENABLE_OBJC_WEAK = YES;
|
||||||
CODE_SIGN_ENTITLEMENTS = TimeSafariShareExtension/TimeSafariShareExtension.entitlements;
|
CODE_SIGN_ENTITLEMENTS = TimeSafariShareExtension/TimeSafariShareExtension.entitlements;
|
||||||
CODE_SIGN_STYLE = Automatic;
|
CODE_SIGN_STYLE = Automatic;
|
||||||
CURRENT_PROJECT_VERSION = 70;
|
CURRENT_PROJECT_VERSION = 72;
|
||||||
DEVELOPMENT_TEAM = GM3FS5JQPH;
|
DEVELOPMENT_TEAM = GM3FS5JQPH;
|
||||||
GCC_C_LANGUAGE_STANDARD = gnu17;
|
GCC_C_LANGUAGE_STANDARD = gnu17;
|
||||||
GENERATE_INFOPLIST_FILE = YES;
|
GENERATE_INFOPLIST_FILE = YES;
|
||||||
INFOPLIST_FILE = TimeSafariShareExtension/Info.plist;
|
INFOPLIST_FILE = TimeSafariShareExtension/Info.plist;
|
||||||
INFOPLIST_KEY_CFBundleDisplayName = Giftopia;
|
INFOPLIST_KEY_CFBundleDisplayName = Giftopia;
|
||||||
INFOPLIST_KEY_NSHumanReadableCopyright = "";
|
INFOPLIST_KEY_NSHumanReadableCopyright = "";
|
||||||
IPHONEOS_DEPLOYMENT_TARGET = 15.5;
|
IPHONEOS_DEPLOYMENT_TARGET = 14.0;
|
||||||
LD_RUNPATH_SEARCH_PATHS = (
|
LD_RUNPATH_SEARCH_PATHS = (
|
||||||
"$(inherited)",
|
"$(inherited)",
|
||||||
"@executable_path/Frameworks",
|
"@executable_path/Frameworks",
|
||||||
"@executable_path/../../Frameworks",
|
"@executable_path/../../Frameworks",
|
||||||
);
|
);
|
||||||
LOCALIZATION_PREFERS_STRING_CATALOGS = YES;
|
LOCALIZATION_PREFERS_STRING_CATALOGS = YES;
|
||||||
MARKETING_VERSION = 1.4.4;
|
MARKETING_VERSION = 1.4.6;
|
||||||
MTL_FAST_MATH = YES;
|
MTL_FAST_MATH = YES;
|
||||||
PRODUCT_BUNDLE_IDENTIFIER = app.timesafari.TimeSafariShareExtension;
|
PRODUCT_BUNDLE_IDENTIFIER = app.timesafari.TimeSafariShareExtension;
|
||||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||||
|
|||||||
@@ -82,10 +82,6 @@ post_install do |installer|
|
|||||||
assertDeploymentTarget(installer)
|
assertDeploymentTarget(installer)
|
||||||
installer.pods_project.targets.each do |target|
|
installer.pods_project.targets.each do |target|
|
||||||
target.build_configurations.each do |config|
|
target.build_configurations.each do |config|
|
||||||
deployment_target = config.build_settings['IPHONEOS_DEPLOYMENT_TARGET'].to_f
|
|
||||||
if deployment_target > 0.0 && deployment_target < 15.5
|
|
||||||
config.build_settings['IPHONEOS_DEPLOYMENT_TARGET'] = '15.5'
|
|
||||||
end
|
|
||||||
config.build_settings['CLANG_ALLOW_NON_MODULAR_INCLUDES_IN_FRAMEWORK_MODULES'] = 'YES'
|
config.build_settings['CLANG_ALLOW_NON_MODULAR_INCLUDES_IN_FRAMEWORK_MODULES'] = 'YES'
|
||||||
merge_sqlite_omit_load_extension_definition(config)
|
merge_sqlite_omit_load_extension_definition(config)
|
||||||
strip_system_sqlite_from_pod_config(config)
|
strip_system_sqlite_from_pod_config(config)
|
||||||
|
|||||||
@@ -177,6 +177,6 @@ SPEC CHECKSUMS:
|
|||||||
TimesafariDailyNotificationPlugin: 69277c884380a9a620f671b68e0327eaa4b3d27d
|
TimesafariDailyNotificationPlugin: 69277c884380a9a620f671b68e0327eaa4b3d27d
|
||||||
ZIPFoundation: dfd3d681c4053ff7e2f7350bc4e53b5dba3f5351
|
ZIPFoundation: dfd3d681c4053ff7e2f7350bc4e53b5dba3f5351
|
||||||
|
|
||||||
PODFILE CHECKSUM: 5736811d271d5309d3e2de8f3eefdbb6632086a3
|
PODFILE CHECKSUM: abe640043e6b8adea745693d980ead03251912f9
|
||||||
|
|
||||||
COCOAPODS: 1.16.2
|
COCOAPODS: 1.16.2
|
||||||
|
|||||||
Generated
+1462
-1522
File diff suppressed because it is too large
Load Diff
+118
-117
@@ -1,138 +1,139 @@
|
|||||||
{
|
{
|
||||||
"name": "giftopia",
|
"name": "giftopia",
|
||||||
"version": "1.4.4",
|
"version": "1.4.7-beta",
|
||||||
"description": "Giftopia App",
|
"description": "Giftopia App",
|
||||||
"author": {
|
"author": {
|
||||||
"name": "Gift Economies Team"
|
"name": "Gift Economies Team"
|
||||||
},
|
},
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"lint": "eslint --ext .js,.ts,.vue --ignore-path .gitignore src",
|
"assets:clean": "rimraf android/app/src/main/res/mipmap-* ios/App/App/Assets.xcassets/**/AppIcon*.png ios/App/App/Assets.xcassets/**/Splash*.png || true",
|
||||||
"lint-fix": "eslint --ext .js,.ts,.vue --ignore-path .gitignore --fix src",
|
|
||||||
"type-check": "tsc --noEmit",
|
|
||||||
"type-check:vue": "vue-tsc --noEmit",
|
|
||||||
"prebuild": "eslint --ext .js,.ts,.vue --ignore-path .gitignore src && node sw_combine.js && node scripts/copy-wasm.js",
|
|
||||||
"test:prerequisites": "node scripts/check-prerequisites.js",
|
|
||||||
"test:unit": "jest",
|
|
||||||
"check:dependencies": "./scripts/check-dependencies.sh",
|
|
||||||
"deps:update-daily-notification-plugin": "npm install @timesafari/daily-notification-plugin@git+https://gitea.anomalistdesign.com/trent_larson/daily-notification-plugin.git#master",
|
|
||||||
"test:all": "npm run lint && npm run type-check && npm run type-check:vue && npm run test:unit && npm run test:web && npm run test:mobile && echo '\n\n\nGotta add the performance tests'",
|
|
||||||
"test:web": "npx playwright test -c playwright.config-local.ts --trace on",
|
|
||||||
"test:mobile": "./scripts/test-mobile.sh",
|
|
||||||
"test:android": "node scripts/test-android.js",
|
|
||||||
"test:ios": "node scripts/test-ios.js",
|
|
||||||
"check:android-device": "adb devices | grep -w 'device' || (echo 'No Android device connected' && exit 1)",
|
|
||||||
"check:ios-device": "xcrun xctrace list devices 2>&1 | grep -w 'Booted' || (echo 'No iOS simulator running' && exit 1)",
|
|
||||||
"auto-run": "./scripts/auto-run.sh",
|
|
||||||
"auto-run:test": "./scripts/auto-run.sh --test",
|
|
||||||
"auto-run:prod": "./scripts/auto-run.sh --prod",
|
|
||||||
"auto-run:ios": "./scripts/auto-run.sh --platform=ios",
|
|
||||||
"auto-run:android": "./scripts/auto-run.sh --platform=android",
|
|
||||||
"auto-run:electron": "./scripts/auto-run.sh --platform=electron",
|
|
||||||
"build:capacitor": "VITE_GIT_HASH=`git log -1 --pretty=format:%h` vite build --mode capacitor --config vite.config.capacitor.mts",
|
|
||||||
"build:capacitor:sync": "npm run build:capacitor && npx cap sync && node scripts/restore-local-plugins.js",
|
|
||||||
"build:native": "vite build && npx cap sync && node scripts/restore-local-plugins.js && bash -c 'source scripts/common.sh && ensure_ios_capacitor_asset_directories' && npx capacitor-assets generate",
|
|
||||||
"assets:config": "npx tsx scripts/assets-config.ts",
|
"assets:config": "npx tsx scripts/assets-config.ts",
|
||||||
"assets:validate": "npx tsx scripts/assets-validator.ts",
|
"assets:validate": "npx tsx scripts/assets-validator.ts",
|
||||||
"assets:validate:android": "./scripts/build-android.sh --assets-only",
|
"assets:validate:android": "./scripts/build-android.sh --assets-only",
|
||||||
"assets:clean": "rimraf android/app/src/main/res/mipmap-* ios/App/App/Assets.xcassets/**/AppIcon*.png ios/App/App/Assets.xcassets/**/Splash*.png || true",
|
"auto-run": "./scripts/auto-run.sh",
|
||||||
"build:ios": "./scripts/build-ios.sh",
|
"auto-run:android": "./scripts/auto-run.sh --platform=android",
|
||||||
"build:ios:dev": "./scripts/build-ios.sh --dev",
|
"auto-run:electron": "./scripts/auto-run.sh --platform=electron",
|
||||||
"build:ios:test": "./scripts/build-ios.sh --test",
|
"auto-run:ios": "./scripts/auto-run.sh --platform=ios",
|
||||||
"build:ios:test:run": "./scripts/build-ios.sh --test --auto-run",
|
"auto-run:prod": "./scripts/auto-run.sh --prod",
|
||||||
"build:ios:prod": "./scripts/build-ios.sh --prod",
|
"auto-run:test": "./scripts/auto-run.sh --test",
|
||||||
"build:ios:prod:run": "./scripts/build-ios.sh --prod --auto-run",
|
|
||||||
"build:ios:debug": "./scripts/build-ios.sh --debug",
|
|
||||||
"build:ios:debug:run": "./scripts/build-ios.sh --debug --auto-run",
|
|
||||||
"build:ios:release": "./scripts/build-ios.sh --release",
|
|
||||||
"build:ios:release:run": "./scripts/build-ios.sh --release --auto-run",
|
|
||||||
"build:ios:studio": "./scripts/build-ios.sh --studio",
|
|
||||||
"build:ios:ipa": "./scripts/build-ios.sh --ipa",
|
|
||||||
"build:ios:clean": "./scripts/build-ios.sh --clean",
|
|
||||||
"build:ios:sync": "./scripts/build-ios.sh --sync",
|
|
||||||
"build:ios:assets": "./scripts/build-ios.sh --assets",
|
|
||||||
"build:ios:deploy": "./scripts/build-ios.sh --deploy",
|
|
||||||
"build:ios:dev:custom": "./scripts/build-ios.sh --dev --api-ip",
|
|
||||||
"build:ios:test:custom": "./scripts/build-ios.sh --test --api-ip",
|
|
||||||
"build:ios:dev:run:custom": "./scripts/build-ios.sh --dev --api-ip --auto-run",
|
|
||||||
"build:ios:test:run:custom": "./scripts/build-ios.sh --test --api-ip --auto-run",
|
|
||||||
"build:web": "./scripts/build-web.sh",
|
|
||||||
"build:web:dev": "./scripts/build-web.sh --dev",
|
|
||||||
"build:web:test": "./scripts/build-web.sh --test",
|
|
||||||
"build:web:prod": "./scripts/build-web.sh --prod",
|
|
||||||
"build:web:docker": "./scripts/build-web.sh --docker",
|
|
||||||
"build:web:docker:test": "./scripts/build-web.sh --docker:test",
|
|
||||||
"build:web:docker:prod": "./scripts/build-web.sh --docker:prod",
|
|
||||||
"build:web:serve": "./scripts/build-web.sh --serve",
|
|
||||||
"build:web:serve:test": "./scripts/build-web.sh --serve --test",
|
|
||||||
"build:web:serve:prod": "./scripts/build-web.sh --serve --prod",
|
|
||||||
"docker:up": "docker-compose up",
|
|
||||||
"docker:up:test": "npm run build:web:build -- --mode test && docker-compose up test",
|
|
||||||
"docker:up:prod": "npm run build:web:build -- --mode production && docker-compose up production",
|
|
||||||
"docker:down": "docker-compose down",
|
|
||||||
"docker:logs": "docker-compose logs -f",
|
|
||||||
"build:electron": "./scripts/build-electron.sh",
|
|
||||||
"build:electron:dev": "./scripts/build-electron.sh --dev",
|
|
||||||
"build:electron:dev:run": "./scripts/build-electron.sh --dev --auto-run",
|
|
||||||
"build:electron:test": "./scripts/build-electron.sh --test",
|
|
||||||
"build:electron:test:run": "./scripts/build-electron.sh --test --auto-run",
|
|
||||||
"build:electron:prod": "./scripts/build-electron.sh --prod",
|
|
||||||
"build:electron:prod:run": "./scripts/build-electron.sh --prod --auto-run",
|
|
||||||
"build:electron:windows": "./scripts/build-electron.sh --prod --windows",
|
|
||||||
"build:electron:windows:dev": "./scripts/build-electron.sh --dev --windows",
|
|
||||||
"build:electron:windows:test": "./scripts/build-electron.sh --test --windows",
|
|
||||||
"build:electron:windows:prod": "./scripts/build-electron.sh --prod --windows",
|
|
||||||
"build:electron:mac": "./scripts/build-electron.sh --prod --mac",
|
|
||||||
"build:electron:mac:dev": "./scripts/build-electron.sh --dev --mac",
|
|
||||||
"build:electron:mac:test": "./scripts/build-electron.sh --test --mac",
|
|
||||||
"build:electron:mac:prod": "./scripts/build-electron.sh --prod --mac",
|
|
||||||
"build:electron:linux": "./scripts/build-electron.sh --prod --linux",
|
|
||||||
"build:electron:linux:dev": "./scripts/build-electron.sh --dev --linux",
|
|
||||||
"build:electron:linux:test": "./scripts/build-electron.sh --test --linux",
|
|
||||||
"build:electron:linux:prod": "./scripts/build-electron.sh --prod --linux",
|
|
||||||
"build:electron:appimage": "./scripts/build-electron.sh --prod --appimage",
|
|
||||||
"build:electron:appimage:dev": "./scripts/build-electron.sh --dev --appimage",
|
|
||||||
"build:electron:appimage:test": "./scripts/build-electron.sh --test --appimage",
|
|
||||||
"build:electron:appimage:prod": "./scripts/build-electron.sh --prod --appimage",
|
|
||||||
"build:electron:deb": "./scripts/build-electron.sh --prod --deb",
|
|
||||||
"build:electron:deb:dev": "./scripts/build-electron.sh --dev --deb",
|
|
||||||
"build:electron:deb:test": "./scripts/build-electron.sh --test --deb",
|
|
||||||
"build:electron:deb:prod": "./scripts/build-electron.sh --prod --deb",
|
|
||||||
"build:electron:dmg": "./scripts/build-electron.sh --prod --dmg",
|
|
||||||
"build:electron:dmg:dev": "./scripts/build-electron.sh --dev --dmg",
|
|
||||||
"build:electron:dmg:test": "./scripts/build-electron.sh --test --dmg",
|
|
||||||
"build:electron:dmg:prod": "./scripts/build-electron.sh --prod --dmg",
|
|
||||||
"markdown:fix": "markdownlint-cli2 --fix",
|
|
||||||
"markdown:check": "markdownlint-cli2",
|
|
||||||
"markdown:setup": "./scripts/setup-markdown-hooks.sh",
|
|
||||||
"prepare": "husky",
|
|
||||||
"guard": "bash ./scripts/build-arch-guard.sh",
|
|
||||||
"guard:test": "bash ./scripts/build-arch-guard.sh --staged",
|
|
||||||
"guard:setup": "npm run prepare && echo '✅ Build Architecture Guard is now active!'",
|
|
||||||
"clean:android": "./scripts/uninstall-android.sh",
|
|
||||||
"clean:ios": "rm -rf ios/App/build ios/App/Pods ios/App/output ios/App/App/public ios/DerivedData ios/capacitor-cordova-ios-plugins ios/App/App/capacitor.config.json ios/App/App/config.xml || true",
|
|
||||||
"clean:electron": "./scripts/build-electron.sh --clean",
|
|
||||||
"clean:all": "npm run clean:ios && npm run clean:android && npm run clean:electron",
|
|
||||||
"build:android": "./scripts/build-android.sh",
|
"build:android": "./scripts/build-android.sh",
|
||||||
"build:android:dev": "./scripts/build-android.sh --dev",
|
"build:android:aab": "./scripts/build-android.sh --aab",
|
||||||
"build:android:test": "./scripts/build-android.sh --test",
|
"build:android:apk": "./scripts/build-android.sh --apk",
|
||||||
"build:android:test:run": "./scripts/build-android.sh --test --auto-run",
|
"build:android:assets": "./scripts/build-android.sh --assets",
|
||||||
"build:android:prod": "./scripts/build-android.sh --prod",
|
"build:android:clean": "./scripts/build-android.sh --clean",
|
||||||
"build:android:prod:run": "./scripts/build-android.sh --prod --auto-run",
|
|
||||||
"build:android:debug": "./scripts/build-android.sh --debug",
|
"build:android:debug": "./scripts/build-android.sh --debug",
|
||||||
"build:android:debug:run": "./scripts/build-android.sh --debug --auto-run",
|
"build:android:debug:run": "./scripts/build-android.sh --debug --auto-run",
|
||||||
|
"build:android:deploy": "./scripts/build-android.sh --deploy",
|
||||||
|
"build:android:dev": "./scripts/build-android.sh --dev",
|
||||||
|
"build:android:dev:custom": "./scripts/build-android.sh --dev --api-ip",
|
||||||
|
"build:android:dev:run:custom": "./scripts/build-android.sh --dev --api-ip --auto-run",
|
||||||
|
"build:android:prod": "./scripts/build-android.sh --prod",
|
||||||
|
"build:android:prod:run": "./scripts/build-android.sh --prod --auto-run",
|
||||||
"build:android:release": "./scripts/build-android.sh --release",
|
"build:android:release": "./scripts/build-android.sh --release",
|
||||||
"build:android:release:run": "./scripts/build-android.sh --release --auto-run",
|
"build:android:release:run": "./scripts/build-android.sh --release --auto-run",
|
||||||
"build:android:studio": "./scripts/build-android.sh --studio",
|
"build:android:studio": "./scripts/build-android.sh --studio",
|
||||||
"build:android:apk": "./scripts/build-android.sh --apk",
|
|
||||||
"build:android:aab": "./scripts/build-android.sh --aab",
|
|
||||||
"build:android:clean": "./scripts/build-android.sh --clean",
|
|
||||||
"build:android:sync": "./scripts/build-android.sh --sync",
|
"build:android:sync": "./scripts/build-android.sh --sync",
|
||||||
"build:android:assets": "./scripts/build-android.sh --assets",
|
"build:android:test": "./scripts/build-android.sh --test",
|
||||||
"build:android:deploy": "./scripts/build-android.sh --deploy",
|
|
||||||
"build:android:dev:custom": "./scripts/build-android.sh --dev --api-ip",
|
|
||||||
"build:android:test:custom": "./scripts/build-android.sh --test --api-ip",
|
"build:android:test:custom": "./scripts/build-android.sh --test --api-ip",
|
||||||
"build:android:dev:run:custom": "./scripts/build-android.sh --dev --api-ip --auto-run",
|
"build:android:test:run": "./scripts/build-android.sh --test --auto-run",
|
||||||
"build:android:test:run:custom": "./scripts/build-android.sh --test --api-ip --auto-run"
|
"build:android:test:run:custom": "./scripts/build-android.sh --test --api-ip --auto-run",
|
||||||
|
"build:capacitor": "VITE_GIT_HASH=`git log -1 --pretty=format:%h` vite build --mode capacitor --config vite.config.capacitor.mts",
|
||||||
|
"build:capacitor:sync": "npm run build:capacitor && npx cap sync && node scripts/restore-local-plugins.js",
|
||||||
|
"build:electron": "./scripts/build-electron.sh",
|
||||||
|
"build:electron:appimage": "./scripts/build-electron.sh --prod --appimage",
|
||||||
|
"build:electron:appimage:dev": "./scripts/build-electron.sh --dev --appimage",
|
||||||
|
"build:electron:appimage:prod": "./scripts/build-electron.sh --prod --appimage",
|
||||||
|
"build:electron:appimage:test": "./scripts/build-electron.sh --test --appimage",
|
||||||
|
"build:electron:deb": "./scripts/build-electron.sh --prod --deb",
|
||||||
|
"build:electron:deb:dev": "./scripts/build-electron.sh --dev --deb",
|
||||||
|
"build:electron:deb:prod": "./scripts/build-electron.sh --prod --deb",
|
||||||
|
"build:electron:deb:test": "./scripts/build-electron.sh --test --deb",
|
||||||
|
"build:electron:dev": "./scripts/build-electron.sh --dev",
|
||||||
|
"build:electron:dev:run": "./scripts/build-electron.sh --dev --auto-run",
|
||||||
|
"build:electron:dmg": "./scripts/build-electron.sh --prod --dmg",
|
||||||
|
"build:electron:dmg:dev": "./scripts/build-electron.sh --dev --dmg",
|
||||||
|
"build:electron:dmg:prod": "./scripts/build-electron.sh --prod --dmg",
|
||||||
|
"build:electron:dmg:test": "./scripts/build-electron.sh --test --dmg",
|
||||||
|
"build:electron:linux": "./scripts/build-electron.sh --prod --linux",
|
||||||
|
"build:electron:linux:dev": "./scripts/build-electron.sh --dev --linux",
|
||||||
|
"build:electron:linux:prod": "./scripts/build-electron.sh --prod --linux",
|
||||||
|
"build:electron:linux:test": "./scripts/build-electron.sh --test --linux",
|
||||||
|
"build:electron:mac": "./scripts/build-electron.sh --prod --mac",
|
||||||
|
"build:electron:mac:dev": "./scripts/build-electron.sh --dev --mac",
|
||||||
|
"build:electron:mac:prod": "./scripts/build-electron.sh --prod --mac",
|
||||||
|
"build:electron:mac:test": "./scripts/build-electron.sh --test --mac",
|
||||||
|
"build:electron:prod": "./scripts/build-electron.sh --prod",
|
||||||
|
"build:electron:prod:run": "./scripts/build-electron.sh --prod --auto-run",
|
||||||
|
"build:electron:test": "./scripts/build-electron.sh --test",
|
||||||
|
"build:electron:test:run": "./scripts/build-electron.sh --test --auto-run",
|
||||||
|
"build:electron:windows": "./scripts/build-electron.sh --prod --windows",
|
||||||
|
"build:electron:windows:dev": "./scripts/build-electron.sh --dev --windows",
|
||||||
|
"build:electron:windows:prod": "./scripts/build-electron.sh --prod --windows",
|
||||||
|
"build:electron:windows:test": "./scripts/build-electron.sh --test --windows",
|
||||||
|
"build:ios": "./scripts/build-ios.sh",
|
||||||
|
"build:ios:assets": "./scripts/build-ios.sh --assets",
|
||||||
|
"build:ios:clean": "./scripts/build-ios.sh --clean",
|
||||||
|
"build:ios:debug": "./scripts/build-ios.sh --debug",
|
||||||
|
"build:ios:debug:run": "./scripts/build-ios.sh --debug --auto-run",
|
||||||
|
"build:ios:deploy": "./scripts/build-ios.sh --deploy",
|
||||||
|
"build:ios:dev": "./scripts/build-ios.sh --dev",
|
||||||
|
"build:ios:dev:custom": "./scripts/build-ios.sh --dev --api-ip",
|
||||||
|
"build:ios:dev:run:custom": "./scripts/build-ios.sh --dev --api-ip --auto-run",
|
||||||
|
"build:ios:ipa": "./scripts/build-ios.sh --ipa",
|
||||||
|
"build:ios:prod": "./scripts/build-ios.sh --prod",
|
||||||
|
"build:ios:prod:run": "./scripts/build-ios.sh --prod --auto-run",
|
||||||
|
"build:ios:release": "./scripts/build-ios.sh --release",
|
||||||
|
"build:ios:release:run": "./scripts/build-ios.sh --release --auto-run",
|
||||||
|
"build:ios:studio": "./scripts/build-ios.sh --studio",
|
||||||
|
"build:ios:sync": "./scripts/build-ios.sh --sync",
|
||||||
|
"build:ios:test": "./scripts/build-ios.sh --test",
|
||||||
|
"build:ios:test:custom": "./scripts/build-ios.sh --test --api-ip",
|
||||||
|
"build:ios:test:run": "./scripts/build-ios.sh --test --auto-run",
|
||||||
|
"build:ios:test:run:custom": "./scripts/build-ios.sh --test --api-ip --auto-run",
|
||||||
|
"build:native": "vite build && npx cap sync && node scripts/restore-local-plugins.js && bash -c 'source scripts/common.sh && ensure_ios_capacitor_asset_directories' && npx capacitor-assets generate --assetPath resources",
|
||||||
|
"build:web": "./scripts/build-web.sh",
|
||||||
|
"build:web:dev": "./scripts/build-web.sh --dev",
|
||||||
|
"build:web:docker": "./scripts/build-web.sh --docker",
|
||||||
|
"build:web:docker:prod": "./scripts/build-web.sh --docker:prod",
|
||||||
|
"build:web:docker:test": "./scripts/build-web.sh --docker:test",
|
||||||
|
"build:web:prod": "./scripts/build-web.sh --prod",
|
||||||
|
"build:web:serve": "./scripts/build-web.sh --serve",
|
||||||
|
"build:web:serve:prod": "./scripts/build-web.sh --serve --prod",
|
||||||
|
"build:web:serve:test": "./scripts/build-web.sh --serve --test",
|
||||||
|
"build:web:test": "./scripts/build-web.sh --test",
|
||||||
|
"check:android-device": "adb devices | grep -w 'device' || (echo 'No Android device connected' && exit 1)",
|
||||||
|
"check:dependencies": "./scripts/check-dependencies.sh",
|
||||||
|
"check:ios-device": "xcrun xctrace list devices 2>&1 | grep -w 'Booted' || (echo 'No iOS simulator running' && exit 1)",
|
||||||
|
"check:prod-bundle": "./scripts/check-prod-bundle.sh",
|
||||||
|
"clean:all": "npm run clean:ios && npm run clean:android && npm run clean:electron",
|
||||||
|
"clean:android": "./scripts/uninstall-android.sh",
|
||||||
|
"clean:electron": "./scripts/build-electron.sh --clean",
|
||||||
|
"clean:ios": "rm -rf ios/App/build ios/App/Pods ios/App/output ios/App/App/public ios/DerivedData ios/capacitor-cordova-ios-plugins ios/App/App/capacitor.config.json ios/App/App/config.xml || true",
|
||||||
|
"deps:update-daily-notification-plugin": "npm install @timesafari/daily-notification-plugin@git+https://gitea.anomalistdesign.com/trent_larson/daily-notification-plugin.git#master",
|
||||||
|
"docker:down": "docker-compose down",
|
||||||
|
"docker:logs": "docker-compose logs -f",
|
||||||
|
"docker:up": "docker-compose up",
|
||||||
|
"docker:up:prod": "npm run build:web:build -- --mode production && docker-compose up production",
|
||||||
|
"docker:up:test": "npm run build:web:build -- --mode test && docker-compose up test",
|
||||||
|
"guard": "bash ./scripts/build-arch-guard.sh",
|
||||||
|
"guard:setup": "npm run prepare && echo 'Build Architecture Guard is now active!'",
|
||||||
|
"guard:test": "bash ./scripts/build-arch-guard.sh --staged",
|
||||||
|
"lint": "eslint --ext .js,.ts,.vue --ignore-path .gitignore src",
|
||||||
|
"lint-fix": "eslint --ext .js,.ts,.vue --ignore-path .gitignore --fix src",
|
||||||
|
"markdown:check": "markdownlint-cli2",
|
||||||
|
"markdown:fix": "markdownlint-cli2 --fix",
|
||||||
|
"markdown:setup": "./scripts/setup-markdown-hooks.sh",
|
||||||
|
"prebuild": "eslint --ext .js,.ts,.vue --ignore-path .gitignore src && node sw_combine.js && node scripts/copy-wasm.js",
|
||||||
|
"prepare": "husky",
|
||||||
|
"test:all": "npm run lint && npm run type-check && npm run type-check:vue && npm run test:unit && npm run test:web && npm run test:mobile && echo '\n\n\nGotta add the performance tests'",
|
||||||
|
"test:android": "node scripts/test-android.js",
|
||||||
|
"test:ios": "node scripts/test-ios.js",
|
||||||
|
"test:mobile": "./scripts/test-mobile.sh",
|
||||||
|
"test:prerequisites": "node scripts/check-prerequisites.js",
|
||||||
|
"test:unit": "jest",
|
||||||
|
"test:web": "npx playwright test -c playwright.config-local.ts --trace on",
|
||||||
|
"type-check": "tsc --noEmit",
|
||||||
|
"type-check:vue": "vue-tsc --noEmit"
|
||||||
},
|
},
|
||||||
"lint-staged": {
|
"lint-staged": {
|
||||||
"*.{js,ts,vue,css,json,yml,yaml}": "eslint --fix || true",
|
"*.{js,ts,vue,css,json,yml,yaml}": "eslint --fix || true",
|
||||||
|
|||||||
+7
-8
@@ -46,20 +46,19 @@ Assets are automatically generated during the build process using `capacitor-ass
|
|||||||
|
|
||||||
```bash
|
```bash
|
||||||
# Generate assets for all platforms
|
# Generate assets for all platforms
|
||||||
npx capacitor-assets generate
|
npx capacitor-assets generate --assetPath resources
|
||||||
|
|
||||||
# Generate assets for specific platform
|
# Generate assets for specific platform
|
||||||
npx capacitor-assets generate --ios
|
npx capacitor-assets generate --ios --assetPath resources
|
||||||
npx capacitor-assets generate --android
|
npx capacitor-assets generate --android --assetPath resources
|
||||||
npx capacitor-assets generate --web
|
npx capacitor-assets generate --web --assetPath resources
|
||||||
```
|
```
|
||||||
|
|
||||||
## Configuration
|
## Configuration
|
||||||
|
|
||||||
`resources/` is this project's canonical asset source. `@capacitor/assets`
|
`resources/` is this project's canonical asset source. Pass it explicitly with
|
||||||
prioritizes a top-level `assets/` directory over `resources/`, so a legacy
|
`--assetPath resources`; `@capacitor/assets` otherwise prioritizes a top-level
|
||||||
`assets/` directory can prevent these assets from being discovered. Remove that
|
`assets/` directory when one exists, even when that directory is empty.
|
||||||
directory when it is empty or obsolete.
|
|
||||||
|
|
||||||
## Version Control
|
## Version Control
|
||||||
|
|
||||||
|
|||||||
+33
-13
@@ -545,7 +545,7 @@ fi
|
|||||||
# Handle assets-only mode
|
# Handle assets-only mode
|
||||||
if [ "$ASSETS_ONLY" = true ]; then
|
if [ "$ASSETS_ONLY" = true ]; then
|
||||||
log_info "Assets-only mode: generating assets"
|
log_info "Assets-only mode: generating assets"
|
||||||
safe_execute "Generating assets" "npx capacitor-assets generate --android" || exit 7
|
safe_execute "Generating assets" "npx capacitor-assets generate --android --assetPath resources" || exit 7
|
||||||
log_success "Assets generation completed successfully!"
|
log_success "Assets generation completed successfully!"
|
||||||
exit 0
|
exit 0
|
||||||
fi
|
fi
|
||||||
@@ -612,25 +612,31 @@ elif [ "$BUILD_MODE" = "production" ]; then
|
|||||||
safe_execute "Building Capacitor version (production)" "npm run build:capacitor -- --mode production" || exit 3
|
safe_execute "Building Capacitor version (production)" "npm run build:capacitor -- --mode production" || exit 3
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# Step 6: Clean Gradle build
|
# Step 6: Sync with Capacitor (before any Gradle assemble, which packages
|
||||||
|
# whatever web bundle and resources are in android/app/src/main at that moment)
|
||||||
|
safe_execute "Syncing with Capacitor" "npx cap sync android" || exit 6
|
||||||
|
|
||||||
|
# Step 6.5: Restore local plugins (capacitor.plugins.json gets overwritten by cap sync)
|
||||||
|
safe_execute "Restoring local plugins" "node scripts/restore-local-plugins.js" || exit 7
|
||||||
|
|
||||||
|
# Step 6.6: Verify the synced web bundle has production server defaults
|
||||||
|
if [ "$BUILD_MODE" = "production" ]; then
|
||||||
|
safe_execute "Checking production bundle" "./scripts/check-prod-bundle.sh android/app/src/main/assets/public" || exit 10
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Step 7: Generate assets
|
||||||
|
safe_execute "Generating assets" "npx capacitor-assets generate --android --assetPath resources" || exit 7
|
||||||
|
|
||||||
|
# Step 8: Clean Gradle build
|
||||||
safe_execute "Cleaning Gradle build" "cd android && ./gradlew clean && cd .." || exit 4
|
safe_execute "Cleaning Gradle build" "cd android && ./gradlew clean && cd .." || exit 4
|
||||||
|
|
||||||
# Step 7: Build based on type
|
# Step 9: Build based on type
|
||||||
if [ "$BUILD_TYPE" = "debug" ]; then
|
if [ "$BUILD_TYPE" = "debug" ]; then
|
||||||
safe_execute "Assembling debug build" "cd android && ./gradlew assembleDebug && cd .." || exit 5
|
safe_execute "Assembling debug build" "cd android && ./gradlew assembleDebug && cd .." || exit 5
|
||||||
elif [ "$BUILD_TYPE" = "release" ]; then
|
elif [ "$BUILD_TYPE" = "release" ]; then
|
||||||
safe_execute "Assembling release build" "cd android && ./gradlew assembleRelease && cd .." || exit 5
|
safe_execute "Assembling release build" "cd android && ./gradlew assembleRelease && cd .." || exit 5
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# Step 8: Sync with Capacitor
|
|
||||||
safe_execute "Syncing with Capacitor" "npx cap sync android" || exit 6
|
|
||||||
|
|
||||||
# Step 8.5: Restore local plugins (capacitor.plugins.json gets overwritten by cap sync)
|
|
||||||
safe_execute "Restoring local plugins" "node scripts/restore-local-plugins.js" || exit 7
|
|
||||||
|
|
||||||
# Step 9: Generate assets
|
|
||||||
safe_execute "Generating assets" "npx capacitor-assets generate --android" || exit 7
|
|
||||||
|
|
||||||
# Step 10: Build APK/AAB if requested
|
# Step 10: Build APK/AAB if requested
|
||||||
if [ "$BUILD_APK" = true ]; then
|
if [ "$BUILD_APK" = true ]; then
|
||||||
if [ "$BUILD_TYPE" = "debug" ]; then
|
if [ "$BUILD_TYPE" = "debug" ]; then
|
||||||
@@ -642,6 +648,9 @@ fi
|
|||||||
|
|
||||||
if [ "$BUILD_AAB" = true ]; then
|
if [ "$BUILD_AAB" = true ]; then
|
||||||
safe_execute "Building AAB" "cd android && ./gradlew bundleRelease && cd .." || exit 5
|
safe_execute "Building AAB" "cd android && ./gradlew bundleRelease && cd .." || exit 5
|
||||||
|
if [ "$BUILD_MODE" = "production" ]; then
|
||||||
|
safe_execute "Checking production AAB" "./scripts/check-prod-bundle.sh android/app/build/outputs/bundle/release/app-release.aab" || exit 10
|
||||||
|
fi
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# Step 11: Auto-run app if requested
|
# Step 11: Auto-run app if requested
|
||||||
@@ -649,7 +658,18 @@ fi
|
|||||||
# we already synced and ran restore-local-plugins.js above, so skip sync here.
|
# we already synced and ran restore-local-plugins.js above, so skip sync here.
|
||||||
if [ "$AUTO_RUN" = true ]; then
|
if [ "$AUTO_RUN" = true ]; then
|
||||||
log_step "Auto-running Android app..."
|
log_step "Auto-running Android app..."
|
||||||
safe_execute "Launching app" "npx cap run android --no-sync" || {
|
# With exactly one connected device, target it directly. Otherwise cap run
|
||||||
|
# shows an interactive picker, which without a terminal exits 0 having
|
||||||
|
# launched nothing, so fail instead of reporting a false success.
|
||||||
|
run_devices=$(adb devices | awk 'NR > 1 && $2 == "device" { print $1 }')
|
||||||
|
run_target=""
|
||||||
|
if [ "$(printf '%s\n' "$run_devices" | grep -c .)" = "1" ]; then
|
||||||
|
run_target="--target $run_devices"
|
||||||
|
elif [ ! -t 0 ]; then
|
||||||
|
log_error "Auto-run needs exactly one connected device when not run from a terminal (found: ${run_devices:-none})"
|
||||||
|
exit 9
|
||||||
|
fi
|
||||||
|
safe_execute "Launching app" "npx cap run android --no-sync $run_target" || {
|
||||||
log_error "Failed to launch Android app"
|
log_error "Failed to launch Android app"
|
||||||
log_info "You can manually run with: npx cap run android --no-sync"
|
log_info "You can manually run with: npx cap run android --no-sync"
|
||||||
exit 9
|
exit 9
|
||||||
|
|||||||
@@ -295,12 +295,7 @@ apply_ios_app_icon_appearances() {
|
|||||||
# Generate iOS assets (capacitor-assets), then apply optional appearance variants.
|
# Generate iOS assets (capacitor-assets), then apply optional appearance variants.
|
||||||
generate_ios_assets() {
|
generate_ios_assets() {
|
||||||
ensure_ios_capacitor_asset_directories
|
ensure_ios_capacitor_asset_directories
|
||||||
if [ -d "assets" ]; then
|
npx capacitor-assets generate --ios --assetPath resources
|
||||||
log_warn "@capacitor/assets prioritizes the top-level assets/ directory over resources/."
|
|
||||||
log_warn "This project intentionally uses resources/ as the canonical asset source."
|
|
||||||
log_warn "Remove the legacy assets/ directory if it is empty or obsolete."
|
|
||||||
fi
|
|
||||||
npx capacitor-assets generate --ios
|
|
||||||
apply_ios_app_icon_appearances
|
apply_ios_app_icon_appearances
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -685,6 +680,11 @@ safe_execute "Installing CocoaPods dependencies" "run_pod_install_with_workaroun
|
|||||||
# Step 6.6: Sync with Capacitor (uses run_cap_sync_with_workaround defined above for Xcode 26)
|
# Step 6.6: Sync with Capacitor (uses run_cap_sync_with_workaround defined above for Xcode 26)
|
||||||
safe_execute "Syncing with Capacitor" "run_cap_sync_with_workaround" || exit 6
|
safe_execute "Syncing with Capacitor" "run_cap_sync_with_workaround" || exit 6
|
||||||
|
|
||||||
|
# Step 6.7: Verify the synced web bundle has production server defaults
|
||||||
|
if [ "$BUILD_MODE" = "production" ]; then
|
||||||
|
safe_execute "Checking production bundle" "./scripts/check-prod-bundle.sh ios/App/App/public" || exit 10
|
||||||
|
fi
|
||||||
|
|
||||||
# Step 7: Generate assets
|
# Step 7: Generate assets
|
||||||
safe_execute "Generating assets" "generate_ios_assets" || exit 7
|
safe_execute "Generating assets" "generate_ios_assets" || exit 7
|
||||||
|
|
||||||
|
|||||||
Executable
+100
@@ -0,0 +1,100 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
# check-prod-bundle.sh
|
||||||
|
# Fails if a built web bundle has test/dev server URLs baked in as defaults.
|
||||||
|
#
|
||||||
|
# Native packaging (gradlew bundleRelease, Xcode archive) ships whatever web
|
||||||
|
# bundle was last synced into the native project, whether it came from a
|
||||||
|
# --test, --dev, or --prod build. Run this on the bundle before uploading.
|
||||||
|
#
|
||||||
|
# Usage:
|
||||||
|
# ./scripts/check-prod-bundle.sh [PATH...]
|
||||||
|
#
|
||||||
|
# Each PATH is a directory (e.g. android/app/src/main/assets/public,
|
||||||
|
# ios/App/App/public, an .xcarchive) or an .aab, .apk, or .ipa file.
|
||||||
|
# With no PATH, checks the synced Android and iOS web assets plus the
|
||||||
|
# release AAB if it exists.
|
||||||
|
#
|
||||||
|
# Detection: the canary URLs below appear in src/ only as AppString.TEST_*
|
||||||
|
# enum values, which minify to `x.TEST_..._SERVER="<url>"`. Any other quoted
|
||||||
|
# occurrence is a VITE_* default compiled in from .env.test or
|
||||||
|
# .env.development (.env.development also uses the test image server).
|
||||||
|
#
|
||||||
|
# Exit codes: 0 = clean, 1 = test defaults found, 2 = bad input
|
||||||
|
|
||||||
|
source "$(dirname "$0")/common.sh"
|
||||||
|
|
||||||
|
CANARY_REGEX='"https://test(-image-api|-notify-api)?\.timesafari\.app"'
|
||||||
|
ENUM_REGEX='TEST[0-9]*_[A-Z_]+="https'
|
||||||
|
|
||||||
|
# Prints offending snippets from the JS files under a directory; returns 1 if any.
|
||||||
|
check_js_dir() {
|
||||||
|
local dir="$1"
|
||||||
|
local js_count
|
||||||
|
js_count=$(find "$dir" -name "*.js" -path "*assets*" | wc -l | tr -d ' ')
|
||||||
|
if [ "$js_count" = "0" ]; then
|
||||||
|
log_error "No JS assets found under $dir"
|
||||||
|
return 2
|
||||||
|
fi
|
||||||
|
|
||||||
|
local hits
|
||||||
|
hits=$(find "$dir" -name "*.js" -path "*assets*" -print0 \
|
||||||
|
| xargs -0 grep -ohE ".{0,40}${CANARY_REGEX}" \
|
||||||
|
| grep -vE "$ENUM_REGEX")
|
||||||
|
if [ -n "$hits" ]; then
|
||||||
|
log_error "Test/dev server defaults found in bundle:"
|
||||||
|
echo "$hits" | sed 's/^/ /' >&2
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
check_path() {
|
||||||
|
local path="$1"
|
||||||
|
local result
|
||||||
|
|
||||||
|
if [ -d "$path" ]; then
|
||||||
|
check_js_dir "$path"
|
||||||
|
result=$?
|
||||||
|
elif [ -f "$path" ]; then
|
||||||
|
case "$path" in
|
||||||
|
*.aab|*.apk|*.ipa) ;;
|
||||||
|
*)
|
||||||
|
log_error "Unsupported file type: $path"
|
||||||
|
return 2
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
local tmp
|
||||||
|
tmp=$(mktemp -d)
|
||||||
|
unzip -q "$path" '*public/assets/*.js' -d "$tmp" 2>/dev/null
|
||||||
|
check_js_dir "$tmp"
|
||||||
|
result=$?
|
||||||
|
rm -rf "$tmp"
|
||||||
|
else
|
||||||
|
log_error "Not found: $path"
|
||||||
|
return 2
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ "$result" = "0" ]; then
|
||||||
|
log_success "Production defaults OK: $path"
|
||||||
|
else
|
||||||
|
log_error "Not a production bundle: $path"
|
||||||
|
fi
|
||||||
|
return $result
|
||||||
|
}
|
||||||
|
|
||||||
|
paths=("$@")
|
||||||
|
if [ ${#paths[@]} -eq 0 ]; then
|
||||||
|
for p in android/app/src/main/assets/public \
|
||||||
|
ios/App/App/public \
|
||||||
|
android/app/build/outputs/bundle/release/app-release.aab; do
|
||||||
|
[ -e "$p" ] && paths+=("$p")
|
||||||
|
done
|
||||||
|
fi
|
||||||
|
|
||||||
|
status=0
|
||||||
|
for p in "${paths[@]}"; do
|
||||||
|
check_path "$p"
|
||||||
|
rc=$?
|
||||||
|
[ $rc -gt $status ] && status=$rc
|
||||||
|
done
|
||||||
|
exit $status
|
||||||
@@ -2,7 +2,7 @@
|
|||||||
/**
|
/**
|
||||||
* Restore Local Capacitor Plugins
|
* Restore Local Capacitor Plugins
|
||||||
*
|
*
|
||||||
* This script ensures that local custom plugins (SafeArea and SharedImage)
|
* This script ensures that local custom plugins (SafeArea, SharedImage, NotificationInspector)
|
||||||
* are present in capacitor.plugins.json after `npx cap sync` runs.
|
* are present in capacitor.plugins.json after `npx cap sync` runs.
|
||||||
*
|
*
|
||||||
* The capacitor.plugins.json file is auto-generated by Capacitor and gets
|
* The capacitor.plugins.json file is auto-generated by Capacitor and gets
|
||||||
@@ -28,6 +28,10 @@ const LOCAL_PLUGINS = [
|
|||||||
{
|
{
|
||||||
pkg: 'SharedImage',
|
pkg: 'SharedImage',
|
||||||
classpath: 'app.timesafari.sharedimage.SharedImagePlugin'
|
classpath: 'app.timesafari.sharedimage.SharedImagePlugin'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
pkg: 'NotificationInspector',
|
||||||
|
classpath: 'app.timesafari.notifications.NotificationInspectorPlugin'
|
||||||
}
|
}
|
||||||
];
|
];
|
||||||
|
|
||||||
|
|||||||
@@ -88,6 +88,25 @@
|
|||||||
Manually mints and uploads 100 delegated day JWTs. Requires an active
|
Manually mints and uploads 100 delegated day JWTs. Requires an active
|
||||||
did:ethr identity and JWT authentication; Test Mode is not used.
|
did:ethr identity and JWT authentication; Test Mode is not used.
|
||||||
</p>
|
</p>
|
||||||
|
<button
|
||||||
|
class="w-full text-md bg-gradient-to-b from-blue-400 to-blue-700 shadow-[inset_0_-1px_0_0_rgba(0,0,0,0.5)] text-white px-4 py-2 rounded-md"
|
||||||
|
:disabled="busy"
|
||||||
|
:class="{ 'opacity-50 cursor-not-allowed': busy }"
|
||||||
|
@click="onBackendRefresh"
|
||||||
|
>
|
||||||
|
Refresh Notifications
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
class="w-full text-md bg-gradient-to-b from-violet-400 to-violet-700 shadow-[inset_0_-1px_0_0_rgba(0,0,0,0.5)] text-white px-4 py-2 rounded-md"
|
||||||
|
:disabled="busy"
|
||||||
|
:class="{ 'opacity-50 cursor-not-allowed': busy }"
|
||||||
|
@click="onSimulateWakeupRefresh"
|
||||||
|
>
|
||||||
|
Simulate WAKEUP_PING (Local)
|
||||||
|
</button>
|
||||||
|
<p class="text-xs text-slate-500">
|
||||||
|
Local simulation only — calls the refresh API directly (no FCM push).
|
||||||
|
</p>
|
||||||
<button
|
<button
|
||||||
class="w-full text-md bg-gradient-to-b from-amber-400 to-amber-700 shadow-[inset_0_-1px_0_0_rgba(0,0,0,0.5)] text-white px-4 py-2 rounded-md"
|
class="w-full text-md bg-gradient-to-b from-amber-400 to-amber-700 shadow-[inset_0_-1px_0_0_rgba(0,0,0,0.5)] text-white px-4 py-2 rounded-md"
|
||||||
:disabled="busy"
|
:disabled="busy"
|
||||||
@@ -109,8 +128,8 @@
|
|||||||
{{ realWakeupStatus.message }}
|
{{ realWakeupStatus.message }}
|
||||||
</p>
|
</p>
|
||||||
<p v-else class="text-xs text-slate-500">
|
<p v-else class="text-xs text-slate-500">
|
||||||
FCM delivery diagnostic only — backend `/debug/send-wakeup`. The app
|
Full pipeline — backend `/debug/send-wakeup` → FCM → WAKEUP_PING
|
||||||
no longer schedules api_* notifications from WAKEUP_PING.
|
handler.
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -158,6 +177,70 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- SECTION F: Mock Timing Presets -->
|
||||||
|
<div class="mb-6">
|
||||||
|
<h2 class="mb-2 font-bold">Mock Timing Presets</h2>
|
||||||
|
<div class="flex flex-wrap gap-2">
|
||||||
|
<button
|
||||||
|
v-for="preset in presets"
|
||||||
|
:key="preset.ms"
|
||||||
|
class="px-3 py-2 rounded border border-slate-300 bg-white text-sm"
|
||||||
|
:class="{
|
||||||
|
'border-blue-500 ring-1 ring-blue-300': intervalMs === preset.ms,
|
||||||
|
}"
|
||||||
|
@click="intervalMs = preset.ms"
|
||||||
|
>
|
||||||
|
{{ preset.label }}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<div class="text-xs text-slate-500 mt-2">
|
||||||
|
Selected interval: <b>{{ intervalLabel }}</b>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- SECTION A: Mock Refresh Controls -->
|
||||||
|
<div class="mb-6">
|
||||||
|
<h2 class="mb-2 font-bold">Mock Refresh Controls</h2>
|
||||||
|
<button
|
||||||
|
class="w-full text-md bg-gradient-to-b from-blue-400 to-blue-700 shadow-[inset_0_-1px_0_0_rgba(0,0,0,0.5)] text-white px-4 py-2 rounded-md"
|
||||||
|
:disabled="busy"
|
||||||
|
:class="{ 'opacity-50 cursor-not-allowed': busy }"
|
||||||
|
@click="onMockRefresh"
|
||||||
|
>
|
||||||
|
Trigger Mock Refresh
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- SECTION B: Wakeup Ping Simulator -->
|
||||||
|
<div class="mb-6">
|
||||||
|
<h2 class="mb-2 font-bold">Wakeup Ping Simulator</h2>
|
||||||
|
<p class="text-xs text-slate-500 mb-2">
|
||||||
|
Exercises the production push handler (not the refresh API shortcut
|
||||||
|
above).
|
||||||
|
</p>
|
||||||
|
<button
|
||||||
|
class="w-full text-md bg-gradient-to-b from-slate-400 to-slate-700 shadow-[inset_0_-1px_0_0_rgba(0,0,0,0.5)] text-white px-4 py-2 rounded-md"
|
||||||
|
:disabled="busy"
|
||||||
|
:class="{ 'opacity-50 cursor-not-allowed': busy }"
|
||||||
|
@click="onWakeupPing"
|
||||||
|
>
|
||||||
|
Simulate WAKEUP_PING
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- SECTION C: Flood Test -->
|
||||||
|
<div class="mb-6">
|
||||||
|
<h2 class="mb-2 font-bold">Flood Test</h2>
|
||||||
|
<button
|
||||||
|
class="w-full text-md bg-gradient-to-b from-rose-400 to-rose-700 shadow-[inset_0_-1px_0_0_rgba(0,0,0,0.5)] text-white px-4 py-2 rounded-md"
|
||||||
|
:disabled="busy"
|
||||||
|
:class="{ 'opacity-50 cursor-not-allowed': busy }"
|
||||||
|
@click="onFloodTest"
|
||||||
|
>
|
||||||
|
Run 20 Refreshes
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
<!-- SECTION D: Pending Notification Inspector -->
|
<!-- SECTION D: Pending Notification Inspector -->
|
||||||
<div class="mb-6">
|
<div class="mb-6">
|
||||||
<div class="flex items-center gap-3 mb-2">
|
<div class="flex items-center gap-3 mb-2">
|
||||||
@@ -275,6 +358,14 @@ type PendingInfo = {
|
|||||||
wallClockSource?: string | null;
|
wallClockSource?: string | null;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const presets = [
|
||||||
|
{ label: "30 sec", ms: 30_000 },
|
||||||
|
{ label: "1 min", ms: 60_000 },
|
||||||
|
{ label: "5 min", ms: 5 * 60_000 },
|
||||||
|
{ label: "10 min", ms: 10 * 60_000 },
|
||||||
|
];
|
||||||
|
|
||||||
|
const intervalMs = ref<number>(60_000);
|
||||||
const busy = ref(false);
|
const busy = ref(false);
|
||||||
const pending = ref<PendingInfo[]>([]);
|
const pending = ref<PendingInfo[]>([]);
|
||||||
const pendingInspectorMessage = ref<string | null>(null);
|
const pendingInspectorMessage = ref<string | null>(null);
|
||||||
@@ -304,6 +395,11 @@ const truncatedFcmToken = computed(() => {
|
|||||||
const eventLog = ref<string[]>([]);
|
const eventLog = ref<string[]>([]);
|
||||||
let unsubscribeEventLog: (() => void) | undefined;
|
let unsubscribeEventLog: (() => void) | undefined;
|
||||||
|
|
||||||
|
const intervalLabel = computed(() => {
|
||||||
|
const preset = presets.find((p) => p.ms === intervalMs.value);
|
||||||
|
return preset?.label ?? `${intervalMs.value}ms`;
|
||||||
|
});
|
||||||
|
|
||||||
function formatIsoMs(ms: number | null | undefined): string {
|
function formatIsoMs(ms: number | null | undefined): string {
|
||||||
if (ms == null || !Number.isFinite(ms)) {
|
if (ms == null || !Number.isFinite(ms)) {
|
||||||
return "";
|
return "";
|
||||||
@@ -327,6 +423,27 @@ async function refreshPending(): Promise<void> {
|
|||||||
pendingInspectorMessage.value = result.inspectorUnavailableMessage ?? null;
|
pendingInspectorMessage.value = result.inspectorUnavailableMessage ?? null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function onMockRefresh(): Promise<void> {
|
||||||
|
await withBusy(async () => {
|
||||||
|
await NotificationDebugService.triggerMockRefresh(intervalMs.value);
|
||||||
|
await refreshPending();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async function onWakeupPing(): Promise<void> {
|
||||||
|
await withBusy(async () => {
|
||||||
|
await NotificationDebugService.simulateWakeupPing();
|
||||||
|
await refreshPending();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async function onFloodTest(): Promise<void> {
|
||||||
|
await withBusy(async () => {
|
||||||
|
await NotificationDebugService.runFloodTest(intervalMs.value);
|
||||||
|
await refreshPending();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
async function onClearNotifications(): Promise<void> {
|
async function onClearNotifications(): Promise<void> {
|
||||||
await withBusy(async () => {
|
await withBusy(async () => {
|
||||||
await NotificationDebugService.clearNotifications();
|
await NotificationDebugService.clearNotifications();
|
||||||
@@ -397,6 +514,20 @@ async function onUploadAlertAuthorization(): Promise<void> {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function onBackendRefresh(): Promise<void> {
|
||||||
|
await withBusy(async () => {
|
||||||
|
await NotificationDebugService.triggerBackendRefresh();
|
||||||
|
await refreshPending();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async function onSimulateWakeupRefresh(): Promise<void> {
|
||||||
|
await withBusy(async () => {
|
||||||
|
await NotificationDebugService.simulateWakeupViaRefresh();
|
||||||
|
await refreshPending();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
function formatRealWakeupStatusMessage(
|
function formatRealWakeupStatusMessage(
|
||||||
result: Awaited<
|
result: Awaited<
|
||||||
ReturnType<typeof NotificationDebugService.sendRealWakeupPing>
|
ReturnType<typeof NotificationDebugService.sendRealWakeupPing>
|
||||||
|
|||||||
@@ -0,0 +1,134 @@
|
|||||||
|
/* eslint-disable @typescript-eslint/no-unused-vars */
|
||||||
|
import { inject, onBeforeUnmount, onMounted } from "vue";
|
||||||
|
import { NotificationIface } from "../constants/app";
|
||||||
|
import { registerToken } from "@/services/notifications/NotificationService";
|
||||||
|
import { refreshNotifications } from "@/services/notifications/NativeNotificationService";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Vue 3 composable for notifications
|
||||||
|
* Provides a concise API for common notification patterns
|
||||||
|
*/
|
||||||
|
|
||||||
|
export const NOTIFICATION_TIMEOUTS = {
|
||||||
|
BRIEF: 1000, // Very brief toasts ("Sent..." messages)
|
||||||
|
SHORT: 2000, // Short notifications (clipboard copies, quick confirmations)
|
||||||
|
STANDARD: 3000, // Standard notifications (success messages, general info)
|
||||||
|
LONG: 5000, // Longer notifications (errors, warnings, important info)
|
||||||
|
VERY_LONG: 7000, // Very long notifications (complex operations)
|
||||||
|
MODAL: -1, // Modal confirmations (no auto-dismiss)
|
||||||
|
} as const;
|
||||||
|
|
||||||
|
export function useNotifications() {
|
||||||
|
// Inject the notify function from the app
|
||||||
|
const notify =
|
||||||
|
inject<(notification: NotificationIface, timeout?: number) => void>(
|
||||||
|
"notify",
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!notify) {
|
||||||
|
throw new Error(
|
||||||
|
"useNotifications must be used within a component that has $notify available",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
let refreshTimer: number | undefined = undefined;
|
||||||
|
let refreshInFlight: Promise<void> | null = null;
|
||||||
|
|
||||||
|
async function refreshNotificationsDebounced(): Promise<void> {
|
||||||
|
if (refreshTimer != null) {
|
||||||
|
window.clearTimeout(refreshTimer);
|
||||||
|
}
|
||||||
|
refreshTimer = window.setTimeout(() => {
|
||||||
|
if (!refreshInFlight) {
|
||||||
|
refreshInFlight = refreshNotifications().finally(() => {
|
||||||
|
refreshInFlight = null;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}, 300);
|
||||||
|
}
|
||||||
|
|
||||||
|
const onResume = () => {
|
||||||
|
void refreshNotificationsDebounced();
|
||||||
|
};
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
void refreshNotificationsDebounced();
|
||||||
|
document.addEventListener("resume", onResume);
|
||||||
|
});
|
||||||
|
|
||||||
|
onBeforeUnmount(() => {
|
||||||
|
document.removeEventListener("resume", onResume);
|
||||||
|
if (refreshTimer != null) {
|
||||||
|
window.clearTimeout(refreshTimer);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||||
|
function success(_notification: NotificationIface, _timeout?: number) {}
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||||
|
function error(_notification: NotificationIface, _timeout?: number) {}
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||||
|
function warning(_notification: NotificationIface, _timeout?: number) {}
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||||
|
function info(_notification: NotificationIface, _timeout?: number) {}
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||||
|
function toast(_title: string, _text?: string, _timeout?: number) {}
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||||
|
function copied(_item: string, _timeout?: number) {}
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||||
|
function sent(_timeout?: number) {}
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||||
|
function confirm(
|
||||||
|
_text: string,
|
||||||
|
_onYes: () => Promise<void>,
|
||||||
|
_timeout?: number,
|
||||||
|
) {}
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||||
|
function confirmationSubmitted(_timeout?: number) {}
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||||
|
function genericError(_timeout?: number) {}
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||||
|
function genericSuccess(_timeout?: number) {}
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||||
|
function alreadyConfirmed(_timeout?: number) {}
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||||
|
function cannotConfirmIssuer(_timeout?: number) {}
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||||
|
function cannotConfirmHidden(_timeout?: number) {}
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||||
|
function notRegistered(_timeout?: number) {}
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||||
|
function notAGive(_timeout?: number) {}
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||||
|
function notificationOff(
|
||||||
|
_title: string,
|
||||||
|
_callback: (success: boolean) => Promise<void>,
|
||||||
|
_timeout?: number,
|
||||||
|
) {}
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||||
|
function downloadStarted(_format: string = "Dexie", _timeout?: number) {}
|
||||||
|
|
||||||
|
return {
|
||||||
|
success,
|
||||||
|
error,
|
||||||
|
warning,
|
||||||
|
info,
|
||||||
|
toast,
|
||||||
|
copied,
|
||||||
|
sent,
|
||||||
|
confirm,
|
||||||
|
confirmationSubmitted,
|
||||||
|
genericError,
|
||||||
|
genericSuccess,
|
||||||
|
alreadyConfirmed,
|
||||||
|
cannotConfirmIssuer,
|
||||||
|
cannotConfirmHidden,
|
||||||
|
notRegistered,
|
||||||
|
notAGive,
|
||||||
|
notificationOff,
|
||||||
|
downloadStarted,
|
||||||
|
/** POST FCM token to `/notifications/register` (same as startup native hook). */
|
||||||
|
registerFcmToken: registerToken,
|
||||||
|
refreshNotifications: refreshNotificationsDebounced,
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -225,7 +225,7 @@ const MIGRATIONS = [
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "008_remove_iViewContent_from_contacts",
|
name: "008_remove_iViewContent_from_contacts",
|
||||||
// Recreate contacts without iViewContent: backup, drop, recreate, restore
|
// Recreate contacts without iViewContent (since we use hideTheirContent instead): backup, drop, recreate, restore
|
||||||
sql: `
|
sql: `
|
||||||
PRAGMA foreign_keys = OFF;
|
PRAGMA foreign_keys = OFF;
|
||||||
|
|
||||||
|
|||||||
@@ -78,10 +78,7 @@ export interface NotificationRegisterRequest extends NotifyApiTestModeFlag {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Body of `POST /notifications/refresh`. The Capacitor app no longer calls this
|
* Body of `POST /notifications/refresh`. The service finds the caller's device
|
||||||
* route (WAKEUP_PING → refresh → `api_*` scheduling was retired). These types
|
|
||||||
* remain because notification-wakeup-service still exposes the endpoint.
|
|
||||||
* The service finds the caller's device
|
|
||||||
* by `deviceId`, or by `fcmToken` when no `deviceId` is sent, and answers 400
|
* by `deviceId`, or by `fcmToken` when no `deviceId` is sent, and answers 400
|
||||||
* when neither is present and non-empty. When both are sent they must name the
|
* when neither is present and non-empty. When both are sent they must name the
|
||||||
* same device, or the answer is 404.
|
* same device, or the answer is 404.
|
||||||
|
|||||||
@@ -23,7 +23,7 @@ import { LRUCache } from "lru-cache";
|
|||||||
import * as R from "ramda";
|
import * as R from "ramda";
|
||||||
|
|
||||||
import { DEFAULT_IMAGE_API_SERVER, NotificationIface } from "../constants/app";
|
import { DEFAULT_IMAGE_API_SERVER, NotificationIface } from "../constants/app";
|
||||||
import { NOTIFICATION_TIMEOUTS } from "../utils/notificationUtils";
|
import { NOTIFICATION_TIMEOUTS } from "../composables/useNotifications";
|
||||||
import { createNotifyHelpers } from "../utils/notify";
|
import { createNotifyHelpers } from "../utils/notify";
|
||||||
import { NOTIFY_PERSONAL_DATA_ERROR } from "../constants/notifications";
|
import { NOTIFY_PERSONAL_DATA_ERROR } from "../constants/notifications";
|
||||||
import { Contact } from "../db/tables/contacts";
|
import { Contact } from "../db/tables/contacts";
|
||||||
|
|||||||
@@ -48,7 +48,6 @@ import {
|
|||||||
configureNativeFetcherIfReady,
|
configureNativeFetcherIfReady,
|
||||||
initializeNativePushAndFirebaseMessaging,
|
initializeNativePushAndFirebaseMessaging,
|
||||||
onNotificationAuthMayBeReady,
|
onNotificationAuthMayBeReady,
|
||||||
runLegacyApiNotificationsCleanupOnce,
|
|
||||||
} from "@/services/notifications";
|
} from "@/services/notifications";
|
||||||
|
|
||||||
logger.log("[Capacitor] 🚀 Starting initialization");
|
logger.log("[Capacitor] 🚀 Starting initialization");
|
||||||
@@ -362,8 +361,6 @@ setTimeout(async () => {
|
|||||||
);
|
);
|
||||||
await registerDeepLinkListener();
|
await registerDeepLinkListener();
|
||||||
logger.info(`[Main] 🎉 Deep link system fully initialized!`);
|
logger.info(`[Main] 🎉 Deep link system fully initialized!`);
|
||||||
// One-time: remove persisted legacy api_* schedules from pre-retirement installs
|
|
||||||
await runLegacyApiNotificationsCleanupOnce();
|
|
||||||
// Firebase Messaging (JS) + Capacitor PushNotifications (FCM/APNs token, delivery listeners)
|
// Firebase Messaging (JS) + Capacitor PushNotifications (FCM/APNs token, delivery listeners)
|
||||||
await initializeNativePushAndFirebaseMessaging();
|
await initializeNativePushAndFirebaseMessaging();
|
||||||
// Configure native fetcher for API-driven daily notifications (activeDid + JWT)
|
// Configure native fetcher for API-driven daily notifications (activeDid + JWT)
|
||||||
|
|||||||
@@ -11,6 +11,8 @@ export type PendingNotificationInfo = {
|
|||||||
|
|
||||||
export interface NotificationInspectorPlugin {
|
export interface NotificationInspectorPlugin {
|
||||||
getPendingNotifications(): Promise<{ pending: PendingNotificationInfo[] }>;
|
getPendingNotifications(): Promise<{ pending: PendingNotificationInfo[] }>;
|
||||||
|
/** Android only: whether the build has a Firebase config (-PfirebaseEnabled). */
|
||||||
|
isFirebaseConfigured(): Promise<{ configured: boolean }>;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const NotificationInspector =
|
export const NotificationInspector =
|
||||||
|
|||||||
@@ -13,8 +13,30 @@
|
|||||||
|
|
||||||
import { Capacitor } from "@capacitor/core";
|
import { Capacitor } from "@capacitor/core";
|
||||||
import type { PushNotificationSchema } from "@capacitor/push-notifications";
|
import type { PushNotificationSchema } from "@capacitor/push-notifications";
|
||||||
|
import type {
|
||||||
|
NotificationRefreshRequest,
|
||||||
|
NotificationRefreshResponse,
|
||||||
|
} from "@/interfaces/notifyApi";
|
||||||
import { DailyNotification } from "@/plugins/DailyNotificationPlugin";
|
import { DailyNotification } from "@/plugins/DailyNotificationPlugin";
|
||||||
|
import { getOrCreateDeviceId } from "./deviceId";
|
||||||
import { REMINDER_ID_DAILY_REMINDER } from "./reminderIds";
|
import { REMINDER_ID_DAILY_REMINDER } from "./reminderIds";
|
||||||
|
import { configureNativeFetcherIfReady } from "./nativeFetcherConfig";
|
||||||
|
import {
|
||||||
|
getNotificationApiBaseUrl,
|
||||||
|
getTestMode,
|
||||||
|
} from "./NotificationDebugConfig";
|
||||||
|
import {
|
||||||
|
logRefreshFailure,
|
||||||
|
logRefreshStarted,
|
||||||
|
logRefreshSuccess,
|
||||||
|
logScheduleReplacement,
|
||||||
|
} from "./notificationLog";
|
||||||
|
import {
|
||||||
|
getNotificationApiHeaders,
|
||||||
|
logSkippingRefreshDueToMissingAuth,
|
||||||
|
notificationApiFailureMessage,
|
||||||
|
readNotificationApiBody,
|
||||||
|
} from "./notificationApiAuth";
|
||||||
import { logNotification } from "./NotificationDebugEvents";
|
import { logNotification } from "./NotificationDebugEvents";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -541,14 +563,200 @@ export class NativeNotificationService implements NotificationServiceInterface {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export type RefreshNotificationsResult = {
|
||||||
|
ok: boolean;
|
||||||
|
scheduledCount: number;
|
||||||
|
status?: number;
|
||||||
|
errorMessage?: string;
|
||||||
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Capacitor push delivery hook. Legacy WAKEUP_PING → /notifications/refresh
|
* Re-applies native API fetcher credentials (JWT pool, active DID) so background
|
||||||
* consumption was retired; AlertSearch uses visible FCM notifications and is
|
* notification workers can run. No UI; safe from push handlers while backgrounded.
|
||||||
* not handled here. Keep logging for diagnostics without scheduling api_* work.
|
*/
|
||||||
|
export async function refreshNotificationsWithDiagnostics(options?: {
|
||||||
|
source?: string;
|
||||||
|
}): Promise<RefreshNotificationsResult> {
|
||||||
|
const startedAt = performance.now();
|
||||||
|
const source = options?.source;
|
||||||
|
logRefreshStarted(source);
|
||||||
|
|
||||||
|
if (!Capacitor.isNativePlatform()) {
|
||||||
|
const errorMessage = "not a native platform";
|
||||||
|
logRefreshFailure(startedAt, errorMessage, undefined, source);
|
||||||
|
return {
|
||||||
|
ok: false,
|
||||||
|
scheduledCount: 0,
|
||||||
|
errorMessage,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const auth = await getNotificationApiHeaders("refresh");
|
||||||
|
if (!auth.ok) {
|
||||||
|
logSkippingRefreshDueToMissingAuth();
|
||||||
|
logRefreshFailure(startedAt, auth.message, undefined, source);
|
||||||
|
return {
|
||||||
|
ok: false,
|
||||||
|
scheduledCount: 0,
|
||||||
|
errorMessage: auth.message,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
let deviceId: string | undefined;
|
||||||
|
try {
|
||||||
|
deviceId = await getOrCreateDeviceId();
|
||||||
|
} catch (err) {
|
||||||
|
logger.warn(
|
||||||
|
"[NativeNotificationService] Could not obtain deviceId; skipping refresh",
|
||||||
|
err,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if (!deviceId) {
|
||||||
|
// The service finds the device by deviceId or fcmToken and answers 400
|
||||||
|
// without either, so there is no request worth sending.
|
||||||
|
const errorMessage = "no deviceId (cannot identify this device)";
|
||||||
|
logRefreshFailure(startedAt, errorMessage, undefined, source);
|
||||||
|
return { ok: false, scheduledCount: 0, errorMessage };
|
||||||
|
}
|
||||||
|
|
||||||
|
const body: NotificationRefreshRequest = {
|
||||||
|
deviceId,
|
||||||
|
platform: Capacitor.getPlatform(),
|
||||||
|
testMode: getTestMode(),
|
||||||
|
};
|
||||||
|
const baseUrl = getNotificationApiBaseUrl();
|
||||||
|
const res = await fetch(`${baseUrl}/notifications/refresh`, {
|
||||||
|
method: "POST",
|
||||||
|
headers: auth.headers,
|
||||||
|
body: JSON.stringify(body),
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!res.ok) {
|
||||||
|
const errorMessage = notificationApiFailureMessage(
|
||||||
|
res.status,
|
||||||
|
await readNotificationApiBody(res),
|
||||||
|
);
|
||||||
|
logger.warn("[NativeNotificationService] refreshNotifications failed", {
|
||||||
|
status: res.status,
|
||||||
|
statusText: res.statusText,
|
||||||
|
errorMessage,
|
||||||
|
});
|
||||||
|
logRefreshFailure(startedAt, errorMessage, res.status, source);
|
||||||
|
return {
|
||||||
|
ok: false,
|
||||||
|
scheduledCount: 0,
|
||||||
|
status: res.status,
|
||||||
|
errorMessage,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const payload = (await res.json()) as NotificationRefreshResponse;
|
||||||
|
const scheduledCount = Array.isArray(payload?.nextNotifications)
|
||||||
|
? payload.nextNotifications.length
|
||||||
|
: 0;
|
||||||
|
await applyNotificationRefreshPayload(payload);
|
||||||
|
logRefreshSuccess(startedAt, scheduledCount, source);
|
||||||
|
return { ok: true, scheduledCount };
|
||||||
|
} catch (err) {
|
||||||
|
logger.error("[NativeNotificationService] Refresh failed", err);
|
||||||
|
const message = err instanceof Error ? err.message : String(err);
|
||||||
|
logRefreshFailure(startedAt, message, undefined, source);
|
||||||
|
return { ok: false, scheduledCount: 0, errorMessage: message };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function refreshNotifications(): Promise<void> {
|
||||||
|
await refreshNotificationsWithDiagnostics();
|
||||||
|
}
|
||||||
|
|
||||||
|
export type NotificationRefreshPayload = {
|
||||||
|
shouldNotify?: boolean;
|
||||||
|
nextNotifications?: Array<{ timestamp?: number }>;
|
||||||
|
};
|
||||||
|
|
||||||
|
// `handleCapacitorPushNotificationReceived` and `applyNotificationRefreshPayload` are used by
|
||||||
|
// DEV notification simulation tooling; they must stay production-safe because that tooling
|
||||||
|
// exercises real flows. (`applyNotificationRefreshPayload` is also used by production refresh.)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Apply a "refresh notifications" payload by clearing and scheduling timestamps via the native plugin.
|
||||||
|
*
|
||||||
|
* This is the shared implementation used by:
|
||||||
|
* - production refresh flow (`refreshNotifications` fetching from backend)
|
||||||
|
* - dev-only debug flows (mock refresh with local payloads)
|
||||||
|
*
|
||||||
|
* Important: This function intentionally mirrors production behavior and does not introduce
|
||||||
|
* any scheduling logic in UI layers.
|
||||||
|
*/
|
||||||
|
export async function applyNotificationRefreshPayload(
|
||||||
|
payload: unknown,
|
||||||
|
): Promise<void> {
|
||||||
|
if (!Capacitor.isNativePlatform()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const data = payload as NotificationRefreshPayload;
|
||||||
|
const nextNotifications = data?.nextNotifications;
|
||||||
|
if (!Array.isArray(nextNotifications)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const timestamps = nextNotifications
|
||||||
|
.map((n) => (n as { timestamp?: unknown })?.timestamp)
|
||||||
|
.filter((t): t is number => typeof t === "number" && Number.isFinite(t));
|
||||||
|
|
||||||
|
if (timestamps.length === 0) {
|
||||||
|
logNotification("Schedule replacement skipped (no valid timestamps)");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Keep existing behavior: ensure background worker credentials are current.
|
||||||
|
await configureNativeFetcherIfReady();
|
||||||
|
|
||||||
|
logScheduleReplacement(timestamps.length);
|
||||||
|
|
||||||
|
if (typeof DailyNotification.clearApiNotifications !== "function") {
|
||||||
|
logger.warn(
|
||||||
|
"[NativeNotificationService] API notification clear unavailable (plugin clearApiNotifications missing); cannot replace schedule",
|
||||||
|
);
|
||||||
|
logNotification(
|
||||||
|
"Schedule replacement aborted (API notification clear unavailable on plugin)",
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
logNotification("Clearing API notifications before refresh");
|
||||||
|
await DailyNotification.clearApiNotifications();
|
||||||
|
logNotification("Cleared API notifications");
|
||||||
|
|
||||||
|
if (typeof DailyNotification.scheduleApiNotifications !== "function") {
|
||||||
|
logger.warn(
|
||||||
|
"[NativeNotificationService] scheduleApiNotifications not available on plugin; cannot apply timestamps",
|
||||||
|
);
|
||||||
|
logNotification(
|
||||||
|
"Schedule replacement aborted (scheduleApiNotifications unavailable)",
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
await DailyNotification.scheduleApiNotifications({ timestamps });
|
||||||
|
logNotification(
|
||||||
|
`Schedule replacement applied (${timestamps.length} timestamp(s))`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Silent FCM/APNs data push: refresh native notification pipeline when requested by backend.
|
||||||
*/
|
*/
|
||||||
export async function handleCapacitorPushNotificationReceived(
|
export async function handleCapacitorPushNotificationReceived(
|
||||||
notification: PushNotificationSchema,
|
notification: PushNotificationSchema,
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
|
if (notification.data?.type === "WAKEUP_PING") {
|
||||||
|
logNotification("WAKEUP_PING handler — invoking refresh");
|
||||||
|
await refreshNotificationsWithDiagnostics({ source: "WAKEUP_PING" });
|
||||||
|
return;
|
||||||
|
}
|
||||||
const type =
|
const type =
|
||||||
typeof notification.data?.type === "string"
|
typeof notification.data?.type === "string"
|
||||||
? notification.data.type
|
? notification.data.type
|
||||||
|
|||||||
@@ -1,12 +1,14 @@
|
|||||||
/**
|
/**
|
||||||
* DEV-only notification testing utilities.
|
* DEV-only notification testing utilities.
|
||||||
*
|
*
|
||||||
* Legacy WAKEUP_PING → /notifications/refresh → api_* tooling was removed in
|
* IMPORTANT:
|
||||||
* the Phase 2 retirement. Remaining helpers cover FCM registration, AlertSearch
|
* This service intentionally routes through the same production notification
|
||||||
* authorization upload, backend URL overrides, and pending-notification inspection.
|
* orchestration paths used by refresh flows, wakeup pushes, and replacement.
|
||||||
|
* Avoid adding duplicate scheduling logic here.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { Capacitor } from "@capacitor/core";
|
import { Capacitor } from "@capacitor/core";
|
||||||
|
import type { PushNotificationSchema } from "@capacitor/push-notifications";
|
||||||
import type {
|
import type {
|
||||||
DebugSendWakeupRequest,
|
DebugSendWakeupRequest,
|
||||||
DebugSendWakeupResponse,
|
DebugSendWakeupResponse,
|
||||||
@@ -36,6 +38,12 @@ import {
|
|||||||
notificationApiFailureMessage,
|
notificationApiFailureMessage,
|
||||||
readNotificationApiBody,
|
readNotificationApiBody,
|
||||||
} from "./notificationApiAuth";
|
} from "./notificationApiAuth";
|
||||||
|
import {
|
||||||
|
applyNotificationRefreshPayload,
|
||||||
|
handleCapacitorPushNotificationReceived,
|
||||||
|
refreshNotificationsWithDiagnostics,
|
||||||
|
type NotificationRefreshPayload,
|
||||||
|
} from "./NativeNotificationService";
|
||||||
import { truncateFcmTokenForLog } from "./notificationLog";
|
import { truncateFcmTokenForLog } from "./notificationLog";
|
||||||
import { DailyNotification } from "@/plugins/DailyNotificationPlugin";
|
import { DailyNotification } from "@/plugins/DailyNotificationPlugin";
|
||||||
import { NotificationInspector } from "@/plugins/NotificationInspectorPlugin";
|
import { NotificationInspector } from "@/plugins/NotificationInspectorPlugin";
|
||||||
@@ -171,10 +179,19 @@ export const NotificationDebugService = {
|
|||||||
return result;
|
return result;
|
||||||
},
|
},
|
||||||
|
|
||||||
/**
|
async triggerBackendRefresh(): Promise<void> {
|
||||||
* Backend `/debug/send-wakeup` → FCM only. App no longer consumes WAKEUP_PING
|
await refreshNotificationsWithDiagnostics({ source: "debug panel" });
|
||||||
* for api_* scheduling; kept for FCM delivery diagnostics until backend Phase 3.
|
},
|
||||||
*/
|
|
||||||
|
/** Local simulation: same API call as a WAKEUP_PING handler (no push payload). */
|
||||||
|
async simulateWakeupViaRefresh(): Promise<void> {
|
||||||
|
logNotification("WAKEUP_PING simulation (local refresh API only)");
|
||||||
|
await refreshNotificationsWithDiagnostics({
|
||||||
|
source: "WAKEUP_PING simulation",
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
/** Full pipeline: backend `/debug/send-wakeup` → FCM → native WAKEUP_PING handler. */
|
||||||
async sendRealWakeupPing(): Promise<SendRealWakeupPingResult> {
|
async sendRealWakeupPing(): Promise<SendRealWakeupPingResult> {
|
||||||
logNotification("Real WAKEUP_PING requested");
|
logNotification("Real WAKEUP_PING requested");
|
||||||
|
|
||||||
@@ -247,6 +264,62 @@ export const NotificationDebugService = {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
|
generateMockNotifications(
|
||||||
|
intervalMs: number = 60_000,
|
||||||
|
): NotificationRefreshPayload {
|
||||||
|
const now = Date.now();
|
||||||
|
const future1 = now + intervalMs;
|
||||||
|
const future2 = now + intervalMs * 2;
|
||||||
|
|
||||||
|
return {
|
||||||
|
shouldNotify: true,
|
||||||
|
nextNotifications: [{ timestamp: future1 }, { timestamp: future2 }],
|
||||||
|
};
|
||||||
|
},
|
||||||
|
|
||||||
|
async triggerMockRefresh(intervalMs?: number): Promise<void> {
|
||||||
|
logNotification("Mock refresh requested");
|
||||||
|
|
||||||
|
const payload = this.generateMockNotifications(intervalMs);
|
||||||
|
const timestamps = payload.nextNotifications?.map((n) => n.timestamp) ?? [];
|
||||||
|
logNotification(`Mock payload generated (${timestamps.length} timestamps)`);
|
||||||
|
|
||||||
|
if (!Capacitor.isNativePlatform()) {
|
||||||
|
logNotification("Mock refresh skipped: not running on native platform");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
await applyNotificationRefreshPayload(payload);
|
||||||
|
logNotification("Mock refresh applied");
|
||||||
|
},
|
||||||
|
|
||||||
|
async simulateWakeupPing(): Promise<void> {
|
||||||
|
logNotification("Simulating WAKEUP_PING (production push handler)");
|
||||||
|
|
||||||
|
if (!Capacitor.isNativePlatform()) {
|
||||||
|
logNotification("WAKEUP_PING simulation skipped: not native platform");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const notification = {
|
||||||
|
title: "WAKEUP_PING",
|
||||||
|
body: "",
|
||||||
|
id: "dev_wakeup_ping",
|
||||||
|
data: { type: "WAKEUP_PING" },
|
||||||
|
} as unknown as PushNotificationSchema;
|
||||||
|
|
||||||
|
await handleCapacitorPushNotificationReceived(notification);
|
||||||
|
},
|
||||||
|
|
||||||
|
async runFloodTest(intervalMs?: number): Promise<void> {
|
||||||
|
logNotification("Flood test started (20 sequential refreshes)");
|
||||||
|
for (let i = 0; i < 20; i++) {
|
||||||
|
logNotification(`Flood iteration ${i + 1}/20`);
|
||||||
|
await this.triggerMockRefresh(intervalMs);
|
||||||
|
}
|
||||||
|
logNotification("Flood test completed");
|
||||||
|
},
|
||||||
|
|
||||||
async clearNotifications(): Promise<void> {
|
async clearNotifications(): Promise<void> {
|
||||||
logNotification("Clear notifications (debug panel)");
|
logNotification("Clear notifications (debug panel)");
|
||||||
|
|
||||||
|
|||||||
@@ -21,6 +21,7 @@ import {
|
|||||||
onMessage,
|
onMessage,
|
||||||
} from "firebase/messaging";
|
} from "firebase/messaging";
|
||||||
import { logger } from "@/utils/logger";
|
import { logger } from "@/utils/logger";
|
||||||
|
import { NotificationInspector } from "@/plugins/NotificationInspectorPlugin";
|
||||||
import { handleCapacitorPushNotificationReceived } from "./NativeNotificationService";
|
import { handleCapacitorPushNotificationReceived } from "./NativeNotificationService";
|
||||||
import { getNotificationApiHeaders } from "./notificationApiAuth";
|
import { getNotificationApiHeaders } from "./notificationApiAuth";
|
||||||
import { deferFcmRegistration } from "./notificationAuthLifecycle";
|
import { deferFcmRegistration } from "./notificationAuthLifecycle";
|
||||||
@@ -67,6 +68,24 @@ async function registerRetrievedToken(
|
|||||||
lastRegisteredFcmToken = trimmed;
|
lastRegisteredFcmToken = trimmed;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Whether PushNotifications.register() is safe to call. Android builds without
|
||||||
|
* -PfirebaseEnabled (FOSS/F-Droid/APK) have no Firebase config, and register()
|
||||||
|
* there throws natively, crashing the app before any JS catch can run.
|
||||||
|
*/
|
||||||
|
async function canRegisterNativePush(): Promise<boolean> {
|
||||||
|
if (Capacitor.getPlatform() !== "android") {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
const { configured } = await NotificationInspector.isFirebaseConfigured();
|
||||||
|
return configured;
|
||||||
|
} catch (err) {
|
||||||
|
logger.warn(`${LOG} Could not check Firebase config; skipping push`, err);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/** Most recent FCM token from native/web push registration (for debug UI). */
|
/** Most recent FCM token from native/web push registration (for debug UI). */
|
||||||
export function getLastKnownFcmToken(): string | null {
|
export function getLastKnownFcmToken(): string | null {
|
||||||
return lastSeenFcmToken ?? lastRegisteredFcmToken;
|
return lastSeenFcmToken ?? lastRegisteredFcmToken;
|
||||||
@@ -80,6 +99,12 @@ export async function reregisterFcmTokenNow(): Promise<string> {
|
|||||||
throw new Error("FCM registration is only available on native platforms");
|
throw new Error("FCM registration is only available on native platforms");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (!(await canRegisterNativePush())) {
|
||||||
|
throw new Error(
|
||||||
|
"Push is unavailable: this Android build has no Firebase config (built without -PfirebaseEnabled)",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
lastRegisteredFcmToken = null;
|
lastRegisteredFcmToken = null;
|
||||||
|
|
||||||
const cached = lastSeenFcmToken?.trim();
|
const cached = lastSeenFcmToken?.trim();
|
||||||
@@ -290,6 +315,13 @@ async function initializeNativePushAndFirebaseMessagingImpl(): Promise<void> {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (!(await canRegisterNativePush())) {
|
||||||
|
logger.info(
|
||||||
|
`${LOG} No Firebase config in this Android build; skipping push registration`,
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
await PushNotifications.register();
|
await PushNotifications.register();
|
||||||
|
|
||||||
if (app) {
|
if (app) {
|
||||||
|
|||||||
@@ -42,7 +42,6 @@ export { uploadAlertSearchAuthorization } from "./alertAuthorization";
|
|||||||
export type { AlertAuthorizationUploadResult } from "./alertAuthorization";
|
export type { AlertAuthorizationUploadResult } from "./alertAuthorization";
|
||||||
|
|
||||||
export { configureNativeFetcherIfReady } from "./nativeFetcherConfig";
|
export { configureNativeFetcherIfReady } from "./nativeFetcherConfig";
|
||||||
export { runLegacyApiNotificationsCleanupOnce } from "./legacyApiNotificationsCleanup";
|
|
||||||
export {
|
export {
|
||||||
deferFcmRegistration,
|
deferFcmRegistration,
|
||||||
flushDeferredFcmRegistration,
|
flushDeferredFcmRegistration,
|
||||||
|
|||||||
@@ -1,101 +0,0 @@
|
|||||||
import { Capacitor } from "@capacitor/core";
|
|
||||||
import { Preferences } from "@capacitor/preferences";
|
|
||||||
import { DailyNotification } from "@/plugins/DailyNotificationPlugin";
|
|
||||||
import {
|
|
||||||
LEGACY_API_NOTIFICATIONS_CLEANUP_KEY,
|
|
||||||
runLegacyApiNotificationsCleanupOnce,
|
|
||||||
} from "./legacyApiNotificationsCleanup";
|
|
||||||
|
|
||||||
jest.mock("@capacitor/core", () => ({
|
|
||||||
Capacitor: {
|
|
||||||
isNativePlatform: jest.fn(),
|
|
||||||
},
|
|
||||||
}));
|
|
||||||
|
|
||||||
jest.mock("@capacitor/preferences", () => ({
|
|
||||||
Preferences: {
|
|
||||||
get: jest.fn(),
|
|
||||||
set: jest.fn(),
|
|
||||||
},
|
|
||||||
}));
|
|
||||||
|
|
||||||
jest.mock("@/plugins/DailyNotificationPlugin", () => ({
|
|
||||||
DailyNotification: {
|
|
||||||
clearApiNotifications: jest.fn(),
|
|
||||||
},
|
|
||||||
}));
|
|
||||||
|
|
||||||
jest.mock("@/utils/logger", () => ({
|
|
||||||
logger: {
|
|
||||||
info: jest.fn(),
|
|
||||||
warn: jest.fn(),
|
|
||||||
error: jest.fn(),
|
|
||||||
debug: jest.fn(),
|
|
||||||
log: jest.fn(),
|
|
||||||
},
|
|
||||||
}));
|
|
||||||
|
|
||||||
const isNativePlatform = Capacitor.isNativePlatform as jest.Mock;
|
|
||||||
const preferencesGet = Preferences.get as jest.Mock;
|
|
||||||
const preferencesSet = Preferences.set as jest.Mock;
|
|
||||||
const clearApiNotifications =
|
|
||||||
DailyNotification.clearApiNotifications as jest.Mock;
|
|
||||||
|
|
||||||
describe("runLegacyApiNotificationsCleanupOnce", () => {
|
|
||||||
beforeEach(() => {
|
|
||||||
jest.clearAllMocks();
|
|
||||||
isNativePlatform.mockReturnValue(true);
|
|
||||||
preferencesGet.mockResolvedValue({ value: null });
|
|
||||||
preferencesSet.mockResolvedValue(undefined);
|
|
||||||
clearApiNotifications.mockResolvedValue(undefined);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("no-ops on non-native platforms", async () => {
|
|
||||||
isNativePlatform.mockReturnValue(false);
|
|
||||||
|
|
||||||
await runLegacyApiNotificationsCleanupOnce();
|
|
||||||
|
|
||||||
expect(preferencesGet).not.toHaveBeenCalled();
|
|
||||||
expect(clearApiNotifications).not.toHaveBeenCalled();
|
|
||||||
expect(preferencesSet).not.toHaveBeenCalled();
|
|
||||||
});
|
|
||||||
|
|
||||||
it("skips cleanup when the migration marker is already set", async () => {
|
|
||||||
preferencesGet.mockResolvedValue({ value: "1" });
|
|
||||||
|
|
||||||
await runLegacyApiNotificationsCleanupOnce();
|
|
||||||
|
|
||||||
expect(clearApiNotifications).not.toHaveBeenCalled();
|
|
||||||
expect(preferencesSet).not.toHaveBeenCalled();
|
|
||||||
});
|
|
||||||
|
|
||||||
it("clears api_* state and writes the marker only after success", async () => {
|
|
||||||
await runLegacyApiNotificationsCleanupOnce();
|
|
||||||
|
|
||||||
expect(clearApiNotifications).toHaveBeenCalledTimes(1);
|
|
||||||
expect(preferencesSet).toHaveBeenCalledWith({
|
|
||||||
key: LEGACY_API_NOTIFICATIONS_CLEANUP_KEY,
|
|
||||||
value: "1",
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
it("does not write the marker when clearApiNotifications fails", async () => {
|
|
||||||
clearApiNotifications.mockRejectedValue(new Error("native failure"));
|
|
||||||
|
|
||||||
await runLegacyApiNotificationsCleanupOnce();
|
|
||||||
|
|
||||||
expect(clearApiNotifications).toHaveBeenCalledTimes(1);
|
|
||||||
expect(preferencesSet).not.toHaveBeenCalled();
|
|
||||||
});
|
|
||||||
|
|
||||||
it("does not write the marker when clearApiNotifications is missing", async () => {
|
|
||||||
const original = DailyNotification.clearApiNotifications;
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
||||||
delete (DailyNotification as any).clearApiNotifications;
|
|
||||||
|
|
||||||
await runLegacyApiNotificationsCleanupOnce();
|
|
||||||
|
|
||||||
expect(preferencesSet).not.toHaveBeenCalled();
|
|
||||||
DailyNotification.clearApiNotifications = original;
|
|
||||||
});
|
|
||||||
});
|
|
||||||
@@ -1,85 +0,0 @@
|
|||||||
/**
|
|
||||||
* One-time upgrade cleanup for legacy API-managed notification schedules
|
|
||||||
* (`api_*`) left behind by the retired WAKEUP_PING → /notifications/refresh path.
|
|
||||||
*
|
|
||||||
* Uses DailyNotification.clearApiNotifications(), which cancels only `api_*`
|
|
||||||
* pending/delivered state (and Android persisted api_* schedule rows). It does
|
|
||||||
* not touch daily reminders, dual/New Activity schedules, AlertSearch, or FCM.
|
|
||||||
*/
|
|
||||||
|
|
||||||
import { Capacitor } from "@capacitor/core";
|
|
||||||
import { Preferences } from "@capacitor/preferences";
|
|
||||||
import { DailyNotification } from "@/plugins/DailyNotificationPlugin";
|
|
||||||
import { logger } from "@/utils/logger";
|
|
||||||
|
|
||||||
/** Preferences key; written only after a successful native cleanup. */
|
|
||||||
export const LEGACY_API_NOTIFICATIONS_CLEANUP_KEY =
|
|
||||||
"legacy_api_notifications_cleanup_v1";
|
|
||||||
|
|
||||||
const DONE_VALUE = "1";
|
|
||||||
|
|
||||||
let inFlight: Promise<void> | null = null;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Idempotent startup hook. Safe when zero `api_*` schedules exist.
|
|
||||||
* On failure, leaves the marker unset so a later launch can retry.
|
|
||||||
*/
|
|
||||||
export async function runLegacyApiNotificationsCleanupOnce(): Promise<void> {
|
|
||||||
if (inFlight) {
|
|
||||||
return inFlight;
|
|
||||||
}
|
|
||||||
inFlight = runCleanup().finally(() => {
|
|
||||||
inFlight = null;
|
|
||||||
});
|
|
||||||
return inFlight;
|
|
||||||
}
|
|
||||||
|
|
||||||
async function runCleanup(): Promise<void> {
|
|
||||||
if (!Capacitor.isNativePlatform()) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
const existing = await Preferences.get({
|
|
||||||
key: LEGACY_API_NOTIFICATIONS_CLEANUP_KEY,
|
|
||||||
});
|
|
||||||
if (existing.value === DONE_VALUE) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
logger.warn(
|
|
||||||
"[legacyApiNotificationsCleanup] Could not read migration marker; skipping this launch",
|
|
||||||
error,
|
|
||||||
);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const clearApiNotifications = (
|
|
||||||
DailyNotification as {
|
|
||||||
clearApiNotifications?: () => Promise<void>;
|
|
||||||
}
|
|
||||||
).clearApiNotifications;
|
|
||||||
|
|
||||||
if (typeof clearApiNotifications !== "function") {
|
|
||||||
logger.warn(
|
|
||||||
"[legacyApiNotificationsCleanup] clearApiNotifications unavailable; will retry on a later launch",
|
|
||||||
);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
await clearApiNotifications.call(DailyNotification);
|
|
||||||
await Preferences.set({
|
|
||||||
key: LEGACY_API_NOTIFICATIONS_CLEANUP_KEY,
|
|
||||||
value: DONE_VALUE,
|
|
||||||
});
|
|
||||||
logger.info(
|
|
||||||
"[legacyApiNotificationsCleanup] Cleared legacy api_* notification state",
|
|
||||||
);
|
|
||||||
} catch (error) {
|
|
||||||
logger.warn(
|
|
||||||
"[legacyApiNotificationsCleanup] Cleanup failed; will retry on next startup",
|
|
||||||
error,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -11,7 +11,7 @@ import { getNotificationDebugOverrideHeaders } from "./NotificationDebugConfig";
|
|||||||
import { shouldBypassNotificationAuth } from "./notificationApiDebugMode";
|
import { shouldBypassNotificationAuth } from "./notificationApiDebugMode";
|
||||||
import { logNotification } from "./NotificationDebugEvents";
|
import { logNotification } from "./NotificationDebugEvents";
|
||||||
|
|
||||||
export type NotificationRequestKind = "register";
|
export type NotificationRequestKind = "register" | "refresh";
|
||||||
|
|
||||||
export type NotificationApiHeadersResult =
|
export type NotificationApiHeadersResult =
|
||||||
| {
|
| {
|
||||||
@@ -133,17 +133,32 @@ export async function getNotificationApiHeaders(
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function logNotificationRequestAuthenticated(
|
||||||
|
kind: NotificationRequestKind,
|
||||||
|
): void {
|
||||||
|
logNotification(
|
||||||
|
kind === "register"
|
||||||
|
? "Register request authenticated"
|
||||||
|
: "Refresh request authenticated",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
export function logNotificationAuthFailure(
|
export function logNotificationAuthFailure(
|
||||||
_kind: NotificationRequestKind,
|
kind: NotificationRequestKind,
|
||||||
message: string,
|
message: string,
|
||||||
): void {
|
): void {
|
||||||
logNotification(`Register auth unavailable: ${message}`);
|
const verb = kind === "register" ? "Register" : "Refresh";
|
||||||
|
logNotification(`${verb} auth unavailable: ${message}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function logWaitingForAuthBeforeRegistration(): void {
|
export function logWaitingForAuthBeforeRegistration(): void {
|
||||||
logNotification("Waiting for auth before registration");
|
logNotification("Waiting for auth before registration");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function logSkippingRefreshDueToMissingAuth(): void {
|
||||||
|
logNotification("Skipping refresh due to missing auth");
|
||||||
|
}
|
||||||
|
|
||||||
export function httpAuthErrorMessage(status: number): string {
|
export function httpAuthErrorMessage(status: number): string {
|
||||||
if (status === 401) {
|
if (status === 401) {
|
||||||
return "unauthorized (expired or invalid auth)";
|
return "unauthorized (expired or invalid auth)";
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
/**
|
/**
|
||||||
* Defers FCM token registration until app auth (active DID + Bearer) is available.
|
* Defers notification register/refresh until app auth (active DID + Bearer) is available.
|
||||||
* Bounded retries avoid racing startup and prevent infinite loops.
|
* Bounded retries avoid racing startup and prevent infinite loops.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
|||||||
@@ -24,6 +24,9 @@ export function logPushNotificationReceived(notification: {
|
|||||||
title: notification.title,
|
title: notification.title,
|
||||||
dataType: type,
|
dataType: type,
|
||||||
});
|
});
|
||||||
|
if (type === "WAKEUP_PING") {
|
||||||
|
logNotification("WAKEUP_PING received — will trigger refresh");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export function logPushNotificationActionPerformed(action: {
|
export function logPushNotificationActionPerformed(action: {
|
||||||
@@ -71,6 +74,44 @@ export function logTokenRegistrationFailure(
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function logRefreshStarted(source?: string): void {
|
||||||
|
logNotification(source ? `Refresh started (${source})` : "Refresh started");
|
||||||
|
}
|
||||||
|
|
||||||
|
function elapsedMsSince(startedAt: number): number {
|
||||||
|
return performance.now() - startedAt;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function logRefreshSuccess(
|
||||||
|
startedAt: number,
|
||||||
|
scheduledCount: number,
|
||||||
|
source?: string,
|
||||||
|
): void {
|
||||||
|
const elapsedMs = Math.round(elapsedMsSince(startedAt));
|
||||||
|
const message = source
|
||||||
|
? `Refresh completed (${source}) in ${elapsedMs}ms (scheduled ${scheduledCount})`
|
||||||
|
: `Refresh completed in ${elapsedMs}ms (scheduled ${scheduledCount})`;
|
||||||
|
logNotification(message);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function logRefreshFailure(
|
||||||
|
startedAt: number,
|
||||||
|
errorMessage: string,
|
||||||
|
status?: number,
|
||||||
|
source?: string,
|
||||||
|
): void {
|
||||||
|
const statusPart = status != null ? ` HTTP ${status}` : "";
|
||||||
|
const elapsedMs = Math.round(elapsedMsSince(startedAt));
|
||||||
|
const message = source
|
||||||
|
? `Refresh failed (${source}) in ${elapsedMs}ms: ${errorMessage}${statusPart}`
|
||||||
|
: `Refresh failed in ${elapsedMs}ms: ${errorMessage}${statusPart}`;
|
||||||
|
logNotification(message);
|
||||||
|
}
|
||||||
|
|
||||||
export function logNotificationClearing(method: string): void {
|
export function logNotificationClearing(method: string): void {
|
||||||
logNotification(`Clearing notifications via ${method}`);
|
logNotification(`Clearing notifications via ${method}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function logScheduleReplacement(count: number): void {
|
||||||
|
logNotification(`Schedule replacement: ${count} notification(s)`);
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user