Merge branch 'master' into retire-legacy-notification

This commit is contained in:
Jose Olarte III
2026-10-04 10:11:35 -04:00
18 changed files with 1094 additions and 1604 deletions
+1 -1
View File
@@ -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
View File
@@ -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
View File
@@ -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
+2 -2
View File
@@ -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"
} }
] ]
@@ -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);
}
}
+16
View File
@@ -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.
+21 -10
View File
@@ -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 */
@@ -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,7 +600,7 @@
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;
@@ -603,7 +614,7 @@
"@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,7 +638,7 @@
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;
@@ -641,7 +652,7 @@
"@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)";
+677 -1419
View File
File diff suppressed because it is too large Load Diff
+118 -117
View File
@@ -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
View File
@@ -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
View File
@@ -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
+6 -6
View File
@@ -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
+100
View File
@@ -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
+5 -1
View File
@@ -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'
} }
]; ];
+1 -1
View File
@@ -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;
@@ -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 =
@@ -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) {