Compare commits
9
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
4ef0a51c6a | ||
|
|
59f503d90a | ||
|
|
7133c4d574 | ||
|
|
d14843622d | ||
|
|
6fb4314b3a | ||
|
|
cbbf9d7bc0 | ||
|
|
150c69a025 | ||
|
|
a271cd1434 | ||
|
|
7bf36b0845 |
@@ -11,7 +11,7 @@
|
||||
|
||||
# Web (development)
|
||||
|
||||
npm run build:web
|
||||
npm run dev
|
||||
|
||||
# Mobile
|
||||
|
||||
|
||||
@@ -13,7 +13,7 @@
|
||||
|
||||
- **File System**: Limited browser APIs
|
||||
|
||||
- **Build**: `npm run build:web` (development build)
|
||||
- **Build**: `npm run dev` (development server), `npm run build:web` (production build into `dist/`)
|
||||
|
||||
### Mobile (Capacitor)
|
||||
|
||||
@@ -59,7 +59,7 @@
|
||||
|
||||
# Web (development)
|
||||
|
||||
npm run build:web
|
||||
npm run dev
|
||||
|
||||
# Mobile
|
||||
|
||||
|
||||
@@ -63,7 +63,7 @@ follow proper review, testing, and documentation procedures.
|
||||
|
||||
- **CI/CD Pipeline**: GitHub Actions, build validation scripts
|
||||
|
||||
- **Service Worker Assembly**: `sw_scripts/`, `sw_combine.js`, WASM copy steps
|
||||
- **WASM Assembly**: WASM copy steps
|
||||
|
||||
## Change Authorization Requirements
|
||||
|
||||
|
||||
@@ -39,7 +39,7 @@
|
||||
|
||||
### Rollback Verification
|
||||
|
||||
- **Web**: `npm run build:web:dev` and `npm run build:web:prod`
|
||||
- **Web**: `npm run dev` and `npm run build:web:prod`
|
||||
|
||||
- **Mobile**: `npm run build:android:test` and `npm run build:ios:test`
|
||||
|
||||
|
||||
@@ -19,7 +19,7 @@
|
||||
|
||||
### After Build System Change
|
||||
|
||||
- [ ] **Web Platform**: Does `npm run build:web:dev` work?
|
||||
- [ ] **Web Platform**: Does `npm run dev` work?
|
||||
|
||||
- [ ] **Mobile Platforms**: Do iOS/Android builds succeed?
|
||||
|
||||
@@ -33,9 +33,9 @@
|
||||
|
||||
### Web Platform
|
||||
|
||||
- **Development**: `npm run build:web:dev` - serve and load app
|
||||
- **Development**: `npm run dev` - serve and load app
|
||||
|
||||
- **Production**: `npm run build:web:prod` - verify SW and WASM present
|
||||
- **Production**: `npm run build:web:prod` - verify WASM present
|
||||
|
||||
### Mobile Platforms
|
||||
|
||||
@@ -98,13 +98,11 @@
|
||||
verify single-instance,
|
||||
icons, signing
|
||||
|
||||
### Service Worker / WASM
|
||||
### WASM
|
||||
|
||||
- **Trigger**: `sw_combine.js`, WASM copy path
|
||||
- **Trigger**: WASM copy path
|
||||
|
||||
- **Validation**: Verify combined SW exists and is injected; page loads offline;
|
||||
|
||||
WASM present
|
||||
- **Validation**: WASM present
|
||||
|
||||
### Docker
|
||||
|
||||
|
||||
@@ -5,7 +5,7 @@ VITE_LOG_LEVEL=info
|
||||
|
||||
# iOS doesn't like spaces in the app title.
|
||||
TIME_SAFARI_APP_TITLE="TimeSafari_Test"
|
||||
VITE_APP_SERVER=https://test.timesafari.app
|
||||
VITE_APP_SERVER=https://test.`
|
||||
# This is the claim ID for actions in the BVC project, with the JWT ID on this environment (not
|
||||
# This is the claim ID for actions in the BVC project, with the JWT ID on this environment (not production).
|
||||
|
||||
|
||||
@@ -5,8 +5,6 @@ dist-electon-build
|
||||
node_modules
|
||||
dist
|
||||
signature.bin
|
||||
# generated during `npm run build`
|
||||
sw_scripts-combined.js
|
||||
*.pem
|
||||
verified.txt
|
||||
myenv
|
||||
|
||||
@@ -2,16 +2,18 @@
|
||||
|
||||
## Android Build — Google Play Services / FOSS Compatibility
|
||||
|
||||
**Firebase is opt-in. Do NOT enable it accidentally.**
|
||||
**Firebase is on by default for Android and iOS builds.** Do not turn it off unless the build is an exceptional FOSS/F-Droid artifact.
|
||||
|
||||
`android/google-services.json` is gitignored and may be present on disk for push notification development, but Firebase is only activated when you explicitly pass `-PfirebaseEnabled` to Gradle:
|
||||
`android/app/google-services.json` is required when Firebase is enabled. It does not decide the switch: a missing file fails the build, and a file on disk is ignored when Firebase is explicitly disabled.
|
||||
|
||||
- **FOSS / APK / Aurora / Zapstore / F-Droid builds**: just `./gradlew assembleRelease` — Firebase stays off even if `google-services.json` is on disk.
|
||||
- **Firebase / FCM / Play Store builds**: `./gradlew bundleRelease -PfirebaseEnabled` — explicitly opt in.
|
||||
- **Default (local, test, production, Play Store)**: `./gradlew assembleRelease` or `./gradlew bundleRelease` — do not pass `-PfirebaseEnabled`.
|
||||
- **Opt out (F-Droid / FOSS)**: `./gradlew assembleRelease -PfirebaseEnabled=false`, or an npm `:nostore` script such as `npm run build:android:test:nostore:run`. Those set `FIREBASE_ENABLED=false`, which forwards `-PfirebaseEnabled=false`. `:nostore` disables Firebase only; it does not remove every Play Services dependency.
|
||||
|
||||
This guard is in `android/app/build.gradle`. Do NOT change this conditional to activate Firebase unconditionally based on file presence alone — that was the bug that broke FOSS distribution in June 2026.
|
||||
iOS builds bundle `ios/App/App/GoogleService-Info.plist` by default (`ios/App/scripts/bundle-firebase-config.sh`, run from `scripts/build-ios.sh` and as an Xcode build phase). Opt out with `FIREBASE_ENABLED=false` (the build script forwards that to both the build and archive `xcodebuild` invocations), `npm run build:ios:prod:nostore`, or the Xcode build setting `FIREBASE_ENABLED=NO`. A missing plist fails an enabled build; the file's presence does not enable Firebase on its own.
|
||||
|
||||
`google-services.json` is intentionally excluded from git (`android/.gitignore`). Never commit it.
|
||||
The guard is in `android/app/build.gradle`. Do NOT activate Firebase from file presence alone — that was the bug that broke FOSS distribution in June 2026.
|
||||
|
||||
Never commit `google-services.json`.
|
||||
|
||||
Full details, incident history, and F-Droid notes: `doc/android-firebase-gms.md`
|
||||
|
||||
|
||||
+79
-232
@@ -9,9 +9,9 @@ This guide explains how to build TimeSafari for different platforms using the co
|
||||
```bash
|
||||
# 🖥️ Web Development
|
||||
npm install # setup -- and pkgx.dev `dev` command before this will set environment with npm, etc
|
||||
npm run build:web:serve -- --test # Start with test endorser server
|
||||
npm run dev # Start development server with hot reload with local endorser server
|
||||
npm run build:web:prod # Production build
|
||||
npm run build:web:serve:test # Test build served from dist/ (test endorser server)
|
||||
npm run build:web # Production build into dist/
|
||||
|
||||
# 📱 Mobile Development
|
||||
npm run build:ios # iOS build (opens Xcode)
|
||||
@@ -219,36 +219,41 @@ Install dependencies:
|
||||
npm install
|
||||
```
|
||||
|
||||
## Web Development
|
||||
## Web Development & Deployment
|
||||
|
||||
### Web Development Commands
|
||||
|
||||
```bash
|
||||
# Local development
|
||||
npm run dev # Start development server with hot reload (shortcut for `build:web:dev`)
|
||||
npm run dev # Start development server with hot reload (writes nothing to dist/)
|
||||
|
||||
# Web builds
|
||||
npm run build:web # Development build (starts dev server with hot reload)
|
||||
# Web builds (each one writes dist/)
|
||||
npm run build:web # Production build (same as build:web:prod)
|
||||
npm run build:web:test # Test environment build (optimized for testing)
|
||||
npm run build:web:prod # Production build (optimized for production)
|
||||
npm run build:web:serve # Build and serve locally for production testing
|
||||
|
||||
# Build, then serve dist/ locally
|
||||
npm run build:web:serve # Production build, then serve
|
||||
npm run build:web:serve:test # Test build, then serve
|
||||
|
||||
# Docker builds
|
||||
npm run build:web:docker # Development build with Docker containerization
|
||||
npm run build:web:docker # Production build with Docker containerization
|
||||
npm run build:web:docker:test # Test build with Docker containerization
|
||||
npm run build:web:docker:prod # Production build with Docker containerization
|
||||
```
|
||||
|
||||
### Local Development
|
||||
|
||||
Start the development server using `npm run dev` (a shortcut for `npm run build:web:dev`) or `npm run build:web` (both start the development server with hot reload).
|
||||
Start the development server with hot reload using `npm run dev`. Open it at `http://localhost:8080/`, not at a wildcard address such as `http://[::]:8080/` or `http://0.0.0.0:8080/`: browsers only allow the cryptography the app needs on HTTPS, `localhost`, `127.0.0.1`, or `[::1]`, so any other plain-HTTP address shows a "Cannot Start Here" page.
|
||||
|
||||
### Web Build for Server
|
||||
|
||||
1. Run the production build using `npm run build:web:prod`
|
||||
1. Run the production build using `npm run build:web` (or `npm run build:web:test` for the test server)
|
||||
2. The built files will be in the `dist` directory
|
||||
3. To test the production build locally, use `npm run build:web:serve` (builds then serves)
|
||||
|
||||
`dist/` is shared with the Android, iOS, Electron, and Capacitor builds, which overwrite it with non-web output. Run the web build immediately before deploying, and confirm that the browser console reports `Loading TimeSafari for platform: web`.
|
||||
|
||||
**Why Use `serve`?**
|
||||
- **Production Testing**: Test your optimized production build locally before deployment
|
||||
- **SPA Routing Validation**: Verify deep linking and navigation work correctly (handles routes like `/discover`, `/account`)
|
||||
@@ -1069,6 +1074,9 @@ npm run build:ios:prod # Production build (builds for production en
|
||||
npm run build:ios:test:run # Test build with auto-run (builds then runs on simulator)
|
||||
npm run build:ios:prod:run # Production build with auto-run (builds then runs on simulator)
|
||||
|
||||
# Firebase off (`:nostore` sets FIREBASE_ENABLED=false, forwarded to xcodebuild as FIREBASE_ENABLED=NO)
|
||||
npm run build:ios:prod:nostore # Same as prod, without Firebase
|
||||
|
||||
# Debug and release builds
|
||||
npm run build:ios:debug # Debug build (builds debug app bundle)
|
||||
npm run build:ios:debug:run # Debug build with auto-run (builds then runs on simulator)
|
||||
@@ -1332,6 +1340,10 @@ npm run build:android:test:run # Test build with auto-run (builds then run
|
||||
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
|
||||
|
||||
# Firebase off (`:nostore` sets FIREBASE_ENABLED=false, which passes -PfirebaseEnabled=false)
|
||||
npm run build:android:test:nostore:run # Same as test:run, without Firebase
|
||||
npm run build:android:prod:nostore # Same as prod, without Firebase
|
||||
|
||||
# Debug and release builds
|
||||
npm run build:android:debug # Debug build (builds debug APK)
|
||||
npm run build:android:debug:run # Debug build with auto-run (builds then runs on emulator)
|
||||
@@ -1468,9 +1480,11 @@ To create a signed `aab` release:
|
||||
|
||||
```bash
|
||||
# if you use pkgx: pkgx +openjdk.org /bin/zsh
|
||||
./gradlew bundleRelease -Dlint.baselines.continue=true -PfirebaseEnabled
|
||||
./gradlew bundleRelease -Dlint.baselines.continue=true
|
||||
```
|
||||
|
||||
Firebase is on by default (`firebaseEnabled=true` in `android/gradle.properties`). Do not pass `-PfirebaseEnabled` for a normal release. F-Droid and other builds that must omit Firebase use `-PfirebaseEnabled=false`, or an npm `:nostore` script (`FIREBASE_ENABLED=false`). `:nostore` only disables Firebase. It does not remove other store-linked dependencies, such as the ML Kit barcode scanner's Play Services library, and it does not by itself make the app run on every device outside the Play Store.
|
||||
|
||||
... and find your `aab` file at app/build/outputs/bundle/release
|
||||
|
||||
To create an unsigned debugging bundle (not usable in Play Store):
|
||||
@@ -1486,8 +1500,6 @@ To create an unsigned debugging bundle (not usable in Play Store):
|
||||
* `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`.
|
||||
|
||||
At play.google.com/console:
|
||||
|
||||
- Go to Production or the Closed Testing and either Create Track or Manage Track.
|
||||
@@ -1603,8 +1615,7 @@ Use the commands above to check and fix code quality issues.
|
||||
|
||||
1. **Environment Setup**: Load platform-specific environment variables
|
||||
2. **Asset Building**: Vite builds optimized web assets
|
||||
3. **PWA Generation**: Service worker and manifest creation
|
||||
4. **Output**: Production-ready files in `dist/` directory
|
||||
3. **Output**: Production-ready files in `dist/` directory
|
||||
|
||||
### Electron Build Process
|
||||
|
||||
@@ -1856,38 +1867,21 @@ npm run build:android:assets
|
||||
|
||||
### Build Process Overview
|
||||
|
||||
TimeSafari's build system follows a multi-stage process that prepares assets,
|
||||
combines scripts, and generates platform-specific outputs.
|
||||
TimeSafari's build system follows a multi-stage process that prepares assets
|
||||
and generates platform-specific outputs.
|
||||
|
||||
#### Pre-Build Preparation
|
||||
|
||||
##### 1. Service Worker Script Preparation
|
||||
|
||||
```bash
|
||||
# Optional: Format third-party service worker scripts
|
||||
npx prettier --write ./sw_scripts/
|
||||
```
|
||||
|
||||
**What this does:**
|
||||
|
||||
- Formats cryptographic libraries (`nacl.js`, `noble-curves.js`,
|
||||
`noble-hashes.js`, etc.)
|
||||
- These scripts are automatically combined during the build process
|
||||
- Improves readability and makes version control diffs cleaner
|
||||
- **Note**: This is optional and only needed when updating third-party scripts
|
||||
|
||||
**2. Automatic Pre-Build Steps**
|
||||
The `prebuild` script automatically runs before any build:
|
||||
#### Pre-Build Script
|
||||
|
||||
```json
|
||||
"prebuild": "eslint --ext .js,.ts,.vue --ignore-path .gitignore src && node sw_combine.js && node scripts/copy-wasm.js"
|
||||
"prebuild": "eslint --ext .js,.ts,.vue --ignore-path .gitignore src && node scripts/copy-wasm.js"
|
||||
```
|
||||
|
||||
**What happens automatically:**
|
||||
npm runs `prebuild` only before a script named `build`. This project defines
|
||||
no `build` script, so none of the `build:*` commands trigger it.
|
||||
|
||||
- **ESLint**: Checks and fixes code formatting in `src/`
|
||||
- **Script Combination**: `sw_combine.js` combines all `sw_scripts/*.js` files
|
||||
into `sw_scripts-combined.js`
|
||||
**What it does when run:**
|
||||
|
||||
- **ESLint**: Checks code formatting in `src/`
|
||||
- **WASM Copy**: `copy-wasm.js` copies SQLite WASM files to `public/wasm/`
|
||||
|
||||
#### Build Process Architecture
|
||||
@@ -1895,11 +1889,10 @@ The `prebuild` script automatically runs before any build:
|
||||
**Web Build Process:**
|
||||
|
||||
```text
|
||||
1. Pre-Build: ESLint + Script Combination + WASM Copy
|
||||
2. Environment Setup: Load .env files, set NODE_ENV
|
||||
3. Vite Build: Bundle web assets with PWA support
|
||||
4. Service Worker: Inject combined scripts into PWA
|
||||
5. Output: Production-ready files in dist/
|
||||
1. Environment Setup: Load .env files, set NODE_ENV
|
||||
2. Type Check: tsc --noEmit (test and production modes)
|
||||
3. Vite Build: Bundle web assets
|
||||
4. Output: Production-ready files in dist/
|
||||
```
|
||||
|
||||
**Electron Build Process:**
|
||||
@@ -1922,34 +1915,6 @@ The `prebuild` script automatically runs before any build:
|
||||
5. Package Creation: APK/IPA generation
|
||||
```
|
||||
|
||||
#### Service Worker Architecture
|
||||
|
||||
**Script Organization:**
|
||||
|
||||
- `sw_scripts/` - Individual third-party scripts
|
||||
- `sw_combine.js` - Combines scripts into single file
|
||||
- `sw_scripts-combined.js` - Combined service worker (317KB, 10K+ lines)
|
||||
- `vite.config.utils.mts` - PWA configuration using combined script
|
||||
|
||||
**PWA Integration:**
|
||||
|
||||
```typescript
|
||||
// vite.config.utils.mts
|
||||
pwaConfig: {
|
||||
strategies: "injectManifest",
|
||||
filename: "sw_scripts-combined.js", // Uses our combined script
|
||||
// ... manifest configuration
|
||||
}
|
||||
```
|
||||
|
||||
**What Gets Combined:**
|
||||
|
||||
- `nacl.js` - NaCl cryptographic library
|
||||
- `noble-curves.js` - Elliptic curve cryptography (177KB)
|
||||
- `noble-hashes.js` - Cryptographic hash functions (91KB)
|
||||
- `safari-notifications.js` - Safari-specific notifications
|
||||
- `additional-scripts.js` - Additional service worker functionality
|
||||
|
||||
#### Process Environment Configuration
|
||||
|
||||
**Environment Files:**
|
||||
@@ -2002,10 +1967,13 @@ device)
|
||||
```folders
|
||||
dist/
|
||||
├── index.html # Main HTML file
|
||||
├── assets/ # Bundled JavaScript/CSS
|
||||
├── sw.js # Service worker (injected)
|
||||
├── manifest.webmanifest # PWA manifest
|
||||
└── wasm/ # SQLite WASM files
|
||||
├── assets/ # Bundled JavaScript, CSS, and SQLite WASM
|
||||
├── img/ # Static images (copied from public/)
|
||||
├── models/ # Static model files (copied from public/)
|
||||
├── favicon.ico
|
||||
├── manifest.webmanifest # Web app manifest, incl. share_target (copied from public/)
|
||||
├── robots.txt
|
||||
└── sw.js # Service worker for the Web Share Target (copied from public/)
|
||||
```
|
||||
|
||||
**Electron Build:**
|
||||
@@ -2027,17 +1995,14 @@ electron/
|
||||
**Manual Steps (Developer Responsibility):**
|
||||
|
||||
- Database migration testing
|
||||
- Service worker script formatting (optional)
|
||||
- Version updates and changelog
|
||||
- Environment-specific builds
|
||||
|
||||
**Automatic Steps (Build System):**
|
||||
|
||||
- Code linting and formatting
|
||||
- Script combination
|
||||
- Type checking (test and production builds)
|
||||
- Asset optimization
|
||||
- Package creation
|
||||
- Service worker injection
|
||||
|
||||
This architecture ensures consistent builds across all platforms while providing
|
||||
flexibility for platform-specific optimizations and manual quality assurance steps.
|
||||
@@ -2062,31 +2027,34 @@ environment setup, Vite build, and optional Docker containerization.
|
||||
|
||||
```bash
|
||||
# Direct script usage
|
||||
./scripts/build-web.sh # Development build
|
||||
./scripts/build-web.sh --dev # Development build (explicit)
|
||||
./scripts/build-web.sh # Production build into dist/
|
||||
./scripts/build-web.sh --prod # Production build (explicit)
|
||||
./scripts/build-web.sh --test # Test environment build
|
||||
./scripts/build-web.sh --prod # Production environment build
|
||||
./scripts/build-web.sh --docker # Build with Docker containerization
|
||||
./scripts/build-web.sh --dev # Start Vite dev server (no dist/)
|
||||
./scripts/build-web.sh --docker # Production build + Docker image
|
||||
./scripts/build-web.sh --docker:test # Test environment + Docker
|
||||
./scripts/build-web.sh --docker:prod # Production environment + Docker
|
||||
./scripts/build-web.sh --serve # Build and serve locally
|
||||
./scripts/build-web.sh --serve # Production build, then serve dist/
|
||||
./scripts/build-web.sh --help # Show help
|
||||
./scripts/build-web.sh --verbose # Enable verbose logging
|
||||
|
||||
# NPM Script Equivalents
|
||||
npm run build:web # Development build
|
||||
npm run dev # Vite dev server
|
||||
npm run build:web # Production build
|
||||
npm run build:web:test # Test environment build
|
||||
npm run build:web:prod # Production environment build
|
||||
npm run build:web:docker # Docker build
|
||||
npm run build:web:docker # Production Docker build
|
||||
npm run build:web:docker:test # Test Docker build
|
||||
npm run build:web:docker:prod # Production Docker build
|
||||
npm run build:web:serve # Production build, then serve
|
||||
npm run build:web:serve:test # Test build, then serve
|
||||
```
|
||||
|
||||
**Build Modes**:
|
||||
|
||||
- **Development**: Starts Vite dev server with hot reload (default)
|
||||
- **Production**: Optimized for production with full minification (default)
|
||||
- **Test**: Optimized for testing with minimal minification
|
||||
- **Production**: Optimized for production with full minification
|
||||
- **Development**: Starts Vite dev server with hot reload (`--dev` alone); with `--serve` or `--docker`, builds `dist/` in development mode
|
||||
|
||||
**Script Features**:
|
||||
|
||||
@@ -2439,9 +2407,7 @@ export async function createBuildConfig(platform: string): Promise<UserConfig> {
|
||||
const appConfig = await loadAppConfig();
|
||||
const isCapacitor = platform === "capacitor";
|
||||
const isElectron = platform === "electron";
|
||||
const isNative = isCapacitor || isElectron;
|
||||
|
||||
// Set platform - PWA is always enabled for web platforms
|
||||
process.env.VITE_PLATFORM = platform;
|
||||
|
||||
return {
|
||||
@@ -2457,7 +2423,6 @@ export async function createBuildConfig(platform: string): Promise<UserConfig> {
|
||||
assetsDir: 'assets',
|
||||
chunkSizeWarningLimit: 1000,
|
||||
rollupOptions: {
|
||||
external: isNative ? ['@capacitor/app'] : [],
|
||||
output: {
|
||||
format: 'esm',
|
||||
generatedCode: { preset: 'es2015' },
|
||||
@@ -2489,12 +2454,7 @@ export async function createBuildConfig(platform: string): Promise<UserConfig> {
|
||||
'@jlongster/sql.js',
|
||||
'absurd-sql',
|
||||
// ... additional dependencies
|
||||
],
|
||||
exclude: isNative ? [
|
||||
'register-service-worker',
|
||||
'workbox-window',
|
||||
// ... native exclusions
|
||||
] : []
|
||||
]
|
||||
}
|
||||
};
|
||||
}
|
||||
@@ -2517,98 +2477,23 @@ export async function createBuildConfig(platform: string): Promise<UserConfig> {
|
||||
|
||||
**File**: `vite.config.web.mts`
|
||||
**Author**: Matthew Raymer
|
||||
**Description**: Vite configuration for web platform with PWA support
|
||||
**Description**: Vite configuration for the web platform
|
||||
|
||||
**Purpose**: Configures Vite for web builds with environment-specific
|
||||
optimizations and PWA features.
|
||||
**Purpose**: Configures Vite for web builds by delegating to the common
|
||||
configuration with the `web` platform.
|
||||
|
||||
**Configuration Structure**:
|
||||
|
||||
```typescript
|
||||
export default defineConfig(async () => createBuildConfig('web'));
|
||||
```
|
||||
|
||||
**Key Features**:
|
||||
|
||||
- **Environment-Specific Configuration**: Different settings for dev/test/prod
|
||||
- **PWA Integration**: Progressive Web App support with service worker
|
||||
- **Build Optimization**: Manual chunk splitting for better caching
|
||||
- **Source Maps**: Environment-specific source map configuration
|
||||
- **Caching Strategies**: Production-specific API caching
|
||||
|
||||
**Environment Configurations**:
|
||||
|
||||
**Development Mode**:
|
||||
|
||||
```typescript
|
||||
{
|
||||
build: {
|
||||
minify: false,
|
||||
sourcemap: true,
|
||||
rollupOptions: { output: { manualChunks: undefined } }
|
||||
},
|
||||
define: {
|
||||
__DEV__: true,
|
||||
__TEST__: false,
|
||||
__PROD__: false
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Test Mode**:
|
||||
|
||||
```typescript
|
||||
{
|
||||
build: {
|
||||
minify: false,
|
||||
sourcemap: true,
|
||||
rollupOptions: { output: { manualChunks: undefined } }
|
||||
},
|
||||
define: {
|
||||
__DEV__: false,
|
||||
__TEST__: true,
|
||||
__PROD__: false
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Production Mode**:
|
||||
|
||||
```typescript
|
||||
{
|
||||
build: {
|
||||
minify: 'terser',
|
||||
sourcemap: false,
|
||||
rollupOptions: {
|
||||
output: {
|
||||
manualChunks: {
|
||||
vendor: ['vue', 'vue-router', 'pinia'],
|
||||
utils: ['luxon', 'ramda', 'zod'],
|
||||
crypto: ['@ethersproject/wallet', '@ethersproject/hdnode'],
|
||||
sql: ['@jlongster/sql.js', 'absurd-sql']
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
define: {
|
||||
__DEV__: false,
|
||||
__TEST__: false,
|
||||
__PROD__: true
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**PWA Configuration**:
|
||||
|
||||
```typescript
|
||||
VitePWA({
|
||||
registerType: 'autoUpdate',
|
||||
manifest: appConfig.pwaConfig?.manifest,
|
||||
devOptions: { enabled: true, type: 'classic' },
|
||||
workbox: {
|
||||
cleanupOutdatedCaches: true,
|
||||
skipWaiting: true,
|
||||
clientsClaim: true,
|
||||
sourcemap: mode !== 'production',
|
||||
maximumFileSizeToCacheInBytes: 10 * 1024 * 1024,
|
||||
runtimeCaching: mode === 'production' ? [/* API caching */] : []
|
||||
}
|
||||
})
|
||||
```
|
||||
- **Minimal Configuration**: Inherits all settings from `vite.config.common.mts`
|
||||
- **Mode Selection**: `--mode test` or `--mode production` (passed by
|
||||
`scripts/build-web.sh`) selects the matching `.env.*` file
|
||||
- **Output**: Writes to `dist/`
|
||||
|
||||
### B.3 vite.config.electron.mts
|
||||
|
||||
@@ -2699,7 +2584,6 @@ export default defineConfig(async () => createBuildConfig('capacitor'));
|
||||
|
||||
- **Minimal Configuration**: Inherits all settings from common config
|
||||
- **Mobile Platform**: Automatically configures for mobile-specific settings
|
||||
- **PWA Disabled**: Progressive Web App features disabled for native apps
|
||||
- **Native Dependencies**: Handles Capacitor-specific dependencies
|
||||
|
||||
### B.5 vite.config.utils.mts
|
||||
@@ -2708,51 +2592,15 @@ export default defineConfig(async () => createBuildConfig('capacitor'));
|
||||
**Author**: Matthew Raymer
|
||||
**Description**: Utility functions for Vite configuration
|
||||
|
||||
**Purpose**: Provides shared configuration loading and PWA manifest generation.
|
||||
**Purpose**: Provides the shared alias configuration and strict ESBuild settings.
|
||||
|
||||
**Key Functions**:
|
||||
|
||||
**loadPackageJson()**: Loads and parses package.json for app configuration
|
||||
**loadAppConfig()**: Returns the alias configuration used by the common and
|
||||
optimized configs
|
||||
|
||||
**loadAppConfig()**: Loads complete application configuration including PWA settings
|
||||
|
||||
**PWA Configuration**:
|
||||
|
||||
```typescript
|
||||
interface PWAConfig {
|
||||
registerType: string;
|
||||
strategies: string;
|
||||
srcDir: string;
|
||||
filename: string;
|
||||
manifest: {
|
||||
name: string;
|
||||
short_name: string;
|
||||
theme_color: string;
|
||||
background_color: string;
|
||||
icons: ManifestIcon[];
|
||||
share_target: ShareTarget;
|
||||
};
|
||||
}
|
||||
```
|
||||
|
||||
**Manifest Icons**:
|
||||
|
||||
- Android Chrome 192x192 PNG
|
||||
- Android Chrome 512x512 PNG
|
||||
- Maskable icons for adaptive UI
|
||||
|
||||
**Share Target Configuration**:
|
||||
|
||||
```typescript
|
||||
share_target: {
|
||||
action: "/share-target",
|
||||
method: "POST",
|
||||
enctype: "multipart/form-data",
|
||||
params: {
|
||||
files: [{ name: "photo", accept: ["image/*"] }]
|
||||
}
|
||||
}
|
||||
```
|
||||
**getStrictESBuildConfig()**: Returns ESBuild settings that fail the build on
|
||||
syntax and duplicate-declaration errors
|
||||
|
||||
**Alias Configuration**:
|
||||
|
||||
@@ -2820,8 +2668,7 @@ All scripts use consistent error handling:
|
||||
|
||||
**Web Platform**:
|
||||
|
||||
- PWA features enabled
|
||||
- Service worker injection
|
||||
- Requires a secure context (HTTPS, `localhost`, `127.0.0.1`, or `[::1]`)
|
||||
- Browser-specific optimizations
|
||||
|
||||
**Electron Platform**:
|
||||
|
||||
@@ -6,6 +6,30 @@ 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).
|
||||
|
||||
|
||||
## [Unreleased]
|
||||
### Changed
|
||||
- `npm run build:web` builds production into `dist/` instead of starting the dev server.
|
||||
- `npm run build:web:serve` builds production (instead of development) before serving.
|
||||
- `npm run build:web:docker` builds production (instead of development) before creating the image.
|
||||
- `npm run build:native` builds with the Capacitor Vite config instead of a bare `vite build`.
|
||||
### Removed
|
||||
- `npm run build:web:dev` is removed; use `npm run dev` for the dev server.
|
||||
- Leftover service-worker build code (`sw_scripts/`, the unused PWA config, and the `register-service-worker` package) is removed.
|
||||
### Fixed
|
||||
- The daily starred-project check (Android and iOS) remembers the newest change it found, so it no longer reports the same changes again the next day.
|
||||
- Sharing a photo into the installed web app (Web Share Target) works again.
|
||||
- `npm run docker:up:test` and `npm run docker:up:prod` run, instead of failing on a missing `build:web:build` script.
|
||||
|
||||
|
||||
## [1.4.9] - 2026.10.11
|
||||
### Fixed
|
||||
- Startup DB error for new installations
|
||||
### Changed
|
||||
- Allow in-app notifications on production (as an advanced feature)
|
||||
- Allow modification of image server URL
|
||||
- Terms & conditions and privacy policy
|
||||
|
||||
|
||||
## [1.4.6] - 2026.09.27
|
||||
### Fixed
|
||||
- Incorrect 'test' server URLs on production.
|
||||
|
||||
+3
-3
@@ -21,13 +21,13 @@
|
||||
# Using npm scripts (recommended):
|
||||
# Production: npm run build:web:docker:prod
|
||||
# Test: npm run build:web:docker:test
|
||||
# Development: npm run build:web:docker
|
||||
# Development: ./scripts/build-web.sh --docker --dev
|
||||
#
|
||||
# Manual workflow:
|
||||
# 1. Build web assets: npm run build:web:build -- --mode production
|
||||
# 1. Build web assets: npm run build:web:prod
|
||||
# 2. Build Docker: docker build -t timesafari:latest .
|
||||
#
|
||||
# Note: For development, use npm run build:web directly (no Docker needed)
|
||||
# Note: For development, use npm run dev directly (no Docker needed)
|
||||
#
|
||||
# Build Arguments:
|
||||
# BUILD_MODE: development, test, or production (default: production)
|
||||
|
||||
@@ -33,45 +33,6 @@ npm run build:android:test:run
|
||||
|
||||
Assumes ADB is installed; see [Android Build](BUILDING.md#android-build) for SDK, emulator, and `PATH` setup.
|
||||
|
||||
#### Building an APK for Android 6 (API 23)
|
||||
|
||||
App versions up to 1.4.6 support Android 6.0 (API 23); later versions require Android 7.0 (API 24) or newer. To run the app on an Android 6 device, build your own APK from the 1.4.6 release commit (`6183afc2`, "bump version to 1.4.6").
|
||||
|
||||
Prerequisites: git, Node.js 20 (see `.nvmrc`), Java 21, and the Android SDK with `ANDROID_HOME` set (see [Android Emulator Without Android Studio](BUILDING.md#android-emulator-without-android-studio-command-line-only) for an SDK setup that skips Android Studio). With [pkgx](https://pkgx.dev), prefix the commands with `pkgx`, e.g. `pkgx npm ci`.
|
||||
|
||||
1. Get the 1.4.6 source:
|
||||
|
||||
```bash
|
||||
git clone https://gitea.anomalistdesign.com/trent_larson/crowd-funder-for-time-pwa.git
|
||||
cd crowd-funder-for-time-pwa
|
||||
git checkout 6183afc2
|
||||
```
|
||||
|
||||
2. Confirm the minimum SDK is 23:
|
||||
|
||||
```bash
|
||||
grep minSdkVersion android/variables.gradle # expect: minSdkVersion = 23
|
||||
```
|
||||
|
||||
3. Install dependencies and build a production APK:
|
||||
|
||||
```bash
|
||||
npm ci
|
||||
npm run build:android:prod -- --apk
|
||||
```
|
||||
|
||||
`npm ci` installs the exact dependency versions locked for 1.4.6. `build:android:prod -- --apk` builds the web app with production server settings, syncs it into the Android project, and runs `./gradlew assembleDebug`. The result is signed with your local Android debug key, so it installs on any device without a release keystore.
|
||||
|
||||
4. Find the APK at `android/app/build/outputs/apk/debug/app-debug.apk` and install it, either with `adb install android/app/build/outputs/apk/debug/app-debug.apk` (USB debugging enabled on the phone) or by copying the file to the phone and opening it (allow "Install unknown apps" for the file manager).
|
||||
|
||||
Notes:
|
||||
|
||||
* **Back up your identity first.** A self-built APK has a different signature from the Play Store app, so Android refuses to install it over the store version. Uninstalling the store version deletes the app's local data, including your identifier keys. Back up your seed phrase (Profile page → "Backup Identifier Seed") before uninstalling, then import it into the self-built app.
|
||||
* This build has Firebase disabled, so push notifications through Google's FCM service are off.
|
||||
* The daily notification plugin calls `java.time`, which Android provides only from 8.0 (API 26). On Android 6, scheduling a daily reminder can crash the app; leave daily reminders off. See [doc/plugin-feedback-android-6-api23-zoneid-fix.md](doc/plugin-feedback-android-6-api23-zoneid-fix.md).
|
||||
* Barcode scanning uses Google Play Services; on devices without them, scanning fails while the rest of the app works.
|
||||
* A self-built APK does not auto-update.
|
||||
|
||||
### iOS
|
||||
|
||||
```bash
|
||||
@@ -378,7 +339,7 @@ The application uses a platform-agnostic database layer with Vue mixins for serv
|
||||
|
||||
**Architecture Decision**: The project uses Vue mixins over Composition API composables for platform service access. See [Architecture Decisions](doc/architecture-decisions.md) for detailed rationale.
|
||||
|
||||
## 📁 Project Structure
|
||||
### 📁 Project Structure
|
||||
|
||||
```text
|
||||
timesafari/
|
||||
@@ -393,6 +354,45 @@ timesafari/
|
||||
└── 📄 doc/README-BUILD-GUARD.md # Guard system documentation
|
||||
```
|
||||
|
||||
### Building an APK for Android 6 (API 23)
|
||||
|
||||
App versions up to 1.4.6 support Android 6.0 (API 23); later versions require Android 7.0 (API 24) or newer. To run the app on an Android 6 device, build your own APK from the 1.4.6 release commit (`6183afc2`, "bump version to 1.4.6").
|
||||
|
||||
Prerequisites: git, Node.js 20 (see `.nvmrc`), Java 21, and the Android SDK with `ANDROID_HOME` set (see [Android Emulator Without Android Studio](BUILDING.md#android-emulator-without-android-studio-command-line-only) for an SDK setup that skips Android Studio). With [pkgx](https://pkgx.dev), prefix the commands with `pkgx`, e.g. `pkgx npm ci`.
|
||||
|
||||
1. Get the 1.4.6 source:
|
||||
|
||||
```bash
|
||||
git clone https://gitea.anomalistdesign.com/trent_larson/crowd-funder-for-time-pwa.git
|
||||
cd crowd-funder-for-time-pwa
|
||||
git checkout 6183afc2
|
||||
```
|
||||
|
||||
2. Confirm the minimum SDK is 23:
|
||||
|
||||
```bash
|
||||
grep minSdkVersion android/variables.gradle # expect: minSdkVersion = 23
|
||||
```
|
||||
|
||||
3. Install dependencies and build a production APK:
|
||||
|
||||
```bash
|
||||
npm ci
|
||||
npm run build:android:prod -- --apk
|
||||
```
|
||||
|
||||
`npm ci` installs the exact dependency versions locked for 1.4.6. `build:android:prod -- --apk` builds the web app with production server settings, syncs it into the Android project, and runs `./gradlew assembleDebug`. The result is signed with your local Android debug key, so it installs on any device without a release keystore.
|
||||
|
||||
4. Find the APK at `android/app/build/outputs/apk/debug/app-debug.apk` and install it, either with `adb install android/app/build/outputs/apk/debug/app-debug.apk` (USB debugging enabled on the phone) or by copying the file to the phone and opening it (allow "Install unknown apps" for the file manager).
|
||||
|
||||
Notes:
|
||||
|
||||
* **Back up your identity first.** A self-built APK has a different signature from the Play Store app, so Android refuses to install it over the store version. Uninstalling the store version deletes the app's local data, including your identifier keys. Back up your seed phrase (Profile page → "Backup Identifier Seed") before uninstalling, then import it into the self-built app.
|
||||
* This build has Firebase disabled, so push notifications through Google's FCM service are off.
|
||||
* The daily notification plugin calls `java.time`, which Android provides only from 8.0 (API 26). On Android 6, scheduling a daily reminder can crash the app; leave daily reminders off. See [doc/plugin-feedback-android-6-api23-zoneid-fix.md](doc/plugin-feedback-android-6-api23-zoneid-fix.md).
|
||||
* Barcode scanning uses Google Play Services; on devices without them, scanning fails while the rest of the app works.
|
||||
* A self-built APK does not auto-update.
|
||||
|
||||
## 🤝 Contributing
|
||||
|
||||
1. **Follow the Build Architecture Guard** - Update BUILDING.md when modifying build files
|
||||
|
||||
+27
-15
@@ -133,20 +133,32 @@ dependencies {
|
||||
|
||||
apply from: 'capacitor.build.gradle'
|
||||
|
||||
// Firebase / Google Play Services are opt-in. Pass -PfirebaseEnabled to any Gradle command
|
||||
// to activate Firebase (FCM push notifications). Without this flag the build works on
|
||||
// F-Droid, Aurora, Zapstore, and plain APK sideloading even when google-services.json
|
||||
// is present on disk (it is gitignored; see AGENTS.md for the full story).
|
||||
try {
|
||||
def servicesJSON = file('google-services.json')
|
||||
if (servicesJSON.exists() && servicesJSON.text && project.hasProperty('firebaseEnabled')) {
|
||||
apply plugin: 'com.google.gms.google-services'
|
||||
logger.info("Firebase enabled: google-services plugin applied")
|
||||
} else if (servicesJSON.exists() && !project.hasProperty('firebaseEnabled')) {
|
||||
logger.info("google-services.json present but firebaseEnabled not set — skipping Firebase plugin (pass -PfirebaseEnabled to enable)")
|
||||
} else {
|
||||
logger.info("google-services.json not found — Firebase plugin not applied")
|
||||
// Firebase is on for every build. Opt out only with -PfirebaseEnabled=false
|
||||
// (F-Droid / other builds that must not apply the google-services plugin).
|
||||
// The config file does not decide the switch: when Firebase is on, a missing
|
||||
// file fails the build; when it is off, a file on disk is ignored.
|
||||
def firebaseEnabledRaw = project.findProperty('firebaseEnabled')
|
||||
def firebaseEnabled = true
|
||||
if (firebaseEnabledRaw != null) {
|
||||
def normalized = firebaseEnabledRaw.toString().trim().toLowerCase()
|
||||
if (normalized == 'false' || normalized == '0' || normalized == 'no') {
|
||||
firebaseEnabled = false
|
||||
}
|
||||
} catch(Exception e) {
|
||||
logger.info("google-services plugin not applied: ${e.message}")
|
||||
}
|
||||
|
||||
if (!firebaseEnabled) {
|
||||
logger.lifecycle('Firebase disabled (-PfirebaseEnabled=false): google-services plugin not applied')
|
||||
} else {
|
||||
def servicesJSON = file('google-services.json')
|
||||
def servicesBody = servicesJSON.isFile() ? servicesJSON.getText('UTF-8') : ''
|
||||
if (!servicesBody.trim() || !servicesBody.contains('mobilesdk_app_id')) {
|
||||
throw new GradleException(
|
||||
'Firebase is enabled, but android/app/google-services.json is missing, empty, or has no mobilesdk_app_id.\n' +
|
||||
'Download it from the Firebase console (Project settings → Your apps → Android app)\n' +
|
||||
'and place it at android/app/google-services.json.\n' +
|
||||
'To build without Firebase (F-Droid / FOSS), pass -PfirebaseEnabled=false.'
|
||||
)
|
||||
}
|
||||
apply plugin: 'com.google.gms.google-services'
|
||||
logger.lifecycle('Firebase enabled: google-services plugin applied')
|
||||
}
|
||||
|
||||
@@ -311,23 +311,25 @@ public class TimeSafariNativeFetcher implements NativeNotificationContentFetcher
|
||||
return prefs.getString(KEY_LAST_ACKED_JWT_ID, null);
|
||||
}
|
||||
|
||||
/**
|
||||
* Store the newest JWT ID in the response (the highest ULID), so the next fetch
|
||||
* asks only for later changes. The server lists them newest first, so the last
|
||||
* item is the oldest.
|
||||
*/
|
||||
private void updateLastAckedJwtIdFromResponse(String responseBody) {
|
||||
try {
|
||||
JsonObject root = JsonParser.parseString(responseBody).getAsJsonObject();
|
||||
if (!root.has("data")) return;
|
||||
JsonArray dataArray = root.getAsJsonArray("data");
|
||||
if (dataArray == null || dataArray.size() == 0) return;
|
||||
JsonObject lastItem = dataArray.get(dataArray.size() - 1).getAsJsonObject();
|
||||
String jwtId = null;
|
||||
if (lastItem.has("jwtId")) {
|
||||
jwtId = lastItem.get("jwtId").getAsString();
|
||||
} else if (lastItem.has("plan")) {
|
||||
JsonObject plan = lastItem.getAsJsonObject("plan");
|
||||
if (plan.has("jwtId")) {
|
||||
jwtId = plan.get("jwtId").getAsString();
|
||||
for (int i = 0; i < dataArray.size(); i++) {
|
||||
String itemJwtId = extractJwtIdFromItem(dataArray.get(i).getAsJsonObject());
|
||||
if (itemJwtId != null && !itemJwtId.isEmpty() && (jwtId == null || itemJwtId.compareTo(jwtId) > 0)) {
|
||||
jwtId = itemJwtId;
|
||||
}
|
||||
}
|
||||
if (jwtId != null && !jwtId.isEmpty()) {
|
||||
if (jwtId != null) {
|
||||
prefs.edit().putString(KEY_LAST_ACKED_JWT_ID, jwtId).apply();
|
||||
}
|
||||
} catch (Exception e) {
|
||||
|
||||
+2
-2
@@ -15,8 +15,8 @@ public class NotificationInspectorPlugin extends Plugin {
|
||||
}
|
||||
|
||||
/**
|
||||
* Whether this build carries a Firebase config, i.e. was built with
|
||||
* -PfirebaseEnabled. Without it, PushNotifications.register() throws on the
|
||||
* Whether this build carries a Firebase config. Normal builds do.
|
||||
* -PfirebaseEnabled=false omits it. 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.
|
||||
|
||||
@@ -22,3 +22,8 @@ org.gradle.jvmargs=-Xmx1536m --add-opens=jdk.compiler/com.sun.tools.javac.api=AL
|
||||
# https://developer.android.com/topic/libraries/support-library/androidx-rn
|
||||
android.useAndroidX=true
|
||||
android.suppressUnsupportedCompileSdk=36
|
||||
|
||||
# Firebase/FCM is on for every Gradle build (Android Studio, ./gradlew, npm scripts).
|
||||
# Opt out with -PfirebaseEnabled=false. android/app/google-services.json does not
|
||||
# toggle this; a missing file fails the build while Firebase is enabled.
|
||||
firebaseEnabled=true
|
||||
|
||||
@@ -0,0 +1,167 @@
|
||||
# Plan: Web Push on the Web Build
|
||||
|
||||
## Goal
|
||||
|
||||
A user of the web build (browser tab or installed web app) can turn on the
|
||||
Daily Reminder and New Activity notifications and receive them, matching what
|
||||
native builds deliver.
|
||||
|
||||
Requirements:
|
||||
|
||||
1. A `DIRECT_NOTIFICATION` push shows its message as sent.
|
||||
2. A `DAILY_CHECK` push shows the count of new activity for the active
|
||||
identity, using the same Endorser query the native fetcher uses.
|
||||
3. The service worker never holds a long-lived credential: it holds only
|
||||
day-scoped JWTs, the same pool native builds hand to their background
|
||||
fetcher.
|
||||
4. The Notifications section of the Account view works on the web build.
|
||||
|
||||
## Can FCM replace Web Push?
|
||||
|
||||
No. Firebase Cloud Messaging on the web (the `firebase/messaging` SDK) is a
|
||||
layer over the browser Push API: `getToken()` requires a registered service
|
||||
worker (`firebase-messaging-sw.js` by default), and that worker must display the
|
||||
notification, the same as a plain Web Push worker. FCM changes only the
|
||||
sending side — the push server would call the FCM HTTP v1 API instead of
|
||||
sending VAPID-signed Web Push requests — and it adds a Google dependency and
|
||||
Firebase config to the web build. It removes none of the work below.
|
||||
|
||||
## Current state
|
||||
|
||||
- `public/sw.js` is registered by `src/main.web.ts` on the web platform. It
|
||||
handles only the Web Share Target POST; it has no `push` or
|
||||
`notificationclick` handler.
|
||||
- The Notifications section of the Account view renders only on native
|
||||
(`v-if="isNativePlatform"`, `src/views/AccountViewView.vue:81`), and the
|
||||
"Notification Push Server" setting is hidden with `v-if="false"`
|
||||
(`src/views/AccountViewView.vue:719`). Web users cannot subscribe.
|
||||
- `src/components/PushNotificationPermission.vue` contains the web
|
||||
subscription flow:
|
||||
- GET `{pushUrl}/web-push/vapid` for the VAPID key (`:242`);
|
||||
- `pushManager.subscribe({ userVisibleOnly: true, ... })` (`:538`);
|
||||
- POST of the subscription to the relative URL `/web-push/subscribe`
|
||||
(`:583`), so the web host must proxy that path to the push server;
|
||||
- `askPermission()` sends the worker the placeholder secret
|
||||
`"temporary-secret"` via `SEND_LOCAL_DATA`, which nothing reads.
|
||||
- The push server sends a JSON payload whose `title` is `DAILY_CHECK` (run the
|
||||
new-activity check) or `DIRECT_NOTIFICATION` (show `message` as-is).
|
||||
- Account data lives in SQLite through absurd-sql, which a service worker
|
||||
cannot open. The worker therefore cannot decrypt the identity and sign its
|
||||
own JWTs.
|
||||
- Native builds solve the same problem with `mintBackgroundJwtTokenPool()`
|
||||
(`src/libs/crypto/backgroundJwtPool.ts`): 100 JWTs, each valid for one UTC
|
||||
day, stored at index `epochDay % 100`, handed to the background fetcher by
|
||||
`configureNativeFetcherIfReady()`
|
||||
(`src/services/notifications/nativeFetcherConfig.ts`). Passkey (`did:peer`)
|
||||
identities cannot mint the pool and raise
|
||||
`BackgroundJwtUnsupportedIdentityError`.
|
||||
|
||||
## Work
|
||||
|
||||
### 1. Service worker push handling (`public/sw.js`)
|
||||
|
||||
- `push`: parse the JSON payload.
|
||||
- `DIRECT_NOTIFICATION`: `showNotification("Direct Message", { body: message })`.
|
||||
- `DAILY_CHECK` (and any other title): take the token for today's slot from the
|
||||
pool, GET `{apiServer}/api/v2/report/claims` with it, count claims newer
|
||||
than the stored `lastNotifiedClaimId`, store the newest ID, and show "There
|
||||
is 1 new activity" / "There are N new activities".
|
||||
- `notificationclick`: close the notification, focus an open app window or open
|
||||
`/`.
|
||||
- Worker state (token pool, `apiServer`, `activeDid`, `lastNotifiedClaimId`)
|
||||
lives in the worker's own IndexedDB database, never in the app's SQLite
|
||||
database.
|
||||
|
||||
### 2. Hand the token pool to the worker
|
||||
|
||||
- On app start, identity change, and pool expiry, mint the pool with
|
||||
`mintBackgroundJwtTokenPool()` and `postMessage` it to the worker together
|
||||
with `apiServer` and `activeDid`. Reuse the trigger points in
|
||||
`src/services/notifications/notificationAuthLifecycle.ts`
|
||||
(`onNotificationAuthMayBeReady`).
|
||||
- Delete the `SEND_LOCAL_DATA` / placeholder-secret path in
|
||||
`PushNotificationPermission.vue`.
|
||||
- Passkey identities: do not offer New Activity notifications (the Daily
|
||||
Reminder needs no token).
|
||||
|
||||
### 3. Empty daily checks
|
||||
|
||||
Chrome requires every push received with `userVisibleOnly: true` to show a
|
||||
notification. A `DAILY_CHECK` that finds nothing new must show one anyway, or
|
||||
Chrome eventually shows its own "This site has been updated in the background"
|
||||
notice. Choose one (see Open questions):
|
||||
|
||||
- show a quiet "No new activity" notification and close it after a short delay;
|
||||
- have the push server skip pushes when there is nothing new, which requires
|
||||
the server to query Endorser on the user's behalf.
|
||||
|
||||
### 4. Re-enable the UI on the web build
|
||||
|
||||
- Render the Notifications section on web when `"PushManager" in window` and a
|
||||
service worker is registered, instead of only on native.
|
||||
- Restore the "Notification Push Server" setting for web.
|
||||
- iOS Safari delivers Web Push only to web apps added to the Home Screen
|
||||
(iOS 16.4+). `public/manifest.webmanifest` is linked from `index.html` and
|
||||
makes the app installable; the UI must explain the Home Screen requirement on
|
||||
iOS.
|
||||
|
||||
### 5. Hosting
|
||||
|
||||
- Confirm each web host (test, production, Docker `docker/*.conf`) proxies
|
||||
`/web-push/subscribe` to the push server, or change the client to POST to
|
||||
`{pushUrl}/web-push/subscribe`.
|
||||
- Serve `sw.js` with `Cache-Control: no-cache` so worker updates reach users
|
||||
promptly.
|
||||
|
||||
### 6. Tests
|
||||
|
||||
- Playwright: register the worker, then deliver pushes with the Chrome DevTools
|
||||
Protocol `ServiceWorker.deliverPushMessage` and assert on the shown
|
||||
notification (`registration.getNotifications()`), for both payload types.
|
||||
- Unit-test the claim-counting function, extracted so it runs outside the
|
||||
worker.
|
||||
|
||||
## Estimate
|
||||
|
||||
Two to four days of work, plus push-server and nginx checks (low confidence:
|
||||
the push server's current behavior and routing are not visible from this
|
||||
repository).
|
||||
|
||||
## Open questions
|
||||
|
||||
- Empty daily checks: silent-notification workaround, or server-side filtering?
|
||||
- Does the push server serve `/web-push/vapid` and accept
|
||||
`/web-push/subscribe`, and does it send `DAILY_CHECK` daily?
|
||||
- Should New Activity on web use the same Endorser query as the native fetcher
|
||||
(starred-plan changes) rather than `/api/v2/report/claims`, so both platforms
|
||||
report the same thing?
|
||||
|
||||
## Rejected
|
||||
|
||||
- **FCM for web:** still needs a service worker; adds a Google dependency to the
|
||||
web build and a server-side switch to the FCM API. See "Can FCM replace Web
|
||||
Push?".
|
||||
- **Reviving the old worker scripts (`sw_scripts/safari-notifications.js`):**
|
||||
they read the Dexie `TimeSafari` IndexedDB database, which no longer holds
|
||||
accounts, and decrypted the identity inside the worker using a secret passed
|
||||
from the page, shipping NaCl and noble crypto libraries into the worker. The
|
||||
day-scoped token pool needs neither.
|
||||
|
||||
## History
|
||||
|
||||
- 2025-02-18 (`2746b8fe`): the Vite config reorganization switched the web build
|
||||
to a Workbox-generated worker, so the custom push, notification-click, and
|
||||
share-target handlers stopped shipping.
|
||||
- 2025-07-18 (`8bc1c521`): VitePWA and the generated worker were removed; the
|
||||
web manifest stopped being generated and linked.
|
||||
- The leftover `sw_scripts/` files and PWA config were deleted, and
|
||||
`public/sw.js` was added for the Web Share Target only.
|
||||
|
||||
## Checklist
|
||||
|
||||
- [ ] Decide the empty-daily-check behavior
|
||||
- [ ] Confirm push-server endpoints and routing per environment
|
||||
- [ ] `push` and `notificationclick` handlers in `public/sw.js`
|
||||
- [ ] Token-pool handoff to the worker; remove the placeholder secret
|
||||
- [ ] Re-enable the web Notifications UI, with the iOS Home Screen note
|
||||
- [ ] Playwright push-delivery tests
|
||||
@@ -104,9 +104,7 @@ Build Scripts:
|
||||
├── scripts/** # All build and automation scripts
|
||||
├── electron/** # Electron build files
|
||||
├── android/** # Android build configuration
|
||||
├── ios/** # iOS build configuration
|
||||
├── sw_scripts/** # Service worker scripts
|
||||
└── sw_combine.js # Service worker combination
|
||||
└── ios/** # iOS build configuration
|
||||
|
||||
Deployment:
|
||||
├── Dockerfile # Docker configuration
|
||||
|
||||
+20
-21
@@ -2,49 +2,47 @@
|
||||
|
||||
## Overview
|
||||
|
||||
The app is designed to work on Android devices with and without Google Play Services (GMS). Firebase/FCM push notifications are an opt-in feature at build time; all other functionality works on GMS-less devices (F-Droid, LineageOS without OpenGApps, etc.).
|
||||
The app is designed to work on Android devices with and without Google Play Services (GMS). Firebase/FCM is **on by default** for local, test, and production builds. GMS-less devices still run the app; FCM degrades when Play Services is absent. F-Droid and other builds that must omit Firebase opt out explicitly.
|
||||
|
||||
## How the opt-in guard works
|
||||
## How the switch works
|
||||
|
||||
`android/app/build.gradle` applies the `com.google.gms.google-services` Gradle plugin only when **both** conditions are true:
|
||||
`android/app/build.gradle` applies `com.google.gms.google-services` unless `firebaseEnabled` is `false`. `android/gradle.properties` sets `firebaseEnabled=true`, so Android Studio, `./gradlew`, and `scripts/build-android.sh` all pick it up. `-PfirebaseEnabled` and `-PfirebaseEnabled=true` stay on. The config file does not decide the switch.
|
||||
|
||||
1. `android/google-services.json` is present on disk
|
||||
2. The Gradle property `firebaseEnabled` is explicitly passed
|
||||
When Firebase is on, `android/app/google-services.json` must exist, be non-empty, and contain `mobilesdk_app_id`. Otherwise configuration fails with an error that names that path. When Firebase is off, the file is ignored even if it is on disk.
|
||||
|
||||
```groovy
|
||||
if (servicesJSON.exists() && servicesJSON.text && project.hasProperty('firebaseEnabled')) {
|
||||
apply plugin: 'com.google.gms.google-services'
|
||||
}
|
||||
```
|
||||
`scripts/build-android.sh` forwards an opt-out: `FIREBASE_ENABLED=false` adds `-PfirebaseEnabled=false` to its Gradle commands. Default npm scripts leave Firebase on. A `:nostore` script sets `FIREBASE_ENABLED=false` and reuses that script (`npm run build:android:test:nostore:run`).
|
||||
|
||||
This means the file can live on disk for development purposes without accidentally activating Firebase.
|
||||
iOS uses the same default. `ios/App/scripts/bundle-firebase-config.sh` copies `ios/App/App/GoogleService-Info.plist` into the app and the share extension. `scripts/build-ios.sh` runs it with `--check` before a device build, and both Xcode targets run it as a build phase. The build and archive `xcodebuild` commands both receive `FIREBASE_ENABLED`. `FIREBASE_ENABLED=false` becomes `FIREBASE_ENABLED=NO` and skips the plist. A `:nostore` script sets that variable (`npm run build:ios:prod:nostore`). A missing or empty plist fails the build when Firebase is on.
|
||||
|
||||
`:nostore` disables Firebase only. It does not strip other store-linked dependencies (the ML Kit barcode scanner still pulls in Play Services) and does not guarantee the build will run on every device outside the Play Store.
|
||||
|
||||
## Build commands
|
||||
|
||||
| Target | Command | Firebase |
|
||||
|---|---|---|
|
||||
| APK / sideload / Zapstore | `./gradlew assembleRelease` | off |
|
||||
| Aurora / Play Store without FCM | `./gradlew bundleRelease` | off |
|
||||
| Play Store with FCM push notifications | `./gradlew bundleRelease -PfirebaseEnabled` | on |
|
||||
| F-Droid | `./gradlew assembleRelease` | off (required) |
|
||||
| Local / test / production APK or AAB | `./gradlew assembleRelease` or `bundleRelease` | on |
|
||||
| Play Store with FCM | `./gradlew bundleRelease` | on |
|
||||
| iOS (Xcode or `scripts/build-ios.sh`) | default | on |
|
||||
| F-Droid / FOSS Android | `./gradlew assembleRelease -PfirebaseEnabled=false` or `npm run build:android:test:nostore` | off |
|
||||
| iOS without Firebase | `FIREBASE_ENABLED=false` or `npm run build:ios:prod:nostore` | off |
|
||||
|
||||
## Behavior on non-GMS devices
|
||||
|
||||
When built with `-PfirebaseEnabled`, Firebase SDKs check for GMS availability at startup and degrade gracefully if it is absent:
|
||||
When Firebase is enabled (the default), Firebase SDKs check for GMS availability at startup and degrade gracefully if it is absent:
|
||||
|
||||
- Firebase initializes but detects no GMS
|
||||
- FCM skips token registration silently (no token, no notifications)
|
||||
- The app continues to work normally
|
||||
|
||||
This means a single Play Store AAB (`bundleRelease -PfirebaseEnabled`) covers both GMS and non-GMS users. GMS users get push notifications; non-GMS users get a fully functional app without them.
|
||||
This means a single Play Store AAB (`bundleRelease`, Firebase on by default) covers both GMS and non-GMS users. GMS users get push notifications; non-GMS users get a fully functional app without them.
|
||||
|
||||
**Aurora Store** pulls the exact APK from Play Store servers, so Aurora users get whichever variant was uploaded. The Play Store AAB built with `-PfirebaseEnabled` is correct for Aurora.
|
||||
**Aurora Store** pulls the exact APK from Play Store servers, so Aurora users get whichever variant was uploaded. The default Play Store AAB (Firebase on) is the one to upload.
|
||||
|
||||
**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 -PfirebaseEnabled=false` build 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.
|
||||
A build with `-PfirebaseEnabled=false` 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.
|
||||
|
||||
@@ -56,7 +54,8 @@ Any code that calls `PushNotifications.register()` or other `FirebaseMessaging`
|
||||
|
||||
- Gitignored (`android/.gitignore` line 80) — never commit it
|
||||
- Contains Firebase project credentials (project number, app ID, API key)
|
||||
- Safe to leave on disk; has no effect unless `-PfirebaseEnabled` is passed
|
||||
- Required for the default (Firebase on) build. Place it at `android/app/google-services.json`
|
||||
- Ignored when you pass `-PfirebaseEnabled=false`; leaving it on disk does not re-enable Firebase
|
||||
- Obtain from the Firebase console: Project Settings → Your apps → Android app → Download `google-services.json`
|
||||
|
||||
## Known GMS dependency: MLKit barcode scanner
|
||||
|
||||
@@ -240,7 +240,7 @@ Complete this section once before your first physical-device push test. If Fireb
|
||||
|
||||
2. The repo gitignores this file — each developer keeps a local copy; never commit it.
|
||||
|
||||
3. Rebuild so Gradle applies the Google Services plugin (`android/app/build.gradle` applies `com.google.gms.google-services` when the file exists). If the file is missing, the build logs: *google-services.json not found, google-services plugin not applied. Push Notifications won't work*.
|
||||
3. Rebuild. Firebase is on by default, so Gradle applies the Google Services plugin. If `android/app/google-services.json` is missing or empty, the build stops and names that path. Opt out only with `-PfirebaseEnabled=false` or `FIREBASE_ENABLED=false`.
|
||||
|
||||
4. Sync Capacitor if you changed native config:
|
||||
|
||||
@@ -928,11 +928,11 @@ Compare with panel **Backend Status** and Event Log error text.
|
||||
|
||||
### Gradle: Push Notifications won't work
|
||||
|
||||
**Symptoms:** Build log: `google-services.json not found, google-services plugin not applied`.
|
||||
**Symptoms:** Build fails: `android/app/google-services.json is missing, empty, or has no mobilesdk_app_id`.
|
||||
|
||||
**Likely causes:** Missing `android/app/google-services.json`.
|
||||
|
||||
**Verification:** File exists on disk; rebuild shows Google Services plugin applied.
|
||||
**Verification:** File exists on disk and the build log says `Firebase enabled: google-services plugin applied`.
|
||||
|
||||
**Fixes:** Download from Firebase Console (package `app.timesafari.app`); place in `android/app/`; `npx cap sync android` and rebuild.
|
||||
|
||||
|
||||
@@ -175,7 +175,6 @@ export async function createBuildConfig(mode: string) {
|
||||
return defineConfig({
|
||||
define: {
|
||||
'process.env.VITE_PLATFORM': JSON.stringify(mode),
|
||||
// PWA is automatically enabled for web platforms via build configuration
|
||||
__IS_MOBILE__: JSON.stringify(isCapacitor),
|
||||
__USE_QR_READER__: JSON.stringify(!isCapacitor)
|
||||
},
|
||||
|
||||
+4
-5
@@ -5,10 +5,9 @@
|
||||
# IMPORTANT: Build web assets first using npm scripts before running docker-compose
|
||||
#
|
||||
# Usage:
|
||||
# Development: npm run build:web:build -- --mode development && docker-compose up dev
|
||||
# Test: npm run build:web:build -- --mode test && docker-compose up test
|
||||
# Production: npm run build:web:build -- --mode production && docker-compose up production
|
||||
# Custom: BUILD_MODE=test npm run build:web:build -- --mode test && docker-compose up custom
|
||||
# Test: npm run docker:up:test (runs npm run build:web:test && docker-compose up test)
|
||||
# Production: npm run docker:up:prod (runs npm run build:web:prod && docker-compose up production)
|
||||
# Custom: npm run build:web:test && BUILD_MODE=test docker-compose up custom
|
||||
#
|
||||
# Environment Variables:
|
||||
# BUILD_MODE: development, test, or production (default: production)
|
||||
@@ -16,7 +15,7 @@
|
||||
# PORT: port to expose (default: 80 for production, 8080 for test)
|
||||
# ENV_FILE: environment file to use (default: .env.production)
|
||||
#
|
||||
# Note: For development, use npm run build:web directly (no Docker needed)
|
||||
# Note: For development, use npm run dev directly (no Docker needed)
|
||||
|
||||
version: '3.8'
|
||||
|
||||
|
||||
+2
-5
@@ -103,7 +103,6 @@ The Dockerfile supports these build arguments:
|
||||
| `BUILD_MODE` | `production` | Build mode: development, staging, or production |
|
||||
| `NODE_ENV` | `production` | Node.js environment |
|
||||
| `VITE_PLATFORM` | `web` | Vite platform type |
|
||||
| PWA | `enabled` | Automatically enabled for web platforms |
|
||||
|
||||
### Environment Variables
|
||||
|
||||
@@ -114,7 +113,6 @@ Docker Compose supports these environment variables:
|
||||
| `BUILD_MODE` | `production` | Build mode |
|
||||
| `NODE_ENV` | `production` | Node environment |
|
||||
| `VITE_PLATFORM` | `web` | Vite platform |
|
||||
| PWA | `enabled` | Automatically enabled for web platforms |
|
||||
| `DEV_PORT` | `5173` | Development port |
|
||||
| `STAGING_PORT` | `8080` | Staging port |
|
||||
| `PROD_PORT` | `80` | Production port |
|
||||
@@ -219,13 +217,12 @@ BUILD_MODE=staging NODE_ENV=staging ./docker/run.sh custom
|
||||
docker build \
|
||||
--build-arg BUILD_MODE=staging \
|
||||
--build-arg NODE_ENV=staging \
|
||||
|
||||
-t timesafari:staging-no-pwa .
|
||||
-t timesafari:staging .
|
||||
|
||||
# Run with custom configuration
|
||||
docker run -d -p 9000:80 \
|
||||
-e VITE_APP_SERVER=https://test.example.com \
|
||||
timesafari:staging-no-pwa
|
||||
timesafari:staging
|
||||
```
|
||||
|
||||
### Docker Compose with Custom Variables
|
||||
|
||||
@@ -13,7 +13,6 @@
|
||||
# BUILD_MODE: development, staging, or production
|
||||
# NODE_ENV: node environment
|
||||
# VITE_PLATFORM: vite platform
|
||||
# PWA: automatically enabled for web platforms
|
||||
# PORT: port to expose
|
||||
# ENV_FILE: environment file to use
|
||||
|
||||
@@ -71,7 +70,6 @@ show_usage() {
|
||||
echo " BUILD_MODE: development, staging, or production"
|
||||
echo " NODE_ENV: node environment"
|
||||
echo " VITE_PLATFORM: vite platform"
|
||||
echo " PWA: automatically enabled for web platforms"
|
||||
echo " PORT: port to expose"
|
||||
echo " ENV_FILE: environment file to use"
|
||||
}
|
||||
@@ -86,7 +84,6 @@ show_build_args() {
|
||||
echo " BUILD_MODE: development"
|
||||
echo " NODE_ENV: development"
|
||||
echo " VITE_PLATFORM: web"
|
||||
echo " PWA: enabled (web platform)"
|
||||
echo " Target: development"
|
||||
echo " Port: 5173"
|
||||
;;
|
||||
@@ -94,7 +91,6 @@ show_build_args() {
|
||||
echo " BUILD_MODE: staging"
|
||||
echo " NODE_ENV: staging"
|
||||
echo " VITE_PLATFORM: web"
|
||||
echo " PWA: enabled (web platform)"
|
||||
echo " Target: staging"
|
||||
echo " Port: 80 (mapped to 8080)"
|
||||
;;
|
||||
@@ -102,7 +98,6 @@ show_build_args() {
|
||||
echo " BUILD_MODE: production"
|
||||
echo " NODE_ENV: production"
|
||||
echo " VITE_PLATFORM: web"
|
||||
echo " PWA: enabled (web platform)"
|
||||
echo " Target: production"
|
||||
echo " Port: 80"
|
||||
;;
|
||||
@@ -110,7 +105,6 @@ show_build_args() {
|
||||
echo " BUILD_MODE: \${BUILD_MODE:-production}"
|
||||
echo " NODE_ENV: \${NODE_ENV:-production}"
|
||||
echo " VITE_PLATFORM: \${VITE_PLATFORM:-web}"
|
||||
echo " PWA: enabled for web platforms"
|
||||
echo " Target: \${BUILD_TARGET:-production}"
|
||||
echo " Port: \${CUSTOM_PORT:-8080}:\${CUSTOM_INTERNAL_PORT:-80}"
|
||||
;;
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<link rel="icon" type="image/svg+xml" href="/favicon.ico" />
|
||||
<link rel="manifest" href="/manifest.webmanifest" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0, viewport-fit=cover, user-scalable=no, interactive-widget=overlays-content" />
|
||||
|
||||
<!-- CORS headers removed to allow images from any domain -->
|
||||
|
||||
@@ -19,8 +19,6 @@
|
||||
C86585DF2ED456DE00824752 /* TimeSafariShareExtension.appex in Embed Foundation Extensions */ = {isa = PBXBuildFile; fileRef = C86585D52ED456DE00824752 /* TimeSafariShareExtension.appex */; settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; };
|
||||
C8C56E142EE0474B00737D0E /* SharedImageUtility.swift in Sources */ = {isa = PBXBuildFile; fileRef = C8C56E132EE0474B00737D0E /* SharedImageUtility.swift */; };
|
||||
C8C56E162EE064CB00737D0E /* SharedImagePlugin.swift in Sources */ = {isa = PBXBuildFile; fileRef = C8C56E152EE064CA00737D0E /* SharedImagePlugin.swift */; };
|
||||
C8E73DD12FC6E5DC0057F59A /* GoogleService-Info.plist in Resources */ = {isa = PBXBuildFile; fileRef = C8E73DD02FC6E5DC0057F59A /* GoogleService-Info.plist */; };
|
||||
C8E73DD22FC6E5DC0057F59A /* GoogleService-Info.plist in Resources */ = {isa = PBXBuildFile; fileRef = C8E73DD02FC6E5DC0057F59A /* GoogleService-Info.plist */; };
|
||||
E9F1A0022EE05A8B00737D01 /* NotificationInspectorPlugin.swift in Sources */ = {isa = PBXBuildFile; fileRef = E9F1A0012EE05A8B00737D01 /* NotificationInspectorPlugin.swift */; };
|
||||
/* End PBXBuildFile section */
|
||||
|
||||
@@ -185,6 +183,7 @@
|
||||
504EC3001FED79650016851F /* Sources */,
|
||||
504EC3011FED79650016851F /* Frameworks */,
|
||||
504EC3021FED79650016851F /* Resources */,
|
||||
F1REBA5E000000000000A001 /* Bundle Firebase Config */,
|
||||
012076E8FFE4BF260A79B034 /* Fix Privacy Manifest */,
|
||||
96A7EF592DF3366D00084D51 /* Fix Privacy Manifest */,
|
||||
C86585E02ED456DE00824752 /* Embed Foundation Extensions */,
|
||||
@@ -207,6 +206,7 @@
|
||||
C86585D12ED456DE00824752 /* Sources */,
|
||||
C86585D22ED456DE00824752 /* Frameworks */,
|
||||
C86585D32ED456DE00824752 /* Resources */,
|
||||
F1REBA5E000000000000A002 /* Bundle Firebase Config */,
|
||||
);
|
||||
buildRules = (
|
||||
);
|
||||
@@ -270,7 +270,6 @@
|
||||
50379B232058CBB4000EE86E /* capacitor.config.json in Resources */,
|
||||
504EC30D1FED79650016851F /* Main.storyboard in Resources */,
|
||||
2FAD9763203C412B000D30F8 /* config.xml in Resources */,
|
||||
C8E73DD12FC6E5DC0057F59A /* GoogleService-Info.plist in Resources */,
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
@@ -278,7 +277,6 @@
|
||||
isa = PBXResourcesBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
C8E73DD22FC6E5DC0057F59A /* GoogleService-Info.plist in Resources */,
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
@@ -342,6 +340,48 @@
|
||||
shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n";
|
||||
showEnvVarsInLog = 0;
|
||||
};
|
||||
F1REBA5E000000000000A001 /* Bundle Firebase Config */ = {
|
||||
isa = PBXShellScriptBuildPhase;
|
||||
alwaysOutOfDate = 1;
|
||||
buildActionMask = 2147483647;
|
||||
enableUserScriptSandboxing = 0;
|
||||
files = (
|
||||
);
|
||||
inputFileListPaths = (
|
||||
);
|
||||
inputPaths = (
|
||||
);
|
||||
name = "Bundle Firebase Config";
|
||||
outputFileListPaths = (
|
||||
);
|
||||
outputPaths = (
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
shellPath = /bin/sh;
|
||||
shellScript = "\"${PROJECT_DIR}/scripts/bundle-firebase-config.sh\"\n";
|
||||
showEnvVarsInLog = 0;
|
||||
};
|
||||
F1REBA5E000000000000A002 /* Bundle Firebase Config */ = {
|
||||
isa = PBXShellScriptBuildPhase;
|
||||
alwaysOutOfDate = 1;
|
||||
buildActionMask = 2147483647;
|
||||
enableUserScriptSandboxing = 0;
|
||||
files = (
|
||||
);
|
||||
inputFileListPaths = (
|
||||
);
|
||||
inputPaths = (
|
||||
);
|
||||
name = "Bundle Firebase Config";
|
||||
outputFileListPaths = (
|
||||
);
|
||||
outputPaths = (
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
shellPath = /bin/sh;
|
||||
shellScript = "\"${PROJECT_DIR}/scripts/bundle-firebase-config.sh\"\n";
|
||||
showEnvVarsInLog = 0;
|
||||
};
|
||||
96A7EF592DF3366D00084D51 /* Fix Privacy Manifest */ = {
|
||||
isa = PBXShellScriptBuildPhase;
|
||||
alwaysOutOfDate = 1;
|
||||
@@ -545,6 +585,7 @@
|
||||
DEVELOPMENT_TEAM = GM3FS5JQPH;
|
||||
ENABLE_APP_SANDBOX = NO;
|
||||
ENABLE_USER_SCRIPT_SANDBOXING = NO;
|
||||
FIREBASE_ENABLED = YES;
|
||||
INFOPLIST_FILE = App/Info.plist;
|
||||
INFOPLIST_KEY_CFBundleDisplayName = Giftopia;
|
||||
IPHONEOS_DEPLOYMENT_TARGET = 15.5;
|
||||
@@ -575,6 +616,7 @@
|
||||
DEVELOPMENT_TEAM = GM3FS5JQPH;
|
||||
ENABLE_APP_SANDBOX = NO;
|
||||
ENABLE_USER_SCRIPT_SANDBOXING = NO;
|
||||
FIREBASE_ENABLED = YES;
|
||||
INFOPLIST_FILE = App/Info.plist;
|
||||
INFOPLIST_KEY_CFBundleDisplayName = Giftopia;
|
||||
IPHONEOS_DEPLOYMENT_TARGET = 15.5;
|
||||
@@ -602,6 +644,8 @@
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 75;
|
||||
DEVELOPMENT_TEAM = GM3FS5JQPH;
|
||||
ENABLE_USER_SCRIPT_SANDBOXING = NO;
|
||||
FIREBASE_ENABLED = YES;
|
||||
GCC_C_LANGUAGE_STANDARD = gnu17;
|
||||
GENERATE_INFOPLIST_FILE = YES;
|
||||
INFOPLIST_FILE = TimeSafariShareExtension/Info.plist;
|
||||
@@ -640,6 +684,8 @@
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 75;
|
||||
DEVELOPMENT_TEAM = GM3FS5JQPH;
|
||||
ENABLE_USER_SCRIPT_SANDBOXING = NO;
|
||||
FIREBASE_ENABLED = YES;
|
||||
GCC_C_LANGUAGE_STANDARD = gnu17;
|
||||
GENERATE_INFOPLIST_FILE = YES;
|
||||
INFOPLIST_FILE = TimeSafariShareExtension/Info.plist;
|
||||
|
||||
@@ -144,20 +144,16 @@ final class TimeSafariNativeFetcher: NativeNotificationContentFetcher {
|
||||
return (s?.isEmpty == false) ? s : nil
|
||||
}
|
||||
|
||||
/// Store the newest JWT ID in the response (the highest ULID), so the next fetch
|
||||
/// asks only for later changes. The server lists them newest first, so the last
|
||||
/// item is the oldest.
|
||||
private func updateLastAckedJwtIdFromResponse(responseBody: String) {
|
||||
guard let data = responseBody.data(using: .utf8),
|
||||
let root = try? JSONSerialization.jsonObject(with: data) as? [String: Any],
|
||||
let dataArray = root["data"] as? [[String: Any]], !dataArray.isEmpty
|
||||
else { return }
|
||||
|
||||
let lastItem = dataArray[dataArray.count - 1]
|
||||
var jwtId: String?
|
||||
if let j = lastItem["jwtId"] as? String {
|
||||
jwtId = j
|
||||
} else if let plan = lastItem["plan"] as? [String: Any], let j = plan["jwtId"] as? String {
|
||||
jwtId = j
|
||||
}
|
||||
if let jwtId = jwtId, !jwtId.isEmpty {
|
||||
if let jwtId = dataArray.compactMap({ extractJwtIdFromItem($0) }).max() {
|
||||
UserDefaults.standard.set(jwtId, forKey: prefsLastAckedKey)
|
||||
}
|
||||
}
|
||||
|
||||
Executable
+80
@@ -0,0 +1,80 @@
|
||||
#!/bin/sh
|
||||
# Bundle GoogleService-Info.plist when Firebase is enabled (the default).
|
||||
# Opt out with FIREBASE_ENABLED=false, or the Xcode build setting
|
||||
# FIREBASE_ENABLED=NO. The plist does not decide the switch: a missing file
|
||||
# fails an enabled build, and an opt-out build does not bundle the plist
|
||||
# even when the file is on disk.
|
||||
#
|
||||
# Xcode run-script phases invoke this with no arguments. build-ios.sh may pass
|
||||
# --check to validate before a long build without copying into a product.
|
||||
set -eu
|
||||
|
||||
check_only=0
|
||||
if [ "${1:-}" = "--check" ]; then
|
||||
check_only=1
|
||||
fi
|
||||
|
||||
if [ -z "${SRCROOT:-}" ]; then
|
||||
script_dir=$(CDPATH= cd -- "$(dirname "$0")" && pwd)
|
||||
SRCROOT=$(CDPATH= cd -- "$script_dir/.." && pwd)
|
||||
fi
|
||||
|
||||
normalized=$(printf '%s' "${FIREBASE_ENABLED:-true}" | tr '[:upper:]' '[:lower:]')
|
||||
plist="${SRCROOT}/App/GoogleService-Info.plist"
|
||||
|
||||
firebase_off=0
|
||||
case "$normalized" in
|
||||
false|0|no) firebase_off=1 ;;
|
||||
esac
|
||||
|
||||
remove_bundled_plist() {
|
||||
base="$1"
|
||||
leaf="$2"
|
||||
if [ -n "$base" ] && [ -n "$leaf" ]; then
|
||||
rm -f "${base}/${leaf}/GoogleService-Info.plist"
|
||||
fi
|
||||
}
|
||||
|
||||
if [ "$firebase_off" -eq 1 ]; then
|
||||
if [ "$check_only" -eq 0 ]; then
|
||||
# Drop the plist from this product, including a copy left by an earlier
|
||||
# Firebase-enabled build. The app and the share extension each run this phase.
|
||||
remove_bundled_plist "${TARGET_BUILD_DIR:-}" "${UNLOCALIZED_RESOURCES_FOLDER_PATH:-}"
|
||||
remove_bundled_plist "${TARGET_BUILD_DIR:-}" "${WRAPPER_NAME:-}"
|
||||
remove_bundled_plist "${BUILT_PRODUCTS_DIR:-}" "${WRAPPER_NAME:-}"
|
||||
# Embed does not delete files that disappeared from the appex, so a plist
|
||||
# copied into PlugIns on an earlier Firebase-enabled build would otherwise stay.
|
||||
if [ -n "${TARGET_BUILD_DIR:-}" ] && [ -n "${WRAPPER_NAME:-}" ]; then
|
||||
for stale in "${TARGET_BUILD_DIR}/${WRAPPER_NAME}/PlugIns/"*.appex/GoogleService-Info.plist; do
|
||||
if [ -f "$stale" ]; then
|
||||
rm -f "$stale"
|
||||
fi
|
||||
done
|
||||
fi
|
||||
fi
|
||||
echo "note: Firebase disabled (FIREBASE_ENABLED=${FIREBASE_ENABLED}); GoogleService-Info.plist will not be bundled."
|
||||
exit 0
|
||||
fi
|
||||
|
||||
if [ ! -s "$plist" ] || ! grep -q "GOOGLE_APP_ID" "$plist"; then
|
||||
echo "error: Firebase is enabled, but ios/App/App/GoogleService-Info.plist is missing, empty, or has no GOOGLE_APP_ID." >&2
|
||||
echo "error: Download it from the Firebase console (Project settings → Your apps → Apple apps)" >&2
|
||||
echo "error: and place it at ios/App/App/GoogleService-Info.plist." >&2
|
||||
echo "error: To build without Firebase, set FIREBASE_ENABLED=false or pass the Xcode build setting FIREBASE_ENABLED=NO." >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [ "$check_only" -eq 1 ]; then
|
||||
echo "note: Firebase enabled; ios/App/App/GoogleService-Info.plist is present."
|
||||
exit 0
|
||||
fi
|
||||
|
||||
if [ -z "${TARGET_BUILD_DIR:-}" ] || [ -z "${UNLOCALIZED_RESOURCES_FOLDER_PATH:-}" ]; then
|
||||
echo "error: Bundle Firebase Config must run as an Xcode build phase (TARGET_BUILD_DIR is unset)." >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
dest_dir="${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}"
|
||||
mkdir -p "$dest_dir"
|
||||
cp "$plist" "${dest_dir}/GoogleService-Info.plist"
|
||||
echo "note: Firebase enabled; bundled GoogleService-Info.plist."
|
||||
Generated
-5
@@ -87,7 +87,6 @@
|
||||
"ramda": "^0.29.1",
|
||||
"readable-stream": "^4.5.2",
|
||||
"reflect-metadata": "^0.1.14",
|
||||
"register-service-worker": "^1.7.2",
|
||||
"simple-vue-camera": "^1.1.3",
|
||||
"sqlite3": "^5.1.7",
|
||||
"stream-browserify": "^3.0.0",
|
||||
@@ -26317,10 +26316,6 @@
|
||||
"node": ">=4"
|
||||
}
|
||||
},
|
||||
"node_modules/register-service-worker": {
|
||||
"version": "1.7.2",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/registry-auth-token": {
|
||||
"version": "3.3.2",
|
||||
"dev": true,
|
||||
|
||||
+15
-7
@@ -26,8 +26,11 @@
|
||||
"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:nostore": "FIREBASE_ENABLED=false npm run build:android:dev",
|
||||
"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:nostore": "FIREBASE_ENABLED=false npm run build:android:prod",
|
||||
"build:android:prod:nostore:run": "FIREBASE_ENABLED=false npm run build:android:prod:run",
|
||||
"build:android:prod:run": "./scripts/build-android.sh --prod --auto-run",
|
||||
"build:android:release": "./scripts/build-android.sh --release",
|
||||
"build:android:release:run": "./scripts/build-android.sh --release --auto-run",
|
||||
@@ -35,6 +38,8 @@
|
||||
"build:android:sync": "./scripts/build-android.sh --sync",
|
||||
"build:android:test": "./scripts/build-android.sh --test",
|
||||
"build:android:test:custom": "./scripts/build-android.sh --test --api-ip",
|
||||
"build:android:test:nostore": "FIREBASE_ENABLED=false npm run build:android:test",
|
||||
"build:android:test:nostore:run": "FIREBASE_ENABLED=false npm run build:android:test: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:capacitor": "VITE_GIT_HASH=`git log -1 --pretty=format:%h` vite build --mode capacitor --config vite.config.capacitor.mts",
|
||||
@@ -78,9 +83,12 @@
|
||||
"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:nostore": "FIREBASE_ENABLED=false npm run build:ios:dev",
|
||||
"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:nostore": "FIREBASE_ENABLED=false npm run build:ios:prod",
|
||||
"build:ios:prod:nostore:run": "FIREBASE_ENABLED=false npm run build:ios:prod:run",
|
||||
"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",
|
||||
@@ -88,11 +96,12 @@
|
||||
"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:nostore": "FIREBASE_ENABLED=false npm run build:ios:test",
|
||||
"build:ios:test:nostore:run": "FIREBASE_ENABLED=false npm run build:ios:test:run",
|
||||
"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:native": "npm run build:capacitor:sync && bash -c 'source scripts/common.sh && ensure_ios_capacitor_asset_directories' && npx capacitor-assets generate --ios --android --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",
|
||||
@@ -110,12 +119,12 @@
|
||||
"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",
|
||||
"dev": "npm run build:web:dev",
|
||||
"dev": "./scripts/build-web.sh --dev",
|
||||
"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",
|
||||
"docker:up:prod": "npm run build:web:prod && docker-compose up production",
|
||||
"docker:up:test": "npm run build:web: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",
|
||||
@@ -124,7 +133,7 @@
|
||||
"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",
|
||||
"prebuild": "eslint --ext .js,.ts,.vue --ignore-path .gitignore src && 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",
|
||||
@@ -220,7 +229,6 @@
|
||||
"ramda": "^0.29.1",
|
||||
"readable-stream": "^4.5.2",
|
||||
"reflect-metadata": "^0.1.14",
|
||||
"register-service-worker": "^1.7.2",
|
||||
"simple-vue-camera": "^1.1.3",
|
||||
"sqlite3": "^5.1.7",
|
||||
"stream-browserify": "^3.0.0",
|
||||
|
||||
@@ -106,7 +106,7 @@ export default defineConfig({
|
||||
*/
|
||||
webServer: {
|
||||
command:
|
||||
"VITE_APP_SERVER=http://localhost:8080 VITE_DEFAULT_ENDORSER_API_SERVER=http://localhost:3000 VITE_DEFAULT_PARTNER_API_SERVER=http://localhost:3000 VITE_DEFAULT_IMAGE_API_SERVER=https://test-image-api.timesafari.app VITE_PASSKEYS_ENABLED=true npm run build:web -- --port=8080",
|
||||
"VITE_APP_SERVER=http://localhost:8080 VITE_DEFAULT_ENDORSER_API_SERVER=http://localhost:3000 VITE_DEFAULT_PARTNER_API_SERVER=http://localhost:3000 VITE_DEFAULT_IMAGE_API_SERVER=https://test-image-api.timesafari.app VITE_PASSKEYS_ENABLED=true npm run dev",
|
||||
url: "http://localhost:8080",
|
||||
reuseExistingServer: !process.env.CI,
|
||||
},
|
||||
|
||||
+37
-37
@@ -1,46 +1,46 @@
|
||||
{
|
||||
"name": "Giftopia",
|
||||
"short_name": "Giftopia",
|
||||
"start_url": "/",
|
||||
"scope": "/",
|
||||
"display": "standalone",
|
||||
"theme_color": "#4a90e2",
|
||||
"background_color": "#ffffff",
|
||||
"icons": [
|
||||
{
|
||||
"src": "../icons/icon-48.webp",
|
||||
"type": "image/png",
|
||||
"sizes": "48x48",
|
||||
"purpose": "any maskable"
|
||||
},
|
||||
{
|
||||
"src": "../icons/icon-72.webp",
|
||||
"type": "image/png",
|
||||
"sizes": "72x72",
|
||||
"purpose": "any maskable"
|
||||
},
|
||||
{
|
||||
"src": "../icons/icon-96.webp",
|
||||
"type": "image/png",
|
||||
"sizes": "96x96",
|
||||
"purpose": "any maskable"
|
||||
},
|
||||
{
|
||||
"src": "../icons/icon-128.webp",
|
||||
"type": "image/png",
|
||||
"sizes": "128x128",
|
||||
"purpose": "any maskable"
|
||||
},
|
||||
{
|
||||
"src": "../icons/icon-192.webp",
|
||||
"type": "image/png",
|
||||
"src": "/img/icons/android-chrome-192x192.png",
|
||||
"sizes": "192x192",
|
||||
"purpose": "any maskable"
|
||||
"type": "image/png"
|
||||
},
|
||||
{
|
||||
"src": "../icons/icon-256.webp",
|
||||
"type": "image/png",
|
||||
"sizes": "256x256",
|
||||
"purpose": "any maskable"
|
||||
},
|
||||
{
|
||||
"src": "../icons/icon-512.webp",
|
||||
"type": "image/png",
|
||||
"src": "/img/icons/android-chrome-512x512.png",
|
||||
"sizes": "512x512",
|
||||
"purpose": "any maskable"
|
||||
"type": "image/png"
|
||||
},
|
||||
{
|
||||
"src": "/img/icons/android-chrome-maskable-192x192.png",
|
||||
"sizes": "192x192",
|
||||
"type": "image/png",
|
||||
"purpose": "maskable"
|
||||
},
|
||||
{
|
||||
"src": "/img/icons/android-chrome-maskable-512x512.png",
|
||||
"sizes": "512x512",
|
||||
"type": "image/png",
|
||||
"purpose": "maskable"
|
||||
}
|
||||
]
|
||||
],
|
||||
"share_target": {
|
||||
"action": "/share-target",
|
||||
"method": "POST",
|
||||
"enctype": "multipart/form-data",
|
||||
"params": {
|
||||
"files": [
|
||||
{
|
||||
"name": "photo",
|
||||
"accept": ["image/*"]
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,68 @@
|
||||
/* eslint-env serviceworker */
|
||||
/**
|
||||
* Service worker for the web build.
|
||||
*
|
||||
* Its only job is the Web Share Target: the manifest's share_target POSTs a
|
||||
* photo to /share-target, which no static server can accept. This worker
|
||||
* stashes the photo in Cache Storage and redirects to /shared-photo, where
|
||||
* src/libs/webShareTarget.ts picks it up. Every other request goes straight to
|
||||
* the network (no fetch handler response, no precaching).
|
||||
*
|
||||
* It also replaces the Workbox worker that older builds registered at this
|
||||
* same URL, and deletes that worker's precaches, which otherwise keep serving
|
||||
* a stale index.html that points at JavaScript files no longer deployed.
|
||||
*/
|
||||
|
||||
const SHARE_TARGET_CACHE = "share-target";
|
||||
// Keep in sync with src/libs/webShareTarget.ts
|
||||
const SHARED_PHOTO_KEY = "/share-target/photo";
|
||||
const FILE_NAME_HEADER = "X-File-Name";
|
||||
|
||||
self.addEventListener("install", () => {
|
||||
self.skipWaiting();
|
||||
});
|
||||
|
||||
self.addEventListener("activate", (event) => {
|
||||
event.waitUntil(
|
||||
(async () => {
|
||||
const names = await caches.keys();
|
||||
await Promise.all(
|
||||
names
|
||||
.filter((name) => name !== SHARE_TARGET_CACHE)
|
||||
.map((name) => caches.delete(name)),
|
||||
);
|
||||
await self.clients.claim();
|
||||
})(),
|
||||
);
|
||||
});
|
||||
|
||||
self.addEventListener("fetch", (event) => {
|
||||
const url = new URL(event.request.url);
|
||||
if (
|
||||
event.request.method !== "POST" ||
|
||||
url.origin !== self.location.origin ||
|
||||
url.pathname !== "/share-target"
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
event.respondWith(
|
||||
(async () => {
|
||||
const formData = await event.request.formData();
|
||||
const photo = formData.get("photo");
|
||||
if (photo instanceof File) {
|
||||
const cache = await caches.open(SHARE_TARGET_CACHE);
|
||||
await cache.put(
|
||||
SHARED_PHOTO_KEY,
|
||||
new Response(photo, {
|
||||
headers: {
|
||||
"Content-Type": photo.type || "application/octet-stream",
|
||||
[FILE_NAME_HEADER]: encodeURIComponent(photo.name || ""),
|
||||
},
|
||||
}),
|
||||
);
|
||||
}
|
||||
return Response.redirect("/shared-photo?source=share-target", 303);
|
||||
})(),
|
||||
);
|
||||
});
|
||||
@@ -15,7 +15,7 @@
|
||||
|
||||
## Commands Run (paste exact logs/snips)
|
||||
|
||||
- [ ] Web: `npm run build:web` / `:prod`
|
||||
- [ ] Web: `npm run dev` / `npm run build:web`
|
||||
- [ ] Electron: `npm run build:electron:dev` / package step
|
||||
- [ ] Mobile: `npm run build:android:test` / iOS equivalent
|
||||
- [ ] Clean/auto-run impacted scripts
|
||||
|
||||
+4
-6
@@ -23,10 +23,10 @@ All scripts automatically handle environment variables for different build types
|
||||
|
||||
#### Build Types and Environment Variables
|
||||
|
||||
| Platform | Mode | PWA Enabled | Native Features | Build Script |
|
||||
|----------|------|-------------|-----------------|--------------|
|
||||
| `web` | web | true | false | `build-web.sh` |
|
||||
| `capacitor` | capacitor | false | true | `build-capacitor.sh` |
|
||||
| Platform | Mode | Native Features | Build Script |
|
||||
|----------|------|-----------------|--------------|
|
||||
| `web` | web | false | `build-web.sh` |
|
||||
| `capacitor` | capacitor | true | `build-capacitor.sh` |
|
||||
|
||||
#### Automatic Environment Setup
|
||||
|
||||
@@ -183,8 +183,6 @@ All scripts automatically configure the correct environment variables for their
|
||||
```bash
|
||||
# Capacitor builds automatically get:
|
||||
export VITE_PLATFORM=capacitor
|
||||
export VITE_PWA_ENABLED=false
|
||||
export VITE_DISABLE_PWA=true
|
||||
export DEBUG_MIGRATIONS=0
|
||||
export VITE_GIT_HASH=<git-hash>
|
||||
|
||||
|
||||
@@ -23,6 +23,7 @@
|
||||
# --assets Generate assets only
|
||||
# --deploy Deploy APK to connected device
|
||||
# --uninstall Uninstall app from connected device
|
||||
# FIREBASE_ENABLED=false Opt out of Firebase (-PfirebaseEnabled=false)
|
||||
# -h, --help Show this help message
|
||||
# -v, --verbose Enable verbose logging
|
||||
#
|
||||
@@ -439,6 +440,9 @@ print_android_usage() {
|
||||
echo " --uninstall Uninstall app from connected device"
|
||||
echo " --api-ip <ip> Custom IP address for claim API (defaults to 10.0.2.2)"
|
||||
echo ""
|
||||
echo "Firebase is on by default. Opt out with FIREBASE_ENABLED=false"
|
||||
echo "(passes -PfirebaseEnabled=false to Gradle)."
|
||||
echo ""
|
||||
echo "Common Options:"
|
||||
echo " -h, --help Show this help message"
|
||||
echo " -v, --verbose Enable verbose logging"
|
||||
@@ -460,6 +464,17 @@ print_android_usage() {
|
||||
# Parse command line arguments
|
||||
parse_android_args "$@"
|
||||
|
||||
# Firebase is on unless FIREBASE_ENABLED is false, 0, or no. Gradle also
|
||||
# defaults firebaseEnabled=true; this only forwards an explicit opt-out.
|
||||
gradle_firebase_arg=""
|
||||
firebase_enabled_normalized=$(printf '%s' "${FIREBASE_ENABLED:-true}" | tr '[:upper:]' '[:lower:]')
|
||||
case "$firebase_enabled_normalized" in
|
||||
false|0|no)
|
||||
gradle_firebase_arg="-PfirebaseEnabled=false"
|
||||
log_info "Firebase disabled for this Android build (FIREBASE_ENABLED=${FIREBASE_ENABLED})"
|
||||
;;
|
||||
esac
|
||||
|
||||
# Print build header
|
||||
print_header "TimeSafari Android Build Process"
|
||||
|
||||
@@ -528,7 +543,7 @@ if [ "$CLEAN_ONLY" = true ]; then
|
||||
log_info "Clean-only mode: cleaning build artifacts"
|
||||
safe_execute "Cleaning Android app" "npm run clean:android" || exit 1
|
||||
safe_execute "Cleaning dist directory" "clean_build_artifacts dist" || exit 1
|
||||
safe_execute "Cleaning Gradle build" "cd android && ./gradlew clean && cd .." || exit 4
|
||||
safe_execute "Cleaning Gradle build" "cd android && ./gradlew clean ${gradle_firebase_arg} && cd .." || exit 4
|
||||
log_success "Clean completed successfully!"
|
||||
exit 0
|
||||
fi
|
||||
@@ -561,7 +576,7 @@ if [ "$DEPLOY_APP" = true ]; then
|
||||
fi
|
||||
|
||||
# Build APK
|
||||
safe_execute "Building APK" "cd android && ./gradlew assembleDebug && cd .." || exit 5
|
||||
safe_execute "Building APK" "cd android && ./gradlew assembleDebug ${gradle_firebase_arg} && cd .." || exit 5
|
||||
|
||||
# Install APK on device
|
||||
safe_execute "Installing APK on device" "adb install -r android/app/build/outputs/apk/debug/app-debug.apk" || exit 6
|
||||
@@ -628,26 +643,26 @@ fi
|
||||
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 ${gradle_firebase_arg} && cd .." || exit 4
|
||||
|
||||
# Step 9: Build based on type
|
||||
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 ${gradle_firebase_arg} && cd .." || exit 5
|
||||
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 ${gradle_firebase_arg} && cd .." || exit 5
|
||||
fi
|
||||
|
||||
# Step 10: Build APK/AAB if requested
|
||||
if [ "$BUILD_APK" = true ]; then
|
||||
if [ "$BUILD_TYPE" = "debug" ]; then
|
||||
safe_execute "Building debug APK" "cd android && ./gradlew assembleDebug && cd .." || exit 5
|
||||
safe_execute "Building debug APK" "cd android && ./gradlew assembleDebug ${gradle_firebase_arg} && cd .." || exit 5
|
||||
else
|
||||
safe_execute "Building release APK" "cd android && ./gradlew assembleRelease && cd .." || exit 5
|
||||
safe_execute "Building release APK" "cd android && ./gradlew assembleRelease ${gradle_firebase_arg} && cd .." || exit 5
|
||||
fi
|
||||
fi
|
||||
|
||||
if [ "$BUILD_AAB" = true ]; then
|
||||
safe_execute "Building AAB" "cd android && ./gradlew bundleRelease && cd .." || exit 5
|
||||
safe_execute "Building AAB" "cd android && ./gradlew bundleRelease ${gradle_firebase_arg} && 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
|
||||
|
||||
@@ -23,8 +23,6 @@ SENSITIVE=(
|
||||
"electron/**"
|
||||
"android/**"
|
||||
"ios/**"
|
||||
"sw_scripts/**"
|
||||
"sw_combine.js"
|
||||
"Dockerfile"
|
||||
"docker/**"
|
||||
"capacitor.config.ts"
|
||||
|
||||
@@ -44,6 +44,9 @@ print_ios_usage() {
|
||||
echo " --auto-run Auto-run app after build"
|
||||
echo " --api-ip <ip> Custom IP address for claim API (uses Capacitor default)"
|
||||
echo ""
|
||||
echo "Firebase is on by default. Opt out with FIREBASE_ENABLED=false"
|
||||
echo "(forwarded to xcodebuild as FIREBASE_ENABLED=NO)."
|
||||
echo ""
|
||||
echo "Common Options:"
|
||||
echo " -h, --help Show this help message"
|
||||
echo " -v, --verbose Enable verbose logging"
|
||||
@@ -321,6 +324,17 @@ clean_ios_build() {
|
||||
log_success "iOS build cleaned"
|
||||
}
|
||||
|
||||
# Map FIREBASE_ENABLED onto the Xcode build setting the run-script phases read.
|
||||
# Unset stays YES. false, 0, and no (any case) become NO.
|
||||
ios_firebase_build_setting() {
|
||||
local normalized
|
||||
normalized=$(printf '%s' "${FIREBASE_ENABLED:-YES}" | tr '[:upper:]' '[:lower:]')
|
||||
case "$normalized" in
|
||||
false|0|no) printf 'NO' ;;
|
||||
*) printf 'YES' ;;
|
||||
esac
|
||||
}
|
||||
|
||||
# Function to build iOS app
|
||||
build_ios_app() {
|
||||
local build_config=""
|
||||
@@ -358,6 +372,7 @@ build_ios_app() {
|
||||
-configuration "$build_config" \
|
||||
-destination "$destination" \
|
||||
build \
|
||||
"FIREBASE_ENABLED=$(ios_firebase_build_setting)" \
|
||||
CODE_SIGN_IDENTITY="" \
|
||||
CODE_SIGNING_REQUIRED=NO \
|
||||
CODE_SIGNING_ALLOWED=NO \
|
||||
@@ -537,6 +552,13 @@ if [ "$ASSETS_ONLY" = true ]; then
|
||||
exit 0
|
||||
fi
|
||||
|
||||
# Firebase is on unless FIREBASE_ENABLED=false. Fail before the web build and
|
||||
# xcodebuild when the plist is missing. Xcode runs the same script as a build
|
||||
# phase so GUI builds get the same check and bundle the plist.
|
||||
if ! ios/App/scripts/bundle-firebase-config.sh --check; then
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Handle deploy-app mode
|
||||
if [ "$DEPLOY_APP" = true ]; then
|
||||
deploy_ios_app
|
||||
@@ -701,6 +723,7 @@ if [ "$BUILD_IPA" = true ]; then
|
||||
-configuration Release \
|
||||
-archivePath build/App.xcarchive \
|
||||
archive \
|
||||
"FIREBASE_ENABLED=$(ios_firebase_build_setting)" \
|
||||
CODE_SIGN_IDENTITY="" \
|
||||
CODE_SIGNING_REQUIRED=NO \
|
||||
CODE_SIGNING_ALLOWED=NO \
|
||||
|
||||
+23
-16
@@ -5,25 +5,31 @@
|
||||
# This script handles the complete web build process including cleanup,
|
||||
# environment setup, Vite build, and optional Docker containerization.
|
||||
#
|
||||
# Every invocation except --dev writes dist/. --dev alone starts the Vite dev
|
||||
# server and writes nothing.
|
||||
#
|
||||
# Usage:
|
||||
# ./scripts/build-web.sh # Development build
|
||||
# ./scripts/build-web.sh --dev # Development build (explicit)
|
||||
# ./scripts/build-web.sh # Production build into dist/
|
||||
# ./scripts/build-web.sh --prod # Production build (explicit)
|
||||
# ./scripts/build-web.sh --test # Test environment build
|
||||
# ./scripts/build-web.sh --prod # Production environment build
|
||||
# ./scripts/build-web.sh --docker # Build with Docker containerization
|
||||
# ./scripts/build-web.sh --dev # Start Vite dev server (no dist/)
|
||||
# ./scripts/build-web.sh --docker # Production build + Docker image
|
||||
# ./scripts/build-web.sh --docker:test # Test environment + Docker
|
||||
# ./scripts/build-web.sh --docker:prod # Production environment + Docker
|
||||
# ./scripts/build-web.sh --serve # Build and serve locally
|
||||
# ./scripts/build-web.sh --serve # Production build, then serve dist/
|
||||
# ./scripts/build-web.sh --help # Show help
|
||||
# ./scripts/build-web.sh --verbose # Enable verbose logging
|
||||
#
|
||||
# NPM Script Equivalents:
|
||||
# npm run build:web # Development build
|
||||
# npm run dev # Vite dev server
|
||||
# npm run build:web # Production build
|
||||
# npm run build:web:test # Test environment build
|
||||
# npm run build:web:prod # Production environment build
|
||||
# npm run build:web:docker # Docker build
|
||||
# npm run build:web:docker # Production Docker build
|
||||
# npm run build:web:docker:test # Test Docker build
|
||||
# npm run build:web:docker:prod # Production Docker build
|
||||
# npm run build:web:serve # Production build, then serve
|
||||
# npm run build:web:serve:test # Test build, then serve
|
||||
#
|
||||
# Exit Codes:
|
||||
# 1 - Web cleanup failed
|
||||
@@ -40,7 +46,7 @@ set -e
|
||||
source "$(dirname "$0")/common.sh"
|
||||
|
||||
# Default values
|
||||
BUILD_MODE="development"
|
||||
BUILD_MODE="production"
|
||||
BUILD_ACTION="build"
|
||||
DOCKER_BUILD=false
|
||||
SERVE_BUILD=false
|
||||
@@ -51,9 +57,10 @@ show_usage() {
|
||||
Usage: $0 [OPTIONS]
|
||||
|
||||
OPTIONS:
|
||||
--dev, --development Build for development environment (default)
|
||||
--prod, --production Build for production environment (default)
|
||||
--test Build for test environment
|
||||
--prod, --production Build for production environment
|
||||
--dev, --development Start the Vite dev server (with --serve or
|
||||
--docker: build for development instead)
|
||||
--docker Build and create Docker container
|
||||
--docker:test Build for test environment and create Docker container
|
||||
--docker:prod Build for production environment and create Docker container
|
||||
@@ -63,18 +70,18 @@ OPTIONS:
|
||||
--env Show environment variables
|
||||
|
||||
EXAMPLES:
|
||||
$0 # Development build
|
||||
$0 # Production build
|
||||
$0 --test # Test environment build
|
||||
$0 --prod # Production environment build
|
||||
$0 --docker # Development + Docker
|
||||
$0 --dev # Development server
|
||||
$0 --docker # Production + Docker
|
||||
$0 --docker:test # Test + Docker
|
||||
$0 --docker:prod # Production + Docker
|
||||
$0 --serve # Build and serve
|
||||
$0 --serve # Production build, then serve
|
||||
|
||||
BUILD MODES:
|
||||
development: Starts Vite development server with hot reload (default)
|
||||
production: Optimized for production with full minification and optimization (default)
|
||||
test: Optimized for testing with minimal minification
|
||||
production: Optimized for production with full minification and optimization
|
||||
development: Starts Vite development server with hot reload
|
||||
|
||||
EOF
|
||||
}
|
||||
|
||||
@@ -161,4 +161,4 @@ echo "✅ Database clearing instructions completed!"
|
||||
echo ""
|
||||
echo "💡 Tip: After clearing the database, restart your development server:"
|
||||
echo " npm run build:electron:dev # For Electron"
|
||||
echo " npm run build:web:dev # For Web"
|
||||
echo " npm run dev # For Web"
|
||||
@@ -283,7 +283,6 @@ const runDeeplinkTests = async (log) => {
|
||||
const buildWebAssets = async (log) => {
|
||||
log('🌐 Building web assets...');
|
||||
execSync('rm -rf dist', { stdio: 'inherit' });
|
||||
execSync('npm run build:web', { stdio: 'inherit' });
|
||||
execSync('npm run build:capacitor', { stdio: 'inherit' });
|
||||
log('✅ Web assets built successfully');
|
||||
};
|
||||
|
||||
@@ -147,7 +147,6 @@ export default config;
|
||||
log('🔄 Building web assets before adding iOS platform...');
|
||||
try {
|
||||
execSync('rm -rf dist', { stdio: 'inherit' });
|
||||
execSync('npm run build:web', { stdio: 'inherit' });
|
||||
execSync('npm run build:capacitor', { stdio: 'inherit' });
|
||||
log('✅ Web assets built successfully');
|
||||
} catch (error) {
|
||||
@@ -416,7 +415,6 @@ const generateTestData = async (log) => {
|
||||
const buildWebAssets = async (log) => {
|
||||
log('🌐 Building web assets...');
|
||||
execSync('rm -rf dist', { stdio: 'inherit' });
|
||||
execSync('npm run build:web', { stdio: 'inherit' });
|
||||
execSync('npm run build:capacitor', { stdio: 'inherit' });
|
||||
log('✅ Web assets built successfully');
|
||||
};
|
||||
|
||||
@@ -298,7 +298,6 @@ export async function logToDb(
|
||||
}
|
||||
}
|
||||
|
||||
// similar method is in the sw_scripts/additional-scripts.js file
|
||||
export async function logConsoleAndDb(
|
||||
message: string,
|
||||
isError = false,
|
||||
|
||||
@@ -302,7 +302,6 @@ export async function logToDb(message: string): Promise<void> {
|
||||
await db.logs.update(todayKey, { message: fullMessage });
|
||||
}
|
||||
|
||||
// similar method is in the sw_scripts/additional-scripts.js file
|
||||
export async function logConsoleAndDb(
|
||||
message: string,
|
||||
isError = false,
|
||||
|
||||
@@ -19,15 +19,28 @@
|
||||
/**
|
||||
* Server-issued ULID on a stored JWT/plan record, used as alertSearch afterId /
|
||||
* beforeId. Not an authentication JWT and not a delegated notification JWT.
|
||||
* Superseded by AlertSearchCursor for "what changed since" queries.
|
||||
*/
|
||||
export type AlertSearchCursorUlid = string;
|
||||
|
||||
/**
|
||||
* Opaque cursor: a position between changes on one server. Responses return
|
||||
* `nextAfterCursor` and `nextBeforeCursor`; requests send them back as
|
||||
* `afterCursor` (changes after it, oldest first) or `beforeCursor` (changes
|
||||
* before it, newest first). Store it as text and never parse or compare it;
|
||||
* its format belongs to the server (see endorser-ch
|
||||
* CHANGE/PLAN-accurate-cursors.md).
|
||||
*/
|
||||
export type AlertSearchCursor = string;
|
||||
|
||||
/**
|
||||
* JWT row from endorser `jwtsWithDidAfterId` (no claim body).
|
||||
* Cursor field: `id`.
|
||||
*/
|
||||
export interface AlertSearchClaimRecord {
|
||||
id: AlertSearchCursorUlid;
|
||||
/** Server change sequence of this row; ascending within a cursor-path list. */
|
||||
changeSeq?: number;
|
||||
issuedAt: string;
|
||||
issuer: string;
|
||||
subject?: string;
|
||||
@@ -54,6 +67,8 @@ export interface AlertSearchJwtWithClaimRecord extends AlertSearchClaimRecord {
|
||||
export interface AlertSearchPlanRecord {
|
||||
handleId: string;
|
||||
jwtId: AlertSearchCursorUlid;
|
||||
/** Server change sequence of this row; ascending within a cursor-path list. */
|
||||
changeSeq?: number;
|
||||
issuerDid?: string;
|
||||
agentDid?: string;
|
||||
fulfillsLinkConfirmed?: boolean | number;
|
||||
@@ -77,6 +92,8 @@ export interface AlertSearchPlanRecord {
|
||||
*/
|
||||
export interface AlertSearchProfileRecord {
|
||||
rowId?: number;
|
||||
/** Server change sequence of this row; ascending within a cursor-path list. */
|
||||
changeSeq?: number;
|
||||
issuerDid: string;
|
||||
updatedAt?: string;
|
||||
description: string;
|
||||
@@ -94,18 +111,43 @@ export interface EndorserAlertSearchData {
|
||||
plansNearby: AlertSearchPlanRecord[];
|
||||
}
|
||||
|
||||
/**
|
||||
* A profile or plan close in meaning to the requester's profile, from partner
|
||||
* `profilesSimilar` / `plansSimilar` (best first; not paged).
|
||||
*/
|
||||
export interface AlertSearchSimilarProfile {
|
||||
similarity: number;
|
||||
profile: AlertSearchProfileRecord;
|
||||
}
|
||||
|
||||
export interface AlertSearchSimilarPlan {
|
||||
similarity: number;
|
||||
plan: AlertSearchPlanRecord;
|
||||
}
|
||||
|
||||
export interface PartnerAlertSearchData {
|
||||
profilesNearby: AlertSearchProfileRecord[];
|
||||
profilesSimilar?: AlertSearchSimilarProfile[];
|
||||
plansSimilar?: AlertSearchSimilarPlan[];
|
||||
}
|
||||
|
||||
/**
|
||||
* Endorser GET/POST /api/v2/report/alertSearch body.
|
||||
* Per-bucket SQL hitLimit is not currently copied onto this envelope.
|
||||
* Timeouts may set `userMessage` instead.
|
||||
* The cursors and `hasMore` are absent from servers that predate cursors.
|
||||
* Timeouts may set `userMessage`; the cursor then does not move.
|
||||
*/
|
||||
export interface EndorserAlertSearchResponse {
|
||||
data: EndorserAlertSearchData;
|
||||
userMessage?: string;
|
||||
/** Send as `afterCursor` to get what changed after this response. */
|
||||
nextAfterCursor?: AlertSearchCursor;
|
||||
/** Send as `beforeCursor` to scroll back past this response. */
|
||||
nextBeforeCursor?: AlertSearchCursor;
|
||||
/**
|
||||
* A list was cut at its limit: call again in the same direction, with
|
||||
* `afterCursor = nextAfterCursor` or `beforeCursor = nextBeforeCursor`.
|
||||
*/
|
||||
hasMore?: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -114,6 +156,15 @@ export interface EndorserAlertSearchResponse {
|
||||
export interface PartnerAlertSearchResponse {
|
||||
data: PartnerAlertSearchData;
|
||||
userMessage?: string;
|
||||
/** Send as `afterCursor` to get what changed after this response. */
|
||||
nextAfterCursor?: AlertSearchCursor;
|
||||
/** Send as `beforeCursor` to scroll back past this response. */
|
||||
nextBeforeCursor?: AlertSearchCursor;
|
||||
/**
|
||||
* A list was cut at its limit: call again in the same direction, with
|
||||
* `afterCursor = nextAfterCursor` or `beforeCursor = nextBeforeCursor`.
|
||||
*/
|
||||
hasMore?: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
+18
-8
@@ -14,18 +14,21 @@
|
||||
* notify-api's push or SMS `alert-authorization` route.
|
||||
* - Native background pool: `mintBackgroundJwtTokenPool` for daily-notification
|
||||
* plugin prefetch. Unrelated to alertSearch and to the delegated batch.
|
||||
* - alertSearch cursor ULID: server-issued record/JWT primary id (26-char
|
||||
* ULID). `afterId` means ids strictly greater than that ULID; `beforeId`
|
||||
* means strictly less. First daily run omits afterId. `beforeId` is for
|
||||
* pagination within a run. These are not auth JWTs and not signed tokens.
|
||||
* - alertSearch cursors: opaque server strings, positions between changes.
|
||||
* To get what changed: send the stored value as `afterCursor`, store each
|
||||
* response's `nextAfterCursor`, and while `hasMore` call again with
|
||||
* `afterCursor = nextAfterCursor`. Every page is complete up to it.
|
||||
* To scroll back, newest first: send `beforeCursor` (any cursor, eg. a
|
||||
* `nextAfterCursor`), then `beforeCursor = nextBeforeCursor` while `hasMore`.
|
||||
* A first run sends neither.
|
||||
* - alertSearch ULIDs (`afterId` / `beforeId`): the older bounds, ignored when
|
||||
* a cursor is sent. Not auth JWTs and not signed tokens.
|
||||
*
|
||||
* Truncation: each endorser bucket query uses a server hit-limit (typically
|
||||
* 50). That flag is not currently returned on the alertSearch JSON envelope.
|
||||
* Timeouts may add `userMessage`. A later caller must still paginate with
|
||||
* beforeId when a bucket may be incomplete.
|
||||
* See endorser-ch CHANGE/PLAN-accurate-cursors.md.
|
||||
*/
|
||||
|
||||
import type {
|
||||
AlertSearchCursor,
|
||||
AlertSearchCursorUlid,
|
||||
CombinedAlertSearchData,
|
||||
EndorserAlertSearchResponse,
|
||||
@@ -47,6 +50,10 @@ export interface AlertSearchLocationBBox {
|
||||
* GET is the planned daily method; the server also accepts POST.
|
||||
*/
|
||||
export interface AlertSearchRequestParams {
|
||||
/** Changes after this cursor, oldest first; the ID and date bounds are then ignored. */
|
||||
afterCursor?: AlertSearchCursor;
|
||||
/** Changes before this cursor (and after `afterCursor`, if given), newest first. */
|
||||
beforeCursor?: AlertSearchCursor;
|
||||
afterId?: AlertSearchCursorUlid;
|
||||
beforeId?: AlertSearchCursorUlid;
|
||||
afterDate?: string;
|
||||
@@ -59,9 +66,12 @@ export interface AlertSearchRequestParams {
|
||||
planHandleIds?: string[];
|
||||
planIds?: string[];
|
||||
handleIds?: string[];
|
||||
/** Partner only: bounding box for profilesSimilar / plansSimilar (everywhere without it). */
|
||||
similarLocation?: AlertSearchLocationBBox;
|
||||
}
|
||||
|
||||
export type {
|
||||
AlertSearchCursor,
|
||||
AlertSearchCursorUlid,
|
||||
CombinedAlertSearchData,
|
||||
EndorserAlertSearchResponse,
|
||||
|
||||
@@ -0,0 +1,52 @@
|
||||
/**
|
||||
* Web Share Target handoff (web build only).
|
||||
*
|
||||
* public/sw.js receives the POST from the manifest's share_target, stores the
|
||||
* photo in Cache Storage under these keys, and redirects to /shared-photo.
|
||||
* Keep the constants in sync with public/sw.js.
|
||||
*/
|
||||
|
||||
import { logger } from "../utils/logger";
|
||||
|
||||
const SHARE_TARGET_CACHE = "share-target";
|
||||
const SHARED_PHOTO_KEY = "/share-target/photo";
|
||||
const FILE_NAME_HEADER = "X-File-Name";
|
||||
|
||||
export interface WebSharedPhoto {
|
||||
blob: Blob;
|
||||
fileName: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Registers public/sw.js, which handles the share target POST.
|
||||
*/
|
||||
export function registerWebShareTargetWorker(): void {
|
||||
if (!("serviceWorker" in navigator)) {
|
||||
return;
|
||||
}
|
||||
navigator.serviceWorker.register("/sw.js").catch((error) => {
|
||||
// Not fatal: only sharing into the app depends on the worker.
|
||||
logger.warn("[WebShareTarget] Service worker registration failed", error);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the photo stashed by the service worker and removes it, or null
|
||||
* when there is none (or Cache Storage is unavailable).
|
||||
*/
|
||||
export async function takeWebSharedPhoto(): Promise<WebSharedPhoto | null> {
|
||||
if (typeof caches === "undefined") {
|
||||
return null;
|
||||
}
|
||||
const cache = await caches.open(SHARE_TARGET_CACHE);
|
||||
const response = await cache.match(SHARED_PHOTO_KEY);
|
||||
if (!response) {
|
||||
return null;
|
||||
}
|
||||
const blob = await response.blob();
|
||||
const fileName = decodeURIComponent(
|
||||
response.headers.get(FILE_NAME_HEADER) || "",
|
||||
);
|
||||
await cache.delete(SHARED_PHOTO_KEY);
|
||||
return { blob, fileName };
|
||||
}
|
||||
+51
-14
@@ -1,20 +1,57 @@
|
||||
import { initializeApp } from "./main.common";
|
||||
import { logger } from "./utils/logger";
|
||||
import { registerWebShareTargetWorker } from "./libs/webShareTarget";
|
||||
|
||||
const platform = process.env.VITE_PLATFORM;
|
||||
|
||||
// PWA service worker is automatically registered by VitePWA plugin
|
||||
|
||||
const app = initializeApp();
|
||||
|
||||
// Note: Worker initialization is now handled by WebPlatformService
|
||||
// This ensures single-point database access and prevents double migrations
|
||||
if (platform === "web" || platform === "development") {
|
||||
logger.debug(
|
||||
"[Web] Database initialization will be handled by WebPlatformService",
|
||||
);
|
||||
} else {
|
||||
logger.debug("[Web] SQL not initialized for platform", { platform });
|
||||
/**
|
||||
* Browsers withhold crypto.subtle outside secure contexts (HTTPS, localhost,
|
||||
* 127.0.0.1, [::1]), and identity creation cannot run without it. Addresses
|
||||
* like http://[::]:8000 or http://192.168.x.x would otherwise fail later with
|
||||
* a misleading "could not open its data" startup error. main.common is loaded
|
||||
* only after this check because importing the router starts navigation.
|
||||
*/
|
||||
function showInsecureContextError(): void {
|
||||
const container = document.getElementById("app");
|
||||
if (!container) {
|
||||
return;
|
||||
}
|
||||
container.innerHTML = `
|
||||
<section style="max-width: 40rem; margin: 3rem auto; padding: 0 1rem; font-family: sans-serif; line-height: 1.5;">
|
||||
<h1 style="font-size: 1.25rem; font-weight: 600;">Time Safari Cannot Start Here</h1>
|
||||
<p>This page must be opened over HTTPS or from <code>localhost</code>.</p>
|
||||
<p>The browser does not treat <code></code> as secure, so it disables the cryptography the app uses for identities.</p>
|
||||
<p>On a local server, use <code>http://localhost:PORT/</code> instead of <code>http://[::]:PORT/</code> or <code>http://0.0.0.0:PORT/</code>.</p>
|
||||
</section>`;
|
||||
const originCode = container.querySelectorAll("code")[1];
|
||||
if (originCode) {
|
||||
originCode.textContent = window.location.origin;
|
||||
}
|
||||
}
|
||||
|
||||
app.mount("#app");
|
||||
if (!window.isSecureContext) {
|
||||
logger.error("[Web] Not a secure context; app will not start", {
|
||||
origin: window.location.origin,
|
||||
});
|
||||
showInsecureContextError();
|
||||
} else {
|
||||
import("./main.common").then(({ initializeApp }) => {
|
||||
const app = initializeApp();
|
||||
|
||||
// Note: Worker initialization is now handled by WebPlatformService
|
||||
// This ensures single-point database access and prevents double migrations
|
||||
if (platform === "web" || platform === "development") {
|
||||
logger.debug(
|
||||
"[Web] Database initialization will be handled by WebPlatformService",
|
||||
);
|
||||
} else {
|
||||
logger.debug("[Web] SQL not initialized for platform", { platform });
|
||||
}
|
||||
|
||||
app.mount("#app");
|
||||
|
||||
// Handles the manifest's share_target POST (see public/sw.js)
|
||||
if (platform === "web") {
|
||||
registerWebShareTargetWorker();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@@ -11,7 +11,7 @@ export type PendingNotificationInfo = {
|
||||
|
||||
export interface NotificationInspectorPlugin {
|
||||
getPendingNotifications(): Promise<{ pending: PendingNotificationInfo[] }>;
|
||||
/** Android only: whether the build has a Firebase config (-PfirebaseEnabled). */
|
||||
/** Android only: whether this build included Firebase (off only with -PfirebaseEnabled=false). */
|
||||
isFirebaseConfigured(): Promise<{ configured: boolean }>;
|
||||
}
|
||||
|
||||
|
||||
@@ -281,8 +281,6 @@ const routes: Array<RouteRecordRaw> = [
|
||||
component: () => import("../views/SharedPhotoView.vue"),
|
||||
},
|
||||
|
||||
// /share-target is also an endpoint in the service worker
|
||||
|
||||
{
|
||||
path: "/start",
|
||||
name: "start",
|
||||
|
||||
@@ -225,14 +225,4 @@ export interface PlatformService {
|
||||
* @throws if the backup copy fails (the original is never deleted without one)
|
||||
*/
|
||||
resetDatabase?(): Promise<string | undefined>;
|
||||
|
||||
// --- PWA/Web-only methods (optional, only implemented on web) ---
|
||||
/**
|
||||
* Registers the service worker for PWA support (web only)
|
||||
*/
|
||||
registerServiceWorker?(): void;
|
||||
|
||||
/**
|
||||
* Returns true if PWA is enabled (web only)
|
||||
*/
|
||||
}
|
||||
|
||||
@@ -69,9 +69,10 @@ async function registerRetrievedToken(
|
||||
}
|
||||
|
||||
/**
|
||||
* 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.
|
||||
* Whether PushNotifications.register() is safe to call. Android builds with
|
||||
* Firebase disabled (-PfirebaseEnabled=false, FOSS/F-Droid) 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") {
|
||||
@@ -101,7 +102,7 @@ export async function reregisterFcmTokenNow(): Promise<string> {
|
||||
|
||||
if (!(await canRegisterNativePush())) {
|
||||
throw new Error(
|
||||
"Push is unavailable: this Android build has no Firebase config (built without -PfirebaseEnabled)",
|
||||
"Push is unavailable: this Android build has no Firebase config (built with -PfirebaseEnabled=false)",
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -1546,9 +1546,6 @@ export class CapacitorPlatformService
|
||||
return false;
|
||||
}
|
||||
|
||||
// --- PWA/Web-only methods (no-op for Capacitor) ---
|
||||
public registerServiceWorker(): void {}
|
||||
|
||||
// Database utility methods - inherited from BaseDatabaseService
|
||||
// generateInsertStatement, updateDefaultSettings, updateActiveDid,
|
||||
// getActiveIdentity, insertNewDidIntoSettings, updateDidSpecificSettings,
|
||||
|
||||
@@ -163,7 +163,4 @@ export class ElectronPlatformService extends CapacitorPlatformService {
|
||||
isWeb(): boolean {
|
||||
return false;
|
||||
}
|
||||
|
||||
// --- PWA/Web-only methods (no-op for Electron) ---
|
||||
public registerServiceWorker(): void {}
|
||||
}
|
||||
|
||||
@@ -136,6 +136,7 @@ import {
|
||||
} from "../constants/app";
|
||||
import { accessToken } from "../libs/crypto";
|
||||
import { base64ToBlob, SHARED_PHOTO_BASE64_KEY } from "../libs/util";
|
||||
import { takeWebSharedPhoto } from "../libs/webShareTarget";
|
||||
import { logger } from "../utils/logger";
|
||||
|
||||
import { PlatformServiceMixin } from "@/utils/PlatformServiceMixin";
|
||||
@@ -221,6 +222,17 @@ export default class SharedPhotoView extends Vue {
|
||||
const activeIdentity = await (this as any).$getActiveIdentity();
|
||||
this.activeDid = activeIdentity.activeDid;
|
||||
|
||||
// Web Share Target: public/sw.js stashes the photo in Cache Storage
|
||||
if (this.$route.query["source"] === "share-target") {
|
||||
const shared = await takeWebSharedPhoto();
|
||||
if (shared) {
|
||||
this.imageBlob = shared.blob;
|
||||
this.imageFileName = shared.fileName;
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// Native share: main.capacitor.ts stores the photo in the temp table
|
||||
const temp = await this.$getTemp(SHARED_PHOTO_BASE64_KEY);
|
||||
const imageB64 = temp?.blobB64 as string;
|
||||
if (temp) {
|
||||
|
||||
@@ -1,55 +0,0 @@
|
||||
import * as path from "path";
|
||||
import { promises as fs } from "fs";
|
||||
import { fileURLToPath } from "url";
|
||||
|
||||
export async function loadAppConfig() {
|
||||
const packageJson = await loadPackageJson();
|
||||
const appName = process.env.TIME_SAFARI_APP_TITLE || packageJson.name;
|
||||
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
||||
|
||||
return {
|
||||
pwaConfig: {
|
||||
manifest: {
|
||||
name: appName,
|
||||
short_name: appName,
|
||||
icons: [
|
||||
{
|
||||
src: "./img/icons/android-chrome-192x192.png",
|
||||
sizes: "192x192",
|
||||
type: "image/png",
|
||||
},
|
||||
{
|
||||
src: "./img/icons/android-chrome-512x512.png",
|
||||
sizes: "512x512",
|
||||
type: "image/png",
|
||||
},
|
||||
{
|
||||
src: "./img/icons/android-chrome-maskable-192x192.png",
|
||||
sizes: "192x192",
|
||||
type: "image/png",
|
||||
purpose: "maskable",
|
||||
},
|
||||
{
|
||||
src: "./img/icons/android-chrome-maskable-512x512.png",
|
||||
sizes: "512x512",
|
||||
type: "image/png",
|
||||
purpose: "maskable",
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
aliasConfig: {
|
||||
"@": path.resolve(path.dirname(__dirname), "src"),
|
||||
buffer: path.resolve(path.dirname(__dirname), "node_modules", "buffer"),
|
||||
"dexie-export-import/dist/import":
|
||||
"dexie-export-import/dist/import/index.js",
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
async function loadPackageJson() {
|
||||
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
||||
const packageJsonPath = path.resolve(path.dirname(__dirname), "package.json");
|
||||
const packageJsonData = await fs.readFile(packageJsonPath, "utf-8");
|
||||
return JSON.parse(packageJsonData);
|
||||
}
|
||||
-1051
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -24,7 +24,7 @@
|
||||
*
|
||||
* Related Files:
|
||||
* - Frontend invite handling: src/libs/endorserServer.ts
|
||||
* - JWT creation: sw_scripts/safari-notifications.js
|
||||
* - JWT creation: src/libs/crypto/vc/index.ts
|
||||
*
|
||||
* @see Documentation on invitation flows in usage-guide.md
|
||||
* @requires @playwright/test
|
||||
|
||||
@@ -58,7 +58,7 @@
|
||||
*
|
||||
* Related Files:
|
||||
* - Project utilities: ./testUtils
|
||||
* - JWT handling: sw_scripts/safari-notifications.js
|
||||
* - JWT handling: src/libs/crypto/vc/index.ts
|
||||
* - Project view: src/views/ProjectView.vue
|
||||
*
|
||||
* @see Documentation in usage-guide.md for project creation workflows
|
||||
|
||||
@@ -63,7 +63,7 @@
|
||||
*
|
||||
* Related Files:
|
||||
* - Gift recording view: src/views/RecordGiftView.vue
|
||||
* - JWT creation: sw_scripts/safari-notifications.js
|
||||
* - JWT creation: src/libs/crypto/vc/index.ts
|
||||
* - Endorser API: src/libs/endorserServer.ts
|
||||
*
|
||||
* @see Documentation in usage-guide.md for gift recording workflows
|
||||
|
||||
@@ -60,7 +60,7 @@
|
||||
*
|
||||
* Related Files:
|
||||
* - Gift recording view: src/views/RecordGiftView.vue
|
||||
* - JWT creation: sw_scripts/safari-notifications.js
|
||||
* - JWT creation: src/libs/crypto/vc/index.ts
|
||||
* - Endorser API: src/libs/endorserServer.ts
|
||||
* - Test utilities: ./testUtils.ts
|
||||
*
|
||||
|
||||
+1
-10
@@ -15,9 +15,7 @@ export async function createBuildConfig(platform: string): Promise<UserConfig> {
|
||||
const appConfig = await loadAppConfig();
|
||||
const isCapacitor = platform === "capacitor";
|
||||
const isElectron = platform === "electron";
|
||||
const isNative = isCapacitor || isElectron;
|
||||
|
||||
// Set platform - PWA is always enabled for web platforms
|
||||
process.env.VITE_PLATFORM = platform;
|
||||
|
||||
// Environment variables are loaded from .env files via dotenv.config() above
|
||||
@@ -74,7 +72,6 @@ export async function createBuildConfig(platform: string): Promise<UserConfig> {
|
||||
'process.env.NODE_ENV': JSON.stringify(process.env.NODE_ENV),
|
||||
'process.env.VITE_PLATFORM': JSON.stringify(platform),
|
||||
'process.env.VITE_LOG_LEVEL': JSON.stringify(process.env.VITE_LOG_LEVEL),
|
||||
// PWA is always enabled for web platforms
|
||||
__dirname: JSON.stringify(process.cwd()),
|
||||
__IS_MOBILE__: JSON.stringify(isCapacitor),
|
||||
__IS_ELECTRON__: JSON.stringify(isElectron),
|
||||
@@ -106,13 +103,7 @@ export async function createBuildConfig(platform: string): Promise<UserConfig> {
|
||||
'absurd-sql',
|
||||
'absurd-sql/dist/indexeddb-main-thread',
|
||||
'absurd-sql/dist/indexeddb-backend'
|
||||
],
|
||||
exclude: isNative ? [
|
||||
'register-service-worker',
|
||||
'workbox-window',
|
||||
'web-push',
|
||||
'serviceworker-webpack-plugin'
|
||||
] : []
|
||||
]
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
@@ -20,7 +20,6 @@
|
||||
*
|
||||
* Environment Variables:
|
||||
* - VITE_PLATFORM: Set to "electron"
|
||||
* - PWA: Disabled for desktop (via build exclusion)
|
||||
*/
|
||||
|
||||
import { defineConfig } from "vite";
|
||||
|
||||
@@ -24,7 +24,6 @@ export async function createOptimizedBuildConfig(mode: string): Promise<UserConf
|
||||
const isElectron = mode === "electron";
|
||||
const isNative = isCapacitor || isElectron;
|
||||
|
||||
// Set platform - PWA is always enabled for web platforms
|
||||
process.env.VITE_PLATFORM = mode;
|
||||
|
||||
return {
|
||||
@@ -120,13 +119,7 @@ export async function createOptimizedBuildConfig(mode: string): Promise<UserConf
|
||||
'vue-router',
|
||||
'pinia',
|
||||
'vue-facing-decorator'
|
||||
],
|
||||
exclude: isNative ? [
|
||||
'register-service-worker',
|
||||
'workbox-window',
|
||||
'web-push',
|
||||
'serviceworker-webpack-plugin'
|
||||
] : []
|
||||
]
|
||||
},
|
||||
// Enhanced performance optimizations
|
||||
esbuild: {
|
||||
|
||||
@@ -1,110 +1,16 @@
|
||||
import * as path from "path";
|
||||
import { fileURLToPath } from 'url';
|
||||
import { promises as fs } from "fs";
|
||||
|
||||
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
||||
|
||||
async function loadPackageJson() {
|
||||
const packageJsonPath = path.join(__dirname, 'package.json');
|
||||
const packageJsonContent = await fs.readFile(packageJsonPath, 'utf-8');
|
||||
return JSON.parse(packageJsonContent);
|
||||
}
|
||||
|
||||
interface ManifestIcon {
|
||||
src: string;
|
||||
sizes: string;
|
||||
type: string;
|
||||
purpose?: string;
|
||||
}
|
||||
|
||||
interface ShareTarget {
|
||||
action: string;
|
||||
method: "POST";
|
||||
enctype: string;
|
||||
params: {
|
||||
files: Array<{
|
||||
name: string;
|
||||
accept: string[];
|
||||
}>;
|
||||
};
|
||||
}
|
||||
|
||||
interface PWAConfig {
|
||||
registerType: string;
|
||||
strategies: string;
|
||||
srcDir: string;
|
||||
filename: string;
|
||||
manifest: {
|
||||
name: string;
|
||||
short_name: string;
|
||||
theme_color: string;
|
||||
background_color: string;
|
||||
icons: ManifestIcon[];
|
||||
share_target: ShareTarget;
|
||||
};
|
||||
}
|
||||
|
||||
interface AppConfig {
|
||||
pwaConfig: PWAConfig;
|
||||
aliasConfig: {
|
||||
[key: string]: string;
|
||||
};
|
||||
}
|
||||
|
||||
export async function loadAppConfig(): Promise<AppConfig> {
|
||||
const packageJson = await loadPackageJson();
|
||||
const appName = process.env.TIME_SAFARI_APP_TITLE || packageJson.name;
|
||||
|
||||
return {
|
||||
pwaConfig: {
|
||||
registerType: "autoUpdate",
|
||||
strategies: "injectManifest",
|
||||
srcDir: ".",
|
||||
filename: "sw_scripts-combined.js",
|
||||
manifest: {
|
||||
name: appName,
|
||||
short_name: appName,
|
||||
theme_color: "#4a90e2",
|
||||
background_color: "#ffffff",
|
||||
icons: [
|
||||
{
|
||||
src: "./img/icons/android-chrome-192x192.png",
|
||||
sizes: "192x192",
|
||||
type: "image/png",
|
||||
},
|
||||
{
|
||||
src: "./img/icons/android-chrome-512x512.png",
|
||||
sizes: "512x512",
|
||||
type: "image/png",
|
||||
},
|
||||
{
|
||||
src: "./img/icons/android-chrome-maskable-192x192.png",
|
||||
sizes: "192x192",
|
||||
type: "image/png",
|
||||
purpose: "maskable",
|
||||
},
|
||||
{
|
||||
src: "./img/icons/android-chrome-maskable-512x512.png",
|
||||
sizes: "512x512",
|
||||
type: "image/png",
|
||||
purpose: "maskable",
|
||||
},
|
||||
],
|
||||
share_target: {
|
||||
action: "/share-target",
|
||||
method: "POST",
|
||||
enctype: "multipart/form-data",
|
||||
params: {
|
||||
files: [
|
||||
{
|
||||
name: "photo",
|
||||
accept: ["image/*"],
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
aliasConfig: {
|
||||
"@": path.resolve(__dirname, "src"),
|
||||
buffer: path.resolve(__dirname, "node_modules", "buffer"),
|
||||
|
||||
Reference in New Issue
Block a user