Compare commits
16 Commits
16kb-pages
...
giftopia-a
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
6d221ee1ca | ||
|
|
823db447ca | ||
| 87ffa025e8 | |||
| 15c9088736 | |||
| ec41dd52d5 | |||
| 463db39a6b | |||
| fe97dff752 | |||
|
|
903047f13b | ||
|
|
48be234af4 | ||
| 6c0907d905 | |||
|
|
8d8bcf2a7e | ||
| a4b47904c8 | |||
|
|
bb890baacf | ||
| dae23300fe | |||
| 9e401febea | |||
| cd4b279703 |
20
AGENTS.md
Normal file
@@ -0,0 +1,20 @@
|
||||
# Agent Instructions for crowd-funder-for-time-pwa
|
||||
|
||||
## Android Build — Google Play Services / FOSS Compatibility
|
||||
|
||||
**Firebase is opt-in. Do NOT enable it accidentally.**
|
||||
|
||||
`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:
|
||||
|
||||
- **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.
|
||||
|
||||
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.
|
||||
|
||||
`google-services.json` is intentionally excluded from git (`android/.gitignore`). Never commit it.
|
||||
|
||||
Full details, incident history, and F-Droid notes: `doc/development/android-firebase-gms.md`
|
||||
|
||||
## Android Build — MLKit Barcode Scanner
|
||||
|
||||
`@capacitor-mlkit/barcode-scanning` depends on `com.google.android.gms:play-services-code-scanner`, which merges `com.google.android.gms.version` into the APK manifest. This is a known long-term issue for strict FOSS/F-Droid builds. For now, the dependency is accepted; barcode scanning simply will not work on GMS-less devices (it fails gracefully at scan time, not at startup). Do not add additional GMS/Firebase dependencies without explicitly acknowledging this trade-off.
|
||||
11
BUILDING.md
@@ -1140,7 +1140,7 @@ export GEM_PATH=$shortened_path
|
||||
##### 1. Bump the version in package.json & CHANGELOG.md for `MARKETING_VERSION`, then `grep CURRENT_PROJECT_VERSION ios/App/App.xcodeproj/project.pbxproj` and add 1 for the numbered version here:
|
||||
|
||||
```bash
|
||||
cd ios/App && xcrun agvtool new-version 68 && perl -p -i -e "s/MARKETING_VERSION = .*;/MARKETING_VERSION = 1.4.2;/g" App.xcodeproj/project.pbxproj && cd -
|
||||
cd ios/App && xcrun agvtool new-version 70 && perl -p -i -e "s/MARKETING_VERSION = .*;/MARKETING_VERSION = 1.4.4;/g" App.xcodeproj/project.pbxproj && cd -
|
||||
# Unfortunately this edits Info.plist directly.
|
||||
#xcrun agvtool new-marketing-version 0.4.5
|
||||
```
|
||||
@@ -1419,8 +1419,8 @@ The recommended way to build for Android is using the automated build script:
|
||||
##### 1. Bump the version in package.json, then update these versions & run:
|
||||
|
||||
```bash
|
||||
perl -p -i -e 's/versionCode .*/versionCode 68/g' android/app/build.gradle
|
||||
perl -p -i -e 's/versionName .*/versionName "1.4.2"/g' android/app/build.gradle
|
||||
perl -p -i -e 's/versionCode .*/versionCode 70/g' android/app/build.gradle
|
||||
perl -p -i -e 's/versionName .*/versionName "1.4.4"/g' android/app/build.gradle
|
||||
```
|
||||
|
||||
##### 2. Build
|
||||
@@ -1458,17 +1458,18 @@ cd -
|
||||
|
||||
- Setup by adding the app/gradle.properties.secrets file (see properties at top
|
||||
of app/build.gradle) and the app/time-safari-upload-key-pkcs12.jks file
|
||||
- In app/build.gradle, bump the versionCode and maybe the versionName
|
||||
- Then `bundleRelease`:
|
||||
|
||||
```bash
|
||||
cd android
|
||||
./gradlew bundleRelease -Dlint.baselines.continue=true
|
||||
./gradlew bundleRelease -Dlint.baselines.continue=true -PfirebaseEnabled
|
||||
cd -
|
||||
```
|
||||
|
||||
... and find your `aab` file at app/build/outputs/bundle/release
|
||||
|
||||
* 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.
|
||||
|
||||
12
CHANGELOG.md
@@ -6,6 +6,18 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
|
||||
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
||||
|
||||
|
||||
## [1.4.4] - 2026.06.21
|
||||
### Changed
|
||||
- More checks for Firebase so that it won't break, eg in Aurora store.
|
||||
|
||||
|
||||
## [1.4.3] - 2026.06.19
|
||||
### Removed
|
||||
- Automatic "Check your starred projects" daily notification
|
||||
### Changed
|
||||
- Positioning for 'Thank' button and entity-type toggle link
|
||||
|
||||
|
||||
## [1.4.2] - 2026.05.24
|
||||
### Changed
|
||||
- Support 16 KB page sizes
|
||||
|
||||
@@ -37,8 +37,8 @@ android {
|
||||
applicationId "app.timesafari.app"
|
||||
minSdkVersion rootProject.ext.minSdkVersion
|
||||
targetSdkVersion rootProject.ext.targetSdkVersion
|
||||
versionCode 68
|
||||
versionName "1.4.2"
|
||||
versionCode 70
|
||||
versionName "1.4.4"
|
||||
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
|
||||
aaptOptions {
|
||||
// Files and dirs to omit from the packaged assets dir, modified to accommodate modern web apps.
|
||||
@@ -130,11 +130,20 @@ 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.text) {
|
||||
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")
|
||||
}
|
||||
} catch(Exception e) {
|
||||
logger.info("google-services.json not found, google-services plugin not applied. Push Notifications won't work")
|
||||
logger.info("google-services plugin not applied: ${e.message}")
|
||||
}
|
||||
|
||||
@@ -9,7 +9,6 @@ import org.timesafari.dailynotification.FetchContext;
|
||||
import org.timesafari.dailynotification.NativeNotificationContentFetcher;
|
||||
import org.timesafari.dailynotification.NotificationContent;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
@@ -47,22 +46,11 @@ public class TimeSafariNativeFetcher implements NativeNotificationContentFetcher
|
||||
// This should query the TimeSafari API for notification content
|
||||
// using the configured apiBaseUrl, activeDid, and jwtToken
|
||||
|
||||
// For now, return a placeholder notification
|
||||
long scheduledTime = fetchContext.scheduledTime != null
|
||||
? fetchContext.scheduledTime
|
||||
: System.currentTimeMillis() + 60000; // 1 minute from now
|
||||
|
||||
NotificationContent content = new NotificationContent(
|
||||
"TimeSafari Update",
|
||||
"Check your starred projects for updates!",
|
||||
scheduledTime
|
||||
);
|
||||
|
||||
List<NotificationContent> results = new ArrayList<>();
|
||||
results.add(content);
|
||||
|
||||
Log.d(TAG, "Returning " + results.size() + " notification(s)");
|
||||
return results;
|
||||
// Not implemented yet: return no content rather than fabricating a
|
||||
// placeholder notification (previously hardcoded "Check your starred
|
||||
// projects for updates!", which showed on every app startup).
|
||||
Log.d(TAG, "Content fetching not yet implemented; returning no notifications");
|
||||
return Collections.<NotificationContent>emptyList();
|
||||
|
||||
} catch (Exception e) {
|
||||
Log.e(TAG, "Fetch failed", e);
|
||||
|
||||
@@ -2,9 +2,9 @@
|
||||
"icon": {
|
||||
"android": {
|
||||
"adaptive": {
|
||||
"background": "#121212",
|
||||
"foreground": "resources/icon.png",
|
||||
"monochrome": "resources/icon.png"
|
||||
"background": "resources/android/icon/icon_background.png",
|
||||
"foreground": "resources/android/icon/icon_foreground.png",
|
||||
"monochrome": "resources/android/icon/icon_monochrome.png"
|
||||
},
|
||||
"target": "android/app/src/main/res"
|
||||
},
|
||||
|
||||
66
doc/android-firebase-gms.md
Normal file
@@ -0,0 +1,66 @@
|
||||
# Android — Firebase, Google Play Services, and FOSS Distribution
|
||||
|
||||
## 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.).
|
||||
|
||||
## How the opt-in guard works
|
||||
|
||||
`android/app/build.gradle` applies the `com.google.gms.google-services` Gradle plugin only when **both** conditions are true:
|
||||
|
||||
1. `android/google-services.json` is present on disk
|
||||
2. The Gradle property `firebaseEnabled` is explicitly passed
|
||||
|
||||
```groovy
|
||||
if (servicesJSON.exists() && servicesJSON.text && project.hasProperty('firebaseEnabled')) {
|
||||
apply plugin: 'com.google.gms.google-services'
|
||||
}
|
||||
```
|
||||
|
||||
This means the file can live on disk for development purposes without accidentally activating Firebase.
|
||||
|
||||
## 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) |
|
||||
|
||||
## Behavior on non-GMS devices
|
||||
|
||||
When built with `-PfirebaseEnabled`, 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.
|
||||
|
||||
**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.
|
||||
|
||||
**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.
|
||||
|
||||
## The `google-services.json` file
|
||||
|
||||
- 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
|
||||
- Obtain from the Firebase console: Project Settings → Your apps → Android app → Download `google-services.json`
|
||||
|
||||
## Known GMS dependency: MLKit barcode scanner
|
||||
|
||||
`@capacitor-mlkit/barcode-scanning` unconditionally depends on `com.google.android.gms:play-services-code-scanner` (present since the plugin was first added at v6.0.0). This merges `com.google.android.gms.version` and `GoogleApiActivity` into the APK manifest regardless of the `-PfirebaseEnabled` flag.
|
||||
|
||||
Practical impact:
|
||||
- **GMS devices**: barcode scanning works normally
|
||||
- **Non-GMS devices**: barcode scanning fails at scan time (not at startup); the app launches and runs normally otherwise
|
||||
|
||||
This is an accepted trade-off. Removing it would require either forking the plugin or introducing a `foss` product flavor that excludes the MLKit plugin entirely — work to undertake if/when F-Droid submission is planned.
|
||||
|
||||
## Incident: June 2026
|
||||
|
||||
Jose Olarte III's `notify-api` branch placed a production `google-services.json` in `android/` to test Firebase Cloud Messaging. The branch was never merged to `master`, but because the file is gitignored it persisted on disk after switching branches. At the time, the Gradle conditional activated Firebase based on file presence alone (no opt-in flag), so all subsequent local builds embedded Firebase and required Google Play Services. This silently broke APK/Aurora/Zapstore distribution.
|
||||
|
||||
**Fix applied:** deleted `google-services.json` from disk, changed the Gradle conditional to require `-PfirebaseEnabled`, and documented the rule in `AGENTS.md`.
|
||||
308
doc/deep-link-debugging-guide.md
Normal file
@@ -0,0 +1,308 @@
|
||||
# Deep Link Debugging Guide for TimeSafari
|
||||
|
||||
This guide helps you debug and fix deep link issues in the TimeSafari Capacitor application.
|
||||
|
||||
## Quick Start
|
||||
|
||||
1. **Check logs**: Use browser dev tools or device console to see detailed logging
|
||||
2. **Test manually**: Use the testing script or browser console commands
|
||||
3. **Verify configuration**: Ensure all platform configurations are correct
|
||||
|
||||
## Common Issues and Solutions
|
||||
|
||||
### Issue 1: App opens but doesn't navigate to the correct page
|
||||
|
||||
**Symptoms:**
|
||||
- Deep link opens the app
|
||||
- App stays on home screen or current page
|
||||
- No error messages visible
|
||||
|
||||
**Debugging Steps:**
|
||||
|
||||
1. **Check console logs** in browser dev tools or device console:
|
||||
```bash
|
||||
# Android
|
||||
adb logcat | grep -E "(TimeSafari|DeepLink|appUrlOpen)"
|
||||
|
||||
# iOS Simulator
|
||||
xcrun simctl spawn booted log stream --predicate 'process == "TimeSafari"'
|
||||
```
|
||||
|
||||
2. **Verify listener registration**:
|
||||
Look for these log messages:
|
||||
```
|
||||
[DeepLink] Registering appUrlOpen listener...
|
||||
[DeepLink] Listener registered successfully
|
||||
```
|
||||
|
||||
3. **Check for event reception**:
|
||||
Look for:
|
||||
```
|
||||
[DeepLink] ========== DEEP LINK EVENT RECEIVED ==========
|
||||
[DeepLink] URL: timesafari://your/url/here
|
||||
```
|
||||
|
||||
4. **Verify URL parsing**:
|
||||
Check if URL components are parsed correctly:
|
||||
```
|
||||
[DeepLinkHandler.parseDeepLink] Parse result: {"path":"claim","params":{"id":"123"},"query":{}}
|
||||
```
|
||||
|
||||
### Issue 2: Listener not receiving events
|
||||
|
||||
**Symptoms:**
|
||||
- No deep link logs appear
|
||||
- App opens but no event processing
|
||||
|
||||
**Solutions:**
|
||||
|
||||
1. **Rebuild and reinstall** the app completely:
|
||||
```bash
|
||||
npm run build:capacitor
|
||||
npx cap sync
|
||||
npx cap run android # or ios
|
||||
```
|
||||
|
||||
2. **Check capacitor.config.json**:
|
||||
```json
|
||||
{
|
||||
"plugins": {
|
||||
"App": {
|
||||
"appUrlOpen": {
|
||||
"handlers": [
|
||||
{
|
||||
"url": "timesafari://*",
|
||||
"autoVerify": true
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
3. **Verify native configuration**:
|
||||
- **Android**: Check `android/app/src/main/AndroidManifest.xml`
|
||||
- **iOS**: Check `ios/App/App/Info.plist`
|
||||
|
||||
### Issue 3: URL scheme not recognized by OS
|
||||
|
||||
**Symptoms:**
|
||||
- "No app found to handle this link" error
|
||||
- OS doesn't open your app
|
||||
|
||||
**Solutions:**
|
||||
|
||||
1. **Android**: Verify intent filter in AndroidManifest.xml:
|
||||
```xml
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.VIEW" />
|
||||
<category android:name="android.intent.category.DEFAULT" />
|
||||
<category android:name="android.intent.category.BROWSABLE" />
|
||||
<data android:scheme="timesafari" />
|
||||
</intent-filter>
|
||||
```
|
||||
|
||||
2. **iOS**: Verify URL types in Info.plist:
|
||||
```xml
|
||||
<key>CFBundleURLTypes</key>
|
||||
<array>
|
||||
<dict>
|
||||
<key>CFBundleURLName</key>
|
||||
<string>app.timesafari</string>
|
||||
<key>CFBundleURLSchemes</key>
|
||||
<array>
|
||||
<string>timesafari</string>
|
||||
</array>
|
||||
</dict>
|
||||
</array>
|
||||
```
|
||||
|
||||
## Testing Tools
|
||||
|
||||
### 1. Automated Testing Script
|
||||
|
||||
Use the provided testing script:
|
||||
|
||||
```bash
|
||||
# Test all URLs on Android
|
||||
./scripts/test-deep-links.sh android
|
||||
|
||||
# Test specific URL on iOS
|
||||
./scripts/test-deep-links.sh ios "timesafari://claim/test123"
|
||||
|
||||
# Monitor logs
|
||||
./scripts/test-deep-links.sh android-logs
|
||||
```
|
||||
|
||||
### 2. Manual Testing Commands
|
||||
|
||||
**Android Emulator:**
|
||||
```bash
|
||||
adb shell am start -W -a android.intent.action.VIEW -d "timesafari://claim/test123" app.timesafari
|
||||
```
|
||||
|
||||
**iOS Simulator:**
|
||||
```bash
|
||||
xcrun simctl openurl booted "timesafari://claim/test123"
|
||||
```
|
||||
|
||||
### 3. Browser Console Testing
|
||||
|
||||
For web/PWA testing, use the browser console:
|
||||
|
||||
```javascript
|
||||
// Test deep link processing directly
|
||||
window.testSingleDeepLink("timesafari://claim/test123");
|
||||
|
||||
// Run all test URLs
|
||||
window.testDeepLinks();
|
||||
```
|
||||
|
||||
## Debugging Steps Checklist
|
||||
|
||||
### Pre-Testing Setup
|
||||
|
||||
- [ ] App is installed on device/emulator
|
||||
- [ ] App has been launched at least once
|
||||
- [ ] Device/emulator is properly connected
|
||||
- [ ] Debugging tools are accessible
|
||||
|
||||
### During Testing
|
||||
|
||||
- [ ] Check console for initialization logs
|
||||
- [ ] Verify listener registration
|
||||
- [ ] Test with simple URL first (e.g., `timesafari://claim/test`)
|
||||
- [ ] Monitor URL parsing logs
|
||||
- [ ] Check router navigation logs
|
||||
|
||||
### Post-Testing Analysis
|
||||
|
||||
- [ ] Review complete log sequence
|
||||
- [ ] Identify where process fails
|
||||
- [ ] Check error messages for clues
|
||||
- [ ] Test with different URL formats
|
||||
|
||||
## Common Log Patterns
|
||||
|
||||
### Successful Deep Link Flow
|
||||
|
||||
```
|
||||
[DeepLink] Registering appUrlOpen listener...
|
||||
[DeepLink] Listener registered successfully
|
||||
[DeepLink] ========== DEEP LINK EVENT RECEIVED ==========
|
||||
[DeepLink] URL: timesafari://claim/test123
|
||||
[DeepLinkHandler] Starting handleDeepLink with URL: timesafari://claim/test123
|
||||
[DeepLinkHandler.parseDeepLink] Parse result: {"path":"claim","params":{"id":"test123"},"query":{}}
|
||||
[DeepLinkHandler.validateAndRoute] Route validation passed. Route name: claim
|
||||
[DeepLinkHandler.validateAndRoute] Router navigation completed successfully
|
||||
[DeepLink] Deep link handled successfully
|
||||
```
|
||||
|
||||
### Failed URL Parsing
|
||||
|
||||
```
|
||||
[DeepLinkHandler.parseDeepLink] Route not found: invalid-route
|
||||
[DeepLinkHandler.parseDeepLink] Available routes: ["claim","project","contact-import",...]
|
||||
[DeepLinkHandler.validateAndRoute] Redirecting to deep-link-error page
|
||||
```
|
||||
|
||||
### Router Navigation Issues
|
||||
|
||||
```
|
||||
[DeepLinkHandler.validateAndRoute] Error routing to route name claim
|
||||
[DeepLinkHandler.validateAndRoute] Navigation params: {"name":"claim","params":{"id":"test123"}}
|
||||
```
|
||||
|
||||
## Platform-Specific Issues
|
||||
|
||||
### Android
|
||||
|
||||
**Issue**: Deep links work in development but not in production build
|
||||
- **Solution**: Ensure `android:exported="true"` in MainActivity
|
||||
|
||||
**Issue**: App doesn't respond to links when running in background
|
||||
- **Solution**: Check `android:launchMode="singleTask"` in AndroidManifest.xml
|
||||
|
||||
### iOS
|
||||
|
||||
**Issue**: Deep links don't work in iOS simulator
|
||||
- **Solution**: Use `xcrun simctl openurl` instead of opening URLs in Safari
|
||||
|
||||
**Issue**: App launches but doesn't process URL
|
||||
- **Solution**: Check for Associated Domains if using universal links
|
||||
|
||||
## Advanced Debugging
|
||||
|
||||
### Enable Capacitor Native Logging
|
||||
|
||||
Add to `capacitor.config.json`:
|
||||
|
||||
```json
|
||||
{
|
||||
"ios": {
|
||||
"loggingBehavior": "debug"
|
||||
},
|
||||
"android": {
|
||||
"loggingBehavior": "debug"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Add Custom Debug Points
|
||||
|
||||
Insert additional logging in your deep link handler:
|
||||
|
||||
```typescript
|
||||
// Add at strategic points in DeepLinkHandler
|
||||
console.log('[DEBUG] Custom checkpoint:', { data: yourData });
|
||||
```
|
||||
|
||||
### Network Debugging
|
||||
|
||||
If deep links involve network requests:
|
||||
|
||||
```bash
|
||||
# Monitor network traffic (Android)
|
||||
adb shell dumpsys connectivity
|
||||
|
||||
# Monitor network traffic (iOS)
|
||||
# Use Xcode Network Debugger
|
||||
```
|
||||
|
||||
## Recovery Strategies
|
||||
|
||||
### If deep links stop working completely:
|
||||
|
||||
1. **Clean rebuild**:
|
||||
```bash
|
||||
rm -rf node_modules
|
||||
npm install
|
||||
npm run build:capacitor
|
||||
npx cap sync
|
||||
```
|
||||
|
||||
2. **Reset device/emulator**:
|
||||
- Clear app data
|
||||
- Uninstall and reinstall
|
||||
- Restart emulator
|
||||
|
||||
3. **Verify basic functionality**:
|
||||
- Test simple navigation within app
|
||||
- Test URL schemes with minimal URLs
|
||||
- Gradually increase complexity
|
||||
|
||||
## Support Resources
|
||||
|
||||
- [Capacitor Deep Links Documentation](https://capacitorjs.com/docs/guides/deep-links)
|
||||
- [Android Intent Filter Guide](https://developer.android.com/guide/components/intents-filters)
|
||||
- [iOS URL Scheme Guide](https://developer.apple.com/documentation/xcode/defining-a-custom-url-scheme-for-your-app)
|
||||
|
||||
## Contact and Feedback
|
||||
|
||||
If you encounter issues not covered in this guide:
|
||||
|
||||
1. Check the project's issue tracker
|
||||
2. Review recent commits for deep link changes
|
||||
3. Test with minimal reproduction case
|
||||
4. Document exact steps and environment details
|
||||
@@ -524,7 +524,7 @@
|
||||
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
|
||||
CODE_SIGN_ENTITLEMENTS = App/App.entitlements;
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 68;
|
||||
CURRENT_PROJECT_VERSION = 70;
|
||||
DEVELOPMENT_TEAM = GM3FS5JQPH;
|
||||
ENABLE_APP_SANDBOX = NO;
|
||||
ENABLE_USER_SCRIPT_SANDBOXING = NO;
|
||||
@@ -535,7 +535,7 @@
|
||||
"$(inherited)",
|
||||
"@executable_path/Frameworks",
|
||||
);
|
||||
MARKETING_VERSION = 1.4.2;
|
||||
MARKETING_VERSION = 1.4.4;
|
||||
OTHER_SWIFT_FLAGS = "$(inherited) \"-D\" \"COCOAPODS\" \"-DDEBUG\"";
|
||||
PRODUCT_BUNDLE_IDENTIFIER = app.timesafari;
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
@@ -553,7 +553,7 @@
|
||||
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
|
||||
CODE_SIGN_ENTITLEMENTS = App/App.entitlements;
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 68;
|
||||
CURRENT_PROJECT_VERSION = 70;
|
||||
DEVELOPMENT_TEAM = GM3FS5JQPH;
|
||||
ENABLE_APP_SANDBOX = NO;
|
||||
ENABLE_USER_SCRIPT_SANDBOXING = NO;
|
||||
@@ -564,7 +564,7 @@
|
||||
"$(inherited)",
|
||||
"@executable_path/Frameworks",
|
||||
);
|
||||
MARKETING_VERSION = 1.4.2;
|
||||
MARKETING_VERSION = 1.4.4;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = app.timesafari;
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
SWIFT_ACTIVE_COMPILATION_CONDITIONS = "";
|
||||
@@ -582,7 +582,7 @@
|
||||
CLANG_ENABLE_OBJC_WEAK = YES;
|
||||
CODE_SIGN_ENTITLEMENTS = TimeSafariShareExtension/TimeSafariShareExtension.entitlements;
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 68;
|
||||
CURRENT_PROJECT_VERSION = 70;
|
||||
DEVELOPMENT_TEAM = GM3FS5JQPH;
|
||||
GCC_C_LANGUAGE_STANDARD = gnu17;
|
||||
GENERATE_INFOPLIST_FILE = YES;
|
||||
@@ -596,7 +596,7 @@
|
||||
"@executable_path/../../Frameworks",
|
||||
);
|
||||
LOCALIZATION_PREFERS_STRING_CATALOGS = YES;
|
||||
MARKETING_VERSION = 1.4.2;
|
||||
MARKETING_VERSION = 1.4.4;
|
||||
MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE;
|
||||
MTL_FAST_MATH = YES;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = app.timesafari.TimeSafariShareExtension;
|
||||
@@ -620,7 +620,7 @@
|
||||
CLANG_ENABLE_OBJC_WEAK = YES;
|
||||
CODE_SIGN_ENTITLEMENTS = TimeSafariShareExtension/TimeSafariShareExtension.entitlements;
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 68;
|
||||
CURRENT_PROJECT_VERSION = 70;
|
||||
DEVELOPMENT_TEAM = GM3FS5JQPH;
|
||||
GCC_C_LANGUAGE_STANDARD = gnu17;
|
||||
GENERATE_INFOPLIST_FILE = YES;
|
||||
@@ -634,7 +634,7 @@
|
||||
"@executable_path/../../Frameworks",
|
||||
);
|
||||
LOCALIZATION_PREFERS_STRING_CATALOGS = YES;
|
||||
MARKETING_VERSION = 1.4.2;
|
||||
MARKETING_VERSION = 1.4.4;
|
||||
MTL_FAST_MATH = YES;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = app.timesafari.TimeSafariShareExtension;
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
|
||||
@@ -76,7 +76,7 @@ PODS:
|
||||
- SQLCipher/common (4.10.0)
|
||||
- SQLCipher/standard (4.10.0):
|
||||
- SQLCipher/common
|
||||
- TimesafariDailyNotificationPlugin (2.0.0):
|
||||
- TimesafariDailyNotificationPlugin (4.0.1):
|
||||
- Capacitor
|
||||
- ZIPFoundation (0.9.20)
|
||||
|
||||
@@ -162,7 +162,7 @@ SPEC CHECKSUMS:
|
||||
nanopb: fad817b59e0457d11a5dfbde799381cd727c1275
|
||||
PromisesObjC: f5707f49cb48b9636751c5b2e7d227e43fba9f47
|
||||
SQLCipher: eb79c64049cb002b4e9fcb30edb7979bf4706dfc
|
||||
TimesafariDailyNotificationPlugin: 3c12e8c39fc27f689f56cf4e57230a8c28611fcc
|
||||
TimesafariDailyNotificationPlugin: 69277c884380a9a620f671b68e0327eaa4b3d27d
|
||||
ZIPFoundation: dfd3d681c4053ff7e2f7350bc4e53b5dba3f5351
|
||||
|
||||
PODFILE CHECKSUM: 87c07d03f36ef38ab0c873802aee1ce9b5d34448
|
||||
|
||||
1611
package-lock.json
generated
@@ -1,7 +1,7 @@
|
||||
|
||||
{
|
||||
"name": "giftopia",
|
||||
"version": "1.4.2",
|
||||
"version": "1.4.4",
|
||||
"description": "Giftopia App",
|
||||
"author": {
|
||||
"name": "Gift Economies Team"
|
||||
@@ -14,6 +14,7 @@
|
||||
"prebuild": "eslint --ext .js,.ts,.vue --ignore-path .gitignore src && node sw_combine.js && node scripts/copy-wasm.js",
|
||||
"test:prerequisites": "node scripts/check-prerequisites.js",
|
||||
"check:dependencies": "./scripts/check-dependencies.sh",
|
||||
"deps:update-daily-notification-plugin": "npm install @timesafari/daily-notification-plugin@git+https://gitea.anomalistdesign.com/trent_larson/daily-notification-plugin.git#master",
|
||||
"test:all": "npm run lint && tsc && npm run test:web && npm run test:mobile && ./scripts/test-safety-check.sh && echo '\n\n\nGotta add the performance tests'",
|
||||
"test:web": "npx playwright test -c playwright.config-local.ts --trace on",
|
||||
"test:mobile": "./scripts/test-mobile.sh",
|
||||
|
||||
BIN
resources/android/icon/icon-background.png
Normal file
|
After Width: | Height: | Size: 704 KiB |
BIN
resources/android/icon/icon-foreground.png
Normal file
|
After Width: | Height: | Size: 242 KiB |
BIN
resources/android/icon/icon-monochrome.png
Normal file
|
After Width: | Height: | Size: 130 KiB |
|
Before Width: | Height: | Size: 279 KiB After Width: | Height: | Size: 736 KiB |
|
Before Width: | Height: | Size: 1.9 MiB After Width: | Height: | Size: 2.5 MiB |
|
Before Width: | Height: | Size: 1.9 MiB After Width: | Height: | Size: 1.9 MiB |
|
Before Width: | Height: | Size: 279 KiB After Width: | Height: | Size: 736 KiB |
BIN
resources/ios/icon/icon-dark.png
Normal file
|
After Width: | Height: | Size: 245 KiB |
|
Before Width: | Height: | Size: 279 KiB After Width: | Height: | Size: 736 KiB |
|
Before Width: | Height: | Size: 1.9 MiB After Width: | Height: | Size: 2.5 MiB |
|
Before Width: | Height: | Size: 1.9 MiB After Width: | Height: | Size: 1.9 MiB |
|
Before Width: | Height: | Size: 1.9 MiB After Width: | Height: | Size: 2.5 MiB |
|
Before Width: | Height: | Size: 1.9 MiB After Width: | Height: | Size: 1.9 MiB |
|
Before Width: | Height: | Size: 279 KiB After Width: | Height: | Size: 736 KiB |
@@ -192,6 +192,113 @@ check_ios_resources() {
|
||||
log_success "iOS resource check completed"
|
||||
}
|
||||
|
||||
# iOS app icon appearance variants (Dark, Tinted, …).
|
||||
# Luminosity appearance values match Apple's asset catalog format (iOS 18+).
|
||||
readonly _IOS_APP_ICON_APPEARANCE_LUMINOSITY="luminosity"
|
||||
readonly _IOS_APP_ICON_APPEARANCE_DARK="dark"
|
||||
readonly _IOS_APP_ICON_APPEARANCE_TINTED="tinted"
|
||||
|
||||
# Each row: source_file|luminosity_value|dest_filename
|
||||
# To add a variant: append one row and place the source PNG under resources/ios/.
|
||||
readonly _IOS_APP_ICON_APPEARANCE_VARIANTS=(
|
||||
"resources/ios/icon/icon-dark.png|${_IOS_APP_ICON_APPEARANCE_DARK}|AppIcon-Dark.png"
|
||||
"resources/ios/icon/icon-tinted.png|${_IOS_APP_ICON_APPEARANCE_TINTED}|AppIcon-Tinted.png"
|
||||
)
|
||||
|
||||
# Update AppIcon.appiconset/Contents.json with one 1024×1024 appearance entry.
|
||||
# Preserves all other images (including capacitor-assets output); replaces any
|
||||
# existing entry for the same luminosity appearance.
|
||||
_update_appicon_contents_for_appearance() {
|
||||
local contents_json="$1"
|
||||
local dest_filename="$2"
|
||||
local luminosity_value="$3"
|
||||
|
||||
local tmp_file
|
||||
tmp_file="$(mktemp)"
|
||||
|
||||
if ! jq --arg filename "$dest_filename" \
|
||||
--arg appearance "$_IOS_APP_ICON_APPEARANCE_LUMINOSITY" \
|
||||
--arg value "$luminosity_value" \
|
||||
'
|
||||
.images |= map(select((.appearances[0].value // "") != $value))
|
||||
| .images += [{
|
||||
"appearances": [{
|
||||
"appearance": $appearance,
|
||||
"value": $value
|
||||
}],
|
||||
"filename": $filename,
|
||||
"idiom": "universal",
|
||||
"platform": "ios",
|
||||
"size": "1024x1024"
|
||||
}]
|
||||
' "$contents_json" > "$tmp_file"; then
|
||||
rm -f "$tmp_file"
|
||||
log_error "Failed to update AppIcon Contents.json for appearance: $luminosity_value"
|
||||
return 1
|
||||
fi
|
||||
|
||||
mv "$tmp_file" "$contents_json"
|
||||
}
|
||||
|
||||
# Install one appearance variant when its source PNG exists.
|
||||
_apply_ios_app_icon_appearance_variant() {
|
||||
local appiconset_dir="$1"
|
||||
local contents_json="$2"
|
||||
local source_file="$3"
|
||||
local luminosity_value="$4"
|
||||
local dest_filename="$5"
|
||||
|
||||
if [ ! -f "$source_file" ]; then
|
||||
log_info "iOS app icon appearance variant not found ($source_file) — skipping"
|
||||
return 0
|
||||
fi
|
||||
|
||||
if [ ! -d "$appiconset_dir" ]; then
|
||||
log_warn "AppIcon.appiconset not found — skipping appearance variant ($luminosity_value)"
|
||||
return 0
|
||||
fi
|
||||
|
||||
if ! cp "$source_file" "$appiconset_dir/$dest_filename"; then
|
||||
log_error "Failed to copy $source_file to $appiconset_dir/$dest_filename"
|
||||
return 1
|
||||
fi
|
||||
|
||||
log_info "Installed iOS app icon appearance variant: $luminosity_value ($dest_filename)"
|
||||
_update_appicon_contents_for_appearance "$contents_json" "$dest_filename" "$luminosity_value"
|
||||
}
|
||||
|
||||
# Post-process capacitor-assets iOS icons: copy optional Dark/Tinted sources and
|
||||
# register them in AppIcon.appiconset/Contents.json.
|
||||
apply_ios_app_icon_appearances() {
|
||||
local appiconset_dir="ios/App/App/Assets.xcassets/AppIcon.appiconset"
|
||||
local contents_json="$appiconset_dir/Contents.json"
|
||||
|
||||
if [ ! -f "$contents_json" ]; then
|
||||
log_info "AppIcon Contents.json not found — skipping appearance variants"
|
||||
return 0
|
||||
fi
|
||||
|
||||
if ! command -v jq &> /dev/null; then
|
||||
log_error "jq is required to install iOS app icon appearance variants (install with: brew install jq)"
|
||||
return 1
|
||||
fi
|
||||
|
||||
local variant source_file luminosity_value dest_filename
|
||||
for variant in "${_IOS_APP_ICON_APPEARANCE_VARIANTS[@]}"; do
|
||||
IFS='|' read -r source_file luminosity_value dest_filename <<< "$variant"
|
||||
_apply_ios_app_icon_appearance_variant \
|
||||
"$appiconset_dir" "$contents_json" \
|
||||
"$source_file" "$luminosity_value" "$dest_filename"
|
||||
done
|
||||
}
|
||||
|
||||
# Generate iOS assets (capacitor-assets), then apply optional appearance variants.
|
||||
generate_ios_assets() {
|
||||
ensure_ios_capacitor_asset_directories
|
||||
npx capacitor-assets generate --ios
|
||||
apply_ios_app_icon_appearances
|
||||
}
|
||||
|
||||
# Function to clean iOS build
|
||||
clean_ios_build() {
|
||||
log_info "Cleaning iOS build artifacts..."
|
||||
@@ -424,8 +531,7 @@ fi
|
||||
# Handle assets-only mode
|
||||
if [ "$ASSETS_ONLY" = true ]; then
|
||||
log_info "Assets-only mode: generating assets"
|
||||
ensure_ios_capacitor_asset_directories
|
||||
safe_execute "Generating assets" "npx capacitor-assets generate --ios" || exit 7
|
||||
safe_execute "Generating assets" "generate_ios_assets" || exit 7
|
||||
log_success "Assets generation completed successfully!"
|
||||
exit 0
|
||||
fi
|
||||
@@ -574,8 +680,7 @@ safe_execute "Installing CocoaPods dependencies" "run_pod_install_with_workaroun
|
||||
safe_execute "Syncing with Capacitor" "run_cap_sync_with_workaround" || exit 6
|
||||
|
||||
# Step 7: Generate assets
|
||||
ensure_ios_capacitor_asset_directories
|
||||
safe_execute "Generating assets" "npx capacitor-assets generate --ios" || exit 7
|
||||
safe_execute "Generating assets" "generate_ios_assets" || exit 7
|
||||
|
||||
# Step 8: Build iOS app
|
||||
safe_execute "Building iOS app" "build_ios_app" || exit 5
|
||||
|
||||
@@ -8,14 +8,14 @@ notifications for conflicted entities * - Template streamlined with computed CSS
|
||||
properties * * @author Matthew Raymer */
|
||||
<template>
|
||||
<div id="sectionGiftedGiver">
|
||||
<label class="block font-bold mb-1">
|
||||
<label class="block font-semibold text-lg capitalize text-center">
|
||||
{{ stepLabel }}
|
||||
</label>
|
||||
<!-- Toggle link for entity type selection -->
|
||||
<div class="text-right mb-4">
|
||||
<div class="text-center mb-4">
|
||||
<button
|
||||
type="button"
|
||||
class="text-sm text-blue-600 hover:text-blue-800 underline font-medium"
|
||||
class="text-xs text-blue-600 hover:underline uppercase"
|
||||
@click="handleToggleEntityType"
|
||||
>
|
||||
{{ toggleLinkText }}
|
||||
|
||||
@@ -62,10 +62,6 @@ export class NativeNotificationService implements NotificationServiceInterface {
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Request notification permissions from the OS
|
||||
* Shows native permission dialog on first call
|
||||
*/
|
||||
/**
|
||||
* Request notification permissions from the OS
|
||||
* Shows native permission dialog on first call
|
||||
|
||||
@@ -1402,54 +1402,6 @@ export default class AccountViewView extends Vue {
|
||||
}, 150);
|
||||
}
|
||||
|
||||
/**
|
||||
* Toggle dev-only 10-minute rollover for daily reminder. Saves the setting and,
|
||||
* if reminder is already on, reschedules so the plugin uses the new interval.
|
||||
*/
|
||||
async toggleReminderFastRollover(): Promise<void> {
|
||||
const next = !this.reminderFastRolloverForTesting;
|
||||
await this.$saveSettings({
|
||||
reminderFastRolloverForTesting: next,
|
||||
});
|
||||
this.reminderFastRolloverForTesting = next;
|
||||
|
||||
if (this.notifyingReminder) {
|
||||
try {
|
||||
const service = NotificationService.getInstance();
|
||||
if (Capacitor.getPlatform() !== "android") {
|
||||
await service.cancelDailyNotification();
|
||||
}
|
||||
const time24h = this.parseTimeTo24Hour(this.notifyingReminderTime);
|
||||
const title = "Daily Reminder";
|
||||
const body =
|
||||
this.notifyingReminderMessage ||
|
||||
"Click to share some gratitude with the world -- even if they're unnamed.";
|
||||
await service.scheduleDailyNotification({
|
||||
time: time24h,
|
||||
title,
|
||||
body,
|
||||
priority: "normal",
|
||||
...(next ? { rolloverIntervalMinutes: 10 } : {}),
|
||||
});
|
||||
this.notify.success(
|
||||
next
|
||||
? "Reminder will repeat every 10 minutes (testing)."
|
||||
: "Reminder will repeat daily (24h).",
|
||||
TIMEOUTS.STANDARD,
|
||||
);
|
||||
} catch (err) {
|
||||
logger.error(
|
||||
"[AccountViewView] Reschedule after fast-rollover toggle failed:",
|
||||
err,
|
||||
);
|
||||
this.notify.error(
|
||||
"Failed to update reminder interval. Please try again.",
|
||||
TIMEOUTS.STANDARD,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse time string (e.g., "5:22 PM") to 24-hour format (e.g., "17:22")
|
||||
*/
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
<template>
|
||||
<QuickNav />
|
||||
|
||||
<!-- CONTENT -->
|
||||
<section id="Content" class="p-6 pb-24 max-w-3xl mx-auto">
|
||||
<TopMessage />
|
||||
|
||||
@@ -109,12 +109,14 @@ Raymer * @version 1.0.0 */
|
||||
|
||||
<div v-if="isUserRegistered" id="sectionRecordSomethingGiven">
|
||||
<!-- Record Quick-Action -->
|
||||
<div class="mb-6">
|
||||
<div class="flex gap-2 items-center mb-2">
|
||||
<div
|
||||
class="bg-slate-100 border border-slate-300 rounded-md px-4 py-2.5 mb-4"
|
||||
>
|
||||
<div class="flex gap-2 justify-between items-center ps-8">
|
||||
<!-- Thank button - always visible and unchanged -->
|
||||
<button
|
||||
type="button"
|
||||
class="text-center text-base uppercase bg-gradient-to-b from-green-400 to-green-700 shadow-[inset_0_-1px_0_0_rgba(0,0,0,0.5)] text-white flex items-center justify-center gap-2 px-4 py-3 rounded-full"
|
||||
class="text-center text-xl uppercase font-bold bg-gradient-to-b from-green-400 to-green-700 shadow-[inset_0_-1px_0_0_rgba(0,0,0,0.5)] text-white flex items-center justify-center gap-2 mx-auto px-6 py-2 rounded-lg"
|
||||
@click="openPersonDialog()"
|
||||
>
|
||||
<font-awesome icon="plus" />
|
||||
@@ -122,25 +124,29 @@ Raymer * @version 1.0.0 */
|
||||
</button>
|
||||
<!-- Plus button - appears when scrolled, positioned over house-chimney icon -->
|
||||
<transition
|
||||
enter-active-class="transition-all duration-1000 ease-out"
|
||||
leave-active-class="transition-all duration-1000 ease-in"
|
||||
enter-from-class="scale-0"
|
||||
enter-to-class="scale-100"
|
||||
leave-from-class="scale-100"
|
||||
leave-to-class="scale-0"
|
||||
enter-active-class="transition-all duration-500 ease-out"
|
||||
leave-active-class="transition-all duration-500 ease-in"
|
||||
enter-from-class="opacity-0"
|
||||
enter-to-class="opacity-100"
|
||||
leave-from-class="opacity-100"
|
||||
leave-to-class="opacity-0"
|
||||
>
|
||||
<button
|
||||
<div
|
||||
v-if="isScrolled"
|
||||
type="button"
|
||||
class="fixed bottom-10 p-4 w-14 h-14 z-50 text-center bg-gradient-to-b from-green-400 to-green-700 shadow-[inset_0_-1px_0_0_rgba(0,0,0,0.5)] text-white rounded-full flex items-center justify-center"
|
||||
:style="getButtonPosition()"
|
||||
@click="openPersonDialog()"
|
||||
class="bg-gradient-to-t from-white to-transparent fixed inset-x-0 bottom-[calc(4.75rem+max(env(safe-area-inset-bottom),var(--safe-area-inset-bottom,0px)))] px-4 pb-3 w-full z-[49]"
|
||||
>
|
||||
<font-awesome icon="plus" />
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
class="text-center text-xl uppercase font-bold bg-gradient-to-b from-green-400 to-green-700 shadow-[inset_0_-1px_0_0_rgba(0,0,0,0.5)] text-white flex items-center justify-center gap-2 mx-auto px-6 py-2 rounded-lg drop-shadow-[0_0_10px_rgba(255,255,255,1)]"
|
||||
@click="openPersonDialog()"
|
||||
>
|
||||
<font-awesome icon="plus" />
|
||||
<span>Thank</span>
|
||||
</button>
|
||||
</div>
|
||||
</transition>
|
||||
<button
|
||||
class="block ms-auto text-sm text-center text-white bg-gradient-to-b from-slate-400 to-slate-700 shadow-[inset_0_-1px_0_0_rgba(0,0,0,0.5)] p-1.5 rounded-full"
|
||||
class="block text-sm text-center text-white bg-gradient-to-b from-slate-400 to-slate-700 shadow-[inset_0_-1px_0_0_rgba(0,0,0,0.5)] p-1.5 rounded-full"
|
||||
@click="openGiftedPrompts()"
|
||||
>
|
||||
<font-awesome
|
||||
@@ -559,7 +565,7 @@ export default class HomeView extends Vue {
|
||||
const scrollTop = appElement
|
||||
? appElement.scrollTop
|
||||
: window.pageYOffset || document.documentElement.scrollTop || 0;
|
||||
const shouldBeScrolled = scrollTop > 100;
|
||||
const shouldBeScrolled = scrollTop > 120;
|
||||
if (this.isScrolled !== shouldBeScrolled) {
|
||||
this.isScrolled = shouldBeScrolled;
|
||||
}
|
||||
|
||||