Compare commits

...

34 Commits

Author SHA1 Message Date
Jose Olarte III
903047f13b style(gift): center entity selection step heading and entity type toggle 2026-06-18 21:09:32 +08:00
Jose Olarte III
48be234af4 fix(home): offset scrolled Thank button for safe-area-inset-bottom 2026-06-17 17:57:13 +08:00
Jose Olarte III
8d8bcf2a7e style(home): rework Thank button and sticky scroll action bar
Replace the floating circular plus FAB with a full-width bottom bar that
matches the inline Thank button. Wrap the quick-action section in a styled
container and raise the scroll threshold to 120px.
2026-06-16 21:48:34 +08:00
dae23300fe point to a single .entitlements file (undo most of previous commit) 2026-06-02 15:50:17 -06:00
9e401febea add 'share' to the entitlements for production, for sharing with this app 2026-06-02 15:46:36 -06:00
cd4b279703 Merge pull request '16kb-pages' (#232) from 16kb-pages into master
Reviewed-on: #232
2026-05-25 20:01:18 +00:00
a3a2d97b9a update version to v 1.4.2 build 68 2026-05-24 21:50:39 -06:00
802050259c update android build, fix ios build for new version of MLKit BarcodeScanner (both build) 2026-05-24 21:24:18 -06:00
efd7d50a84 fix build error 2026-05-24 19:12:40 -06:00
39c389cda8 make do-not-pair group verbiage more clear 2026-05-24 18:38:56 -06:00
93fdcaf7ff fix timing error for a click (that only showed in firefox) 2026-05-24 18:29:11 -06:00
ad419efa0d utilize 'userMessage' if sent by server 2026-05-24 16:23:47 -06:00
ce45ddb2bd update after 'audit fix' 2026-05-24 16:23:09 -06:00
7d306bd204 add first cut for 16kb page sizes, all by Claude 2026-05-10 10:15:10 -06:00
9713313a40 fix HTML syntax warning 2026-05-10 09:43:46 -06:00
Jose Olarte III
ffa7bac319 fix(ios): ensure capacitor-assets output dirs exist on fresh clones
Gitignored AppIcon.appiconset and Splash.imageset are absent after clone,
which made `capacitor-assets generate --ios` fail (missing paths and
Contents.json). Add ensure_ios_capacitor_asset_directories in common.sh
to mkdir and seed minimal Contents.json when needed; call it from
build-ios.sh before asset generation and from the build:native npm script.
Document the behavior in ios/.gitignore.
2026-04-13 16:20:51 +08:00
e0e0a0a183 bump version and add -beta 2026-04-05 20:08:24 -06:00
ea662f4430 bump to v 1.3.13 (for a web release) 2026-04-05 19:58:36 -06:00
81647e1f3c make terms & conditions into a separate page 2026-04-05 19:21:43 -06:00
bf1ee78025 allow a custom error message to stay on the screen indefinitely 2026-03-29 19:11:49 -06:00
Jose Olarte III
66b7d0f46e docs(readme): expand Setup & Building quick start for all platforms
Restructure the quick start with Web, Android, and iOS subheadings; put
each npm command in its own code block; fold the test-page step into the
Web section. Document Android (build:android:test:run + ADB, link to
BUILDING.md) and iOS (build:ios:studio + Xcode prerequisites).
2026-03-26 19:41:03 +08:00
Jose Olarte III
63dcf44125 fix(ios): make build-ios.sh work on current simulators and trim xcodebuild noise
Use generic/platform=iOS Simulator instead of a fixed device name so CLI builds
do not fail when that simulator is not installed (e.g. newer Xcode runtimes).

Pass -quiet to xcodebuild and enable SWIFT_SUPPRESS_WARNINGS plus
GCC_WARN_INHIBIT_ALL_WARNINGS for scripted builds and IPA archive/export so
terminal output stays smaller; full diagnostics remain available in Xcode.
2026-03-26 19:40:07 +08:00
cf1ecdfb4c add registration for new contacts that are unregistered 2026-03-22 20:20:33 -06:00
e9ad61b780 don't delete a gift image on an edit unless they hit 'save' 2026-03-22 20:07:59 -06:00
ad8df3eb93 fix problem where canceling an edit deletes an image 2026-03-22 20:06:58 -06:00
05d346edce add project selection for one that this 'fulfills' 2026-03-22 17:58:46 -06:00
e259e60fa7 bump version and add "-beta" 2026-03-22 17:39:46 -06:00
821de3f006 do not toggle off the 'advanced' section in account view with the 'general' toggle is disabled 2026-03-22 09:53:56 -06:00
43f83031d4 rename app from "Gifties" to "Giftopia" 2026-03-21 16:27:21 -06:00
688a48a332 bump to version 1.3.12 build 67 2026-03-21 16:22:14 -06:00
8938c242ee change more files to name the app "Gifties" 2026-03-20 19:33:04 -06:00
358af42afd rename from "Gift Economies" to "Gifties" 2026-03-19 21:18:11 -06:00
59c00241b8 add the nearest-neighbor feature to the claim screen 2026-03-19 20:24:09 -06:00
33ec90e571 move the 'discover' page 'starred' word to be on the same level 2026-03-18 19:44:25 -06:00
47 changed files with 5001 additions and 3898 deletions

View File

@@ -1 +1 @@
18.19.0
20.18.1

2
.nvmrc
View File

@@ -1 +1 @@
18.19.0
20.18.1

View File

@@ -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 68 && perl -p -i -e "s/MARKETING_VERSION = .*;/MARKETING_VERSION = 1.4.2;/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 68/g' android/app/build.gradle
perl -p -i -e 's/versionName .*/versionName "1.4.2"/g' android/app/build.gradle
```
##### 2. Build

View File

@@ -6,9 +6,24 @@ 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.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]

View File

@@ -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).

View File

@@ -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 68
versionName "1.4.2"
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 {

View File

@@ -2,8 +2,8 @@
android {
compileOptions {
sourceCompatibility JavaVersion.VERSION_17
targetCompatibility JavaVersion.VERSION_17
sourceCompatibility JavaVersion.VERSION_21
targetCompatibility JavaVersion.VERSION_21
}
}

View File

@@ -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"
},

View File

@@ -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>

View File

@@ -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
}
}

View File

@@ -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 = [:]

View File

@@ -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'
}

View File

@@ -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"
},

View File

@@ -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'
},

View File

@@ -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
View File

@@ -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

View File

@@ -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 = 68;
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.2;
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 = 68;
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.2;
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 = 68;
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.2;
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 = 68;
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.2;
MTL_FAST_MATH = YES;
PRODUCT_BUNDLE_IDENTIFIER = app.timesafari.TimeSafariShareExtension;
PRODUCT_NAME = "$(TARGET_NAME)";

View File

@@ -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>

View File

@@ -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

View File

@@ -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):
- 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
SQLCipher: eb79c64049cb002b4e9fcb30edb7979bf4706dfc
TimesafariDailyNotificationPlugin: 3c12e8c39fc27f689f56cf4e57230a8c28611fcc
ZIPFoundation: b8c29ea7ae353b309bc810586181fd073cb3312c
ZIPFoundation: dfd3d681c4053ff7e2f7350bc4e53b5dba3f5351
PODFILE CHECKSUM: 6d92bfa46c6c2d31d19b8c0c38f56a8ae9fd222f
PODFILE CHECKSUM: 87c07d03f36ef38ab0c873802aee1ce9b5d34448
COCOAPODS: 1.16.2

7521
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,7 +1,8 @@
{
"name": "timesafari",
"version": "1.3.8-beta",
"description": "Gift Economies Application",
"name": "giftopia",
"version": "1.4.2",
"description": "Giftopia App",
"author": {
"name": "Gift Economies Team"
},
@@ -28,7 +29,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 +139,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",

View File

@@ -222,7 +222,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 +234,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,6 +424,7 @@ fi
# Handle assets-only mode
if [ "$ASSETS_ONLY" = true ]; then
log_info "Assets-only mode: generating assets"
ensure_ios_capacitor_asset_directories
safe_execute "Generating assets" "npx capacitor-assets generate --ios" || exit 7
log_success "Assets generation completed successfully!"
exit 0
@@ -555,6 +574,7 @@ safe_execute "Installing CocoaPods dependencies" "run_pod_install_with_workaroun
safe_execute "Syncing with Capacitor" "run_cap_sync_with_workaround" || exit 6
# Step 7: Generate assets
ensure_ios_capacitor_asset_directories
safe_execute "Generating assets" "npx capacitor-assets generate --ios" || exit 7
# Step 8: Build iOS app
@@ -564,16 +584,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

View File

@@ -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
@@ -344,4 +365,5 @@ export -f check_command check_directory check_file
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 print_usage parse_args
export -f ensure_ios_capacitor_asset_directories

View File

@@ -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 }}

View File

@@ -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>

View File

@@ -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;

View File

@@ -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",

View File

@@ -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.",

View File

@@ -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;

View File

@@ -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",

View File

@@ -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);
}
},
);

View File

@@ -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;
@@ -2119,6 +2116,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 +2127,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 +2237,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 +2357,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

View File

@@ -398,7 +398,165 @@
</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
-->
<h2
@@ -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();
});
}

View File

@@ -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

View File

@@ -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,

View File

@@ -263,7 +263,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 +301,7 @@ export default class GiftedDetails extends Vue {
giverName = "";
hideBackButton = false;
imageUrl = "";
imageUrlToDelete = "";
message = "";
offerId = "";
prevCredToEdit?: GenericCredWrapper<GiveActionClaim>;
@@ -517,7 +517,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 +529,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 +545,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 +570,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 +727,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,

View File

@@ -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
View 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>

View File

@@ -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>

View File

@@ -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;
}

View File

@@ -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>

View File

@@ -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();
}
/**

View File

@@ -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

View File

@@ -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,
);
}
}

View File

@@ -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;

View File

@@ -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();