Compare commits
44 Commits
notify-api
...
giftopia-a
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
6d221ee1ca | ||
|
|
823db447ca | ||
| 87ffa025e8 | |||
| 15c9088736 | |||
| ec41dd52d5 | |||
| 463db39a6b | |||
| fe97dff752 | |||
|
|
903047f13b | ||
|
|
48be234af4 | ||
| 6c0907d905 | |||
|
|
8d8bcf2a7e | ||
| a4b47904c8 | |||
|
|
bb890baacf | ||
| dae23300fe | |||
| 9e401febea | |||
| cd4b279703 | |||
| a3a2d97b9a | |||
| 802050259c | |||
| efd7d50a84 | |||
| 39c389cda8 | |||
| 93fdcaf7ff | |||
| ad419efa0d | |||
| ce45ddb2bd | |||
| 7d306bd204 | |||
| 9713313a40 | |||
|
|
ffa7bac319 | ||
| e0e0a0a183 | |||
| ea662f4430 | |||
| 81647e1f3c | |||
| bf1ee78025 | |||
|
|
66b7d0f46e | ||
|
|
63dcf44125 | ||
| cf1ecdfb4c | |||
| e9ad61b780 | |||
| ad8df3eb93 | |||
| 05d346edce | |||
| e259e60fa7 | |||
| 821de3f006 | |||
| 43f83031d4 | |||
| 688a48a332 | |||
| 8938c242ee | |||
| 358af42afd | |||
| 59c00241b8 | |||
| 33ec90e571 |
@@ -1 +1 @@
|
||||
18.19.0
|
||||
20.18.1
|
||||
|
||||
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.
|
||||
19
BUILDING.md
@@ -333,11 +333,11 @@ The `serve` functionality provides a local HTTP server for testing production bu
|
||||
- If there are DB changes: before updating the test server, open browser(s) with
|
||||
current version to test DB migrations.
|
||||
|
||||
- Update the ClickUp tasks & CHANGELOG.md & the version in package.json, run
|
||||
- Update the ClickUp tasks & CHANGELOG.md & the version in package.json, run:
|
||||
`npm install`.
|
||||
|
||||
- Run a build to make sure package-lock version is updated, linting works, etc:
|
||||
`npm install && npm run build:web`
|
||||
- Run a build to make sure linting works, etc:
|
||||
`npm run build:web`
|
||||
|
||||
- Commit everything (since the commit hash is used the app).
|
||||
|
||||
@@ -346,7 +346,7 @@ current version to test DB migrations.
|
||||
|
||||
- Tag with the new version,
|
||||
[online](https://gitea.anomalistdesign.com/trent_larson/crowd-funder-for-time-pwa/releases) or
|
||||
`git tag 1.0.2 && git push origin 1.0.2`.
|
||||
`git tag 1.3.13 && git push origin 1.3.13`.
|
||||
|
||||
- For test, build the app:
|
||||
|
||||
@@ -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 65 && perl -p -i -e "s/MARKETING_VERSION = .*;/MARKETING_VERSION = 1.3.8;/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 65/g' android/app/build.gradle
|
||||
perl -p -i -e 's/versionName .*/versionName "1.3.8"/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.
|
||||
|
||||
29
CHANGELOG.md
@@ -6,9 +6,36 @@ 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.3.8] - 2026
|
||||
## [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
|
||||
|
||||
|
||||
## [1.3.13] - 2026.04.05
|
||||
### Added
|
||||
- Ability to select project that the current one fulfills
|
||||
- Separate Terms & Conditions page (required for SMS campaigns)
|
||||
### Fixed
|
||||
- Edits to a 'give' would delete the image
|
||||
|
||||
|
||||
## [1.3.12] - 2026.03.21
|
||||
### Added
|
||||
- Device wake-up for notifications
|
||||
### Changed
|
||||
- Rename to "Gifties"
|
||||
|
||||
|
||||
## [1.3.7]
|
||||
|
||||
23
README.md
@@ -15,10 +15,31 @@ Quick start:
|
||||
|
||||
```bash
|
||||
npm install
|
||||
```
|
||||
|
||||
### Web
|
||||
|
||||
```bash
|
||||
npm run build:web:dev
|
||||
```
|
||||
|
||||
To be able to take action on the platform: go to [the test page](http://localhost:8080/test) and click "Become User 0".
|
||||
Then go to [the test page](http://localhost:8080/test) and click "Become User 0" to take action on the platform.
|
||||
|
||||
### Android
|
||||
|
||||
```bash
|
||||
npm run build:android:test:run
|
||||
```
|
||||
|
||||
Assumes ADB is installed; see [Android Build](BUILDING.md#android-build) for SDK, emulator, and `PATH` setup.
|
||||
|
||||
### iOS
|
||||
|
||||
```bash
|
||||
npm run build:ios:studio
|
||||
```
|
||||
|
||||
Assumes Xcode and Xcode Command Line Tools are installed.
|
||||
|
||||
See [BUILDING.md](BUILDING.md) for comprehensive build instructions for all platforms (Web, Electron, iOS, Android, Docker).
|
||||
|
||||
|
||||
@@ -29,16 +29,16 @@ android {
|
||||
compileSdk rootProject.ext.compileSdkVersion
|
||||
|
||||
compileOptions {
|
||||
sourceCompatibility JavaVersion.VERSION_17
|
||||
targetCompatibility JavaVersion.VERSION_17
|
||||
sourceCompatibility JavaVersion.VERSION_21
|
||||
targetCompatibility JavaVersion.VERSION_21
|
||||
}
|
||||
|
||||
defaultConfig {
|
||||
applicationId "app.timesafari.app"
|
||||
minSdkVersion rootProject.ext.minSdkVersion
|
||||
targetSdkVersion rootProject.ext.targetSdkVersion
|
||||
versionCode 65
|
||||
versionName "1.3.8"
|
||||
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.
|
||||
@@ -72,13 +72,14 @@ android {
|
||||
}
|
||||
packagingOptions {
|
||||
jniLibs {
|
||||
// Required for 16 KB page-size support: keep native libs uncompressed and
|
||||
// page-aligned inside the APK (default on AGP 8.x with minSdk 23+, set
|
||||
// explicitly so it does not regress).
|
||||
useLegacyPackaging = false
|
||||
pickFirsts += ['**/lib/x86_64/libbarhopper_v3.so', '**/lib/x86_64/libimage_processing_util_jni.so', '**/lib/x86_64/libsqlcipher.so']
|
||||
}
|
||||
}
|
||||
|
||||
// Configure for 16 KB page size compatibility
|
||||
|
||||
|
||||
// Enable bundle builds (without which it doesn't work right for bundleDebug vs bundleRelease)
|
||||
bundle {
|
||||
language {
|
||||
@@ -129,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}")
|
||||
}
|
||||
|
||||
@@ -2,8 +2,8 @@
|
||||
|
||||
android {
|
||||
compileOptions {
|
||||
sourceCompatibility JavaVersion.VERSION_17
|
||||
targetCompatibility JavaVersion.VERSION_17
|
||||
sourceCompatibility JavaVersion.VERSION_21
|
||||
targetCompatibility JavaVersion.VERSION_21
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"appId": "app.timesafari",
|
||||
"appName": "TimeSafari",
|
||||
"appName": "Giftopia",
|
||||
"webDir": "dist",
|
||||
"server": {
|
||||
"cleartext": true
|
||||
@@ -34,12 +34,12 @@
|
||||
"iosIsEncryption": false,
|
||||
"iosBiometric": {
|
||||
"biometricAuth": false,
|
||||
"biometricTitle": "Biometric login for TimeSafari"
|
||||
"biometricTitle": "Biometric login for Giftopia"
|
||||
},
|
||||
"androidIsEncryption": false,
|
||||
"androidBiometric": {
|
||||
"biometricAuth": false,
|
||||
"biometricTitle": "Biometric login for TimeSafari"
|
||||
"biometricTitle": "Biometric login for Giftopia"
|
||||
},
|
||||
"electronIsEncryption": false
|
||||
},
|
||||
@@ -100,7 +100,7 @@
|
||||
},
|
||||
"buildOptions": {
|
||||
"appId": "app.timesafari",
|
||||
"productName": "TimeSafari",
|
||||
"productName": "Giftopia",
|
||||
"directories": {
|
||||
"output": "dist-electron-packages"
|
||||
},
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<?xml version='1.0' encoding='utf-8'?>
|
||||
<resources>
|
||||
<string name="app_name">TimeSafari</string>
|
||||
<string name="title_activity_main">TimeSafari</string>
|
||||
<string name="app_name">Giftopia</string>
|
||||
<string name="title_activity_main">Giftopia</string>
|
||||
<string name="package_name">timesafari.app</string>
|
||||
<string name="custom_url_scheme">timesafari.app</string>
|
||||
</resources>
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
ext {
|
||||
androidxAppCompatVersion = project.hasProperty('androidxAppCompatVersion') ? rootProject.ext.androidxAppCompatVersion : '1.6.1'
|
||||
androidxAppCompatVersion = project.hasProperty('androidxAppCompatVersion') ? rootProject.ext.androidxAppCompatVersion : '1.7.0'
|
||||
cordovaAndroidVersion = project.hasProperty('cordovaAndroidVersion') ? rootProject.ext.cordovaAndroidVersion : '10.1.1'
|
||||
}
|
||||
|
||||
@@ -9,7 +9,7 @@ buildscript {
|
||||
mavenCentral()
|
||||
}
|
||||
dependencies {
|
||||
classpath 'com.android.tools.build:gradle:8.2.1'
|
||||
classpath 'com.android.tools.build:gradle:8.7.2'
|
||||
}
|
||||
}
|
||||
|
||||
@@ -17,10 +17,10 @@ apply plugin: 'com.android.library'
|
||||
|
||||
android {
|
||||
namespace "capacitor.cordova.android.plugins"
|
||||
compileSdk project.hasProperty('compileSdkVersion') ? rootProject.ext.compileSdkVersion : 34
|
||||
compileSdk project.hasProperty('compileSdkVersion') ? rootProject.ext.compileSdkVersion : 35
|
||||
defaultConfig {
|
||||
minSdkVersion project.hasProperty('minSdkVersion') ? rootProject.ext.minSdkVersion : 22
|
||||
targetSdkVersion project.hasProperty('targetSdkVersion') ? rootProject.ext.targetSdkVersion : 34
|
||||
minSdkVersion project.hasProperty('minSdkVersion') ? rootProject.ext.minSdkVersion : 23
|
||||
targetSdkVersion project.hasProperty('targetSdkVersion') ? rootProject.ext.targetSdkVersion : 35
|
||||
versionCode 1
|
||||
versionName "1.0"
|
||||
}
|
||||
@@ -28,8 +28,8 @@ android {
|
||||
abortOnError false
|
||||
}
|
||||
compileOptions {
|
||||
sourceCompatibility JavaVersion.VERSION_17
|
||||
targetCompatibility JavaVersion.VERSION_17
|
||||
sourceCompatibility JavaVersion.VERSION_21
|
||||
targetCompatibility JavaVersion.VERSION_21
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
// DO NOT EDIT THIS FILE! IT IS GENERATED EACH TIME "capacitor update" IS RUN
|
||||
ext {
|
||||
cdvMinSdkVersion = project.hasProperty('minSdkVersion') ? rootProject.ext.minSdkVersion : 22
|
||||
cdvMinSdkVersion = project.hasProperty('minSdkVersion') ? rootProject.ext.minSdkVersion : 23
|
||||
// Plugin gradle extensions can append to this to have code run at the end.
|
||||
cdvPluginPostBuildExtras = []
|
||||
cordovaConfig = [:]
|
||||
|
||||
@@ -13,4 +13,11 @@ ext {
|
||||
androidxJunitVersion = '1.1.5'
|
||||
androidxEspressoCoreVersion = '3.5.1'
|
||||
cordovaAndroidVersion = '10.1.1'
|
||||
|
||||
// Pin CameraX to 1.4.2: first stable line shipping a 16 KB page-size-aligned
|
||||
// libimage_processing_util_jni.so. The barcode-scanning plugin still defaults to 1.1.0.
|
||||
androidxCameraCamera2Version = '1.4.2'
|
||||
androidxCameraCoreVersion = '1.4.2'
|
||||
androidxCameraLifecycleVersion = '1.4.2'
|
||||
androidxCameraViewVersion = '1.4.2'
|
||||
}
|
||||
@@ -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"
|
||||
},
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"appId": "app.timesafari",
|
||||
"appName": "TimeSafari",
|
||||
"appName": "Giftopia",
|
||||
"webDir": "dist",
|
||||
"server": {
|
||||
"cleartext": true
|
||||
@@ -34,12 +34,12 @@
|
||||
"iosIsEncryption": false,
|
||||
"iosBiometric": {
|
||||
"biometricAuth": false,
|
||||
"biometricTitle": "Biometric login for TimeSafari"
|
||||
"biometricTitle": "Biometric login for Giftopia"
|
||||
},
|
||||
"androidIsEncryption": false,
|
||||
"androidBiometric": {
|
||||
"biometricAuth": false,
|
||||
"biometricTitle": "Biometric login for TimeSafari"
|
||||
"biometricTitle": "Biometric login for Giftopia"
|
||||
},
|
||||
"electronIsEncryption": false
|
||||
}
|
||||
@@ -73,7 +73,7 @@
|
||||
},
|
||||
"buildOptions": {
|
||||
"appId": "app.timesafari",
|
||||
"productName": "TimeSafari",
|
||||
"productName": "Giftopia",
|
||||
"directories": {
|
||||
"output": "dist-electron-packages"
|
||||
},
|
||||
|
||||
@@ -2,7 +2,7 @@ import { CapacitorConfig } from '@capacitor/cli';
|
||||
|
||||
const config: CapacitorConfig = {
|
||||
appId: 'app.timesafari',
|
||||
appName: 'TimeSafari',
|
||||
appName: 'Giftopia',
|
||||
webDir: 'dist',
|
||||
server: {
|
||||
cleartext: true
|
||||
@@ -36,12 +36,12 @@ const config: CapacitorConfig = {
|
||||
iosIsEncryption: false,
|
||||
iosBiometric: {
|
||||
biometricAuth: false,
|
||||
biometricTitle: 'Biometric login for TimeSafari'
|
||||
biometricTitle: 'Biometric login for Giftopia'
|
||||
},
|
||||
androidIsEncryption: false,
|
||||
androidBiometric: {
|
||||
biometricAuth: false,
|
||||
biometricTitle: 'Biometric login for TimeSafari'
|
||||
biometricTitle: 'Biometric login for Giftopia'
|
||||
},
|
||||
electronIsEncryption: false
|
||||
},
|
||||
@@ -100,7 +100,7 @@ const config: CapacitorConfig = {
|
||||
},
|
||||
buildOptions: {
|
||||
appId: 'app.timesafari',
|
||||
productName: 'TimeSafari',
|
||||
productName: 'Giftopia',
|
||||
directories: {
|
||||
output: 'dist-electron-packages'
|
||||
},
|
||||
|
||||
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
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"appId": "app.timesafari",
|
||||
"appName": "TimeSafari",
|
||||
"appName": "Giftopia",
|
||||
"webDir": "dist",
|
||||
"server": {
|
||||
"cleartext": true
|
||||
@@ -34,12 +34,12 @@
|
||||
"iosIsEncryption": false,
|
||||
"iosBiometric": {
|
||||
"biometricAuth": false,
|
||||
"biometricTitle": "Biometric login for TimeSafari"
|
||||
"biometricTitle": "Biometric login for Giftopia"
|
||||
},
|
||||
"androidIsEncryption": false,
|
||||
"androidBiometric": {
|
||||
"biometricAuth": false,
|
||||
"biometricTitle": "Biometric login for TimeSafari"
|
||||
"biometricTitle": "Biometric login for Giftopia"
|
||||
},
|
||||
"electronIsEncryption": false
|
||||
}
|
||||
@@ -72,7 +72,7 @@
|
||||
},
|
||||
"buildOptions": {
|
||||
"appId": "app.timesafari",
|
||||
"productName": "TimeSafari",
|
||||
"productName": "Giftopia",
|
||||
"directories": {
|
||||
"output": "dist-electron-packages"
|
||||
},
|
||||
|
||||
3
ios/.gitignore
vendored
@@ -17,6 +17,7 @@ App/App/config.xml
|
||||
App/App.xcodeproj/xcuserdata/*.xcuserdatad/
|
||||
App/App.xcodeproj/*.xcuserstate
|
||||
|
||||
# Generated Icons from capacitor-assets (also Contents.json which is confusing; see BUILDING.md)
|
||||
# Generated by capacitor-assets at build time (not in repo). Fresh clones lack these
|
||||
# folders; scripts/common.sh ensure_ios_capacitor_asset_directories creates them before generate.
|
||||
App/App/Assets.xcassets/AppIcon.appiconset
|
||||
App/App/Assets.xcassets/Splash.imageset
|
||||
|
||||
@@ -452,7 +452,7 @@
|
||||
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
|
||||
GCC_WARN_UNUSED_FUNCTION = YES;
|
||||
GCC_WARN_UNUSED_VARIABLE = YES;
|
||||
IPHONEOS_DEPLOYMENT_TARGET = 13.0;
|
||||
IPHONEOS_DEPLOYMENT_TARGET = 14.0;
|
||||
MTL_ENABLE_DEBUG_INFO = YES;
|
||||
ONLY_ACTIVE_ARCH = YES;
|
||||
SDKROOT = iphoneos;
|
||||
@@ -508,7 +508,7 @@
|
||||
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
|
||||
GCC_WARN_UNUSED_FUNCTION = YES;
|
||||
GCC_WARN_UNUSED_VARIABLE = YES;
|
||||
IPHONEOS_DEPLOYMENT_TARGET = 13.0;
|
||||
IPHONEOS_DEPLOYMENT_TARGET = 14.0;
|
||||
MTL_ENABLE_DEBUG_INFO = NO;
|
||||
SDKROOT = iphoneos;
|
||||
SWIFT_COMPILATION_MODE = wholemodule;
|
||||
@@ -524,17 +524,18 @@
|
||||
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
|
||||
CODE_SIGN_ENTITLEMENTS = App/App.entitlements;
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 65;
|
||||
CURRENT_PROJECT_VERSION = 70;
|
||||
DEVELOPMENT_TEAM = GM3FS5JQPH;
|
||||
ENABLE_APP_SANDBOX = NO;
|
||||
ENABLE_USER_SCRIPT_SANDBOXING = NO;
|
||||
INFOPLIST_FILE = App/Info.plist;
|
||||
IPHONEOS_DEPLOYMENT_TARGET = 13.0;
|
||||
INFOPLIST_KEY_CFBundleDisplayName = Giftopia;
|
||||
IPHONEOS_DEPLOYMENT_TARGET = 15.5;
|
||||
LD_RUNPATH_SEARCH_PATHS = (
|
||||
"$(inherited)",
|
||||
"@executable_path/Frameworks",
|
||||
);
|
||||
MARKETING_VERSION = 1.3.8;
|
||||
MARKETING_VERSION = 1.4.4;
|
||||
OTHER_SWIFT_FLAGS = "$(inherited) \"-D\" \"COCOAPODS\" \"-DDEBUG\"";
|
||||
PRODUCT_BUNDLE_IDENTIFIER = app.timesafari;
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
@@ -552,17 +553,18 @@
|
||||
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
|
||||
CODE_SIGN_ENTITLEMENTS = App/App.entitlements;
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 65;
|
||||
CURRENT_PROJECT_VERSION = 70;
|
||||
DEVELOPMENT_TEAM = GM3FS5JQPH;
|
||||
ENABLE_APP_SANDBOX = NO;
|
||||
ENABLE_USER_SCRIPT_SANDBOXING = NO;
|
||||
INFOPLIST_FILE = App/Info.plist;
|
||||
IPHONEOS_DEPLOYMENT_TARGET = 13.0;
|
||||
INFOPLIST_KEY_CFBundleDisplayName = Giftopia;
|
||||
IPHONEOS_DEPLOYMENT_TARGET = 15.5;
|
||||
LD_RUNPATH_SEARCH_PATHS = (
|
||||
"$(inherited)",
|
||||
"@executable_path/Frameworks",
|
||||
);
|
||||
MARKETING_VERSION = 1.3.8;
|
||||
MARKETING_VERSION = 1.4.4;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = app.timesafari;
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
SWIFT_ACTIVE_COMPILATION_CONDITIONS = "";
|
||||
@@ -580,12 +582,12 @@
|
||||
CLANG_ENABLE_OBJC_WEAK = YES;
|
||||
CODE_SIGN_ENTITLEMENTS = TimeSafariShareExtension/TimeSafariShareExtension.entitlements;
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 65;
|
||||
CURRENT_PROJECT_VERSION = 70;
|
||||
DEVELOPMENT_TEAM = GM3FS5JQPH;
|
||||
GCC_C_LANGUAGE_STANDARD = gnu17;
|
||||
GENERATE_INFOPLIST_FILE = YES;
|
||||
INFOPLIST_FILE = TimeSafariShareExtension/Info.plist;
|
||||
INFOPLIST_KEY_CFBundleDisplayName = TimeSafari;
|
||||
INFOPLIST_KEY_CFBundleDisplayName = Giftopia;
|
||||
INFOPLIST_KEY_NSHumanReadableCopyright = "";
|
||||
IPHONEOS_DEPLOYMENT_TARGET = 14.0;
|
||||
LD_RUNPATH_SEARCH_PATHS = (
|
||||
@@ -594,7 +596,7 @@
|
||||
"@executable_path/../../Frameworks",
|
||||
);
|
||||
LOCALIZATION_PREFERS_STRING_CATALOGS = YES;
|
||||
MARKETING_VERSION = 1.3.8;
|
||||
MARKETING_VERSION = 1.4.4;
|
||||
MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE;
|
||||
MTL_FAST_MATH = YES;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = app.timesafari.TimeSafariShareExtension;
|
||||
@@ -618,12 +620,12 @@
|
||||
CLANG_ENABLE_OBJC_WEAK = YES;
|
||||
CODE_SIGN_ENTITLEMENTS = TimeSafariShareExtension/TimeSafariShareExtension.entitlements;
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 65;
|
||||
CURRENT_PROJECT_VERSION = 70;
|
||||
DEVELOPMENT_TEAM = GM3FS5JQPH;
|
||||
GCC_C_LANGUAGE_STANDARD = gnu17;
|
||||
GENERATE_INFOPLIST_FILE = YES;
|
||||
INFOPLIST_FILE = TimeSafariShareExtension/Info.plist;
|
||||
INFOPLIST_KEY_CFBundleDisplayName = TimeSafari;
|
||||
INFOPLIST_KEY_CFBundleDisplayName = Giftopia;
|
||||
INFOPLIST_KEY_NSHumanReadableCopyright = "";
|
||||
IPHONEOS_DEPLOYMENT_TARGET = 14.0;
|
||||
LD_RUNPATH_SEARCH_PATHS = (
|
||||
@@ -632,7 +634,7 @@
|
||||
"@executable_path/../../Frameworks",
|
||||
);
|
||||
LOCALIZATION_PREFERS_STRING_CATALOGS = YES;
|
||||
MARKETING_VERSION = 1.3.8;
|
||||
MARKETING_VERSION = 1.4.4;
|
||||
MTL_FAST_MATH = YES;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = app.timesafari.TimeSafariShareExtension;
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
<key>CFBundleDevelopmentRegion</key>
|
||||
<string>en</string>
|
||||
<key>CFBundleDisplayName</key>
|
||||
<string>TimeSafari</string>
|
||||
<string>Giftopia</string>
|
||||
<key>CFBundleExecutable</key>
|
||||
<string>$(EXECUTABLE_NAME)</string>
|
||||
<key>CFBundleIdentifier</key>
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
require_relative '../../node_modules/@capacitor/ios/scripts/pods_helpers'
|
||||
|
||||
platform :ios, '13.0'
|
||||
platform :ios, '15.5'
|
||||
use_frameworks!
|
||||
|
||||
# workaround to avoid Xcode caching of Pods that requires
|
||||
@@ -30,9 +30,4 @@ end
|
||||
|
||||
post_install do |installer|
|
||||
assertDeploymentTarget(installer)
|
||||
installer.pods_project.targets.each do |target|
|
||||
target.build_configurations.each do |config|
|
||||
config.build_settings['EXCLUDED_ARCHS[sdk=iphonesimulator*]'] = 'arm64'
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@@ -1,94 +1,84 @@
|
||||
PODS:
|
||||
- Capacitor (6.2.1):
|
||||
- Capacitor (7.6.4):
|
||||
- CapacitorCordova
|
||||
- CapacitorApp (6.0.2):
|
||||
- CapacitorApp (7.1.2):
|
||||
- Capacitor
|
||||
- CapacitorCamera (6.1.2):
|
||||
- CapacitorCamera (7.0.5):
|
||||
- Capacitor
|
||||
- CapacitorClipboard (6.0.2):
|
||||
- CapacitorClipboard (7.0.4):
|
||||
- Capacitor
|
||||
- CapacitorCommunitySqlite (6.0.2):
|
||||
- CapacitorCommunitySqlite (7.0.3):
|
||||
- Capacitor
|
||||
- SQLCipher
|
||||
- ZIPFoundation
|
||||
- CapacitorCordova (6.2.1)
|
||||
- CapacitorFilesystem (6.0.3):
|
||||
- CapacitorCordova (7.6.4)
|
||||
- CapacitorFilesystem (7.1.8):
|
||||
- Capacitor
|
||||
- CapacitorMlkitBarcodeScanning (6.2.0):
|
||||
- IONFilesystemLib (~> 1.1.1)
|
||||
- CapacitorMlkitBarcodeScanning (7.5.0):
|
||||
- Capacitor
|
||||
- GoogleMLKit/BarcodeScanning (= 5.0.0)
|
||||
- CapacitorShare (6.0.3):
|
||||
- GoogleMLKit/BarcodeScanning (= 7.0.0)
|
||||
- CapacitorShare (7.0.4):
|
||||
- Capacitor
|
||||
- CapacitorStatusBar (6.0.2):
|
||||
- CapacitorStatusBar (7.0.6):
|
||||
- Capacitor
|
||||
- CapawesomeCapacitorFilePicker (6.2.0):
|
||||
- CapawesomeCapacitorFilePicker (7.2.0):
|
||||
- Capacitor
|
||||
- GoogleDataTransport (9.4.1):
|
||||
- GoogleUtilities/Environment (~> 7.7)
|
||||
- nanopb (< 2.30911.0, >= 2.30908.0)
|
||||
- PromisesObjC (< 3.0, >= 1.2)
|
||||
- GoogleMLKit/BarcodeScanning (5.0.0):
|
||||
- GoogleDataTransport (10.1.0):
|
||||
- nanopb (~> 3.30910.0)
|
||||
- PromisesObjC (~> 2.4)
|
||||
- GoogleMLKit/BarcodeScanning (7.0.0):
|
||||
- GoogleMLKit/MLKitCore
|
||||
- MLKitBarcodeScanning (~> 4.0.0)
|
||||
- GoogleMLKit/MLKitCore (5.0.0):
|
||||
- MLKitCommon (~> 10.0.0)
|
||||
- GoogleToolboxForMac/DebugUtils (2.3.2):
|
||||
- GoogleToolboxForMac/Defines (= 2.3.2)
|
||||
- GoogleToolboxForMac/Defines (2.3.2)
|
||||
- GoogleToolboxForMac/Logger (2.3.2):
|
||||
- GoogleToolboxForMac/Defines (= 2.3.2)
|
||||
- "GoogleToolboxForMac/NSData+zlib (2.3.2)":
|
||||
- GoogleToolboxForMac/Defines (= 2.3.2)
|
||||
- "GoogleToolboxForMac/NSDictionary+URLArguments (2.3.2)":
|
||||
- GoogleToolboxForMac/DebugUtils (= 2.3.2)
|
||||
- GoogleToolboxForMac/Defines (= 2.3.2)
|
||||
- "GoogleToolboxForMac/NSString+URLArguments (= 2.3.2)"
|
||||
- "GoogleToolboxForMac/NSString+URLArguments (2.3.2)"
|
||||
- GoogleUtilities/Environment (7.13.3):
|
||||
- MLKitBarcodeScanning (~> 6.0.0)
|
||||
- GoogleMLKit/MLKitCore (7.0.0):
|
||||
- MLKitCommon (~> 12.0.0)
|
||||
- GoogleToolboxForMac/Defines (4.2.1)
|
||||
- GoogleToolboxForMac/Logger (4.2.1):
|
||||
- GoogleToolboxForMac/Defines (= 4.2.1)
|
||||
- "GoogleToolboxForMac/NSData+zlib (4.2.1)":
|
||||
- GoogleToolboxForMac/Defines (= 4.2.1)
|
||||
- GoogleUtilities/Environment (8.1.0):
|
||||
- GoogleUtilities/Privacy
|
||||
- PromisesObjC (< 3.0, >= 1.2)
|
||||
- GoogleUtilities/Logger (7.13.3):
|
||||
- GoogleUtilities/Logger (8.1.0):
|
||||
- GoogleUtilities/Environment
|
||||
- GoogleUtilities/Privacy
|
||||
- GoogleUtilities/Privacy (7.13.3)
|
||||
- GoogleUtilities/UserDefaults (7.13.3):
|
||||
- GoogleUtilities/Privacy (8.1.0)
|
||||
- GoogleUtilities/UserDefaults (8.1.0):
|
||||
- GoogleUtilities/Logger
|
||||
- GoogleUtilities/Privacy
|
||||
- GoogleUtilitiesComponents (1.1.0):
|
||||
- GoogleUtilities/Logger
|
||||
- GTMSessionFetcher/Core (3.5.0)
|
||||
- MLImage (1.0.0-beta5)
|
||||
- MLKitBarcodeScanning (4.0.0):
|
||||
- MLKitCommon (~> 10.0)
|
||||
- MLKitVision (~> 6.0)
|
||||
- MLKitCommon (10.0.0):
|
||||
- GoogleDataTransport (~> 9.0)
|
||||
- GoogleToolboxForMac/Logger (~> 2.1)
|
||||
- "GoogleToolboxForMac/NSData+zlib (~> 2.1)"
|
||||
- "GoogleToolboxForMac/NSDictionary+URLArguments (~> 2.1)"
|
||||
- GoogleUtilities/UserDefaults (~> 7.0)
|
||||
- GoogleUtilitiesComponents (~> 1.0)
|
||||
- GTMSessionFetcher/Core (< 4.0, >= 1.1)
|
||||
- MLKitVision (6.0.0):
|
||||
- GoogleToolboxForMac/Logger (~> 2.1)
|
||||
- "GoogleToolboxForMac/NSData+zlib (~> 2.1)"
|
||||
- GTMSessionFetcher/Core (< 4.0, >= 1.1)
|
||||
- MLImage (= 1.0.0-beta5)
|
||||
- MLKitCommon (~> 10.0)
|
||||
- nanopb (2.30910.0):
|
||||
- nanopb/decode (= 2.30910.0)
|
||||
- nanopb/encode (= 2.30910.0)
|
||||
- nanopb/decode (2.30910.0)
|
||||
- nanopb/encode (2.30910.0)
|
||||
- IONFilesystemLib (1.1.2)
|
||||
- MLImage (1.0.0-beta6)
|
||||
- MLKitBarcodeScanning (6.0.0):
|
||||
- MLKitCommon (~> 12.0)
|
||||
- MLKitVision (~> 8.0)
|
||||
- MLKitCommon (12.0.0):
|
||||
- GoogleDataTransport (~> 10.0)
|
||||
- GoogleToolboxForMac/Logger (< 5.0, >= 4.2.1)
|
||||
- "GoogleToolboxForMac/NSData+zlib (< 5.0, >= 4.2.1)"
|
||||
- GoogleUtilities/Logger (~> 8.0)
|
||||
- GoogleUtilities/UserDefaults (~> 8.0)
|
||||
- GTMSessionFetcher/Core (< 4.0, >= 3.3.2)
|
||||
- MLKitVision (8.0.0):
|
||||
- GoogleToolboxForMac/Logger (< 5.0, >= 4.2.1)
|
||||
- "GoogleToolboxForMac/NSData+zlib (< 5.0, >= 4.2.1)"
|
||||
- GTMSessionFetcher/Core (< 4.0, >= 3.3.2)
|
||||
- MLImage (= 1.0.0-beta6)
|
||||
- MLKitCommon (~> 12.0)
|
||||
- nanopb (3.30910.0):
|
||||
- nanopb/decode (= 3.30910.0)
|
||||
- nanopb/encode (= 3.30910.0)
|
||||
- nanopb/decode (3.30910.0)
|
||||
- nanopb/encode (3.30910.0)
|
||||
- PromisesObjC (2.4.0)
|
||||
- SQLCipher (4.9.0):
|
||||
- SQLCipher/standard (= 4.9.0)
|
||||
- SQLCipher/common (4.9.0)
|
||||
- SQLCipher/standard (4.9.0):
|
||||
- SQLCipher (4.10.0):
|
||||
- SQLCipher/standard (= 4.10.0)
|
||||
- SQLCipher/common (4.10.0)
|
||||
- SQLCipher/standard (4.10.0):
|
||||
- SQLCipher/common
|
||||
- TimesafariDailyNotificationPlugin (2.0.0):
|
||||
- TimesafariDailyNotificationPlugin (4.0.1):
|
||||
- Capacitor
|
||||
- ZIPFoundation (0.9.19)
|
||||
- ZIPFoundation (0.9.20)
|
||||
|
||||
DEPENDENCIES:
|
||||
- "Capacitor (from `../../node_modules/@capacitor/ios`)"
|
||||
@@ -110,8 +100,8 @@ SPEC REPOS:
|
||||
- GoogleMLKit
|
||||
- GoogleToolboxForMac
|
||||
- GoogleUtilities
|
||||
- GoogleUtilitiesComponents
|
||||
- GTMSessionFetcher
|
||||
- IONFilesystemLib
|
||||
- MLImage
|
||||
- MLKitBarcodeScanning
|
||||
- MLKitCommon
|
||||
@@ -148,33 +138,33 @@ EXTERNAL SOURCES:
|
||||
:path: "../../node_modules/@timesafari/daily-notification-plugin"
|
||||
|
||||
SPEC CHECKSUMS:
|
||||
Capacitor: c95400d761e376be9da6be5a05f226c0e865cebf
|
||||
CapacitorApp: e1e6b7d05e444d593ca16fd6d76f2b7c48b5aea7
|
||||
CapacitorCamera: 9bc7b005d0e6f1d5f525b8137045b60cffffce79
|
||||
CapacitorClipboard: 4443c3cdb7c77b1533dfe3ff0f9f7756aa8579df
|
||||
CapacitorCommunitySqlite: 0299d20f4b00c2e6aa485a1d8932656753937b9b
|
||||
CapacitorCordova: 8d93e14982f440181be7304aa9559ca631d77fff
|
||||
CapacitorFilesystem: 59270a63c60836248812671aa3b15df673fbaf74
|
||||
CapacitorMlkitBarcodeScanning: 7652be9c7922f39203a361de735d340ae37e134e
|
||||
CapacitorShare: d2a742baec21c8f3b92b361a2fbd2401cdd8288e
|
||||
CapacitorStatusBar: b16799a26320ffa52f6c8b01737d5a95bbb8f3eb
|
||||
CapawesomeCapacitorFilePicker: c40822f0a39f86855321943c7829d52bca7f01bd
|
||||
GoogleDataTransport: 6c09b596d841063d76d4288cc2d2f42cc36e1e2a
|
||||
GoogleMLKit: 90ba06e028795a50261f29500d238d6061538711
|
||||
GoogleToolboxForMac: 8bef7c7c5cf7291c687cf5354f39f9db6399ad34
|
||||
GoogleUtilities: ea963c370a38a8069cc5f7ba4ca849a60b6d7d15
|
||||
GoogleUtilitiesComponents: 679b2c881db3b615a2777504623df6122dd20afe
|
||||
Capacitor: 69dc07ebc6bd064747c5e76922f97e4862d9cc23
|
||||
CapacitorApp: f01a913211780e0718dae9750442c3e23f96e106
|
||||
CapacitorCamera: 9e952270be355797f769aa835bb7643a96c871fe
|
||||
CapacitorClipboard: d1f123674cf413125db816a45e8f70e8770972fc
|
||||
CapacitorCommunitySqlite: 4813d82ad33001e612a39d313cb5d28066cbafda
|
||||
CapacitorCordova: e343e95a672ff73e21a77a80257b52fb609b47d5
|
||||
CapacitorFilesystem: c63fc54df41e5a6761785a7f3c49dc696c22e296
|
||||
CapacitorMlkitBarcodeScanning: afd6fc431b550026a2c052e11ab2b71c7ae30011
|
||||
CapacitorShare: 25f7fc5dd0e4edbde5d6801c6de5d14a8b450a41
|
||||
CapacitorStatusBar: 416e9e53fd6397e668d4a181cd2131617d949bd6
|
||||
CapawesomeCapacitorFilePicker: 0f4a913a00e39dd77213449f0d917e92f35a5ca9
|
||||
GoogleDataTransport: aae35b7ea0c09004c3797d53c8c41f66f219d6a7
|
||||
GoogleMLKit: eff9e23ec1d90ea4157a1ee2e32a4f610c5b3318
|
||||
GoogleToolboxForMac: d1a2cbf009c453f4d6ded37c105e2f67a32206d8
|
||||
GoogleUtilities: 00c88b9a86066ef77f0da2fab05f65d7768ed8e1
|
||||
GTMSessionFetcher: 5aea5ba6bd522a239e236100971f10cb71b96ab6
|
||||
MLImage: 1824212150da33ef225fbd3dc49f184cf611046c
|
||||
MLKitBarcodeScanning: 9cb0ec5ec65bbb5db31de4eba0a3289626beab4e
|
||||
MLKitCommon: afcd11b6c0735066a0dde8b4bf2331f6197cbca2
|
||||
MLKitVision: 90922bca854014a856f8b649d1f1f04f63fd9c79
|
||||
nanopb: 438bc412db1928dac798aa6fd75726007be04262
|
||||
IONFilesystemLib: 21a63377696b2d8fab5632ecfb7d2ac67bddb68a
|
||||
MLImage: 0ad1c5f50edd027672d8b26b0fee78a8b4a0fc56
|
||||
MLKitBarcodeScanning: 0a3064da0a7f49ac24ceb3cb46a5bc67496facd2
|
||||
MLKitCommon: 07c2c33ae5640e5380beaaa6e4b9c249a205542d
|
||||
MLKitVision: 45e79d68845a2de77e2dd4d7f07947f0ed157b0e
|
||||
nanopb: fad817b59e0457d11a5dfbde799381cd727c1275
|
||||
PromisesObjC: f5707f49cb48b9636751c5b2e7d227e43fba9f47
|
||||
SQLCipher: 31878d8ebd27e5c96db0b7cb695c96e9f8ad77da
|
||||
TimesafariDailyNotificationPlugin: 3c12e8c39fc27f689f56cf4e57230a8c28611fcc
|
||||
ZIPFoundation: b8c29ea7ae353b309bc810586181fd073cb3312c
|
||||
SQLCipher: eb79c64049cb002b4e9fcb30edb7979bf4706dfc
|
||||
TimesafariDailyNotificationPlugin: 69277c884380a9a620f671b68e0327eaa4b3d27d
|
||||
ZIPFoundation: dfd3d681c4053ff7e2f7350bc4e53b5dba3f5351
|
||||
|
||||
PODFILE CHECKSUM: 6d92bfa46c6c2d31d19b8c0c38f56a8ae9fd222f
|
||||
PODFILE CHECKSUM: 87c07d03f36ef38ab0c873802aee1ce9b5d34448
|
||||
|
||||
COCOAPODS: 1.16.2
|
||||
|
||||
8702
package-lock.json
generated
36
package.json
@@ -1,7 +1,8 @@
|
||||
|
||||
{
|
||||
"name": "timesafari",
|
||||
"version": "1.3.8-beta",
|
||||
"description": "Gift Economies Application",
|
||||
"name": "giftopia",
|
||||
"version": "1.4.4",
|
||||
"description": "Giftopia App",
|
||||
"author": {
|
||||
"name": "Gift Economies Team"
|
||||
},
|
||||
@@ -13,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",
|
||||
@@ -28,7 +30,7 @@
|
||||
"auto-run:electron": "./scripts/auto-run.sh --platform=electron",
|
||||
"build:capacitor": "VITE_GIT_HASH=`git log -1 --pretty=format:%h` vite build --mode capacitor --config vite.config.capacitor.mts",
|
||||
"build:capacitor:sync": "npm run build:capacitor && npx cap sync && node scripts/restore-local-plugins.js",
|
||||
"build:native": "vite build && npx cap sync && node scripts/restore-local-plugins.js && npx capacitor-assets generate",
|
||||
"build:native": "vite build && npx cap sync && node scripts/restore-local-plugins.js && bash -c 'source scripts/common.sh && ensure_ios_capacitor_asset_directories' && npx capacitor-assets generate",
|
||||
"assets:config": "npx tsx scripts/assets-config.ts",
|
||||
"assets:validate": "npx tsx scripts/assets-validator.ts",
|
||||
"assets:validate:android": "./scripts/build-android.sh --assets-only",
|
||||
@@ -138,19 +140,19 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"@capacitor-community/electron": "^5.0.1",
|
||||
"@capacitor-community/sqlite": "6.0.2",
|
||||
"@capacitor-mlkit/barcode-scanning": "^6.0.0",
|
||||
"@capacitor/android": "^6.2.0",
|
||||
"@capacitor/app": "^6.0.0",
|
||||
"@capacitor/camera": "^6.0.0",
|
||||
"@capacitor/cli": "^6.2.0",
|
||||
"@capacitor/clipboard": "^6.0.2",
|
||||
"@capacitor/core": "^6.2.0",
|
||||
"@capacitor/filesystem": "^6.0.0",
|
||||
"@capacitor/ios": "^6.2.0",
|
||||
"@capacitor/share": "^6.0.3",
|
||||
"@capacitor/status-bar": "^6.0.2",
|
||||
"@capawesome/capacitor-file-picker": "^6.2.0",
|
||||
"@capacitor-community/sqlite": "^7.0.3",
|
||||
"@capacitor-mlkit/barcode-scanning": "^7.5.0",
|
||||
"@capacitor/android": "^7.6.4",
|
||||
"@capacitor/app": "^7.1.0",
|
||||
"@capacitor/camera": "^7.0.5",
|
||||
"@capacitor/cli": "^7.6.4",
|
||||
"@capacitor/clipboard": "^7.0.4",
|
||||
"@capacitor/core": "^7.6.4",
|
||||
"@capacitor/filesystem": "^7.1.8",
|
||||
"@capacitor/ios": "^7.6.4",
|
||||
"@capacitor/share": "^7.0.4",
|
||||
"@capacitor/status-bar": "^7.0.6",
|
||||
"@capawesome/capacitor-file-picker": "^7.2.0",
|
||||
"@dicebear/collection": "^5.4.1",
|
||||
"@dicebear/core": "^5.4.1",
|
||||
"@ethersproject/hdnode": "^5.7.0",
|
||||
|
||||
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..."
|
||||
@@ -222,7 +329,9 @@ build_ios_app() {
|
||||
|
||||
if [ "$BUILD_TYPE" = "debug" ]; then
|
||||
build_config="Debug"
|
||||
destination="platform=iOS Simulator,name=iPhone 15 Pro"
|
||||
# Use device SDK — prebuilt MLKit frameworks (MLImage, MLKitBarcodeScanning) ship
|
||||
# iOS device slices only and cannot link against the iOS Simulator SDK.
|
||||
destination="generic/platform=iOS"
|
||||
else
|
||||
build_config="Release"
|
||||
destination="platform=iOS,id=auto"
|
||||
@@ -232,18 +341,34 @@ build_ios_app() {
|
||||
|
||||
cd ios/App
|
||||
|
||||
# Build the app
|
||||
xcodebuild -workspace App.xcworkspace \
|
||||
# Prevent pkgx-managed libs (e.g. zlib) from leaking into the iOS SDK linker.
|
||||
unset LIBRARY_PATH
|
||||
unset DYLD_LIBRARY_PATH
|
||||
unset DYLD_FALLBACK_LIBRARY_PATH
|
||||
|
||||
# Build the app:
|
||||
# -quiet: skip the huge export VAR dump (compiler warnings still show unless suppressed below).
|
||||
# SWIFT_SUPPRESS_WARNINGS / GCC_WARN_INHIBIT_ALL_WARNINGS: quiet CLI output from Pods + plugins;
|
||||
# build in Xcode for full diagnostics. Real errors still fail the build.
|
||||
local build_exit=0
|
||||
xcodebuild -quiet \
|
||||
-workspace App.xcworkspace \
|
||||
-scheme "$scheme" \
|
||||
-configuration "$build_config" \
|
||||
-destination "$destination" \
|
||||
build \
|
||||
CODE_SIGN_IDENTITY="" \
|
||||
CODE_SIGNING_REQUIRED=NO \
|
||||
CODE_SIGNING_ALLOWED=NO
|
||||
CODE_SIGNING_ALLOWED=NO \
|
||||
SWIFT_SUPPRESS_WARNINGS=YES \
|
||||
GCC_WARN_INHIBIT_ALL_WARNINGS=YES || build_exit=$?
|
||||
|
||||
cd ../..
|
||||
|
||||
if [ $build_exit -ne 0 ]; then
|
||||
return $build_exit
|
||||
fi
|
||||
|
||||
log_success "iOS app built successfully"
|
||||
}
|
||||
|
||||
@@ -406,7 +531,7 @@ fi
|
||||
# Handle assets-only mode
|
||||
if [ "$ASSETS_ONLY" = true ]; then
|
||||
log_info "Assets-only mode: generating assets"
|
||||
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
|
||||
@@ -555,7 +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
|
||||
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
|
||||
@@ -564,16 +689,19 @@ safe_execute "Building iOS app" "build_ios_app" || exit 5
|
||||
if [ "$BUILD_IPA" = true ]; then
|
||||
log_info "Building IPA package..."
|
||||
cd ios/App
|
||||
xcodebuild -workspace App.xcworkspace \
|
||||
xcodebuild -quiet \
|
||||
-workspace App.xcworkspace \
|
||||
-scheme App \
|
||||
-configuration Release \
|
||||
-archivePath build/App.xcarchive \
|
||||
archive \
|
||||
CODE_SIGN_IDENTITY="" \
|
||||
CODE_SIGNING_REQUIRED=NO \
|
||||
CODE_SIGNING_ALLOWED=NO
|
||||
CODE_SIGNING_ALLOWED=NO \
|
||||
SWIFT_SUPPRESS_WARNINGS=YES \
|
||||
GCC_WARN_INHIBIT_ALL_WARNINGS=YES
|
||||
|
||||
xcodebuild -exportArchive \
|
||||
xcodebuild -quiet -exportArchive \
|
||||
-archivePath build/App.xcarchive \
|
||||
-exportPath build/ \
|
||||
-exportOptionsPlist exportOptions.plist
|
||||
|
||||
@@ -337,6 +337,27 @@ parse_args() {
|
||||
fi
|
||||
}
|
||||
|
||||
# iOS: capacitor-assets writes into AppIcon.appiconset and Splash.imageset under
|
||||
# Assets.xcassets. Those paths are gitignored (generated). On a fresh clone the
|
||||
# folders and Contents.json are missing; the tool opens Contents.json before writing
|
||||
# PNGs, so we create minimal asset-catalog stubs when absent.
|
||||
ensure_ios_capacitor_asset_directories() {
|
||||
local base="ios/App/App/Assets.xcassets"
|
||||
if [ ! -d "$base" ]; then
|
||||
log_warn "Missing $base — cannot prepare iOS asset directories"
|
||||
return 0
|
||||
fi
|
||||
mkdir -p "$base/AppIcon.appiconset" "$base/Splash.imageset"
|
||||
local minimal_contents='{"images":[],"info":{"author":"xcode","version":1}}'
|
||||
if [ ! -f "$base/AppIcon.appiconset/Contents.json" ]; then
|
||||
printf '%s\n' "$minimal_contents" > "$base/AppIcon.appiconset/Contents.json"
|
||||
fi
|
||||
if [ ! -f "$base/Splash.imageset/Contents.json" ]; then
|
||||
printf '%s\n' "$minimal_contents" > "$base/Splash.imageset/Contents.json"
|
||||
fi
|
||||
log_debug "Ensured iOS capacitor-assets output directories exist"
|
||||
}
|
||||
|
||||
# Export functions for use in child scripts
|
||||
export -f log_info log_success log_warn log_error log_debug log_step
|
||||
export -f measure_time print_header print_footer
|
||||
@@ -345,3 +366,4 @@ export -f safe_execute check_venv get_git_hash
|
||||
export -f clean_build_artifacts validate_env_vars
|
||||
export -f setup_build_env setup_app_directories load_env_file print_env_vars
|
||||
export -f print_usage parse_args
|
||||
export -f ensure_ios_capacitor_asset_directories
|
||||
@@ -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 }}
|
||||
|
||||
@@ -129,7 +129,7 @@
|
||||
@click="disabled ? notifyLocked() : addGroup()"
|
||||
>
|
||||
<font-awesome icon="plus" class="text-sm" />
|
||||
New Group
|
||||
New Do-Not-Pair Group
|
||||
</button>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -39,7 +39,7 @@ import { PlanData } from "../interfaces/records";
|
||||
import { NotificationIface } from "../constants/app";
|
||||
|
||||
/**
|
||||
* MeetingProjectDialog - Dialog for selecting a project link for a meeting
|
||||
* ProjectSelectionDialog - Dialog for selecting a project
|
||||
*
|
||||
* Features:
|
||||
* - EntityGrid integration for project selection
|
||||
@@ -52,7 +52,7 @@ import { NotificationIface } from "../constants/app";
|
||||
EntityGrid,
|
||||
},
|
||||
})
|
||||
export default class MeetingProjectDialog extends Vue {
|
||||
export default class ProjectSelectionDialog extends Vue {
|
||||
/** Whether the dialog is visible */
|
||||
visible = false;
|
||||
|
||||
@@ -6,8 +6,8 @@
|
||||
export enum AppString {
|
||||
// This is used in titles and verbiage inside the app.
|
||||
// There is also an app name without spaces, for packaging in the package.json file used in the manifest.
|
||||
APP_NAME = "Gift Economies",
|
||||
APP_NAME_NO_SPACES = "GiftEconomies",
|
||||
APP_NAME = "Giftopia",
|
||||
APP_NAME_NO_SPACES = APP_NAME,
|
||||
|
||||
PROD_ENDORSER_API_SERVER = "https://api.endorser.ch",
|
||||
TEST_ENDORSER_API_SERVER = "https://test-api.endorser.ch",
|
||||
|
||||
@@ -1175,11 +1175,6 @@ export const NOTIFY_GIFTED_DETAILS_DELETE_IMAGE_CONFIRM = {
|
||||
message: "",
|
||||
};
|
||||
|
||||
export const NOTIFY_GIFTED_DETAILS_DELETE_IMAGE_ERROR = {
|
||||
title: "Error",
|
||||
message: "There was a problem deleting the image.",
|
||||
};
|
||||
|
||||
export const NOTIFY_GIFTED_DETAILS_NO_IDENTIFIER = {
|
||||
title: "Missing Identifier",
|
||||
message: "You must select an identifier before you can record a give.",
|
||||
|
||||
@@ -80,6 +80,7 @@ export interface PlanActionClaim extends ClaimObject {
|
||||
agent?: { identifier: string };
|
||||
description?: string;
|
||||
endTime?: string;
|
||||
fulfills?: { "@type": string; identifier?: string; lastClaimId?: string };
|
||||
identifier?: string;
|
||||
image?: string;
|
||||
lastClaimId?: string;
|
||||
|
||||
@@ -136,6 +136,11 @@ const routes: Array<RouteRecordRaw> = [
|
||||
name: "help-onboarding",
|
||||
component: () => import("../views/HelpOnboardingView.vue"),
|
||||
},
|
||||
{
|
||||
path: "/help-terms",
|
||||
name: "help-terms",
|
||||
component: () => import("../views/HelpTermsView.vue"),
|
||||
},
|
||||
{
|
||||
path: "/",
|
||||
name: "home",
|
||||
|
||||
@@ -123,10 +123,10 @@ export class CapacitorQRScanner implements QRScannerService {
|
||||
|
||||
// Add listener for barcode scans
|
||||
const handle = await BarcodeScanner.addListener(
|
||||
"barcodeScanned",
|
||||
"barcodesScanned",
|
||||
(result) => {
|
||||
if (this.scanListener && result.barcode?.rawValue) {
|
||||
this.scanListener.onScan(result.barcode.rawValue);
|
||||
if (this.scanListener && result.barcodes?.[0]?.rawValue) {
|
||||
this.scanListener.onScan(result.barcodes[0].rawValue);
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -293,16 +293,12 @@
|
||||
<h3
|
||||
data-testid="advancedSettings"
|
||||
class="text-blue-500 text-sm font-semibold mb-3 cursor-pointer"
|
||||
@click="toggleShowGeneralAdvanced"
|
||||
@click="showAdvanced = !showAdvanced"
|
||||
>
|
||||
{{
|
||||
showGeneralAdvanced
|
||||
? "Hide Advanced Settings"
|
||||
: "Show Advanced Settings"
|
||||
}}
|
||||
{{ showAdvanced ? "Hide Advanced Settings" : "Show Advanced Settings" }}
|
||||
</h3>
|
||||
<section
|
||||
v-if="showGeneralAdvanced"
|
||||
v-if="showAdvanced"
|
||||
id="sectionAdvanced"
|
||||
aria-labelledby="advancedHeading"
|
||||
>
|
||||
@@ -1095,6 +1091,7 @@ export default class AccountViewView extends Vue {
|
||||
this.previousPasskeyExpirationMinutes = this.passkeyExpirationMinutes;
|
||||
this.searchBox = settings.searchBoxes?.[0] || null;
|
||||
this.showGeneralAdvanced = !!settings.showGeneralAdvanced;
|
||||
this.showAdvanced = this.showGeneralAdvanced;
|
||||
this.showShortcutBvc = !!settings.showShortcutBvc;
|
||||
this.warnIfProdServer = !!settings.warnIfProdServer;
|
||||
this.warnIfTestServer = !!settings.warnIfTestServer;
|
||||
@@ -1405,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")
|
||||
*/
|
||||
@@ -2119,6 +2068,10 @@ export default class AccountViewView extends Vue {
|
||||
hasLocation: result.includeLocation,
|
||||
});
|
||||
|
||||
if (response.data.userMessage) {
|
||||
this.notify.info(response.data.userMessage);
|
||||
}
|
||||
|
||||
return result;
|
||||
} else {
|
||||
logger.debug("[AccountViewView] No profile data found in response:", {
|
||||
@@ -2126,6 +2079,10 @@ export default class AccountViewView extends Vue {
|
||||
hasData: !!response.data,
|
||||
hasDataData: !!(response.data && response.data.data),
|
||||
});
|
||||
|
||||
if (response.data?.userMessage) {
|
||||
this.notify.info(response.data.userMessage);
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
@@ -2232,6 +2189,10 @@ export default class AccountViewView extends Vue {
|
||||
status: response.status,
|
||||
});
|
||||
|
||||
if (response.data?.userMessage) {
|
||||
this.notify.info(response.data.userMessage);
|
||||
}
|
||||
|
||||
return true;
|
||||
} catch (error: unknown) {
|
||||
// Handle specific HTTP status codes cleanly to suppress console spam
|
||||
@@ -2348,6 +2309,10 @@ export default class AccountViewView extends Vue {
|
||||
status: response.status,
|
||||
});
|
||||
|
||||
if (response.data?.userMessage) {
|
||||
this.notify.info(response.data.userMessage);
|
||||
}
|
||||
|
||||
return true;
|
||||
} catch (error: unknown) {
|
||||
// Handle specific HTTP status codes cleanly to suppress console spam
|
||||
|
||||
@@ -398,6 +398,164 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!--
|
||||
Network Connections section: shows nearest neighbors in the registration
|
||||
graph for all DIDs in this claim. The same conventions and styling are used
|
||||
in UserProfileView.vue for user-profile nearest neighbors. Keep changes in sync.
|
||||
-->
|
||||
<div v-if="activeDid && hasVisibleNeighbors" class="mt-8">
|
||||
<h2 class="text-lg font-semibold mb-3">
|
||||
Network Connections
|
||||
<button
|
||||
title="What is this?"
|
||||
class="ml-1 align-middle"
|
||||
@click="showNeighborsInfo = true"
|
||||
>
|
||||
<font-awesome
|
||||
icon="circle-info"
|
||||
class="text-base text-blue-500 cursor-pointer"
|
||||
/>
|
||||
</button>
|
||||
</h2>
|
||||
|
||||
<!-- Info modal for network connections explanation -->
|
||||
<div
|
||||
v-if="showNeighborsInfo"
|
||||
class="fixed inset-0 bg-black bg-opacity-50 flex items-start justify-center pt-16 z-50"
|
||||
@click.self="showNeighborsInfo = false"
|
||||
>
|
||||
<div class="bg-white rounded-lg p-6 mx-4 max-w-md">
|
||||
<h3 class="text-lg font-semibold mb-2">Network Connections</h3>
|
||||
<p class="text-sm text-slate-700">
|
||||
This section shows
|
||||
{{
|
||||
Object.values(claimNeighbors).flat().length === 1
|
||||
? "a contact that is"
|
||||
: "contacts that are"
|
||||
}}
|
||||
nearer to the people involved in this activity. If you want more
|
||||
information, reach out to one of them and ask for an introduction.
|
||||
</p>
|
||||
<button
|
||||
class="mt-4 w-full bg-blue-600 text-white py-2 rounded-md"
|
||||
@click="showNeighborsInfo = false"
|
||||
>
|
||||
Got it
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-if="loadingNeighbors">
|
||||
<div class="flex justify-center items-center py-8">
|
||||
<font-awesome
|
||||
icon="spinner"
|
||||
class="fa-spin-pulse text-2xl text-slate-400"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
v-else-if="neighborsError"
|
||||
class="bg-red-50 border border-red-300 rounded-md p-4"
|
||||
>
|
||||
<div class="flex items-start gap-2">
|
||||
<font-awesome
|
||||
icon="exclamation-triangle"
|
||||
class="text-red-500 mt-0.5"
|
||||
/>
|
||||
<p class="text-red-700">{{ neighborsError }}</p>
|
||||
</div>
|
||||
</div>
|
||||
<div v-else-if="Object.keys(claimNeighbors).length > 0">
|
||||
<div v-for="(neighbors, did) in claimNeighbors" :key="did" class="mb-4">
|
||||
<h3
|
||||
v-if="Object.keys(claimNeighbors).length > 1"
|
||||
class="text-sm font-medium text-slate-600 mb-1"
|
||||
>
|
||||
Near {{ didInfo(did as string) }}:
|
||||
</h3>
|
||||
<!-- DID has no linked neighbors on this server -->
|
||||
<p
|
||||
v-if="neighbors.length === 0"
|
||||
class="text-sm text-slate-500 italic"
|
||||
>
|
||||
Nobody on this server is linked to {{ didInfo(did as string) }}. The
|
||||
data may be a mistake, or a test, or a reference to someone on a
|
||||
different system. Anyway, we have no way to contact them.
|
||||
</p>
|
||||
<div v-else class="space-y-2">
|
||||
<div
|
||||
v-for="neighbor in neighbors"
|
||||
:key="neighbor.did"
|
||||
class="bg-slate-50 border border-slate-300 rounded-md"
|
||||
>
|
||||
<div class="flex items-center justify-between gap-3 p-3">
|
||||
<div class="flex items-center gap-2 flex-1 min-w-0">
|
||||
<button
|
||||
title="Copy claim link and expand"
|
||||
class="text-blue-600 flex-shrink-0"
|
||||
@click="onNeighborExpandClick(neighbor.did)"
|
||||
>
|
||||
<font-awesome
|
||||
:icon="
|
||||
expandedNeighborDid === neighbor.did
|
||||
? 'chevron-down'
|
||||
: 'chevron-right'
|
||||
"
|
||||
class="text-sm"
|
||||
/>
|
||||
{{ getNeighborDisplayName(neighbor.did) }}
|
||||
</button>
|
||||
<span :class="getRelationBadgeClass(neighbor.relation)">
|
||||
{{ getRelationLabel(neighbor.relation) }}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
v-if="expandedNeighborDid === neighbor.did"
|
||||
class="border-t border-slate-300 p-3 bg-white"
|
||||
>
|
||||
<router-link
|
||||
:to="{ path: '/did/' + encodeURIComponent(neighbor.did) }"
|
||||
class="text-blue-600 hover:text-blue-800 font-medium underline"
|
||||
>
|
||||
Go to contact info
|
||||
</router-link>
|
||||
and send them the claim link from your clipboard. Ask them for
|
||||
an introduction.
|
||||
<div
|
||||
v-if="neighborIsNotInContacts(neighbor.did)"
|
||||
class="flex flex-col gap-1 mt-2"
|
||||
>
|
||||
<p class="text-xs text-slate-600">
|
||||
This person is connected to you, but they are not in this
|
||||
device's contacts. Copy this DID link and check on another
|
||||
device or check with different people.
|
||||
</p>
|
||||
<span class="flex items-center gap-1 min-w-0">
|
||||
<span class="text-xs truncate text-slate-600 min-w-0">
|
||||
{{ neighbor.did }}
|
||||
</span>
|
||||
<button
|
||||
title="Copy DID Link"
|
||||
class="text-blue-600 hover:text-blue-800 underline cursor-pointer flex-shrink-0"
|
||||
@click.prevent="
|
||||
copyTextToClipboard(
|
||||
'DID link',
|
||||
`${APP_SERVER}/deep-link/did/${encodeURIComponent(neighbor.did)}`,
|
||||
)
|
||||
"
|
||||
>
|
||||
<font-awesome icon="copy" class="text-sm" />
|
||||
</button>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!--
|
||||
Note that a similar section is found in ConfirmGiftView.vue, and kinda in HiddenDidDialog.vue
|
||||
-->
|
||||
@@ -631,12 +789,19 @@ export default class ClaimView extends Vue {
|
||||
numConfsNotVisible = 0; // number of hidden DIDs in the confirmerIdList, minus the issuer if they aren't visible
|
||||
providersForGive: ProviderInfo[] = [];
|
||||
showIdCopy = false;
|
||||
showNeighborsInfo = false;
|
||||
showVeriClaimDump = false;
|
||||
veriClaim = serverUtil.BLANK_GENERIC_SERVER_RECORD;
|
||||
veriClaimDump = "";
|
||||
veriClaimDidsVisible: { [key: string]: string[] } = {};
|
||||
windowDeepLink = window.location.href; // changed in the setup for deep linking
|
||||
|
||||
// Network Connections state (same pattern as UserProfileView.vue)
|
||||
claimNeighbors: Record<string, Array<{ did: string; relation: string }>> = {};
|
||||
expandedNeighborDid: string | null = null;
|
||||
loadingNeighbors = false;
|
||||
neighborsError = "";
|
||||
|
||||
APP_SERVER = APP_SERVER;
|
||||
R = R;
|
||||
yaml = yaml;
|
||||
@@ -745,6 +910,16 @@ export default class ClaimView extends Vue {
|
||||
return (claim as { image?: string })?.image;
|
||||
}
|
||||
|
||||
/**
|
||||
* Whether the Network Connections section should be shown.
|
||||
* Hidden if the only DIDs in claimNeighbors are the active user,
|
||||
* or if there are no entries at all (after filtering).
|
||||
*/
|
||||
get hasVisibleNeighbors(): boolean {
|
||||
const keys = Object.keys(this.claimNeighbors);
|
||||
return keys.length > 0 || this.loadingNeighbors;
|
||||
}
|
||||
|
||||
resetThisValues() {
|
||||
this.confirmerIdList = [];
|
||||
this.confsVisibleErrorMessage = "";
|
||||
@@ -759,6 +934,10 @@ export default class ClaimView extends Vue {
|
||||
this.isEditedGlobalId = false;
|
||||
this.numConfsNotVisible = 0;
|
||||
this.providersForGive = [];
|
||||
this.claimNeighbors = {};
|
||||
this.expandedNeighborDid = null;
|
||||
this.loadingNeighbors = false;
|
||||
this.neighborsError = "";
|
||||
this.veriClaim = serverUtil.BLANK_GENERIC_SERVER_RECORD;
|
||||
this.veriClaimDump = "";
|
||||
this.veriClaimDidsVisible = {};
|
||||
@@ -825,6 +1004,7 @@ export default class ClaimView extends Vue {
|
||||
const claimId = this.$route.params.id as string;
|
||||
if (claimId) {
|
||||
await this.loadClaim(claimId, this.activeDid);
|
||||
await this.loadClaimNeighbors();
|
||||
} else {
|
||||
this.notify.error("No claim ID was provided.");
|
||||
}
|
||||
@@ -1000,6 +1180,125 @@ export default class ClaimView extends Vue {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Loads nearest neighbors for all DIDs in this claim via the
|
||||
* endorser-ch claimNearestNeighbors endpoint.
|
||||
* Same display conventions as UserProfileView.vue's loadNeighbors.
|
||||
*/
|
||||
async loadClaimNeighbors() {
|
||||
if (!this.veriClaim.id) return;
|
||||
|
||||
this.loadingNeighbors = true;
|
||||
this.neighborsError = "";
|
||||
try {
|
||||
const url =
|
||||
this.apiServer +
|
||||
"/api/claim/claimNearestNeighbors/" +
|
||||
encodeURIComponent(this.veriClaim.id as string);
|
||||
const headers = await serverUtil.getHeaders(this.activeDid);
|
||||
const resp = await this.axios.get(url, { headers });
|
||||
if (resp.status === 200) {
|
||||
const raw = resp.data.data || {};
|
||||
// Filter out the current user's own DID entry — their neighbors
|
||||
// aren't useful here since "You" is already known.
|
||||
const filtered: Record<
|
||||
string,
|
||||
Array<{ did: string; relation: string }>
|
||||
> = {};
|
||||
for (const [did, neighbors] of Object.entries(raw)) {
|
||||
if (did === this.activeDid) continue;
|
||||
filtered[did] = neighbors as Array<{
|
||||
did: string;
|
||||
relation: string;
|
||||
}>;
|
||||
}
|
||||
this.claimNeighbors = filtered;
|
||||
} else {
|
||||
this.claimNeighbors = {};
|
||||
this.neighborsError = "Failed to load network connections.";
|
||||
}
|
||||
} catch (error) {
|
||||
await this.$logError(
|
||||
"Error loading claim neighbors: " + JSON.stringify(error),
|
||||
);
|
||||
this.claimNeighbors = {};
|
||||
this.neighborsError =
|
||||
"An error occurred while loading network connections.";
|
||||
} finally {
|
||||
this.loadingNeighbors = false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets display name for a neighbor DID (same as UserProfileView.vue)
|
||||
*/
|
||||
getNeighborDisplayName(did: string): string {
|
||||
return serverUtil.didInfo(
|
||||
did,
|
||||
this.activeDid,
|
||||
this.allMyDids,
|
||||
this.allContacts,
|
||||
);
|
||||
}
|
||||
|
||||
neighborIsNotInContacts(did: string): boolean {
|
||||
return !this.allContacts.some((contact) => contact.did === did);
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets human-readable label for relation type (same as UserProfileView.vue)
|
||||
*/
|
||||
getRelationLabel(relation: string): string {
|
||||
switch (relation) {
|
||||
case "REGISTERED_BY_YOU":
|
||||
return "Registered by You";
|
||||
case "REGISTERED_YOU":
|
||||
return "Registered You";
|
||||
case "TARGET":
|
||||
return "Yourself";
|
||||
default:
|
||||
return relation;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets CSS classes for relation badge styling (same as UserProfileView.vue)
|
||||
*/
|
||||
getRelationBadgeClass(relation: string): string {
|
||||
const baseClasses =
|
||||
"text-xs font-semibold px-2 py-1 rounded whitespace-nowrap";
|
||||
switch (relation) {
|
||||
case "REGISTERED_BY_YOU":
|
||||
return `${baseClasses} bg-blue-100 text-blue-700`;
|
||||
case "REGISTERED_YOU":
|
||||
return `${baseClasses} bg-green-100 text-green-700`;
|
||||
case "TARGET":
|
||||
return `${baseClasses} bg-purple-100 text-purple-700`;
|
||||
default:
|
||||
return `${baseClasses} bg-slate-100 text-slate-700`;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles clicking expand on a neighbor - copies claim link and toggles
|
||||
*/
|
||||
async onNeighborExpandClick(did: string) {
|
||||
if (this.expandedNeighborDid === did) {
|
||||
this.expandedNeighborDid = null;
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
await copyToClipboard(this.windowDeepLink);
|
||||
this.notify.copied("Claim link");
|
||||
} catch (error) {
|
||||
this.$logAndConsole(`Error copying claim link: ${error}`, true);
|
||||
this.notify.error("Failed to copy claim link.");
|
||||
}
|
||||
|
||||
this.expandedNeighborDid = did;
|
||||
}
|
||||
|
||||
async showFullClaim(claimId: string) {
|
||||
const url =
|
||||
this.apiServer + "/api/claim/full/" + encodeURIComponent(claimId);
|
||||
@@ -1110,6 +1409,7 @@ export default class ClaimView extends Vue {
|
||||
(this.$router as Router).push(route).then(async () => {
|
||||
this.resetThisValues();
|
||||
await this.loadClaim(claimId, this.activeDid);
|
||||
await this.loadClaimNeighbors();
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -118,11 +118,13 @@ import {
|
||||
CONTACT_CSV_HEADER,
|
||||
CONTACT_IMPORT_CONFIRM_URL_PATH_TIME_SAFARI,
|
||||
generateEndorserJwtUrlForAccount,
|
||||
register,
|
||||
setVisibilityUtil,
|
||||
} from "../libs/endorserServer";
|
||||
import UserNameDialog from "../components/UserNameDialog.vue";
|
||||
import { retrieveAccountMetadata } from "../libs/util";
|
||||
|
||||
import { AxiosError } from "axios";
|
||||
import { Account } from "@/db/tables/accounts";
|
||||
import { PlatformServiceMixin } from "@/utils/PlatformServiceMixin";
|
||||
import {
|
||||
@@ -139,7 +141,10 @@ import {
|
||||
NOTIFY_QR_URL_COPIED,
|
||||
NOTIFY_QR_CODE_HELP,
|
||||
NOTIFY_QR_DID_COPIED,
|
||||
NOTIFY_QR_REGISTRATION_SUBMITTED,
|
||||
NOTIFY_QR_REGISTRATION_ERROR,
|
||||
createQRContactAddedMessage,
|
||||
createQRRegistrationSuccessMessage,
|
||||
QR_TIMEOUT_MEDIUM,
|
||||
QR_TIMEOUT_STANDARD,
|
||||
QR_TIMEOUT_LONG,
|
||||
@@ -204,6 +209,7 @@ export default class ContactQRScanFull extends Vue {
|
||||
activeDid = "";
|
||||
apiServer = "";
|
||||
givenName = "";
|
||||
hideRegisterPromptOnNewContact = false;
|
||||
isRegistered = false;
|
||||
profileImageUrl = "";
|
||||
qrValue = "";
|
||||
@@ -278,6 +284,8 @@ export default class ContactQRScanFull extends Vue {
|
||||
|
||||
this.apiServer = settings.apiServer || "";
|
||||
this.givenName = settings.firstName || "";
|
||||
this.hideRegisterPromptOnNewContact =
|
||||
!!settings.hideRegisterPromptOnNewContact;
|
||||
this.isRegistered = !!settings.isRegistered;
|
||||
this.profileImageUrl = settings.profileImageUrl || "";
|
||||
|
||||
@@ -575,6 +583,34 @@ export default class ContactQRScanFull extends Vue {
|
||||
createQRContactAddedMessage(!!this.activeDid),
|
||||
QR_TIMEOUT_STANDARD,
|
||||
);
|
||||
|
||||
if (
|
||||
this.isRegistered &&
|
||||
!this.hideRegisterPromptOnNewContact &&
|
||||
!contact.registered
|
||||
) {
|
||||
setTimeout(() => {
|
||||
this.$notify(
|
||||
{
|
||||
group: "modal",
|
||||
type: "confirm",
|
||||
title: "Register",
|
||||
text: "Do you want to register them?",
|
||||
onCancel: async (stopAsking?: boolean) => {
|
||||
await this.handleRegistrationPromptResponse(stopAsking);
|
||||
},
|
||||
onNo: async (stopAsking?: boolean) => {
|
||||
await this.handleRegistrationPromptResponse(stopAsking);
|
||||
},
|
||||
onYes: async () => {
|
||||
await this.register(contact);
|
||||
},
|
||||
promptToStopAsking: true,
|
||||
},
|
||||
-1,
|
||||
);
|
||||
}, 500);
|
||||
}
|
||||
} catch (error) {
|
||||
logger.error("Error saving contact to database:", {
|
||||
did: contact.did,
|
||||
@@ -585,6 +621,74 @@ export default class ContactQRScanFull extends Vue {
|
||||
}
|
||||
}
|
||||
|
||||
async register(contact: Contact) {
|
||||
logger.debug("Submitting contact registration", {
|
||||
did: contact.did,
|
||||
name: contact.name,
|
||||
});
|
||||
|
||||
try {
|
||||
const regResult = await register(
|
||||
this.activeDid,
|
||||
this.apiServer,
|
||||
this.axios,
|
||||
contact,
|
||||
);
|
||||
this.notify.toast("Submitted", NOTIFY_QR_REGISTRATION_SUBMITTED.message);
|
||||
if (regResult.success) {
|
||||
contact.registered = true;
|
||||
await this.$updateContact(contact.did, { registered: true });
|
||||
logger.debug("Contact registration successful", { did: contact.did });
|
||||
|
||||
this.notify.success(
|
||||
createQRRegistrationSuccessMessage(contact.name || ""),
|
||||
QR_TIMEOUT_LONG,
|
||||
);
|
||||
} else {
|
||||
this.notify.error(
|
||||
(regResult.error as string) || NOTIFY_QR_REGISTRATION_ERROR.message,
|
||||
QR_TIMEOUT_LONG,
|
||||
);
|
||||
}
|
||||
} catch (error) {
|
||||
logger.error("Error registering contact:", {
|
||||
did: contact.did,
|
||||
error: error instanceof Error ? error.message : String(error),
|
||||
stack: error instanceof Error ? error.stack : undefined,
|
||||
});
|
||||
let userMessage = "There was an error.";
|
||||
const serverError = error as AxiosError;
|
||||
if (serverError) {
|
||||
if (
|
||||
serverError.response?.data &&
|
||||
typeof serverError.response.data === "object" &&
|
||||
"message" in serverError.response.data
|
||||
) {
|
||||
userMessage = (serverError.response.data as { message: string })
|
||||
.message;
|
||||
} else if (serverError.message) {
|
||||
userMessage = serverError.message;
|
||||
} else {
|
||||
userMessage = JSON.stringify(serverError.toJSON());
|
||||
}
|
||||
} else {
|
||||
userMessage = error as string;
|
||||
}
|
||||
this.notify.error(userMessage, QR_TIMEOUT_LONG);
|
||||
}
|
||||
}
|
||||
|
||||
private async handleRegistrationPromptResponse(
|
||||
stopAsking?: boolean,
|
||||
): Promise<void> {
|
||||
if (stopAsking) {
|
||||
await this.$saveSettings({
|
||||
hideRegisterPromptOnNewContact: stopAsking,
|
||||
});
|
||||
this.hideRegisterPromptOnNewContact = stopAsking;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Vue lifecycle hook - component mounted
|
||||
* Sets up event listeners and starts scanning automatically
|
||||
|
||||
@@ -897,7 +897,7 @@ export default class DiscoverView extends Vue {
|
||||
public computedStarredTabStyleClassNames() {
|
||||
return {
|
||||
"inline-block": true,
|
||||
"py-3": true,
|
||||
"py-2": true,
|
||||
"rounded-t-lg": true,
|
||||
"border-b-2": true,
|
||||
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
<template>
|
||||
<QuickNav />
|
||||
|
||||
<!-- CONTENT -->
|
||||
<section id="Content" class="p-6 pb-24 max-w-3xl mx-auto">
|
||||
<TopMessage />
|
||||
@@ -263,7 +265,6 @@ import { showSeedPhraseReminder } from "@/utils/seedPhraseReminder";
|
||||
import {
|
||||
NOTIFY_GIFTED_DETAILS_RETRIEVAL_ERROR,
|
||||
NOTIFY_GIFTED_DETAILS_DELETE_IMAGE_CONFIRM,
|
||||
NOTIFY_GIFTED_DETAILS_DELETE_IMAGE_ERROR,
|
||||
NOTIFY_GIFTED_DETAILS_NO_IDENTIFIER,
|
||||
NOTIFY_GIFT_ERROR_NEGATIVE_AMOUNT,
|
||||
NOTIFY_GIFTED_DETAILS_RECORDING_GIVE,
|
||||
@@ -302,6 +303,7 @@ export default class GiftedDetails extends Vue {
|
||||
giverName = "";
|
||||
hideBackButton = false;
|
||||
imageUrl = "";
|
||||
imageUrlToDelete = "";
|
||||
message = "";
|
||||
offerId = "";
|
||||
prevCredToEdit?: GenericCredWrapper<GiveActionClaim>;
|
||||
@@ -517,7 +519,10 @@ export default class GiftedDetails extends Vue {
|
||||
}
|
||||
|
||||
cancel() {
|
||||
this.deleteImage(); // not awaiting, so they'll go back immediately
|
||||
// Only delete freshly uploaded images, not ones from an existing claim
|
||||
if (this.imageUrl && this.imageUrl !== this.prevCredToEdit?.claim?.image) {
|
||||
this.deleteImage(this.imageUrl); // not awaiting, so they'll go back immediately
|
||||
}
|
||||
if (this.destinationPathAfter) {
|
||||
(this.$router as Router).push({ path: this.destinationPathAfter });
|
||||
} else {
|
||||
@@ -526,7 +531,10 @@ export default class GiftedDetails extends Vue {
|
||||
}
|
||||
|
||||
cancelBack() {
|
||||
this.deleteImage(); // not awaiting, so they'll go back immediately
|
||||
// Only delete freshly uploaded images, not ones from an existing claim
|
||||
if (this.imageUrl && this.imageUrl !== this.prevCredToEdit?.claim?.image) {
|
||||
this.deleteImage(this.imageUrl); // not awaiting, so they'll go back immediately
|
||||
}
|
||||
(this.$router as Router).back();
|
||||
}
|
||||
|
||||
@@ -539,13 +547,18 @@ export default class GiftedDetails extends Vue {
|
||||
confirmDeleteImage() {
|
||||
this.notify.confirm(
|
||||
NOTIFY_GIFTED_DETAILS_DELETE_IMAGE_CONFIRM.message,
|
||||
this.deleteImage,
|
||||
() => {
|
||||
// Stage the image for deletion on submit rather than deleting immediately,
|
||||
// so that canceling the edit doesn't destroy the referenced image.
|
||||
this.imageUrlToDelete = this.imageUrl;
|
||||
this.imageUrl = "";
|
||||
},
|
||||
TIMEOUTS.LONG,
|
||||
);
|
||||
}
|
||||
|
||||
async deleteImage() {
|
||||
if (!this.imageUrl) {
|
||||
async deleteImage(imageUrl: string) {
|
||||
if (!imageUrl) {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
@@ -559,38 +572,21 @@ export default class GiftedDetails extends Vue {
|
||||
);
|
||||
}
|
||||
const response = await this.axios.delete(
|
||||
DEFAULT_IMAGE_API_SERVER +
|
||||
"/image/" +
|
||||
encodeURIComponent(this.imageUrl),
|
||||
DEFAULT_IMAGE_API_SERVER + "/image/" + encodeURIComponent(imageUrl),
|
||||
{ headers },
|
||||
);
|
||||
if (response.status === 204) {
|
||||
// don't bother with a notification
|
||||
// (either they'll simply continue or they're canceling and going back)
|
||||
} else {
|
||||
logger.error("Problem deleting image:", response);
|
||||
this.notify.error(
|
||||
NOTIFY_GIFTED_DETAILS_DELETE_IMAGE_ERROR.message,
|
||||
TIMEOUTS.LONG,
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
this.imageUrl = "";
|
||||
} catch (error) {
|
||||
logger.error("Error deleting image:", error);
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
if ((error as any)?.response?.status === 404) {
|
||||
logger.log("Weird: the image was already deleted.", error);
|
||||
|
||||
this.imageUrl = "";
|
||||
|
||||
// it already doesn't exist so we won't say anything to the user
|
||||
logger.log("Image was already deleted:", error);
|
||||
} else {
|
||||
this.notify.error(
|
||||
NOTIFY_GIFTED_DETAILS_DELETE_IMAGE_ERROR.message,
|
||||
TIMEOUTS.LONG,
|
||||
);
|
||||
logger.error("Failed to delete image from server:", error);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -733,6 +729,12 @@ export default class GiftedDetails extends Vue {
|
||||
TIMEOUTS.LONG,
|
||||
);
|
||||
} else {
|
||||
// Delete the old image from storage now that the edit is saved
|
||||
if (this.imageUrlToDelete) {
|
||||
this.deleteImage(this.imageUrlToDelete); // not awaiting
|
||||
this.imageUrlToDelete = "";
|
||||
}
|
||||
|
||||
this.notify.success(
|
||||
NOTIFY_GIFTED_DETAILS_GIFT_RECORDED.message,
|
||||
TIMEOUTS.SHORT,
|
||||
|
||||
@@ -32,6 +32,9 @@
|
||||
>
|
||||
<h3>Troubleshooting Notifications</h3>
|
||||
|
||||
Note that the notifications will not arrive exactly at the time you set
|
||||
(because phones don't let non-alarm-apps set exact alarms).
|
||||
|
||||
<h4>Check your in-app notification settings</h4>
|
||||
<ul>
|
||||
<li>Tap <strong>Profile</strong> in the bottom bar</li>
|
||||
|
||||
144
src/views/HelpTermsView.vue
Normal file
@@ -0,0 +1,144 @@
|
||||
<template>
|
||||
<QuickNav />
|
||||
|
||||
<!-- CONTENT -->
|
||||
<section id="Content" class="p-6 pb-24 max-w-3xl mx-auto">
|
||||
<!-- Sub View Heading -->
|
||||
<div id="SubViewHeading" class="flex gap-4 items-start mb-8">
|
||||
<h1 class="grow text-xl text-center font-semibold leading-tight">
|
||||
Terms & Conditions and Privacy Policies
|
||||
</h1>
|
||||
|
||||
<!-- Back -->
|
||||
<a
|
||||
class="order-first text-lg text-center leading-none p-1"
|
||||
@click="$router.go(-1)"
|
||||
>
|
||||
<font-awesome icon="chevron-left" class="block text-center w-[1em]" />
|
||||
</a>
|
||||
|
||||
<!-- Help button -->
|
||||
<router-link
|
||||
:to="{ name: 'help' }"
|
||||
class="block ms-auto text-sm text-center text-white bg-gradient-to-b from-blue-400 to-blue-700 shadow-[inset_0_-1px_0_0_rgba(0,0,0,0.5)] p-1.5 rounded-full"
|
||||
>
|
||||
<font-awesome icon="question" class="block text-center w-[1em]" />
|
||||
</router-link>
|
||||
</div>
|
||||
|
||||
<!-- eslint-disable prettier/prettier -->
|
||||
<div>
|
||||
<p style="display:inline; align-items: center">
|
||||
This work is public domain. (If you like rules, reference
|
||||
<a
|
||||
href="http://creativecommons.org/publicdomain/zero/1.0?ref=chooser-v1"
|
||||
target="_blank"
|
||||
rel="license noopener noreferrer"
|
||||
>
|
||||
<span class="text-blue-500 mr-1">CC0 1.0</span>
|
||||
<img
|
||||
src="../assets/help/creative-commons-circle.svg"
|
||||
alt="CC circle"
|
||||
width="20"
|
||||
class="display: inline"
|
||||
/>
|
||||
<img
|
||||
src="../assets/help/creative-commons-zero.svg"
|
||||
alt="CC zero"
|
||||
width="20"
|
||||
style="display: inline"
|
||||
/>
|
||||
</a>
|
||||
)
|
||||
</p>
|
||||
<p class="mt-4">
|
||||
This is offered freely, with the hope that it helps but without any
|
||||
warranty or guarantee. When you share data or even look at information here,
|
||||
you accept the risk that goes with those activities. In other words,
|
||||
if you expect some functionality or you expect some protection, and you
|
||||
feel it is appropriate to force those expectations on the system or its
|
||||
operators or creators, then you are not allowed to use it.
|
||||
</p>
|
||||
<p class="mt-4">
|
||||
Here is how your data is used:
|
||||
</p>
|
||||
<ul class="list-disc list-outside ml-4">
|
||||
<li>
|
||||
If sending images, a server stores them. They can be removed by editing
|
||||
each claim and deleting the image.
|
||||
</li>
|
||||
<li>
|
||||
If sending other partner system data (eg. to Trustroots) a public key
|
||||
and message data are stored on a server. Those can be removed via
|
||||
direct personal request (email
|
||||
<a :href="`mailto:${SUPPORT_EMAIL}`" class="text-blue-500">
|
||||
{{ SUPPORT_EMAIL }}
|
||||
</a>).
|
||||
</li>
|
||||
<li>
|
||||
For all other claim data,
|
||||
<a
|
||||
href="https://endorser.ch/privacy-policy"
|
||||
target="_blank"
|
||||
class="text-blue-500"
|
||||
>
|
||||
the Endorser Service has this Privacy Policy.
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
<div class="mt-4">
|
||||
<!--
|
||||
This section is for Twilio's A2P Campaign requirements.
|
||||
They say: Ensure it includes the program name, description, message/data rates, message frequency, support contact info, and opt-out instructions (HELP and STOP in bold).
|
||||
They link here for a sample: https://help.twilio.com/articles/223134847-Industry-standards-for-US-Short-Code-Terms-of-Service
|
||||
-->
|
||||
Here are the details for SMS notifications:
|
||||
<ul class="list-disc list-outside ml-4">
|
||||
<li>You may opt to receive SMS messages for two purposes:
|
||||
<ul class="list-disc list-outside ml-4">
|
||||
<li>A daily reminder message</li>
|
||||
<li>A notification of new activity for items that you are watching</li>
|
||||
</ul>
|
||||
</li>
|
||||
<li>
|
||||
Before enabling these notifications, you must register your phone number and give permission to use it for searches.
|
||||
</li>
|
||||
<li>
|
||||
Once your phone number is registered and linked to your DID, you can enable or disable either kind of SMS message.
|
||||
You can disable these any time with the same toggle.
|
||||
</li>
|
||||
<li>
|
||||
If you lose your credentials, you can register your phone with a different DID.
|
||||
Then you can enable and disable notifications for your phone.
|
||||
</li>
|
||||
<li>
|
||||
Carriers are not liable for delayed or undelivered messages.
|
||||
</li>
|
||||
<li>
|
||||
As always, message and data rates may apply for any messages sent to you from us and to us from you.
|
||||
You will receive at most one of each kind of message per day.
|
||||
If you have any questions about your text plan or data plan, it is best to contact your wireless provider.
|
||||
</li>
|
||||
<li>
|
||||
Our servers will only store your phone number and the type of notifications you have enabled,
|
||||
along with the explicit signed permission to use it for searches.
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
<!-- eslint-enable -->
|
||||
</section>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { Component, Vue } from "vue-facing-decorator";
|
||||
|
||||
import QuickNav from "../components/QuickNav.vue";
|
||||
import { SUPPORT_EMAIL } from "../constants/app";
|
||||
|
||||
@Component({ components: { QuickNav } })
|
||||
export default class HelpTermsView extends Vue {
|
||||
SUPPORT_EMAIL = SUPPORT_EMAIL;
|
||||
}
|
||||
</script>
|
||||
@@ -480,46 +480,14 @@
|
||||
</p>
|
||||
|
||||
<h2 class="text-xl font-semibold">What are the terms & conditions and the privacy policy?</h2>
|
||||
<p style="display:inline; align-items: center">
|
||||
This work is public domain. (If you like rules, reference
|
||||
<a href="http://creativecommons.org/publicdomain/zero/1.0?ref=chooser-v1" target="_blank" rel="license noopener noreferrer">
|
||||
<span class="text-blue-500 mr-1">CC0 1.0</span>
|
||||
<img
|
||||
src="../assets/help/creative-commons-circle.svg"
|
||||
alt="CC circle"
|
||||
width="20"
|
||||
class="display: inline"
|
||||
/>
|
||||
<img
|
||||
src="../assets/help/creative-commons-zero.svg"
|
||||
alt="CC zero"
|
||||
width="20"
|
||||
style="display: inline"
|
||||
/>
|
||||
</a>
|
||||
.) This is offered freely, with the hope that it helps but without any warranty or guarantee;
|
||||
if it helps you then enjoy using it,
|
||||
but if you may try to forcibly collect damages for things you think it should do (or not do)
|
||||
then don't use it.
|
||||
<br />
|
||||
As for data & privacy:
|
||||
<p>
|
||||
<router-link
|
||||
class="text-blue-500"
|
||||
:to="{ name: 'help-terms' }"
|
||||
>
|
||||
Read them here.
|
||||
</router-link>
|
||||
</p>
|
||||
<ul class="list-disc list-outside ml-4">
|
||||
<li>
|
||||
If sending images, a server stores them. They can be removed by editing each claim
|
||||
and deleting the image.
|
||||
</li>
|
||||
<li>
|
||||
If sending other partner system data (eg. to Trustroots) a public key and message
|
||||
data are stored on a server. Those can be removed via direct personal request (via contact below).
|
||||
</li>
|
||||
<li>
|
||||
For all other claim data,
|
||||
<a href="https://endorser.ch/privacy-policy" target="_blank" class="text-blue-500">
|
||||
the Endorser Service has this Privacy Policy.
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
<h2 class="text-xl font-semibold">How can I contribute?</h2>
|
||||
<p>
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -114,6 +114,49 @@
|
||||
@assign="handleRepresentativeAssigned"
|
||||
/>
|
||||
|
||||
<!-- Parent Project Selection -->
|
||||
<div class="w-full flex items-stretch my-4">
|
||||
<div
|
||||
class="flex items-center gap-2 grow border border-slate-400 border-r-0 last:border-r px-3 py-2 rounded-l last:rounded overflow-hidden cursor-pointer hover:bg-slate-100"
|
||||
@click="openParentProjectDialog"
|
||||
>
|
||||
<div>
|
||||
<font-awesome icon="folder" class="text-slate-400" />
|
||||
</div>
|
||||
<div class="overflow-hidden">
|
||||
<div
|
||||
:class="{
|
||||
'text-sm font-semibold': parentProjectHandleId,
|
||||
'text-slate-400': !parentProjectHandleId,
|
||||
}"
|
||||
class="truncate"
|
||||
>
|
||||
{{
|
||||
parentProjectHandleId
|
||||
? parentProjectName || "Parent Project"
|
||||
: "Select Parent Project\u2026"
|
||||
}}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<button
|
||||
v-if="parentProjectHandleId"
|
||||
class="text-rose-600 px-3 py-2 border border-slate-400 border-l-0 rounded-r hover:bg-rose-600 hover:text-white hover:border-rose-600"
|
||||
@click="unsetParentProject"
|
||||
>
|
||||
<font-awesome icon="trash-can" />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<ProjectSelectionDialog
|
||||
ref="parentProjectDialog"
|
||||
:active-did="activeDid"
|
||||
:all-my-dids="allMyDids"
|
||||
:all-contacts="allContacts"
|
||||
:notify="$notify"
|
||||
@assign="handleParentProjectSelected"
|
||||
/>
|
||||
|
||||
<div class="mb-4">
|
||||
<p v-if="shouldShowOwnershipWarning">
|
||||
<span class="text-red-500">Beware!</span>
|
||||
@@ -283,6 +326,7 @@ import { LeafletMouseEvent } from "leaflet";
|
||||
import EntityIcon from "../components/EntityIcon.vue";
|
||||
import ImageMethodDialog from "../components/ImageMethodDialog.vue";
|
||||
import ProjectRepresentativeDialog from "../components/ProjectRepresentativeDialog.vue";
|
||||
import ProjectSelectionDialog from "../components/ProjectSelectionDialog.vue";
|
||||
import QuickNav from "../components/QuickNav.vue";
|
||||
import {
|
||||
AppString,
|
||||
@@ -311,6 +355,7 @@ import {
|
||||
PROJECT_TIMEOUT_VERY_LONG,
|
||||
} from "../constants/notifications";
|
||||
import { PlanActionClaim } from "../interfaces/claims";
|
||||
import { PlanData } from "../interfaces/records";
|
||||
import {
|
||||
createEndorserJwtVcFromClaim,
|
||||
getHeaders,
|
||||
@@ -378,6 +423,7 @@ import { logger } from "../utils/logger";
|
||||
components: {
|
||||
EntityIcon,
|
||||
ImageMethodDialog,
|
||||
ProjectSelectionDialog,
|
||||
ProjectRepresentativeDialog,
|
||||
LMap,
|
||||
LMarker,
|
||||
@@ -429,6 +475,8 @@ export default class NewEditProjectView extends Vue {
|
||||
latitude = 0;
|
||||
longitude = 0;
|
||||
numAccounts = 0;
|
||||
parentProjectHandleId = "";
|
||||
parentProjectName = "";
|
||||
projectId = "";
|
||||
projectIssuerDid = "";
|
||||
sendToTrustroots = false;
|
||||
@@ -510,6 +558,10 @@ export default class NewEditProjectView extends Vue {
|
||||
);
|
||||
}
|
||||
}
|
||||
if (this.fullClaim?.fulfills?.identifier) {
|
||||
this.parentProjectHandleId = this.fullClaim.fulfills.identifier;
|
||||
this.loadParentProjectName(this.parentProjectHandleId);
|
||||
}
|
||||
if (this.fullClaim.startTime) {
|
||||
const localDateTime = DateTime.fromISO(
|
||||
this.fullClaim.startTime as string,
|
||||
@@ -623,6 +675,14 @@ export default class NewEditProjectView extends Vue {
|
||||
} else {
|
||||
delete vcClaim.agent;
|
||||
}
|
||||
if (this.parentProjectHandleId) {
|
||||
vcClaim.fulfills = {
|
||||
"@type": "PlanAction",
|
||||
identifier: this.parentProjectHandleId,
|
||||
};
|
||||
} else {
|
||||
delete vcClaim.fulfills;
|
||||
}
|
||||
if (this.imageUrl) {
|
||||
vcClaim.image = this.imageUrl;
|
||||
} else {
|
||||
@@ -1075,5 +1135,33 @@ export default class NewEditProjectView extends Vue {
|
||||
unsetRepresentative(): void {
|
||||
this.agentDid = "";
|
||||
}
|
||||
|
||||
openParentProjectDialog(): void {
|
||||
(this.$refs.parentProjectDialog as ProjectSelectionDialog).open();
|
||||
}
|
||||
|
||||
handleParentProjectSelected(project: PlanData): void {
|
||||
this.parentProjectHandleId = project.handleId;
|
||||
this.parentProjectName = project.name;
|
||||
}
|
||||
|
||||
unsetParentProject(): void {
|
||||
this.parentProjectHandleId = "";
|
||||
this.parentProjectName = "";
|
||||
}
|
||||
|
||||
private async loadParentProjectName(handleId: string): Promise<void> {
|
||||
try {
|
||||
const url =
|
||||
this.apiServer + "/api/claim/byHandle/" + encodeURIComponent(handleId);
|
||||
const headers = await getHeaders(this.activeDid);
|
||||
const resp = await this.axios.get(url, { headers });
|
||||
if (resp.status === 200 && resp.data?.claim?.name) {
|
||||
this.parentProjectName = resp.data.claim.name;
|
||||
}
|
||||
} catch {
|
||||
// Parent project name will remain empty
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
@@ -267,7 +267,7 @@
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<MeetingProjectDialog
|
||||
<ProjectSelectionDialog
|
||||
ref="meetingProjectDialog"
|
||||
:active-did="activeDid"
|
||||
:all-my-dids="allMyDids"
|
||||
@@ -366,7 +366,8 @@
|
||||
Do Not Pair Together
|
||||
</h4>
|
||||
<p class="text-xs text-gray-500 mb-2">
|
||||
People in the same group will not be matched with each other.
|
||||
People in the same do-not-pair group will not be matched with each
|
||||
other.
|
||||
</p>
|
||||
<p v-if="hasActiveMatches" class="text-xs text-amber-600 mb-2">
|
||||
Erase matches to change restrictions.
|
||||
@@ -585,7 +586,7 @@ import TopMessage from "../components/TopMessage.vue";
|
||||
import MeetingMembersList from "../components/MeetingMembersList.vue";
|
||||
import MeetingMemberMatch from "../components/MeetingMemberMatch.vue";
|
||||
import MeetingExclusionGroups from "../components/MeetingExclusionGroups.vue";
|
||||
import MeetingProjectDialog from "../components/MeetingProjectDialog.vue";
|
||||
import ProjectSelectionDialog from "../components/ProjectSelectionDialog.vue";
|
||||
import ProjectIcon from "../components/ProjectIcon.vue";
|
||||
import {
|
||||
errorStringForLog,
|
||||
@@ -637,7 +638,7 @@ interface MeetingSetupInputs {
|
||||
MeetingMembersList,
|
||||
MeetingMemberMatch,
|
||||
MeetingExclusionGroups,
|
||||
MeetingProjectDialog,
|
||||
ProjectSelectionDialog,
|
||||
ProjectIcon,
|
||||
},
|
||||
mixins: [PlatformServiceMixin],
|
||||
@@ -1468,7 +1469,7 @@ export default class OnboardMeetingView extends Vue {
|
||||
* Open the project link selection dialog
|
||||
*/
|
||||
openProjectLinkDialog(): void {
|
||||
(this.$refs.meetingProjectDialog as MeetingProjectDialog).open();
|
||||
(this.$refs.meetingProjectDialog as ProjectSelectionDialog).open();
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -193,7 +193,7 @@
|
||||
class="bg-slate-100 px-4 py-3 rounded-md"
|
||||
>
|
||||
<h3 class="text-sm uppercase font-semibold mt-3">
|
||||
Projects That Contribute To This
|
||||
These Projects Are Part Of This
|
||||
</h3>
|
||||
<!--
|
||||
centering because long, wrapped project names didn't left align with blank
|
||||
@@ -218,7 +218,7 @@
|
||||
<div>
|
||||
<div v-if="fulfilledByThis" class="bg-slate-100 px-4 py-3 rounded-md">
|
||||
<h3 class="text-sm uppercase font-semibold mb-3">
|
||||
Projects Getting Contributions From This
|
||||
This Project Is Part Of These
|
||||
</h3>
|
||||
<!--
|
||||
centering because long, wrapped project names didn't left align with blank
|
||||
|
||||
@@ -223,7 +223,7 @@ export default class QuickActionBvcBeginView extends Vue {
|
||||
);
|
||||
this.notify.error(
|
||||
timeResult?.error || NOTIFY_BVC_TIME_ERROR.message,
|
||||
TIMEOUTS.LONG,
|
||||
timeResult?.error ? TIMEOUTS.MODAL : TIMEOUTS.LONG,
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -251,7 +251,7 @@ export default class QuickActionBvcBeginView extends Vue {
|
||||
);
|
||||
this.notify.error(
|
||||
attendResult?.error || NOTIFY_BVC_ATTENDANCE_ERROR.message,
|
||||
TIMEOUTS.LONG,
|
||||
attendResult?.error ? TIMEOUTS.MODAL : TIMEOUTS.LONG,
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -276,7 +276,7 @@ export default class QuickActionBvcBeginView extends Vue {
|
||||
logger.error("[QuickActionBvcBeginView] Error sending claims:", error);
|
||||
this.notify.error(
|
||||
error.userMessage || NOTIFY_BVC_SUBMISSION_ERROR.message,
|
||||
TIMEOUTS.LONG,
|
||||
error.userMessage ? TIMEOUTS.MODAL : TIMEOUTS.LONG,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -54,7 +54,11 @@
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<!-- Nearest Neighbors Section -->
|
||||
<!--
|
||||
Network Connections section: shows nearest neighbors in the registration
|
||||
graph for this user profile. The same conventions and styling are used in
|
||||
ClaimView.vue for claim-level nearest neighbors. Keep changes in sync.
|
||||
-->
|
||||
<div
|
||||
v-if="
|
||||
profile.issuerDid !== activeDid && // only show neighbors if they're not current user
|
||||
@@ -63,7 +67,46 @@
|
||||
"
|
||||
class="mt-6"
|
||||
>
|
||||
<h2 class="text-lg font-semibold mb-3">Network Connections</h2>
|
||||
<h2 class="text-lg font-semibold mb-3">
|
||||
Network Connections
|
||||
<button
|
||||
title="What is this?"
|
||||
class="ml-1 align-middle"
|
||||
@click="showNeighborsInfo = true"
|
||||
>
|
||||
<font-awesome
|
||||
icon="circle-info"
|
||||
class="text-base text-blue-500 cursor-pointer"
|
||||
/>
|
||||
</button>
|
||||
</h2>
|
||||
|
||||
<!-- Info modal for network connections explanation -->
|
||||
<div
|
||||
v-if="showNeighborsInfo"
|
||||
class="fixed inset-0 bg-black bg-opacity-50 flex items-start justify-center pt-16 z-50"
|
||||
@click.self="showNeighborsInfo = false"
|
||||
>
|
||||
<div class="bg-white rounded-lg p-6 mx-4 max-w-md">
|
||||
<h3 class="text-lg font-semibold mb-2">Network Connections</h3>
|
||||
<p class="text-sm text-slate-700">
|
||||
This section shows
|
||||
{{
|
||||
neighbors.length === 1
|
||||
? "a contact that is"
|
||||
: "contacts that are"
|
||||
}}
|
||||
nearer to this person. If you want more information, reach out to
|
||||
one of them and ask for an introduction.
|
||||
</p>
|
||||
<button
|
||||
class="mt-4 w-full bg-blue-600 text-white py-2 rounded-md"
|
||||
@click="showNeighborsInfo = false"
|
||||
>
|
||||
Got it
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-if="loadingNeighbors">
|
||||
<div class="flex justify-center items-center py-8">
|
||||
@@ -124,8 +167,8 @@
|
||||
>
|
||||
Go to contact info
|
||||
</router-link>
|
||||
and send them the link in your clipboard and ask for an
|
||||
introduction to this person.
|
||||
and send them the profile link from your clipboard. Ask them to
|
||||
introduce you to this person.
|
||||
<div
|
||||
v-if="
|
||||
getNeighborDisplayName(neighbor.did) === '' ||
|
||||
@@ -269,6 +312,7 @@ export default class UserProfileView extends Vue {
|
||||
neighborsError = "";
|
||||
partnerApiServer = DEFAULT_PARTNER_API_SERVER;
|
||||
profile: UserProfile | null = null;
|
||||
showNeighborsInfo = false;
|
||||
|
||||
// make this function available to the Vue template
|
||||
didInfo = didInfo;
|
||||
|
||||
@@ -130,11 +130,13 @@ test('Add contact, record gift, confirm gift', async ({ page }) => {
|
||||
|
||||
// Refresh home view and check gift
|
||||
await page.goto('./');
|
||||
// Wait for async data (e.g. "new offers" banner) to finish loading so the layout
|
||||
// is stable before clicking — otherwise a layout shift can redirect the click.
|
||||
await page.waitForLoadState('networkidle');
|
||||
|
||||
// Firefox complains on load the initial feed here when we use the test server.
|
||||
// It may be similar to the CORS problem below.
|
||||
const item = await page.locator('li:first-child').filter({ hasText: finalTitle });
|
||||
await item.locator('[data-testid="circle-info-link"]').click();
|
||||
const item = page.locator('li').filter({ hasText: finalTitle }).locator('[data-testid="circle-info-link"]');
|
||||
await expect(item).toBeVisible();
|
||||
await item.click();
|
||||
await expect(page.getByRole('heading', { name: 'Verifiable Claim Details' })).toBeVisible();
|
||||
await expect(page.getByText(finalTitle, { exact: true })).toBeVisible();
|
||||
|
||||
|
||||