Compare commits

...

30 Commits

Author SHA1 Message Date
Jose Olarte III
8827c4a973 fix(electron): resolve TypeScript errors in Electron build configuration
- Create separate Electron-specific capacitor config
- Update build script to not copy main config to Electron directory
- Fix TypeScript compilation by excluding main config from Electron tsconfig

Resolves TypeScript compilation errors in npm run build:electron:dev
2025-08-27 19:04:27 +08:00
aa346a9abd feat: for confirm list, allow to see 30 minutes before meeting start 2025-08-26 20:29:26 -06:00
9ea2f96106 fix: Fix onboard-meeting-members deep link with groupId.
This was cherry-picked from master-patch
2025-08-26 20:28:31 -06:00
623bf12ecd doc: add simpler instruction to get started with testing 2025-08-26 19:25:31 -06:00
Matthew Raymer
427660d686 docs: enhance serve command documentation in BUILDING.md
- Clarify build:web:serve purpose as "production testing"
- Add "Why Use serve?" section explaining benefits
- Document SPA routing support for deep links (/discover, /account)
- Add dedicated "Local Serving with serve" technical section
- Explain server options (npx serve vs Python fallback)
- Improve developer understanding of when and why to use serve

Fixes documentation gap identified in serve command usage
2025-08-26 09:58:35 +00:00
Matthew Raymer
643f31c43a Merge branch 'master' into build-web-serve-test 2025-08-26 09:52:49 +00:00
2c6b787fa2 Merge pull request 'Show current user in ContactGiftingView' (#155) from contact-gifting-current-user into master
Reviewed-on: #155
2025-08-26 05:00:10 -04:00
Jose Olarte III
ec53452220 Chore: lint fix 2025-08-26 16:52:32 +08:00
Jose Olarte III
ec326495b2 Merge branch 'master' into contact-gifting-current-user 2025-08-26 16:51:09 +08:00
Jose Olarte III
cc50c38d13 Chore: simplify wording for person entities
- Replaced all instances of "Unnamed Member" with "Someone Unnamed"
- Removed unused UNNAMED_MEMBER constant
- Renamed UNNAMED_PERSON to THAT_UNNAMED_PERSON to match its value
- Renamed UNNAMED_USER to UNNAMED_PERSON and changed the value to "unnamed person"
2025-08-26 16:32:57 +08:00
3969167d92 Merge pull request 'feat: implement safe area insets for Android and add development tooling' (#182) from android-safe-area-insets into master
Reviewed-on: #182
2025-08-26 03:29:50 -04:00
Jose Olarte III
9dfb2fda27 Chore: regenerate podfile.lock checksum 2025-08-26 15:30:29 +08:00
Jose Olarte III
d3aa2e40a0 Merge branch 'master' into android-safe-area-insets 2025-08-26 15:26:18 +08:00
Jose Olarte III
08cda50f13 Merge branch 'master' into android-safe-area-insets 2025-08-26 15:20:12 +08:00
716a23e76b Merge pull request 'fix/deep-link-views: use proper #Content wrapper' (#184) from deep-link-views-safe-area-inset into master
Reviewed-on: #184
2025-08-26 02:45:30 -04:00
Jose Olarte III
dc857f9119 Revert: linting in package.json 2025-08-25 15:11:31 +08:00
Jose Olarte III
3a8652fd8d docs: revert misplaced changelog 2025-08-22 22:07:37 +08:00
Jose Olarte III
c2949c4dbf Merge branch 'master' into android-safe-area-insets 2025-08-22 21:10:17 +08:00
Jose Olarte III
4ba58145d0 feat: implement safe area insets for Android and add development tooling
- Add @capacitor/status-bar dependency for safe area detection
- Implement SafeAreaPlugin for Android with proper inset calculation
- Create safeAreaInset.js utility for CSS custom property injection
- Update Android manifest and build configuration for plugin
- Integrate safe area handling across Vue components and views
- Update iOS Podfile and Android gradle configurations
- Add commitlint and husky for commit message validation

Technical changes:
- SafeAreaPlugin uses WindowInsets API for Android R+ devices
- Fallback detection for navigation bar and gesture bar heights
- CSS custom properties: --safe-area-inset-{top,bottom,left,right}
- Platform-specific detection (Android WebView only)
- StatusBar plugin integration for top inset calculation
2025-08-22 21:06:11 +08:00
Matthew Raymer
e3cc22245c fix(build): resolve web build script export error and add missing serve dependency
- Fix malformed multi-line comment in .env.test causing shell parsing failure
- Add serve@14.2.4 as dev dependency to eliminate build-time installation prompts
- Resolves "export: ' production).=': not a valid identifier" error
- Test environment builds now complete successfully without user interaction

Fixes test environment build blocking issue
2025-08-19 11:38:32 +00:00
Jose Olarte III
eb44e7b51e Chore: convert "unnamed" into constant
- "Unnamed/Unknown" simplified into just "Unnamed"
- Phrase variations have their own constants
2025-08-18 20:33:19 +08:00
Jose Olarte III
ca8d72e1c9 Fix: remove non-applicable IDs
- Projects use handleID, not DID
2025-08-18 16:43:15 +08:00
Jose Olarte III
a4528c5703 Refactor: eliminate "special" entity type and use DID-based logic
Replace string-based entity type matching with DID-based logic for "You" and "Unnamed" entities. Treat these as regular person entities instead of special types.

- Remove "special" type from EntitySelectionEvent interface
- Update EntityGrid to emit "You" and "Unnamed" as person entities
- Simplify SpecialEntityCard emit structure (remove entityType parameter)
- Refactor GiftedDialog to process all person entities with DID-based logic
- Update ContactGiftingView and HomeView to use DID-based entity creation
- Remove string literals "You" and "Unnamed" from method signatures
2025-08-15 13:22:49 +08:00
Jose Olarte III
6acebb66ef Merge branch 'master' into contact-gifting-current-user 2025-08-15 10:28:13 +08:00
b3f7026afe Merge branch 'master' into contact-gifting-current-user 2025-08-12 06:35:08 -04:00
Jose Olarte III
ec1a725832 Refactor: improve dialog logic and entity handling
- Split openDialog into separate methods to improve code readability and maintainability through method extraction
- Add receiver name fallback in GiftedDialog when receiver exists but has no name
- Enhance shouldShowYouEntity to prevent selecting "You" as both giver and recipient
- Improve labeling of "(No name)" entities while retaining original entity object properties
- Apply special styling to "Unnamed" and "(No Name)" entities
2025-08-12 18:34:47 +08:00
Jose Olarte III
6d316c2b3f Fix: restored prop to show/hide "Show All" 2025-08-11 19:14:35 +08:00
Jose Olarte III
24f6730572 Fix: remove redundant method 2025-08-11 19:06:42 +08:00
Jose Olarte III
0fc44b31bf Merge branch 'master' into contact-gifting-current-user 2025-08-11 19:01:06 +08:00
Jose Olarte III
bed2c7106a Added: current user in ContactGiftingView
- "You" is displayed conditionally, similar to GiftedDialog behavior
- "Show All" is hidden in GiftedDialog when accessed from ContactGiftingView (redundant)
2025-07-28 18:46:56 +08:00
51 changed files with 1569 additions and 314 deletions

View File

@@ -251,7 +251,7 @@ npm run build:web:dev # Start development server with hot reload
npm run build:web # Development build (starts dev server with hot reload) npm run build:web # Development build (starts dev server with hot reload)
npm run build:web:test # Test environment build (optimized for testing) npm run build:web:test # Test environment build (optimized for testing)
npm run build:web:prod # Production build (optimized for production) npm run build:web:prod # Production build (optimized for production)
npm run build:web:serve # Build and serve locally (builds then serves) npm run build:web:serve # Build and serve locally for production testing
# Docker builds # Docker builds
npm run build:web:docker # Development build with Docker containerization npm run build:web:docker # Development build with Docker containerization
@@ -269,6 +269,12 @@ Start the development server using `npm run build:web:dev` or `npm run build:web
2. The built files will be in the `dist` directory 2. The built files will be in the `dist` directory
3. To test the production build locally, use `npm run build:web:serve` (builds then serves) 3. To test the production build locally, use `npm run build:web:serve` (builds then serves)
**Why Use `serve`?**
- **Production Testing**: Test your optimized production build locally before deployment
- **SPA Routing Validation**: Verify deep linking and navigation work correctly (handles routes like `/discover`, `/account`)
- **Performance Testing**: Test the minified and optimized build locally
- **Deployment Validation**: Ensure built files work correctly when served by a real HTTP server
You'll likely want to use test locations for the Endorser & image & partner servers; see "DEFAULT_ENDORSER_API_SERVER" & "DEFAULT_IMAGE_API_SERVER" & "DEFAULT_PARTNER_API_SERVER" below. You'll likely want to use test locations for the Endorser & image & partner servers; see "DEFAULT_ENDORSER_API_SERVER" & "DEFAULT_IMAGE_API_SERVER" & "DEFAULT_PARTNER_API_SERVER" below.
### Web Build Script Details ### Web Build Script Details
@@ -288,7 +294,7 @@ All web build commands use the `./scripts/build-web.sh` script, which provides:
- **Clean Build**: Removes previous `dist/` directory - **Clean Build**: Removes previous `dist/` directory
- **Vite Build**: Executes `npx vite build --config vite.config.web.mts` - **Vite Build**: Executes `npx vite build --config vite.config.web.mts`
- **Docker Support**: Optional Docker containerization - **Docker Support**: Optional Docker containerization
- **Local Serving**: Built-in HTTP server for testing builds - **Local Serving**: Built-in HTTP server for testing builds with SPA routing support
**Direct Script Usage:** **Direct Script Usage:**
@@ -324,6 +330,25 @@ All web build commands use the `./scripts/build-web.sh` script, which provides:
- `5` - Serve command failed - `5` - Serve command failed
- `6` - Invalid build mode - `6` - Invalid build mode
### Local Serving with `serve`
The `serve` functionality provides a local HTTP server for testing production builds:
**What It Does:**
1. **Builds** the application using Vite
2. **Serves** the built files from the `dist/` directory
3. **Handles SPA Routing** - serves `index.html` for all routes (fixes 404s on `/discover`, `/account`, etc.)
**Server Options:**
- **Primary**: `npx serve -s dist -l 8080` (recommended - full SPA support)
- **Fallback**: Python HTTP server (limited SPA routing support)
**Use Cases:**
- Testing production builds before deployment
- Validating SPA routing behavior
- Performance testing of optimized builds
- Debugging production build issues locally
### Compile and minify for test & production ### Compile and minify for test & production
- If there are DB changes: before updating the test server, open browser(s) with - If there are DB changes: before updating the test server, open browser(s) with
@@ -592,7 +617,8 @@ The Electron build process follows a multi-stage approach:
#### **Stage 2: Capacitor Sync** #### **Stage 2: Capacitor Sync**
- Copies web assets to Electron app directory - Copies web assets to Electron app directory
- Syncs Capacitor configuration and plugins - Uses Electron-specific Capacitor configuration (not copied from main config)
- Syncs Capacitor plugins for Electron platform
- Prepares native module bindings - Prepares native module bindings
#### **Stage 3: TypeScript Compile** #### **Stage 3: TypeScript Compile**

View File

@@ -18,7 +18,7 @@ npm install
npm run build:web:serve -- --test npm run build:web:serve -- --test
``` ```
To be able to make submissions: go to "profile" (bottom left), go to the bottom and expand "Show Advanced Settings", go to the bottom and to the "Test Page", and finally "Become User 0" to see all the functionality. To be able to take action on the platform: go to [the test page](http://localhost:8080/test) and click "Become User 0".
See [BUILDING.md](BUILDING.md) for comprehensive build instructions for all platforms (Web, Electron, iOS, Android, Docker). See [BUILDING.md](BUILDING.md) for comprehensive build instructions for all platforms (Web, Electron, iOS, Android, Docker).

View File

@@ -16,6 +16,7 @@ dependencies {
implementation project(':capacitor-clipboard') implementation project(':capacitor-clipboard')
implementation project(':capacitor-filesystem') implementation project(':capacitor-filesystem')
implementation project(':capacitor-share') implementation project(':capacitor-share')
implementation project(':capacitor-status-bar')
implementation project(':capawesome-capacitor-file-picker') implementation project(':capawesome-capacitor-file-picker')
} }

View File

@@ -14,6 +14,7 @@
android:label="@string/title_activity_main" android:label="@string/title_activity_main"
android:launchMode="singleTask" android:launchMode="singleTask"
android:screenOrientation="portrait" android:screenOrientation="portrait"
android:windowSoftInputMode="adjustResize"
android:theme="@style/AppTheme.NoActionBarLaunch"> android:theme="@style/AppTheme.NoActionBarLaunch">
<intent-filter> <intent-filter>
<action android:name="android.intent.action.MAIN" /> <action android:name="android.intent.action.MAIN" />

View File

@@ -27,6 +27,10 @@
"pkg": "@capacitor/share", "pkg": "@capacitor/share",
"classpath": "com.capacitorjs.plugins.share.SharePlugin" "classpath": "com.capacitorjs.plugins.share.SharePlugin"
}, },
{
"pkg": "@capacitor/status-bar",
"classpath": "com.capacitorjs.plugins.statusbar.StatusBarPlugin"
},
{ {
"pkg": "@capawesome/capacitor-file-picker", "pkg": "@capawesome/capacitor-file-picker",
"classpath": "io.capawesome.capacitorjs.plugins.filepicker.FilePickerPlugin" "classpath": "io.capawesome.capacitorjs.plugins.filepicker.FilePickerPlugin"

View File

@@ -1,7 +1,16 @@
package app.timesafari; package app.timesafari;
import android.os.Bundle; import android.os.Bundle;
import android.view.View;
import android.view.WindowManager;
import android.view.WindowInsetsController;
import android.view.WindowInsets;
import android.os.Build;
import android.webkit.WebView;
import android.webkit.WebSettings;
import android.webkit.WebViewClient;
import com.getcapacitor.BridgeActivity; import com.getcapacitor.BridgeActivity;
import app.timesafari.safearea.SafeAreaPlugin;
//import com.getcapacitor.community.sqlite.SQLite; //import com.getcapacitor.community.sqlite.SQLite;
public class MainActivity extends BridgeActivity { public class MainActivity extends BridgeActivity {
@@ -9,7 +18,39 @@ public class MainActivity extends BridgeActivity {
public void onCreate(Bundle savedInstanceState) { public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState); super.onCreate(savedInstanceState);
// Enable edge-to-edge display for modern Android
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
// Android 11+ (API 30+)
getWindow().setDecorFitsSystemWindows(false);
// Set up system UI visibility for edge-to-edge
WindowInsetsController controller = getWindow().getInsetsController();
if (controller != null) {
controller.setSystemBarsAppearance(
WindowInsetsController.APPEARANCE_LIGHT_STATUS_BARS |
WindowInsetsController.APPEARANCE_LIGHT_NAVIGATION_BARS,
WindowInsetsController.APPEARANCE_LIGHT_STATUS_BARS |
WindowInsetsController.APPEARANCE_LIGHT_NAVIGATION_BARS
);
controller.setSystemBarsBehavior(WindowInsetsController.BEHAVIOR_SHOW_TRANSIENT_BARS_BY_SWIPE);
}
} else {
// Legacy Android (API 21-29)
getWindow().getDecorView().setSystemUiVisibility(
View.SYSTEM_UI_FLAG_LAYOUT_STABLE |
View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION |
View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN |
View.SYSTEM_UI_FLAG_LIGHT_STATUS_BAR |
View.SYSTEM_UI_FLAG_LIGHT_NAVIGATION_BAR
);
}
// Register SafeArea plugin
registerPlugin(SafeAreaPlugin.class);
// Initialize SQLite // Initialize SQLite
//registerPlugin(SQLite.class); //registerPlugin(SQLite.class);
} }
} }

View File

@@ -0,0 +1,44 @@
package app.timesafari.safearea;
import android.os.Build;
import android.view.WindowInsets;
import com.getcapacitor.JSObject;
import com.getcapacitor.Plugin;
import com.getcapacitor.PluginCall;
import com.getcapacitor.PluginMethod;
import com.getcapacitor.annotation.CapacitorPlugin;
@CapacitorPlugin(name = "SafeArea")
public class SafeAreaPlugin extends Plugin {
@PluginMethod
public void getSafeAreaInsets(PluginCall call) {
JSObject result = new JSObject();
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
WindowInsets insets = getActivity().getWindow().getDecorView().getRootWindowInsets();
if (insets != null) {
int top = insets.getInsets(WindowInsets.Type.statusBars()).top;
int bottom = insets.getInsets(WindowInsets.Type.navigationBars()).bottom;
int left = insets.getInsets(WindowInsets.Type.systemBars()).left;
int right = insets.getInsets(WindowInsets.Type.systemBars()).right;
result.put("top", top);
result.put("bottom", bottom);
result.put("left", left);
result.put("right", right);
call.resolve(result);
return;
}
}
// Fallback values
result.put("top", 0);
result.put("bottom", 0);
result.put("left", 0);
result.put("right", 0);
call.resolve(result);
}
}

View File

@@ -18,5 +18,14 @@
<style name="AppTheme.NoActionBarLaunch" parent="Theme.SplashScreen"> <style name="AppTheme.NoActionBarLaunch" parent="Theme.SplashScreen">
<item name="android:background">@drawable/splash</item> <item name="android:background">@drawable/splash</item>
<item name="android:windowTranslucentStatus">false</item>
<item name="android:windowTranslucentNavigation">false</item>
<item name="android:windowDrawsSystemBarBackgrounds">true</item>
<item name="android:statusBarColor">@android:color/transparent</item>
<item name="android:navigationBarColor">@android:color/transparent</item>
<item name="android:windowLightStatusBar">true</item>
<item name="android:windowLightNavigationBar">true</item>
<item name="android:enforceStatusBarContrast">false</item>
<item name="android:enforceNavigationBarContrast">false</item>
</style> </style>
</resources> </resources>

View File

@@ -23,5 +23,8 @@ project(':capacitor-filesystem').projectDir = new File('../node_modules/@capacit
include ':capacitor-share' include ':capacitor-share'
project(':capacitor-share').projectDir = new File('../node_modules/@capacitor/share/android') project(':capacitor-share').projectDir = new File('../node_modules/@capacitor/share/android')
include ':capacitor-status-bar'
project(':capacitor-status-bar').projectDir = new File('../node_modules/@capacitor/status-bar/android')
include ':capawesome-capacitor-file-picker' include ':capawesome-capacitor-file-picker'
project(':capawesome-capacitor-file-picker').projectDir = new File('../node_modules/@capawesome/capacitor-file-picker/android') project(':capawesome-capacitor-file-picker').projectDir = new File('../node_modules/@capawesome/capacitor-file-picker/android')

View File

@@ -0,0 +1,116 @@
import { CapacitorConfig } from '@capacitor/cli';
const config: CapacitorConfig = {
appId: 'app.timesafari',
appName: 'TimeSafari',
webDir: 'dist',
server: {
cleartext: true
},
plugins: {
App: {
appUrlOpen: {
handlers: [
{
url: 'timesafari://*',
autoVerify: true
}
]
}
},
SplashScreen: {
launchShowDuration: 3000,
launchAutoHide: true,
backgroundColor: '#ffffff',
androidSplashResourceName: 'splash',
androidScaleType: 'CENTER_CROP',
showSpinner: false,
androidSpinnerStyle: 'large',
iosSpinnerStyle: 'small',
spinnerColor: '#999999',
splashFullScreen: true,
splashImmersive: true
},
CapSQLite: {
iosDatabaseLocation: 'Library/CapacitorDatabase',
iosIsEncryption: false,
iosBiometric: {
biometricAuth: false,
biometricTitle: 'Biometric login for TimeSafari'
},
androidIsEncryption: false,
androidBiometric: {
biometricAuth: false,
biometricTitle: 'Biometric login for TimeSafari'
},
electronIsEncryption: false
}
},
ios: {
contentInset: 'never',
allowsLinkPreview: true,
scrollEnabled: true,
limitsNavigationsToAppBoundDomains: true,
backgroundColor: '#ffffff',
allowNavigation: [
'*.timesafari.app',
'*.jsdelivr.net',
'api.endorser.ch'
]
},
android: {
allowMixedContent: true,
captureInput: true,
webContentsDebuggingEnabled: false,
allowNavigation: [
'*.timesafari.app',
'*.jsdelivr.net',
'api.endorser.ch',
'10.0.2.2:3000'
]
},
electron: {
deepLinking: {
schemes: ['timesafari']
},
buildOptions: {
appId: 'app.timesafari',
productName: 'TimeSafari',
directories: {
output: 'dist-electron-packages'
},
files: [
'dist/**/*',
'electron/**/*'
],
mac: {
category: 'public.app-category.productivity',
target: [
{
target: 'dmg',
arch: ['x64', 'arm64']
}
]
},
win: {
target: [
{
target: 'nsis',
arch: ['x64']
}
]
},
linux: {
target: [
{
target: 'AppImage',
arch: ['x64']
}
],
category: 'Utility'
}
}
}
};
export default config;

View File

@@ -56,7 +56,6 @@
"version": "6.0.2", "version": "6.0.2",
"resolved": "https://registry.npmjs.org/@capacitor-community/sqlite/-/sqlite-6.0.2.tgz", "resolved": "https://registry.npmjs.org/@capacitor-community/sqlite/-/sqlite-6.0.2.tgz",
"integrity": "sha512-sj+2SPLu7E/3dM3xxcWwfNomG+aQHuN96/EFGrOtp4Dv30/2y5oIPyi6hZGjQGjPc5GDNoTQwW7vxWNzybjuMg==", "integrity": "sha512-sj+2SPLu7E/3dM3xxcWwfNomG+aQHuN96/EFGrOtp4Dv30/2y5oIPyi6hZGjQGjPc5GDNoTQwW7vxWNzybjuMg==",
"license": "MIT",
"dependencies": { "dependencies": {
"jeep-sqlite": "^2.7.2" "jeep-sqlite": "^2.7.2"
}, },

View File

@@ -1,6 +1,6 @@
{ {
"compileOnSave": true, "compileOnSave": true,
"include": ["./src/**/*", "./capacitor.config.ts", "./capacitor.config.js"], "include": ["./src/**/*"],
"compilerOptions": { "compilerOptions": {
"outDir": "./build", "outDir": "./build",
"importHelpers": true, "importHelpers": true,

View File

@@ -18,6 +18,7 @@ def capacitor_pods
pod 'CapacitorClipboard', :path => '../../node_modules/@capacitor/clipboard' pod 'CapacitorClipboard', :path => '../../node_modules/@capacitor/clipboard'
pod 'CapacitorFilesystem', :path => '../../node_modules/@capacitor/filesystem' pod 'CapacitorFilesystem', :path => '../../node_modules/@capacitor/filesystem'
pod 'CapacitorShare', :path => '../../node_modules/@capacitor/share' pod 'CapacitorShare', :path => '../../node_modules/@capacitor/share'
pod 'CapacitorStatusBar', :path => '../../node_modules/@capacitor/status-bar'
pod 'CapawesomeCapacitorFilePicker', :path => '../../node_modules/@capawesome/capacitor-file-picker' pod 'CapawesomeCapacitorFilePicker', :path => '../../node_modules/@capawesome/capacitor-file-picker'
end end

View File

@@ -19,6 +19,8 @@ PODS:
- GoogleMLKit/BarcodeScanning (= 5.0.0) - GoogleMLKit/BarcodeScanning (= 5.0.0)
- CapacitorShare (6.0.3): - CapacitorShare (6.0.3):
- Capacitor - Capacitor
- CapacitorStatusBar (6.0.2):
- Capacitor
- CapawesomeCapacitorFilePicker (6.2.0): - CapawesomeCapacitorFilePicker (6.2.0):
- Capacitor - Capacitor
- GoogleDataTransport (9.4.1): - GoogleDataTransport (9.4.1):
@@ -96,6 +98,7 @@ DEPENDENCIES:
- "CapacitorFilesystem (from `../../node_modules/@capacitor/filesystem`)" - "CapacitorFilesystem (from `../../node_modules/@capacitor/filesystem`)"
- "CapacitorMlkitBarcodeScanning (from `../../node_modules/@capacitor-mlkit/barcode-scanning`)" - "CapacitorMlkitBarcodeScanning (from `../../node_modules/@capacitor-mlkit/barcode-scanning`)"
- "CapacitorShare (from `../../node_modules/@capacitor/share`)" - "CapacitorShare (from `../../node_modules/@capacitor/share`)"
- "CapacitorStatusBar (from `../../node_modules/@capacitor/status-bar`)"
- "CapawesomeCapacitorFilePicker (from `../../node_modules/@capawesome/capacitor-file-picker`)" - "CapawesomeCapacitorFilePicker (from `../../node_modules/@capawesome/capacitor-file-picker`)"
SPEC REPOS: SPEC REPOS:
@@ -134,6 +137,8 @@ EXTERNAL SOURCES:
:path: "../../node_modules/@capacitor-mlkit/barcode-scanning" :path: "../../node_modules/@capacitor-mlkit/barcode-scanning"
CapacitorShare: CapacitorShare:
:path: "../../node_modules/@capacitor/share" :path: "../../node_modules/@capacitor/share"
CapacitorStatusBar:
:path: "../../node_modules/@capacitor/status-bar"
CapawesomeCapacitorFilePicker: CapawesomeCapacitorFilePicker:
:path: "../../node_modules/@capawesome/capacitor-file-picker" :path: "../../node_modules/@capawesome/capacitor-file-picker"
@@ -147,6 +152,7 @@ SPEC CHECKSUMS:
CapacitorFilesystem: 59270a63c60836248812671aa3b15df673fbaf74 CapacitorFilesystem: 59270a63c60836248812671aa3b15df673fbaf74
CapacitorMlkitBarcodeScanning: 7652be9c7922f39203a361de735d340ae37e134e CapacitorMlkitBarcodeScanning: 7652be9c7922f39203a361de735d340ae37e134e
CapacitorShare: d2a742baec21c8f3b92b361a2fbd2401cdd8288e CapacitorShare: d2a742baec21c8f3b92b361a2fbd2401cdd8288e
CapacitorStatusBar: b16799a26320ffa52f6c8b01737d5a95bbb8f3eb
CapawesomeCapacitorFilePicker: c40822f0a39f86855321943c7829d52bca7f01bd CapawesomeCapacitorFilePicker: c40822f0a39f86855321943c7829d52bca7f01bd
GoogleDataTransport: 6c09b596d841063d76d4288cc2d2f42cc36e1e2a GoogleDataTransport: 6c09b596d841063d76d4288cc2d2f42cc36e1e2a
GoogleMLKit: 90ba06e028795a50261f29500d238d6061538711 GoogleMLKit: 90ba06e028795a50261f29500d238d6061538711
@@ -163,6 +169,6 @@ SPEC CHECKSUMS:
SQLCipher: 31878d8ebd27e5c96db0b7cb695c96e9f8ad77da SQLCipher: 31878d8ebd27e5c96db0b7cb695c96e9f8ad77da
ZIPFoundation: b8c29ea7ae353b309bc810586181fd073cb3312c ZIPFoundation: b8c29ea7ae353b309bc810586181fd073cb3312c
PODFILE CHECKSUM: 60f54b19c5a7a07343ab5ba9e5db49019fd86aa0 PODFILE CHECKSUM: 5fa870b031c7c4e0733e2f96deaf81866c75ff7d
COCOAPODS: 1.16.2 COCOAPODS: 1.16.2

605
package-lock.json generated
View File

@@ -20,6 +20,7 @@
"@capacitor/filesystem": "^6.0.0", "@capacitor/filesystem": "^6.0.0",
"@capacitor/ios": "^6.2.0", "@capacitor/ios": "^6.2.0",
"@capacitor/share": "^6.0.3", "@capacitor/share": "^6.0.3",
"@capacitor/status-bar": "^6.0.2",
"@capawesome/capacitor-file-picker": "^6.2.0", "@capawesome/capacitor-file-picker": "^6.2.0",
"@dicebear/collection": "^5.4.1", "@dicebear/collection": "^5.4.1",
"@dicebear/core": "^5.4.1", "@dicebear/core": "^5.4.1",
@@ -137,6 +138,7 @@
"postcss": "^8.4.38", "postcss": "^8.4.38",
"prettier": "^3.2.5", "prettier": "^3.2.5",
"rimraf": "^6.0.1", "rimraf": "^6.0.1",
"serve": "^14.2.4",
"tailwindcss": "^3.4.1", "tailwindcss": "^3.4.1",
"ts-jest": "^29.4.0", "ts-jest": "^29.4.0",
"tsx": "^4.20.4", "tsx": "^4.20.4",
@@ -2346,6 +2348,14 @@
"@capacitor/core": "^6.0.0" "@capacitor/core": "^6.0.0"
} }
}, },
"node_modules/@capacitor/status-bar": {
"version": "6.0.2",
"resolved": "https://registry.npmjs.org/@capacitor/status-bar/-/status-bar-6.0.2.tgz",
"integrity": "sha512-AmRIX6QvFemItlY7/69ARkIAqitRQqJ2qwgZmD1KqgFb78pH+XFXm1guvS/a8CuOOm/IqZ4ddDbl20yxtBqzGA==",
"peerDependencies": {
"@capacitor/core": "^6.0.0"
}
},
"node_modules/@capawesome/capacitor-file-picker": { "node_modules/@capawesome/capacitor-file-picker": {
"version": "6.2.0", "version": "6.2.0",
"resolved": "https://registry.npmjs.org/@capawesome/capacitor-file-picker/-/capacitor-file-picker-6.2.0.tgz", "resolved": "https://registry.npmjs.org/@capawesome/capacitor-file-picker/-/capacitor-file-picker-6.2.0.tgz",
@@ -11751,6 +11761,13 @@
"node": ">=10.0.0" "node": ">=10.0.0"
} }
}, },
"node_modules/@zeit/schemas": {
"version": "2.36.0",
"resolved": "https://registry.npmjs.org/@zeit/schemas/-/schemas-2.36.0.tgz",
"integrity": "sha512-7kjMwcChYEzMKjeex9ZFXkt1AyNov9R5HZtjBKVsmVpw7pa7ZtlCGvCBC2vnnXctaYN+aRI61HjIqeetZW5ROg==",
"dev": true,
"license": "MIT"
},
"node_modules/@zxing/text-encoding": { "node_modules/@zxing/text-encoding": {
"version": "0.9.0", "version": "0.9.0",
"resolved": "https://registry.npmjs.org/@zxing/text-encoding/-/text-encoding-0.9.0.tgz", "resolved": "https://registry.npmjs.org/@zxing/text-encoding/-/text-encoding-0.9.0.tgz",
@@ -11813,9 +11830,8 @@
"version": "1.3.8", "version": "1.3.8",
"resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz", "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz",
"integrity": "sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==", "integrity": "sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==",
"devOptional": true,
"license": "MIT", "license": "MIT",
"optional": true,
"peer": true,
"dependencies": { "dependencies": {
"mime-types": "~2.1.34", "mime-types": "~2.1.34",
"negotiator": "0.6.3" "negotiator": "0.6.3"
@@ -11940,6 +11956,16 @@
"optional": true, "optional": true,
"peer": true "peer": true
}, },
"node_modules/ansi-align": {
"version": "3.0.1",
"resolved": "https://registry.npmjs.org/ansi-align/-/ansi-align-3.0.1.tgz",
"integrity": "sha512-IOfwwBF5iczOjp/WeY4YxyjqAFMQoZufdQWDd19SEExbVLNXqvpzSJ/M7Za4/sCPmQ0+GRquoA7bGcINcxew6w==",
"dev": true,
"license": "ISC",
"dependencies": {
"string-width": "^4.1.0"
}
},
"node_modules/ansi-escapes": { "node_modules/ansi-escapes": {
"version": "4.3.2", "version": "4.3.2",
"resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-4.3.2.tgz", "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-4.3.2.tgz",
@@ -12144,6 +12170,27 @@
"license": "ISC", "license": "ISC",
"optional": true "optional": true
}, },
"node_modules/arch": {
"version": "2.2.0",
"resolved": "https://registry.npmjs.org/arch/-/arch-2.2.0.tgz",
"integrity": "sha512-Of/R0wqp83cgHozfIYLbBMnej79U/SVGOOyuB3VVFv1NRM/PSFMK12x9KVtiYzJqmnU5WR2qp0Z5rHb7sWGnFQ==",
"dev": true,
"funding": [
{
"type": "github",
"url": "https://github.com/sponsors/feross"
},
{
"type": "patreon",
"url": "https://www.patreon.com/feross"
},
{
"type": "consulting",
"url": "https://feross.org/support"
}
],
"license": "MIT"
},
"node_modules/are-we-there-yet": { "node_modules/are-we-there-yet": {
"version": "3.0.1", "version": "3.0.1",
"resolved": "https://registry.npmjs.org/are-we-there-yet/-/are-we-there-yet-3.0.1.tgz", "resolved": "https://registry.npmjs.org/are-we-there-yet/-/are-we-there-yet-3.0.1.tgz",
@@ -12940,6 +12987,153 @@
"dev": true, "dev": true,
"license": "ISC" "license": "ISC"
}, },
"node_modules/boxen": {
"version": "7.0.0",
"resolved": "https://registry.npmjs.org/boxen/-/boxen-7.0.0.tgz",
"integrity": "sha512-j//dBVuyacJbvW+tvZ9HuH03fZ46QcaKvvhZickZqtB271DxJ7SNRSNxrV/dZX0085m7hISRZWbzWlJvx/rHSg==",
"dev": true,
"license": "MIT",
"dependencies": {
"ansi-align": "^3.0.1",
"camelcase": "^7.0.0",
"chalk": "^5.0.1",
"cli-boxes": "^3.0.0",
"string-width": "^5.1.2",
"type-fest": "^2.13.0",
"widest-line": "^4.0.1",
"wrap-ansi": "^8.0.1"
},
"engines": {
"node": ">=14.16"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/boxen/node_modules/ansi-regex": {
"version": "6.2.0",
"resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.2.0.tgz",
"integrity": "sha512-TKY5pyBkHyADOPYlRT9Lx6F544mPl0vS5Ew7BJ45hA08Q+t3GjbueLliBWN3sMICk6+y7HdyxSzC4bWS8baBdg==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=12"
},
"funding": {
"url": "https://github.com/chalk/ansi-regex?sponsor=1"
}
},
"node_modules/boxen/node_modules/ansi-styles": {
"version": "6.2.1",
"resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.1.tgz",
"integrity": "sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=12"
},
"funding": {
"url": "https://github.com/chalk/ansi-styles?sponsor=1"
}
},
"node_modules/boxen/node_modules/camelcase": {
"version": "7.0.1",
"resolved": "https://registry.npmjs.org/camelcase/-/camelcase-7.0.1.tgz",
"integrity": "sha512-xlx1yCK2Oc1APsPXDL2LdlNP6+uu8OCDdhOBSVT279M/S+y75O30C2VuD8T2ogdePBBl7PfPF4504tnLgX3zfw==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=14.16"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/boxen/node_modules/chalk": {
"version": "5.6.0",
"resolved": "https://registry.npmjs.org/chalk/-/chalk-5.6.0.tgz",
"integrity": "sha512-46QrSQFyVSEyYAgQ22hQ+zDa60YHA4fBstHmtSApj1Y5vKtG27fWowW03jCk5KcbXEWPZUIR894aARCA/G1kfQ==",
"dev": true,
"license": "MIT",
"engines": {
"node": "^12.17.0 || ^14.13 || >=16.0.0"
},
"funding": {
"url": "https://github.com/chalk/chalk?sponsor=1"
}
},
"node_modules/boxen/node_modules/emoji-regex": {
"version": "9.2.2",
"resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz",
"integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==",
"dev": true,
"license": "MIT"
},
"node_modules/boxen/node_modules/string-width": {
"version": "5.1.2",
"resolved": "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz",
"integrity": "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==",
"dev": true,
"license": "MIT",
"dependencies": {
"eastasianwidth": "^0.2.0",
"emoji-regex": "^9.2.2",
"strip-ansi": "^7.0.1"
},
"engines": {
"node": ">=12"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/boxen/node_modules/strip-ansi": {
"version": "7.1.0",
"resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.0.tgz",
"integrity": "sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"ansi-regex": "^6.0.1"
},
"engines": {
"node": ">=12"
},
"funding": {
"url": "https://github.com/chalk/strip-ansi?sponsor=1"
}
},
"node_modules/boxen/node_modules/type-fest": {
"version": "2.19.0",
"resolved": "https://registry.npmjs.org/type-fest/-/type-fest-2.19.0.tgz",
"integrity": "sha512-RAH822pAdBgcNMAfWnCBU3CFZcfZ/i1eZjwFU/dsLKumyuuP3niueg2UAukXYF0E2AAoc82ZSSf9J0WQBinzHA==",
"dev": true,
"license": "(MIT OR CC0-1.0)",
"engines": {
"node": ">=12.20"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/boxen/node_modules/wrap-ansi": {
"version": "8.1.0",
"resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-8.1.0.tgz",
"integrity": "sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"ansi-styles": "^6.1.0",
"string-width": "^5.0.1",
"strip-ansi": "^7.0.1"
},
"engines": {
"node": ">=12"
},
"funding": {
"url": "https://github.com/chalk/wrap-ansi?sponsor=1"
}
},
"node_modules/bplist-creator": { "node_modules/bplist-creator": {
"version": "0.1.0", "version": "0.1.0",
"resolved": "https://registry.npmjs.org/bplist-creator/-/bplist-creator-0.1.0.tgz", "resolved": "https://registry.npmjs.org/bplist-creator/-/bplist-creator-0.1.0.tgz",
@@ -13726,6 +13920,22 @@
"url": "https://github.com/chalk/chalk?sponsor=1" "url": "https://github.com/chalk/chalk?sponsor=1"
} }
}, },
"node_modules/chalk-template": {
"version": "0.4.0",
"resolved": "https://registry.npmjs.org/chalk-template/-/chalk-template-0.4.0.tgz",
"integrity": "sha512-/ghrgmhfY8RaSdeo43hNXxpoHAtxdbskUHjPpfqUWGttFgycUhYPGx3YZBCnUCvOa7Doivn1IZec3DEGFoMgLg==",
"dev": true,
"license": "MIT",
"dependencies": {
"chalk": "^4.1.2"
},
"engines": {
"node": ">=12"
},
"funding": {
"url": "https://github.com/chalk/chalk-template?sponsor=1"
}
},
"node_modules/char-regex": { "node_modules/char-regex": {
"version": "1.0.2", "version": "1.0.2",
"resolved": "https://registry.npmjs.org/char-regex/-/char-regex-1.0.2.tgz", "resolved": "https://registry.npmjs.org/char-regex/-/char-regex-1.0.2.tgz",
@@ -13950,6 +14160,19 @@
"node": ">=6" "node": ">=6"
} }
}, },
"node_modules/cli-boxes": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/cli-boxes/-/cli-boxes-3.0.0.tgz",
"integrity": "sha512-/lzGpEWL/8PfI0BmBOPRwp0c/wFNX1RdUML3jK/RcSBA9T8mZDdQpqYBKtCFTOfQbwPqWEOpjqW+Fnayc0969g==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=10"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/cli-cursor": { "node_modules/cli-cursor": {
"version": "3.1.0", "version": "3.1.0",
"resolved": "https://registry.npmjs.org/cli-cursor/-/cli-cursor-3.1.0.tgz", "resolved": "https://registry.npmjs.org/cli-cursor/-/cli-cursor-3.1.0.tgz",
@@ -14006,6 +14229,24 @@
"node": ">=8" "node": ">=8"
} }
}, },
"node_modules/clipboardy": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/clipboardy/-/clipboardy-3.0.0.tgz",
"integrity": "sha512-Su+uU5sr1jkUy1sGRpLKjKrvEOVXgSgiSInwa/qeID6aJ07yh+5NWc3h2QfjHjBnfX4LhtFcuAWKUsJ3r+fjbg==",
"dev": true,
"license": "MIT",
"dependencies": {
"arch": "^2.2.0",
"execa": "^5.1.1",
"is-wsl": "^2.2.0"
},
"engines": {
"node": "^12.20.0 || ^14.13.1 || >=16.0.0"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/cliui": { "node_modules/cliui": {
"version": "8.0.1", "version": "8.0.1",
"resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz", "resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz",
@@ -14172,9 +14413,8 @@
"version": "2.0.18", "version": "2.0.18",
"resolved": "https://registry.npmjs.org/compressible/-/compressible-2.0.18.tgz", "resolved": "https://registry.npmjs.org/compressible/-/compressible-2.0.18.tgz",
"integrity": "sha512-AF3r7P5dWxL8MxyITRMlORQNaOA2IkAFaTr4k7BUumjPtRpGDTZpl0Pb1XCO6JeDCBdp126Cgs9sMxqSjgYyRg==", "integrity": "sha512-AF3r7P5dWxL8MxyITRMlORQNaOA2IkAFaTr4k7BUumjPtRpGDTZpl0Pb1XCO6JeDCBdp126Cgs9sMxqSjgYyRg==",
"devOptional": true,
"license": "MIT", "license": "MIT",
"optional": true,
"peer": true,
"dependencies": { "dependencies": {
"mime-db": ">= 1.43.0 < 2" "mime-db": ">= 1.43.0 < 2"
}, },
@@ -14448,6 +14688,16 @@
"license": "ISC", "license": "ISC",
"optional": true "optional": true
}, },
"node_modules/content-disposition": {
"version": "0.5.2",
"resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.2.tgz",
"integrity": "sha512-kRGRZw3bLlFISDBgwTSA1TMBFN6J6GWDeubmDE3AF+3+yXL8hTWv8r5rkLbqYXY4RjPk/EzHnClI3zQf1cFmHA==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">= 0.6"
}
},
"node_modules/conventional-changelog": { "node_modules/conventional-changelog": {
"version": "3.1.25", "version": "3.1.25",
"resolved": "https://registry.npmjs.org/conventional-changelog/-/conventional-changelog-3.1.25.tgz", "resolved": "https://registry.npmjs.org/conventional-changelog/-/conventional-changelog-3.1.25.tgz",
@@ -19240,6 +19490,19 @@
"node": ">=0.10.0" "node": ">=0.10.0"
} }
}, },
"node_modules/is-port-reachable": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/is-port-reachable/-/is-port-reachable-4.0.0.tgz",
"integrity": "sha512-9UoipoxYmSk6Xy7QFgRv2HDyaysmgSG75TFQs6S+3pDM7ZhKTF/bskZV+0UlABHzKjNVhPjYCLfeZUEg1wXxig==",
"dev": true,
"license": "MIT",
"engines": {
"node": "^12.20.0 || ^14.13.1 || >=16.0.0"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/is-stream": { "node_modules/is-stream": {
"version": "2.0.1", "version": "2.0.1",
"resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.1.tgz", "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.1.tgz",
@@ -24431,9 +24694,8 @@
"version": "1.54.0", "version": "1.54.0",
"resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.54.0.tgz", "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.54.0.tgz",
"integrity": "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ==", "integrity": "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ==",
"devOptional": true,
"license": "MIT", "license": "MIT",
"optional": true,
"peer": true,
"engines": { "engines": {
"node": ">= 0.6" "node": ">= 0.6"
} }
@@ -25897,6 +26159,13 @@
"node": ">=0.10.0" "node": ">=0.10.0"
} }
}, },
"node_modules/path-is-inside": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/path-is-inside/-/path-is-inside-1.0.2.tgz",
"integrity": "sha512-DUWJr3+ULp4zXmol/SZkFf3JGsS9/SIv+Y3Rt93/UjPpDpklB5f1er4O3POIbUuUJ3FXgqte2Q7SrU6zAqwk8w==",
"dev": true,
"license": "(WTFPL OR MIT)"
},
"node_modules/path-key": { "node_modules/path-key": {
"version": "3.1.1", "version": "3.1.1",
"resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz",
@@ -25938,6 +26207,13 @@
"node": ">=16 || 14 >=14.17" "node": ">=16 || 14 >=14.17"
} }
}, },
"node_modules/path-to-regexp": {
"version": "3.3.0",
"resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-3.3.0.tgz",
"integrity": "sha512-qyCH421YQPS2WFDxDjftfc1ZR5WKQzVzqsp4n9M2kQhVOo/ByahFoUNJfl58kOcEGfQ//7weFTDhm+ss8Ecxgw==",
"dev": true,
"license": "MIT"
},
"node_modules/path-type": { "node_modules/path-type": {
"version": "4.0.0", "version": "4.0.0",
"resolved": "https://registry.npmjs.org/path-type/-/path-type-4.0.0.tgz", "resolved": "https://registry.npmjs.org/path-type/-/path-type-4.0.0.tgz",
@@ -28103,6 +28379,30 @@
"integrity": "sha512-CiD3ZSanZqcMPRhtfct5K9f7i3OLCcBBWsJjLh1gW9RO/nS94sVzY59iS+fgYBOBqaBpf4EzfqUF3j9IG+xo8A==", "integrity": "sha512-CiD3ZSanZqcMPRhtfct5K9f7i3OLCcBBWsJjLh1gW9RO/nS94sVzY59iS+fgYBOBqaBpf4EzfqUF3j9IG+xo8A==",
"license": "MIT" "license": "MIT"
}, },
"node_modules/registry-auth-token": {
"version": "3.3.2",
"resolved": "https://registry.npmjs.org/registry-auth-token/-/registry-auth-token-3.3.2.tgz",
"integrity": "sha512-JL39c60XlzCVgNrO+qq68FoNb56w/m7JYvGR2jT5iR1xBrUA3Mfx5Twk5rqTThPmQKMWydGmq8oFtDlxfrmxnQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"rc": "^1.1.6",
"safe-buffer": "^5.0.1"
}
},
"node_modules/registry-url": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/registry-url/-/registry-url-3.1.0.tgz",
"integrity": "sha512-ZbgR5aZEdf4UKZVBPYIgaglBmSF2Hi94s2PcIHhRGFjKYu+chjJdYfHn4rt3hB6eCKLJ8giVIIfgMa1ehDfZKA==",
"dev": true,
"license": "MIT",
"dependencies": {
"rc": "^1.0.1"
},
"engines": {
"node": ">=0.10.0"
}
},
"node_modules/regjsgen": { "node_modules/regjsgen": {
"version": "0.8.0", "version": "0.8.0",
"resolved": "https://registry.npmjs.org/regjsgen/-/regjsgen-0.8.0.tgz", "resolved": "https://registry.npmjs.org/regjsgen/-/regjsgen-0.8.0.tgz",
@@ -29211,6 +29511,115 @@
"url": "https://github.com/sponsors/sindresorhus" "url": "https://github.com/sponsors/sindresorhus"
} }
}, },
"node_modules/serve": {
"version": "14.2.4",
"resolved": "https://registry.npmjs.org/serve/-/serve-14.2.4.tgz",
"integrity": "sha512-qy1S34PJ/fcY8gjVGszDB3EXiPSk5FKhUa7tQe0UPRddxRidc2V6cNHPNewbE1D7MAkgLuWEt3Vw56vYy73tzQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"@zeit/schemas": "2.36.0",
"ajv": "8.12.0",
"arg": "5.0.2",
"boxen": "7.0.0",
"chalk": "5.0.1",
"chalk-template": "0.4.0",
"clipboardy": "3.0.0",
"compression": "1.7.4",
"is-port-reachable": "4.0.0",
"serve-handler": "6.1.6",
"update-check": "1.5.4"
},
"bin": {
"serve": "build/main.js"
},
"engines": {
"node": ">= 14"
}
},
"node_modules/serve-handler": {
"version": "6.1.6",
"resolved": "https://registry.npmjs.org/serve-handler/-/serve-handler-6.1.6.tgz",
"integrity": "sha512-x5RL9Y2p5+Sh3D38Fh9i/iQ5ZK+e4xuXRd/pGbM4D13tgo/MGwbttUk8emytcr1YYzBYs+apnUngBDFYfpjPuQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"bytes": "3.0.0",
"content-disposition": "0.5.2",
"mime-types": "2.1.18",
"minimatch": "3.1.2",
"path-is-inside": "1.0.2",
"path-to-regexp": "3.3.0",
"range-parser": "1.2.0"
}
},
"node_modules/serve-handler/node_modules/brace-expansion": {
"version": "1.1.12",
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz",
"integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==",
"dev": true,
"license": "MIT",
"dependencies": {
"balanced-match": "^1.0.0",
"concat-map": "0.0.1"
}
},
"node_modules/serve-handler/node_modules/bytes": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/bytes/-/bytes-3.0.0.tgz",
"integrity": "sha512-pMhOfFDPiv9t5jjIXkHosWmkSyQbvsgEVNkz0ERHbuLh2T/7j4Mqqpz523Fe8MVY89KC6Sh/QfS2sM+SjgFDcw==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">= 0.8"
}
},
"node_modules/serve-handler/node_modules/mime-db": {
"version": "1.33.0",
"resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.33.0.tgz",
"integrity": "sha512-BHJ/EKruNIqJf/QahvxwQZXKygOQ256myeN/Ew+THcAa5q+PjyTTMMeNQC4DZw5AwfvelsUrA6B67NKMqXDbzQ==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">= 0.6"
}
},
"node_modules/serve-handler/node_modules/mime-types": {
"version": "2.1.18",
"resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.18.tgz",
"integrity": "sha512-lc/aahn+t4/SWV/qcmumYjymLsWfN3ELhpmVuUFjgsORruuZPVSwAQryq+HHGvO/SI2KVX26bx+En+zhM8g8hQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"mime-db": "~1.33.0"
},
"engines": {
"node": ">= 0.6"
}
},
"node_modules/serve-handler/node_modules/minimatch": {
"version": "3.1.2",
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz",
"integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==",
"dev": true,
"license": "ISC",
"dependencies": {
"brace-expansion": "^1.1.7"
},
"engines": {
"node": "*"
}
},
"node_modules/serve-handler/node_modules/range-parser": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.0.tgz",
"integrity": "sha512-kA5WQoNVo4t9lNx2kQNFCxKeBl5IbbSNBl1M/tLkw9WCn+hxNBAW5Qh8gdhs63CJnhjJ2zQWFoqPJP2sK1AV5A==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">= 0.6"
}
},
"node_modules/serve-static": { "node_modules/serve-static": {
"version": "1.16.2", "version": "1.16.2",
"resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.16.2.tgz", "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.16.2.tgz",
@@ -29342,6 +29751,106 @@
"node": ">= 0.8" "node": ">= 0.8"
} }
}, },
"node_modules/serve/node_modules/ajv": {
"version": "8.12.0",
"resolved": "https://registry.npmjs.org/ajv/-/ajv-8.12.0.tgz",
"integrity": "sha512-sRu1kpcO9yLtYxBKvqfTeh9KzZEwO3STyX1HT+4CaDzC6HpTGYhIhPIzj9XuKU7KYDwnaeh5hcOwjy1QuJzBPA==",
"dev": true,
"license": "MIT",
"dependencies": {
"fast-deep-equal": "^3.1.1",
"json-schema-traverse": "^1.0.0",
"require-from-string": "^2.0.2",
"uri-js": "^4.2.2"
},
"funding": {
"type": "github",
"url": "https://github.com/sponsors/epoberezkin"
}
},
"node_modules/serve/node_modules/bytes": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/bytes/-/bytes-3.0.0.tgz",
"integrity": "sha512-pMhOfFDPiv9t5jjIXkHosWmkSyQbvsgEVNkz0ERHbuLh2T/7j4Mqqpz523Fe8MVY89KC6Sh/QfS2sM+SjgFDcw==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">= 0.8"
}
},
"node_modules/serve/node_modules/chalk": {
"version": "5.0.1",
"resolved": "https://registry.npmjs.org/chalk/-/chalk-5.0.1.tgz",
"integrity": "sha512-Fo07WOYGqMfCWHOzSXOt2CxDbC6skS/jO9ynEcmpANMoPrD+W1r1K6Vx7iNm+AQmETU1Xr2t+n8nzkV9t6xh3w==",
"dev": true,
"license": "MIT",
"engines": {
"node": "^12.17.0 || ^14.13 || >=16.0.0"
},
"funding": {
"url": "https://github.com/chalk/chalk?sponsor=1"
}
},
"node_modules/serve/node_modules/compression": {
"version": "1.7.4",
"resolved": "https://registry.npmjs.org/compression/-/compression-1.7.4.tgz",
"integrity": "sha512-jaSIDzP9pZVS4ZfQ+TzvtiWhdpFhE2RDHz8QJkpX9SIpLq88VueF5jJw6t+6CUQcAoA6t+x89MLrWAqpfDE8iQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"accepts": "~1.3.5",
"bytes": "3.0.0",
"compressible": "~2.0.16",
"debug": "2.6.9",
"on-headers": "~1.0.2",
"safe-buffer": "5.1.2",
"vary": "~1.1.2"
},
"engines": {
"node": ">= 0.8.0"
}
},
"node_modules/serve/node_modules/debug": {
"version": "2.6.9",
"resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz",
"integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==",
"dev": true,
"license": "MIT",
"dependencies": {
"ms": "2.0.0"
}
},
"node_modules/serve/node_modules/json-schema-traverse": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz",
"integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==",
"dev": true,
"license": "MIT"
},
"node_modules/serve/node_modules/ms": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz",
"integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==",
"dev": true,
"license": "MIT"
},
"node_modules/serve/node_modules/on-headers": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/on-headers/-/on-headers-1.0.2.tgz",
"integrity": "sha512-pZAE+FJLoyITytdqK0U5s+FIpjN0JP3OzFi/u8Rx+EV5/W+JTWGXG8xFzevE7AjBfDqHv/8vL8qQsIhHnqRkrA==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">= 0.8"
}
},
"node_modules/serve/node_modules/safe-buffer": {
"version": "5.1.2",
"resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz",
"integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==",
"dev": true,
"license": "MIT"
},
"node_modules/set-blocking": { "node_modules/set-blocking": {
"version": "2.0.0", "version": "2.0.0",
"resolved": "https://registry.npmjs.org/set-blocking/-/set-blocking-2.0.0.tgz", "resolved": "https://registry.npmjs.org/set-blocking/-/set-blocking-2.0.0.tgz",
@@ -31671,6 +32180,17 @@
"browserslist": ">= 4.21.0" "browserslist": ">= 4.21.0"
} }
}, },
"node_modules/update-check": {
"version": "1.5.4",
"resolved": "https://registry.npmjs.org/update-check/-/update-check-1.5.4.tgz",
"integrity": "sha512-5YHsflzHP4t1G+8WGPlvKbJEbAJGCgw+Em+dGR1KmBUbr1J36SJBqlHLjR7oob7sco5hWHGQVcr9B2poIVDDTQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"registry-auth-token": "3.3.2",
"registry-url": "3.1.0"
}
},
"node_modules/uri-js": { "node_modules/uri-js": {
"version": "4.4.1", "version": "4.4.1",
"resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz",
@@ -31779,9 +32299,8 @@
"version": "1.1.2", "version": "1.1.2",
"resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz",
"integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==", "integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==",
"devOptional": true,
"license": "MIT", "license": "MIT",
"optional": true,
"peer": true,
"engines": { "engines": {
"node": ">= 0.8" "node": ">= 0.8"
} }
@@ -32633,6 +33152,76 @@
"string-width": "^1.0.2 || 2 || 3 || 4" "string-width": "^1.0.2 || 2 || 3 || 4"
} }
}, },
"node_modules/widest-line": {
"version": "4.0.1",
"resolved": "https://registry.npmjs.org/widest-line/-/widest-line-4.0.1.tgz",
"integrity": "sha512-o0cyEG0e8GPzT4iGHphIOh0cJOV8fivsXxddQasHPHfoZf1ZexrfeA21w2NaEN1RHE+fXlfISmOE8R9N3u3Qig==",
"dev": true,
"license": "MIT",
"dependencies": {
"string-width": "^5.0.1"
},
"engines": {
"node": ">=12"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/widest-line/node_modules/ansi-regex": {
"version": "6.2.0",
"resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.2.0.tgz",
"integrity": "sha512-TKY5pyBkHyADOPYlRT9Lx6F544mPl0vS5Ew7BJ45hA08Q+t3GjbueLliBWN3sMICk6+y7HdyxSzC4bWS8baBdg==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=12"
},
"funding": {
"url": "https://github.com/chalk/ansi-regex?sponsor=1"
}
},
"node_modules/widest-line/node_modules/emoji-regex": {
"version": "9.2.2",
"resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz",
"integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==",
"dev": true,
"license": "MIT"
},
"node_modules/widest-line/node_modules/string-width": {
"version": "5.1.2",
"resolved": "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz",
"integrity": "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==",
"dev": true,
"license": "MIT",
"dependencies": {
"eastasianwidth": "^0.2.0",
"emoji-regex": "^9.2.2",
"strip-ansi": "^7.0.1"
},
"engines": {
"node": ">=12"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/widest-line/node_modules/strip-ansi": {
"version": "7.1.0",
"resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.0.tgz",
"integrity": "sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"ansi-regex": "^6.0.1"
},
"engines": {
"node": ">=12"
},
"funding": {
"url": "https://github.com/chalk/strip-ansi?sponsor=1"
}
},
"node_modules/wonka": { "node_modules/wonka": {
"version": "6.3.5", "version": "6.3.5",
"resolved": "https://registry.npmjs.org/wonka/-/wonka-6.3.5.tgz", "resolved": "https://registry.npmjs.org/wonka/-/wonka-6.3.5.tgz",

View File

@@ -150,6 +150,7 @@
"@capacitor/filesystem": "^6.0.0", "@capacitor/filesystem": "^6.0.0",
"@capacitor/ios": "^6.2.0", "@capacitor/ios": "^6.2.0",
"@capacitor/share": "^6.0.3", "@capacitor/share": "^6.0.3",
"@capacitor/status-bar": "^6.0.2",
"@capawesome/capacitor-file-picker": "^6.2.0", "@capawesome/capacitor-file-picker": "^6.2.0",
"@dicebear/collection": "^5.4.1", "@dicebear/collection": "^5.4.1",
"@dicebear/core": "^5.4.1", "@dicebear/core": "^5.4.1",
@@ -267,6 +268,7 @@
"postcss": "^8.4.38", "postcss": "^8.4.38",
"prettier": "^3.2.5", "prettier": "^3.2.5",
"rimraf": "^6.0.1", "rimraf": "^6.0.1",
"serve": "^14.2.4",
"tailwindcss": "^3.4.1", "tailwindcss": "^3.4.1",
"ts-jest": "^29.4.0", "ts-jest": "^29.4.0",
"tsx": "^4.20.4", "tsx": "^4.20.4",

View File

@@ -181,7 +181,7 @@ sync_capacitor() {
copy_web_assets() { copy_web_assets() {
log_info "Copying web assets to Electron" log_info "Copying web assets to Electron"
safe_execute "Copying assets" "cp -r dist/* electron/app/" safe_execute "Copying assets" "cp -r dist/* electron/app/"
safe_execute "Copying config" "cp capacitor.config.json electron/capacitor.config.json" # Note: Electron has its own capacitor.config.ts file, so we don't copy the main config
} }
# Compile TypeScript # Compile TypeScript

View File

@@ -4,7 +4,7 @@
<!-- Messages in the upper-right - https://github.com/emmanuelsw/notiwind --> <!-- Messages in the upper-right - https://github.com/emmanuelsw/notiwind -->
<NotificationGroup group="alert"> <NotificationGroup group="alert">
<div <div
class="fixed z-[120] top-[max(1rem,env(safe-area-inset-top))] right-4 left-4 sm:left-auto sm:w-full sm:max-w-sm flex flex-col items-start justify-end" class="fixed z-[120] top-[max(1rem,env(safe-area-inset-top),var(--safe-area-inset-top,0px))] right-4 left-4 sm:left-auto sm:w-full sm:max-w-sm flex flex-col items-start justify-end"
> >
<Notification <Notification
v-slot="{ notifications, close }" v-slot="{ notifications, close }"
@@ -175,7 +175,9 @@
"-permission", "-mute", "-off" "-permission", "-mute", "-off"
--> -->
<NotificationGroup group="modal"> <NotificationGroup group="modal">
<div class="fixed z-[100] top-[env(safe-area-inset-top)] inset-x-0 w-full"> <div
class="fixed z-[100] top-[max(env(safe-area-inset-top),var(--safe-area-inset-top,0px))] inset-x-0 w-full"
>
<Notification <Notification
v-slot="{ notifications, close }" v-slot="{ notifications, close }"
enter="transform ease-out duration-300 transition" enter="transform ease-out duration-300 transition"
@@ -506,13 +508,32 @@ export default class App extends Vue {
<style> <style>
#Content { #Content {
padding-left: max(1.5rem, env(safe-area-inset-left)); padding-left: max(
padding-right: max(1.5rem, env(safe-area-inset-right)); 1.5rem,
padding-top: max(1.5rem, env(safe-area-inset-top)); env(safe-area-inset-left),
padding-bottom: max(1.5rem, env(safe-area-inset-bottom)); var(--safe-area-inset-left, 0px)
);
padding-right: max(
1.5rem,
env(safe-area-inset-right),
var(--safe-area-inset-right, 0px)
);
padding-top: max(
1.5rem,
env(safe-area-inset-top),
var(--safe-area-inset-top, 0px)
);
padding-bottom: max(
1.5rem,
env(safe-area-inset-bottom),
var(--safe-area-inset-bottom, 0px)
);
} }
#QuickNav ~ #Content { #QuickNav ~ #Content {
padding-bottom: calc(env(safe-area-inset-bottom) + 6.333rem); padding-bottom: calc(
max(env(safe-area-inset-bottom), var(--safe-area-inset-bottom, 0px)) +
6.333rem
);
} }
</style> </style>

View File

@@ -22,7 +22,7 @@ projects, and special entities with selection. * * @author Matthew Raymer */
<!-- "Unnamed" entity --> <!-- "Unnamed" entity -->
<SpecialEntityCard <SpecialEntityCard
entity-type="unnamed" entity-type="unnamed"
label="Unnamed" :label="unnamedEntityName"
icon="circle-question" icon="circle-question"
:entity-data="unnamedEntityData" :entity-data="unnamedEntityData"
:notify="notify" :notify="notify"
@@ -83,6 +83,7 @@ import ShowAllCard from "./ShowAllCard.vue";
import { Contact } from "../db/tables/contacts"; import { Contact } from "../db/tables/contacts";
import { PlanData } from "../interfaces/records"; import { PlanData } from "../interfaces/records";
import { NotificationIface } from "../constants/app"; import { NotificationIface } from "../constants/app";
import { UNNAMED_ENTITY_NAME } from "@/constants/entities";
/** /**
* EntityGrid - Unified grid layout for displaying people or projects * EntityGrid - Unified grid layout for displaying people or projects
@@ -159,6 +160,10 @@ export default class EntityGrid extends Vue {
@Prop({ default: "other party" }) @Prop({ default: "other party" })
conflictContext!: string; conflictContext!: string;
/** Whether to hide the "Show All" navigation */
@Prop({ default: false })
hideShowAll!: boolean;
/** /**
* Function to determine which entities to display (allows parent control) * Function to determine which entities to display (allows parent control)
* *
@@ -245,7 +250,9 @@ export default class EntityGrid extends Vue {
* Whether to show the "Show All" navigation * Whether to show the "Show All" navigation
*/ */
get shouldShowAll(): boolean { get shouldShowAll(): boolean {
return this.entities.length > 0 && this.showAllRoute !== ""; return (
!this.hideShowAll && this.entities.length > 0 && this.showAllRoute !== ""
);
} }
/** /**
@@ -271,10 +278,17 @@ export default class EntityGrid extends Vue {
get unnamedEntityData(): { did: string; name: string } { get unnamedEntityData(): { did: string; name: string } {
return { return {
did: "", did: "",
name: "Unnamed", name: UNNAMED_ENTITY_NAME,
}; };
} }
/**
* Get the unnamed entity name constant
*/
get unnamedEntityName(): string {
return UNNAMED_ENTITY_NAME;
}
/** /**
* Check if a person DID is conflicted * Check if a person DID is conflicted
*/ */
@@ -304,16 +318,13 @@ export default class EntityGrid extends Vue {
/** /**
* Handle special entity selection from SpecialEntityCard * Handle special entity selection from SpecialEntityCard
* Treat "You" and "Unnamed" as person entities
*/ */
handleEntitySelected(event: { handleEntitySelected(event: { data: { did?: string; name: string } }): void {
type: string; // Convert special entities to person entities since they represent people
entityType: string;
data: { did?: string; name: string };
}): void {
this.emitEntitySelected({ this.emitEntitySelected({
type: "special", type: "person",
entityType: event.entityType, data: event.data as Contact,
data: event.data,
}); });
} }
@@ -321,13 +332,11 @@ export default class EntityGrid extends Vue {
@Emit("entity-selected") @Emit("entity-selected")
emitEntitySelected(data: { emitEntitySelected(data: {
type: "person" | "project" | "special"; type: "person" | "project";
entityType?: string; data: Contact | PlanData;
data: Contact | PlanData | { did?: string; name: string };
}): { }): {
type: "person" | "project" | "special"; type: "person" | "project";
entityType?: string; data: Contact | PlanData;
data: Contact | PlanData | { did?: string; name: string };
} { } {
return data; return data;
} }

View File

@@ -27,6 +27,7 @@ Matthew Raymer */
:show-all-query-params="showAllQueryParams" :show-all-query-params="showAllQueryParams"
:notify="notify" :notify="notify"
:conflict-context="conflictContext" :conflict-context="conflictContext"
:hide-show-all="hideShowAll"
@entity-selected="handleEntitySelected" @entity-selected="handleEntitySelected"
/> />
@@ -55,9 +56,8 @@ interface EntityData {
* Entity selection event data structure * Entity selection event data structure
*/ */
interface EntitySelectionEvent { interface EntitySelectionEvent {
type: "person" | "project" | "special"; type: "person" | "project";
entityType?: string; data: Contact | PlanData;
data: Contact | PlanData | EntityData;
} }
/** /**
@@ -154,6 +154,10 @@ export default class EntitySelectionStep extends Vue {
@Prop() @Prop()
notify?: (notification: NotificationIface, timeout?: number) => void; notify?: (notification: NotificationIface, timeout?: number) => void;
/** Whether to hide the "Show All" navigation */
@Prop({ default: false })
hideShowAll!: boolean;
/** /**
* CSS classes for the cancel button * CSS classes for the cancel button
*/ */

View File

@@ -42,8 +42,8 @@ computed CSS properties * * @author Matthew Raymer */
<p class="text-xs text-slate-500 leading-1 -mb-1 uppercase"> <p class="text-xs text-slate-500 leading-1 -mb-1 uppercase">
{{ label }} {{ label }}
</p> </p>
<h3 class="font-semibold truncate"> <h3 :class="nameClasses">
{{ entity?.name || "Unnamed" }} {{ displayName }}
</h3> </h3>
</div> </div>
@@ -62,6 +62,7 @@ import { Component, Prop, Vue } from "vue-facing-decorator";
import EntityIcon from "./EntityIcon.vue"; import EntityIcon from "./EntityIcon.vue";
import ProjectIcon from "./ProjectIcon.vue"; import ProjectIcon from "./ProjectIcon.vue";
import { Contact } from "../db/tables/contacts"; import { Contact } from "../db/tables/contacts";
import { UNNAMED_ENTITY_NAME } from "@/constants/entities";
/** /**
* Entity interface for both person and project entities * Entity interface for both person and project entities
@@ -138,6 +139,38 @@ export default class EntitySummaryButton extends Vue {
return this.editable ? "text-blue-500" : "text-slate-400"; return this.editable ? "text-blue-500" : "text-slate-400";
} }
/**
* Computed CSS classes for the entity name
*/
get nameClasses(): string {
const baseClasses = "font-semibold truncate";
// Add italic styling for special "Unnamed" or entities without set names
if (!this.entity?.name || this.entity?.did === "") {
return `${baseClasses} italic text-slate-500`;
}
return baseClasses;
}
/**
* Computed display name for the entity
*/
get displayName(): string {
// If the entity has a set name, use that name
if (this.entity?.name) {
return this.entity.name;
}
// If the entity is the special "Unnamed", use "Unnamed"
if (this.entity?.did === "") {
return UNNAMED_ENTITY_NAME;
}
// If the entity does not have a set name, but is not the special "Unnamed", use their DID
return this.entity?.did;
}
/** /**
* Handle click event - only call function prop if editable * Handle click event - only call function prop if editable
* Allows parent to control edit behavior and validation * Allows parent to control edit behavior and validation

View File

@@ -29,6 +29,7 @@
:unit-code="unitCode" :unit-code="unitCode"
:offer-id="offerId" :offer-id="offerId"
:notify="$notify" :notify="$notify"
:hide-show-all="hideShowAll"
@entity-selected="handleEntitySelected" @entity-selected="handleEntitySelected"
@cancel="cancel" @cancel="cancel"
/> />
@@ -87,6 +88,7 @@ import {
NOTIFY_GIFTED_DETAILS_NO_IDENTIFIER, NOTIFY_GIFTED_DETAILS_NO_IDENTIFIER,
NOTIFY_GIFTED_DETAILS_RECORDING_GIVE, NOTIFY_GIFTED_DETAILS_RECORDING_GIVE,
} from "@/constants/notifications"; } from "@/constants/notifications";
import { UNNAMED_ENTITY_NAME } from "@/constants/entities";
@Component({ @Component({
components: { components: {
@@ -114,6 +116,7 @@ export default class GiftedDialog extends Vue {
@Prop() fromProjectId = ""; @Prop() fromProjectId = "";
@Prop() toProjectId = ""; @Prop() toProjectId = "";
@Prop() isFromProjectView = false; @Prop() isFromProjectView = false;
@Prop() hideShowAll = false;
@Prop({ default: "person" }) giverEntityType = "person" as @Prop({ default: "person" }) giverEntityType = "person" as
| "person" | "person"
| "project"; | "project";
@@ -224,15 +227,6 @@ export default class GiftedDialog extends Vue {
this.allMyDids = await retrieveAccountDids(); this.allMyDids = await retrieveAccountDids();
if (this.giver && !this.giver.name) {
this.giver.name = didInfo(
this.giver.did,
this.activeDid,
this.allMyDids,
this.allContacts,
);
}
if ( if (
this.giverEntityType === "project" || this.giverEntityType === "project" ||
this.recipientEntityType === "project" this.recipientEntityType === "project"
@@ -455,14 +449,14 @@ export default class GiftedDialog extends Vue {
if (contact) { if (contact) {
this.giver = { this.giver = {
did: contact.did, did: contact.did,
name: contact.name || contact.did, name: contact.name,
}; };
} else { } else {
// Only set to "Unnamed" if no giver is currently set // Only set to "Unnamed" if no giver is currently set
if (!this.giver || !this.giver.did) { if (!this.giver || !this.giver.did) {
this.giver = { this.giver = {
did: "", did: "",
name: "Unnamed", name: UNNAMED_ENTITY_NAME,
}; };
} }
} }
@@ -517,14 +511,14 @@ export default class GiftedDialog extends Vue {
if (contact) { if (contact) {
this.receiver = { this.receiver = {
did: contact.did, did: contact.did,
name: contact.name || contact.did, name: contact.name,
}; };
} else { } else {
// Only set to "Unnamed" if no receiver is currently set // Only set to "Unnamed" if no receiver is currently set
if (!this.receiver || !this.receiver.did) { if (!this.receiver || !this.receiver.did) {
this.receiver = { this.receiver = {
did: "", did: "",
name: "Unnamed", name: UNNAMED_ENTITY_NAME,
}; };
} }
} }
@@ -566,20 +560,21 @@ export default class GiftedDialog extends Vue {
/** /**
* Handle entity selection from EntitySelectionStep * Handle entity selection from EntitySelectionStep
* @param entity - The selected entity (person, project, or special) with stepType * @param entity - The selected entity (person or project) with stepType
*/ */
handleEntitySelected(entity: { handleEntitySelected(entity: {
type: "person" | "project" | "special"; type: "person" | "project";
entityType?: string; data: Contact | PlanData;
data: Contact | PlanData | { did?: string; name: string };
stepType: string; stepType: string;
}) { }) {
if (entity.type === "person") { if (entity.type === "person") {
const contact = entity.data as Contact; const contact = entity.data as Contact;
// Apply DID-based logic for person entities
const processedContact = this.processPersonEntity(contact);
if (entity.stepType === "giver") { if (entity.stepType === "giver") {
this.selectGiver(contact); this.selectGiver(processedContact);
} else { } else {
this.selectRecipient(contact); this.selectRecipient(processedContact);
} }
} else if (entity.type === "project") { } else if (entity.type === "project") {
const project = entity.data as PlanData; const project = entity.data as PlanData;
@@ -588,33 +583,22 @@ export default class GiftedDialog extends Vue {
} else { } else {
this.selectRecipientProject(project); this.selectRecipientProject(project);
} }
} else if (entity.type === "special") { }
// Handle special entities like "You" and "Unnamed" }
if (entity.entityType === "you") {
// "You" entity selected /**
const youEntity = { * Processes person entities using DID-based logic for "You" and "Unnamed"
did: this.activeDid, */
name: "You", private processPersonEntity(contact: Contact): Contact {
}; if (contact.did === this.activeDid) {
if (entity.stepType === "giver") { // If DID matches active DID, create "You" entity
this.giver = youEntity; return { ...contact, name: "You" };
} else { } else if (!contact.did || contact.did === "") {
this.receiver = youEntity; // If DID is empty/null, create "Unnamed" entity
} return { ...contact, name: UNNAMED_ENTITY_NAME };
this.firstStep = false; } else {
} else if (entity.entityType === "unnamed") { // Return the contact as-is
// "Unnamed" entity selected return contact;
const unnamedEntity = {
did: "",
name: "Unnamed",
};
if (entity.stepType === "giver") {
this.giver = unnamedEntity;
} else {
this.receiver = unnamedEntity;
}
this.firstStep = false;
}
} }
} }

View File

@@ -81,7 +81,7 @@
<div class="flex items-center justify-between"> <div class="flex items-center justify-between">
<div class="flex items-center"> <div class="flex items-center">
<h3 class="text-lg font-medium"> <h3 class="text-lg font-medium">
{{ member.name || "Unnamed Member" }} {{ member.name || unnamedMember }}
</h3> </h3>
<div <div
v-if="!getContactFor(member.did) && member.did !== activeDid" v-if="!getContactFor(member.did) && member.did !== activeDid"
@@ -177,6 +177,7 @@ import {
NOTIFY_ADD_CONTACT_FIRST, NOTIFY_ADD_CONTACT_FIRST,
NOTIFY_CONTINUE_WITHOUT_ADDING, NOTIFY_CONTINUE_WITHOUT_ADDING,
} from "@/constants/notifications"; } from "@/constants/notifications";
import { SOMEONE_UNNAMED } from "@/constants/entities";
interface Member { interface Member {
admitted: boolean; admitted: boolean;
@@ -220,6 +221,13 @@ export default class MembersList extends Vue {
apiServer = ""; apiServer = "";
contacts: Array<Contact> = []; contacts: Array<Contact> = [];
/**
* Get the unnamed member constant
*/
get unnamedMember(): string {
return SOMEONE_UNNAMED;
}
async created() { async created() {
this.notify = createNotifyHelpers(this.$notify); this.notify = createNotifyHelpers(this.$notify);

View File

@@ -25,7 +25,7 @@ conflict detection. * * @author Matthew Raymer */
</div> </div>
<h3 :class="nameClasses"> <h3 :class="nameClasses">
{{ person.name || person.did || "Unnamed" }} {{ displayName }}
</h3> </h3>
</li> </li>
</template> </template>
@@ -98,9 +98,27 @@ export default class PersonCard extends Vue {
return `${baseClasses} text-slate-400`; return `${baseClasses} text-slate-400`;
} }
// Add italic styling for entities without set names
if (!this.person.name) {
return `${baseClasses} italic text-slate-500`;
}
return baseClasses; return baseClasses;
} }
/**
* Computed display name for the person
*/
get displayName(): string {
// If the entity has a set name, use that name
if (this.person.name) {
return this.person.name;
}
// If the entity does not have a set name
return this.person.did;
}
/** /**
* Handle card click - emit if selectable and not conflicted, show warning if conflicted * Handle card click - emit if selectable and not conflicted, show warning if conflicted
*/ */
@@ -114,7 +132,7 @@ export default class PersonCard extends Vue {
group: "alert", group: "alert",
type: "warning", type: "warning",
title: "Cannot Select", title: "Cannot Select",
text: `You cannot select "${this.person.name || this.person.did || "Unnamed"}" because they are already selected as the ${this.conflictContext}.`, text: `You cannot select "${this.displayName}" because they are already selected as the ${this.conflictContext}.`,
}, },
3000, 3000,
); );

View File

@@ -10,7 +10,7 @@ Comprehensive error handling * * @author Matthew Raymer * @version 1.0.0 * @file
PhotoDialog.vue */ PhotoDialog.vue */
<template> <template>
<div v-if="visible" class="dialog-overlay> <div v-if="visible" class="dialog-overlay">
<div class="dialog relative"> <div class="dialog relative">
<div class="text-lg text-center font-light relative z-50"> <div class="text-lg text-center font-light relative z-50">
<div id="ViewHeading" :class="headingClasses"> <div id="ViewHeading" :class="headingClasses">

View File

@@ -15,7 +15,7 @@ issuer information. * * @author Matthew Raymer */
<h3 <h3
class="text-xs font-medium text-ellipsis whitespace-nowrap overflow-hidden" class="text-xs font-medium text-ellipsis whitespace-nowrap overflow-hidden"
> >
{{ project.name || "Unnamed Project" }} {{ project.name || unnamedProject }}
</h3> </h3>
<div class="text-xs text-slate-500 truncate"> <div class="text-xs text-slate-500 truncate">
@@ -31,6 +31,7 @@ import ProjectIcon from "./ProjectIcon.vue";
import { PlanData } from "../interfaces/records"; import { PlanData } from "../interfaces/records";
import { Contact } from "../db/tables/contacts"; import { Contact } from "../db/tables/contacts";
import { didInfo } from "../libs/endorserServer"; import { didInfo } from "../libs/endorserServer";
import { UNNAMED_PROJECT } from "@/constants/entities";
/** /**
* ProjectCard - Displays a project entity with selection capability * ProjectCard - Displays a project entity with selection capability
@@ -63,6 +64,13 @@ export default class ProjectCard extends Vue {
@Prop({ required: true }) @Prop({ required: true })
allContacts!: Contact[]; allContacts!: Contact[];
/**
* Get the unnamed project constant
*/
get unnamedProject(): string {
return UNNAMED_PROJECT;
}
/** /**
* Computed display name for the project issuer * Computed display name for the project issuer
*/ */

View File

@@ -115,6 +115,7 @@ import { urlBase64ToUint8Array } from "../libs/crypto/vc/util";
import * as libsUtil from "../libs/util"; import * as libsUtil from "../libs/util";
import { logger } from "../utils/logger"; import { logger } from "../utils/logger";
import { PlatformServiceMixin } from "../utils/PlatformServiceMixin"; import { PlatformServiceMixin } from "../utils/PlatformServiceMixin";
import { UNNAMED_ENTITY_NAME } from "@/constants/entities";
// Example interface for error // Example interface for error
interface ErrorResponse { interface ErrorResponse {
@@ -602,7 +603,7 @@ export default class PushNotificationPermission extends Vue {
* Returns the default message for direct push * Returns the default message for direct push
*/ */
get notificationMessagePlaceholder(): string { get notificationMessagePlaceholder(): string {
return "Click to share some gratitude with the world -- even if they're unnamed."; return `Click to share some gratitude with the world -- even if they're ${UNNAMED_ENTITY_NAME.toLowerCase()}.`;
} }
/** /**

View File

@@ -2,7 +2,7 @@
<!-- QUICK NAV --> <!-- QUICK NAV -->
<nav <nav
id="QuickNav" id="QuickNav"
class="fixed bottom-0 left-0 right-0 bg-slate-200 z-50 pb-[env(safe-area-inset-bottom)]" class="fixed bottom-0 left-0 right-0 bg-slate-200 z-50 pb-[max(env(safe-area-inset-bottom),var(--safe-area-inset-bottom,0px))]"
> >
<ul class="flex text-2xl px-6 py-2 gap-1 max-w-3xl mx-auto"> <ul class="flex text-2xl px-6 py-2 gap-1 max-w-3xl mx-auto">
<!-- Home Feed --> <!-- Home Feed -->

View File

@@ -124,8 +124,6 @@ export default class SpecialEntityCard extends Vue {
handleClick(): void { handleClick(): void {
if (this.selectable && !this.conflicted) { if (this.selectable && !this.conflicted) {
this.emitEntitySelected({ this.emitEntitySelected({
type: "special",
entityType: this.entityType,
data: this.entityData, data: this.entityData,
}); });
} else if (this.conflicted && this.notify) { } else if (this.conflicted && this.notify) {
@@ -145,13 +143,7 @@ export default class SpecialEntityCard extends Vue {
// Emit methods using @Emit decorator // Emit methods using @Emit decorator
@Emit("entity-selected") @Emit("entity-selected")
emitEntitySelected(data: { emitEntitySelected(data: { data: { did?: string; name: string } }): {
type: string;
entityType: string;
data: { did?: string; name: string };
}): {
type: string;
entityType: string;
data: { did?: string; name: string }; data: { did?: string; name: string };
} { } {
return data; return data;

View File

@@ -1,5 +1,7 @@
<template> <template>
<div class="absolute right-5 top-[max(0.75rem,env(safe-area-inset-top))]"> <div
class="absolute right-5 top-[max(0.75rem,env(safe-area-inset-top),var(--safe-area-inset-top,0px))]"
>
<span class="align-center text-red-500 mr-2">{{ message }}</span> <span class="align-center text-red-500 mr-2">{{ message }}</span>
<span class="ml-2"> <span class="ml-2">
<router-link <router-link

14
src/constants/entities.ts Normal file
View File

@@ -0,0 +1,14 @@
/**
* Constants for entity-related strings, particularly for unnamed/unknown person entities
*/
// Core unnamed entity names
export const UNNAMED_ENTITY_NAME = "Unnamed";
// Descriptive phrases for unnamed entities
export const SOMEONE_UNNAMED = "Someone Unnamed";
export const THAT_UNNAMED_PERSON = "That unnamed person";
export const UNNAMED_PERSON = "unnamed person";
// Project-related unnamed entities
export const UNNAMED_PROJECT = "Unnamed Project";

View File

@@ -1,4 +1,5 @@
import axios from "axios"; import axios from "axios";
import { THAT_UNNAMED_PERSON } from "./entities";
// Notification message constants for user-facing notifications // Notification message constants for user-facing notifications
// Add new notification messages here as needed // Add new notification messages here as needed
@@ -873,7 +874,7 @@ export const NOTIFY_CONTACT_LINK_COPIED = {
// Template for registration success message // Template for registration success message
// Used in: ContactsView.vue (register method - registration success with contact name) // Used in: ContactsView.vue (register method - registration success with contact name)
export const getRegisterPersonSuccessMessage = (name?: string): string => export const getRegisterPersonSuccessMessage = (name?: string): string =>
`${name || "That unnamed person"} ${NOTIFY_REGISTER_PERSON_SUCCESS.message}`; `${name || THAT_UNNAMED_PERSON} ${NOTIFY_REGISTER_PERSON_SUCCESS.message}`;
// Template for visibility success message // Template for visibility success message
// Used in: ContactsView.vue (setVisibility method - visibility success with contact name) // Used in: ContactsView.vue (setVisibility method - visibility success with contact name)
@@ -1378,7 +1379,7 @@ export function createQRContactAddedMessage(hasVisibility: boolean): string {
export function createQRRegistrationSuccessMessage( export function createQRRegistrationSuccessMessage(
contactName: string, contactName: string,
): string { ): string {
return `${contactName || "That unnamed person"}${NOTIFY_QR_REGISTRATION_SUCCESS.message}`; return `${contactName || THAT_UNNAMED_PERSON}${NOTIFY_QR_REGISTRATION_SUCCESS.message}`;
} }
// ContactQRScanShowView.vue timeout constants // ContactQRScanShowView.vue timeout constants

View File

@@ -60,6 +60,7 @@ import { PlanSummaryRecord } from "../interfaces/records";
import { logger } from "../utils/logger"; import { logger } from "../utils/logger";
import { PlatformServiceFactory } from "@/services/PlatformServiceFactory"; import { PlatformServiceFactory } from "@/services/PlatformServiceFactory";
import { APP_SERVER } from "@/constants/app"; import { APP_SERVER } from "@/constants/app";
import { SOMEONE_UNNAMED } from "@/constants/entities";
/** /**
* Standard context for schema.org data * Standard context for schema.org data
@@ -309,7 +310,7 @@ export function didInfoForContact(
showDidForVisible: boolean = false, showDidForVisible: boolean = false,
// eslint-disable-next-line @typescript-eslint/no-explicit-any // eslint-disable-next-line @typescript-eslint/no-explicit-any
): { known: boolean; displayName: string; profileImageUrl?: string } { ): { known: boolean; displayName: string; profileImageUrl?: string } {
if (!did) return { displayName: "Someone Not Named", known: false }; if (!did) return { displayName: SOMEONE_UNNAMED, known: false };
if (did === activeDid) { if (did === activeDid) {
return { displayName: "You", known: true }; return { displayName: "You", known: true };
} else if (contact) { } else if (contact) {

View File

@@ -33,6 +33,7 @@ import { logger } from "../utils/logger";
import { PlatformServiceFactory } from "../services/PlatformServiceFactory"; import { PlatformServiceFactory } from "../services/PlatformServiceFactory";
import { IIdentifier } from "@veramo/core"; import { IIdentifier } from "@veramo/core";
import { DEFAULT_ROOT_DERIVATION_PATH } from "./crypto"; import { DEFAULT_ROOT_DERIVATION_PATH } from "./crypto";
import { UNNAMED_PERSON } from "@/constants/entities";
// Consolidate this with src/utils/PlatformServiceMixin.mapQueryResultToValues // Consolidate this with src/utils/PlatformServiceMixin.mapQueryResultToValues
function mapQueryResultToValues( function mapQueryResultToValues(
@@ -192,7 +193,7 @@ export const nameForContact = (
): string => { ): string => {
return ( return (
(contact?.name as string) || (contact?.name as string) ||
(capitalize ? "This" : "this") + " unnamed user" (capitalize ? "This" : "this") + " " + UNNAMED_PERSON
); );
}; };

View File

@@ -35,6 +35,7 @@ import { handleApiError } from "./services/api";
import { AxiosError } from "axios"; import { AxiosError } from "axios";
import { DeepLinkHandler } from "./services/deepLinks"; import { DeepLinkHandler } from "./services/deepLinks";
import { logger, safeStringify } from "./utils/logger"; import { logger, safeStringify } from "./utils/logger";
import "./utils/safeAreaInset";
logger.log("[Capacitor] 🚀 Starting initialization"); logger.log("[Capacitor] 🚀 Starting initialization");
logger.log("[Capacitor] Platform:", process.env.VITE_PLATFORM); logger.log("[Capacitor] Platform:", process.env.VITE_PLATFORM);

View File

@@ -74,13 +74,13 @@ export class DeepLinkHandler {
* @throws {DeepLinkError} If validation fails or route is invalid * @throws {DeepLinkError} If validation fails or route is invalid
*/ */
async handleDeepLink(url: string): Promise<void> { async handleDeepLink(url: string): Promise<void> {
logger.info(`[DeepLink] 🚀 Starting deeplink processing for URL: ${url}`); logger.debug(`[DeepLink] 🚀 Starting deeplink processing for URL: ${url}`);
try { try {
logger.info(`[DeepLink] 📍 Parsing URL: ${url}`); logger.debug(`[DeepLink] 📍 Parsing URL: ${url}`);
const { path, params, query } = this.parseDeepLink(url); const { path, params, query } = this.parseDeepLink(url);
logger.info(`[DeepLink] ✅ URL parsed successfully:`, { logger.debug(`[DeepLink] ✅ URL parsed successfully:`, {
path, path,
params: Object.keys(params), params: Object.keys(params),
query: Object.keys(query), query: Object.keys(query),
@@ -93,10 +93,10 @@ export class DeepLinkHandler {
Object.entries(params).map(([key, value]) => [key, value ?? ""]), Object.entries(params).map(([key, value]) => [key, value ?? ""]),
); );
logger.info(`[DeepLink] 🧹 Parameters sanitized:`, sanitizedParams); logger.debug(`[DeepLink] 🧹 Parameters sanitized:`, sanitizedParams);
await this.validateAndRoute(path, sanitizedParams, query); await this.validateAndRoute(path, sanitizedParams, query);
logger.info(`[DeepLink] 🎯 Deeplink processing completed successfully`); logger.debug(`[DeepLink] 🎯 Deeplink processing completed successfully`);
} catch (error) { } catch (error) {
logger.error(`[DeepLink] ❌ Deeplink processing failed:`, { logger.error(`[DeepLink] ❌ Deeplink processing failed:`, {
url, url,
@@ -159,7 +159,7 @@ export class DeepLinkHandler {
logger.debug(`[DeepLink] 🔗 Query parameters extracted:`, query); logger.debug(`[DeepLink] 🔗 Query parameters extracted:`, query);
} }
logger.info(`[DeepLink] ✅ Parse completed:`, { logger.debug(`[DeepLink] ✅ Parse completed:`, {
routePath, routePath,
pathParams: pathParams.length, pathParams: pathParams.length,
queryParams: Object.keys(query).length, queryParams: Object.keys(query).length,
@@ -186,7 +186,7 @@ export class DeepLinkHandler {
params: Record<string, string>, params: Record<string, string>,
query: Record<string, string>, query: Record<string, string>,
): Promise<void> { ): Promise<void> {
logger.info( logger.debug(
`[DeepLink] 🎯 Starting validation and routing for path: ${path}`, `[DeepLink] 🎯 Starting validation and routing for path: ${path}`,
); );
@@ -197,11 +197,11 @@ export class DeepLinkHandler {
logger.debug(`[DeepLink] 🔍 Validating route path: ${path}`); logger.debug(`[DeepLink] 🔍 Validating route path: ${path}`);
// Validate route exists // Validate route exists
const validRoute = routeSchema.parse(path) as DeepLinkRoute; const validRoute = routeSchema.parse(path) as DeepLinkRoute;
logger.info(`[DeepLink] ✅ Route validation passed: ${validRoute}`); logger.debug(`[DeepLink] ✅ Route validation passed: ${validRoute}`);
// Get route configuration // Get route configuration
const routeConfig = ROUTE_MAP[validRoute]; const routeConfig = ROUTE_MAP[validRoute];
logger.info(`[DeepLink] 📋 Route config retrieved:`, routeConfig); logger.debug(`[DeepLink] 📋 Route config retrieved:`, routeConfig);
if (!routeConfig) { if (!routeConfig) {
logger.error(`[DeepLink] ❌ No route config found for: ${validRoute}`); logger.error(`[DeepLink] ❌ No route config found for: ${validRoute}`);
@@ -209,7 +209,7 @@ export class DeepLinkHandler {
} }
routeName = routeConfig.name; routeName = routeConfig.name;
logger.info(`[DeepLink] 🎯 Route name resolved: ${routeName}`); logger.debug(`[DeepLink] 🎯 Route name resolved: ${routeName}`);
} catch (error) { } catch (error) {
logger.error(`[DeepLink] ❌ Route validation failed:`, { logger.error(`[DeepLink] ❌ Route validation failed:`, {
path, path,
@@ -228,14 +228,14 @@ export class DeepLinkHandler {
}, },
}); });
logger.info( logger.debug(
`[DeepLink] 🔄 Redirected to error page for invalid route: ${path}`, `[DeepLink] 🔄 Redirected to error page for invalid route: ${path}`,
); );
return; return;
} }
// Continue with parameter validation // Continue with parameter validation
logger.info( logger.debug(
`[DeepLink] 🔍 Starting parameter validation for route: ${routeName}`, `[DeepLink] 🔍 Starting parameter validation for route: ${routeName}`,
); );
@@ -258,7 +258,7 @@ export class DeepLinkHandler {
if (pathSchema) { if (pathSchema) {
logger.debug(`[DeepLink] 🔍 Validating path parameters:`, params); logger.debug(`[DeepLink] 🔍 Validating path parameters:`, params);
validatedPathParams = await pathSchema.parseAsync(params); validatedPathParams = await pathSchema.parseAsync(params);
logger.info( logger.debug(
`[DeepLink] ✅ Path parameters validated:`, `[DeepLink] ✅ Path parameters validated:`,
validatedPathParams, validatedPathParams,
); );
@@ -270,7 +270,7 @@ export class DeepLinkHandler {
if (querySchema) { if (querySchema) {
logger.debug(`[DeepLink] 🔍 Validating query parameters:`, query); logger.debug(`[DeepLink] 🔍 Validating query parameters:`, query);
validatedQueryParams = await querySchema.parseAsync(query); validatedQueryParams = await querySchema.parseAsync(query);
logger.info( logger.debug(
`[DeepLink] ✅ Query parameters validated:`, `[DeepLink] ✅ Query parameters validated:`,
validatedQueryParams, validatedQueryParams,
); );
@@ -299,7 +299,7 @@ export class DeepLinkHandler {
}, },
}); });
logger.info( logger.debug(
`[DeepLink] 🔄 Redirected to error page for invalid parameters`, `[DeepLink] 🔄 Redirected to error page for invalid parameters`,
); );
return; return;
@@ -307,7 +307,7 @@ export class DeepLinkHandler {
// Attempt navigation // Attempt navigation
try { try {
logger.info(`[DeepLink] 🚀 Attempting navigation:`, { logger.debug(`[DeepLink] 🚀 Attempting navigation:`, {
routeName, routeName,
pathParams: validatedPathParams, pathParams: validatedPathParams,
queryParams: validatedQueryParams, queryParams: validatedQueryParams,
@@ -319,7 +319,7 @@ export class DeepLinkHandler {
query: validatedQueryParams, query: validatedQueryParams,
}); });
logger.info(`[DeepLink] ✅ Navigation successful to: ${routeName}`); logger.debug(`[DeepLink] ✅ Navigation successful to: ${routeName}`);
} catch (error) { } catch (error) {
logger.error(`[DeepLink] ❌ Navigation failed:`, { logger.error(`[DeepLink] ❌ Navigation failed:`, {
routeName, routeName,
@@ -342,7 +342,7 @@ export class DeepLinkHandler {
}, },
}); });
logger.info( logger.debug(
`[DeepLink] 🔄 Redirected to error page for navigation failure`, `[DeepLink] 🔄 Redirected to error page for navigation failure`,
); );
} }

226
src/utils/safeAreaInset.js Normal file
View File

@@ -0,0 +1,226 @@
/**
* Safe Area Inset Injection for Android WebView
*
* This script injects safe area inset values into CSS environment variables
* when running in Android WebView, since Android doesn't natively support
* CSS env(safe-area-inset-*) variables like iOS does.
*/
// Check if we're running in Android WebView with Capacitor
const isAndroidWebView = () => {
// Check if we're on iOS - if so, skip this script entirely
const isIOS =
/iPad|iPhone|iPod/.test(navigator.userAgent) ||
(navigator.platform === "MacIntel" && navigator.maxTouchPoints > 1);
if (isIOS) {
return false;
}
// Check if we're on Android
const isAndroid = /Android/.test(navigator.userAgent);
// Check if we have Capacitor (required for Android WebView)
const hasCapacitor = window.Capacitor !== undefined;
// Only run on Android with Capacitor
return isAndroid && hasCapacitor;
};
// Wait for Capacitor to be available
const waitForCapacitor = () => {
return new Promise((resolve) => {
if (window.Capacitor) {
resolve(window.Capacitor);
return;
}
// Wait for Capacitor to be available
const checkCapacitor = () => {
if (window.Capacitor) {
resolve(window.Capacitor);
} else {
setTimeout(checkCapacitor, 100);
}
};
checkCapacitor();
});
};
// Inject safe area inset values into CSS custom properties
const injectSafeAreaInsets = async () => {
try {
// Wait for Capacitor to be available
const Capacitor = await waitForCapacitor();
// Try to get safe area insets using StatusBar plugin (which is already available)
let top = 0,
bottom = 0,
left = 0,
right = 0;
try {
// Use StatusBar plugin to get status bar height
if (Capacitor.Plugins.StatusBar) {
const statusBarInfo = await Capacitor.Plugins.StatusBar.getInfo();
// Status bar height is typically the top safe area inset
top = statusBarInfo.overlays ? 0 : statusBarInfo.height || 0;
}
} catch (error) {
// Status bar info not available, will use fallback
}
// Detect navigation bar and gesture bar heights
const detectNavigationBar = () => {
const screenHeight = window.screen.height;
const screenWidth = window.screen.width;
const windowHeight = window.innerHeight;
const devicePixelRatio = window.devicePixelRatio || 1;
// Calculate navigation bar height
let navBarHeight = 0;
// Method 1: Direct comparison (most reliable)
if (windowHeight < screenHeight) {
navBarHeight = screenHeight - windowHeight;
}
// Method 2: Check for gesture navigation indicators
if (navBarHeight === 0) {
// Look for common gesture navigation patterns
const isTallDevice = screenHeight > 2000;
const isModernDevice = screenHeight > 1800;
const hasHighDensity = devicePixelRatio >= 2.5;
if (isTallDevice && hasHighDensity) {
// Modern gesture-based device
navBarHeight = 12; // Typical gesture bar height
} else if (isModernDevice) {
// Modern device with traditional navigation
navBarHeight = 48; // Traditional navigation bar height
}
}
// Method 3: Check visual viewport (more accurate for WebView)
if (navBarHeight === 0) {
if (window.visualViewport) {
const visualHeight = window.visualViewport.height;
if (visualHeight < windowHeight) {
navBarHeight = windowHeight - visualHeight;
}
}
}
// Method 4: Device-specific estimation based on screen dimensions
if (navBarHeight === 0) {
// Common Android navigation bar heights in pixels
const commonNavBarHeights = {
"1080x2400": 48, // Common 1080p devices
"1440x3200": 64, // QHD devices
"720x1600": 32, // HD devices
};
const resolution = `${screenWidth}x${screenHeight}`;
const estimatedHeight = commonNavBarHeights[resolution];
if (estimatedHeight) {
navBarHeight = estimatedHeight;
} else {
// Fallback: estimate based on screen height
navBarHeight = screenHeight > 2000 ? 48 : 32;
}
}
return navBarHeight;
};
// Get navigation bar height
bottom = detectNavigationBar();
// If we still don't have a top value, estimate it
if (top === 0) {
const screenHeight = window.screen.height;
// Common status bar heights: 24dp (48px) for most devices, 32dp (64px) for some
top = screenHeight > 1920 ? 64 : 48;
}
// Left/right safe areas are rare on Android
left = 0;
right = 0;
// Create CSS custom properties
const style = document.createElement("style");
style.textContent = `
:root {
--safe-area-inset-top: ${top}px;
--safe-area-inset-bottom: ${bottom}px;
--safe-area-inset-left: ${left}px;
--safe-area-inset-right: ${right}px;
}
`;
// Inject the style into the document head
document.head.appendChild(style);
// Also set CSS environment variables if supported
if (CSS.supports("env(safe-area-inset-top)")) {
document.documentElement.style.setProperty(
"--env-safe-area-inset-top",
`${top}px`,
);
document.documentElement.style.setProperty(
"--env-safe-area-inset-bottom",
`${bottom}px`,
);
document.documentElement.style.setProperty(
"--env-safe-area-inset-left",
`${left}px`,
);
document.documentElement.style.setProperty(
"--env-safe-area-inset-right",
`${right}px`,
);
}
} catch (error) {
// Error injecting safe area insets, will use fallback values
}
};
// Initialize when DOM is ready
const initializeSafeArea = () => {
// Check if we should run this script at all
if (!isAndroidWebView()) {
return;
}
// Add a small delay to ensure WebView is fully initialized
setTimeout(() => {
injectSafeAreaInsets();
}, 100);
};
if (document.readyState === "loading") {
document.addEventListener("DOMContentLoaded", initializeSafeArea);
} else {
initializeSafeArea();
}
// Re-inject on orientation change (only on Android)
window.addEventListener("orientationchange", () => {
if (isAndroidWebView()) {
setTimeout(() => injectSafeAreaInsets(), 100);
}
});
// Re-inject on resize (only on Android)
window.addEventListener("resize", () => {
if (isAndroidWebView()) {
setTimeout(() => injectSafeAreaInsets(), 100);
}
});
// Export for use in other modules
export { injectSafeAreaInsets, isAndroidWebView };

View File

@@ -58,7 +58,8 @@
v-if="!isRegistered" v-if="!isRegistered"
:passkeys-enabled="PASSKEYS_ENABLED" :passkeys-enabled="PASSKEYS_ENABLED"
:given-name="givenName" :given-name="givenName"
message="Before you can publicly announce a new project or time commitment, a friend needs to register you." message="Before you can publicly announce a new project or time commitment,
a friend needs to register you."
/> />
<!-- Notifications --> <!-- Notifications -->

View File

@@ -17,20 +17,40 @@
<!-- Results List --> <!-- Results List -->
<ul class="border-t border-slate-300"> <ul class="border-t border-slate-300">
<li class="border-b border-slate-300 py-3"> <!-- "You" entity -->
<li v-if="shouldShowYouEntity" class="border-b border-slate-300 py-3">
<h2 class="text-base flex gap-4 items-center"> <h2 class="text-base flex gap-4 items-center">
<span class="grow flex gap-2 items-center font-medium"> <span class="grow flex gap-2 items-center font-medium">
<font-awesome <font-awesome icon="hand" class="text-blue-500 text-4xl shrink-0" />
icon="circle-question" <span class="text-ellipsis overflow-hidden text-blue-500">You</span>
class="text-slate-400 text-4xl"
/>
<span class="italic text-slate-400">(Not Named)</span>
</span> </span>
<span class="text-right"> <span class="text-right">
<button <button
type="button" type="button"
class="block w-full text-center text-sm uppercase bg-gradient-to-b from-blue-400 to-blue-700 shadow-[inset_0_-1px_0_0_rgba(0,0,0,0.5)] text-white px-3 py-1.5 rounded-md" class="block w-full text-center text-sm uppercase bg-gradient-to-b from-blue-400 to-blue-700 shadow-[inset_0_-1px_0_0_rgba(0,0,0,0.5)] text-white px-3 py-1.5 rounded-md"
@click="openDialog('Unnamed')" @click="openDialog({ did: activeDid, name: 'You' })"
>
<font-awesome icon="gift" class="fa-fw"></font-awesome>
</button>
</span>
</h2>
</li>
<li class="border-b border-slate-300 py-3">
<h2 class="text-base flex gap-4 items-center">
<span class="grow flex gap-2 items-center font-medium">
<font-awesome
icon="circle-question"
class="text-slate-400 text-4xl shrink-0"
/>
<span class="text-ellipsis overflow-hidden italic text-slate-500">{{
unnamedEntityName
}}</span>
</span>
<span class="text-right">
<button
type="button"
class="block w-full text-center text-sm uppercase bg-gradient-to-b from-blue-400 to-blue-700 shadow-[inset_0_-1px_0_0_rgba(0,0,0,0.5)] text-white px-3 py-1.5 rounded-md"
@click="openDialog({ did: '', name: unnamedEntityName })"
> >
<font-awesome icon="gift" class="fa-fw"></font-awesome> <font-awesome icon="gift" class="fa-fw"></font-awesome>
</button> </button>
@@ -43,14 +63,22 @@
class="border-b border-slate-300 py-3" class="border-b border-slate-300 py-3"
> >
<h2 class="text-base flex gap-4 items-center"> <h2 class="text-base flex gap-4 items-center">
<span class="grow flex gap-2 items-center font-medium"> <span
class="grow flex gap-2 items-center font-medium overflow-hidden"
>
<EntityIcon <EntityIcon
:contact="contact" :contact="contact"
:icon-size="34" :icon-size="34"
class="inline-block align-middle border border-slate-300 rounded-full overflow-hidden" class="inline-block align-middle border border-slate-300 rounded-full overflow-hidden shrink-0"
/> />
<span v-if="contact.name">{{ contact.name }}</span> <span v-if="contact.name" class="text-ellipsis overflow-hidden">{{
<span v-else class="italic text-slate-400">(No name)</span> contact.name
}}</span>
<span
v-else
class="text-ellipsis overflow-hidden italic text-slate-500"
>{{ contact.did }}</span
>
</span> </span>
<span class="text-right"> <span class="text-right">
<button <button
@@ -72,6 +100,7 @@
:from-project-id="fromProjectId" :from-project-id="fromProjectId"
:to-project-id="toProjectId" :to-project-id="toProjectId"
:is-from-project-view="isFromProjectView" :is-from-project-view="isFromProjectView"
:hide-show-all="true"
/> />
</section> </section>
</template> </template>
@@ -89,6 +118,7 @@ import { GiverReceiverInputInfo } from "../libs/util";
import { logger } from "../utils/logger"; import { logger } from "../utils/logger";
import { PlatformServiceMixin } from "@/utils/PlatformServiceMixin"; import { PlatformServiceMixin } from "@/utils/PlatformServiceMixin";
import { createNotifyHelpers, TIMEOUTS } from "@/utils/notify"; import { createNotifyHelpers, TIMEOUTS } from "@/utils/notify";
import { UNNAMED_ENTITY_NAME } from "@/constants/entities";
@Component({ @Component({
components: { GiftedDialog, QuickNav, EntityIcon }, components: { GiftedDialog, QuickNav, EntityIcon },
mixins: [PlatformServiceMixin], mixins: [PlatformServiceMixin],
@@ -188,147 +218,151 @@ export default class ContactGiftingView extends Vue {
} }
} }
openDialog(contact?: GiverReceiverInputInfo | "Unnamed") { openDialog(contact?: GiverReceiverInputInfo) {
if (contact === "Unnamed") { // Determine the selected entity based on contact type
// Special case: Handle "Unnamed" contacts for both givers and recipients const selectedEntity = this.createEntityFromContact(contact);
let recipient: GiverReceiverInputInfo;
let giver: GiverReceiverInputInfo | undefined;
if (this.stepType === "giver") { // Create giver and recipient based on step type and selected entity
// We're selecting a giver, so preserve the existing recipient from context const { giver, recipient } = this.createGiverAndRecipient(selectedEntity);
if (this.recipientEntityType === "project") {
recipient = {
did: this.recipientProjectHandleId,
name: this.recipientProjectName,
image: this.recipientProjectImage,
handleId: this.recipientProjectHandleId,
};
} else {
// Preserve the existing recipient from context
if (this.recipientDid === this.activeDid) {
// Recipient was "You"
recipient = { did: this.activeDid, name: "You" };
} else if (this.recipientDid) {
// Recipient was a regular contact
recipient = {
did: this.recipientDid,
name: this.recipientProjectName || "Someone",
};
} else {
// Fallback to "You" if no recipient was previously selected
recipient = { did: this.activeDid, name: "You" };
}
}
giver = undefined; // Will be set to "Unnamed" in GiftedDialog
} else {
// We're selecting a recipient, so recipient is "Unnamed" and giver is preserved from context
recipient = { did: "", name: "Unnamed" };
// Preserve the existing giver from the context // Open the dialog
if (this.giverEntityType === "project") { (this.$refs.giftedDialog as GiftedDialog).open(
giver = { giver,
did: this.giverProjectHandleId, recipient,
name: this.giverProjectName, this.offerId,
image: this.giverProjectImage, this.prompt,
handleId: this.giverProjectHandleId, this.description,
}; this.amountInput,
} else if (this.giverDid) { this.unitCode,
giver = { );
did: this.giverDid,
name: this.giverProjectName || "Someone",
};
} else {
giver = { did: this.activeDid, name: "You" };
}
}
(this.$refs.giftedDialog as GiftedDialog).open( // Move to Step 2 - entities are already set by the open() call
giver, (this.$refs.giftedDialog as GiftedDialog).moveToStep2();
recipient, }
this.offerId,
this.prompt,
this.description,
this.amountInput,
this.unitCode,
);
// Move to Step 2 - entities are already set by the open() call /**
(this.$refs.giftedDialog as GiftedDialog).moveToStep2(); * Creates an entity object from the contact parameter
* Uses DID-based logic to determine "You" and "Unnamed" entities
*/
private createEntityFromContact(
contact?: GiverReceiverInputInfo,
): GiverReceiverInputInfo | undefined {
if (!contact) {
return undefined;
}
// Handle GiverReceiverInputInfo object
if (contact.did === this.activeDid) {
// If DID matches active DID, create "You" entity
return { did: this.activeDid, name: "You" };
} else if (!contact.did || contact.did === "") {
// If DID is empty/null, create "Unnamed" entity
return { did: "", name: UNNAMED_ENTITY_NAME };
} else { } else {
// Regular case: contact is a GiverReceiverInputInfo // Create a copy of the contact to avoid modifying the original
let giver: GiverReceiverInputInfo; return { ...contact };
let recipient: GiverReceiverInputInfo; }
}
if (this.stepType === "giver") { /**
// We're selecting a giver, so the contact becomes the giver * Creates giver and recipient objects based on step type and selected entity
giver = contact as GiverReceiverInputInfo; // Safe because we know contact is not "Unnamed" or undefined */
private createGiverAndRecipient(selectedEntity?: GiverReceiverInputInfo): {
giver: GiverReceiverInputInfo | undefined;
recipient: GiverReceiverInputInfo;
} {
if (this.stepType === "giver") {
// We're selecting a giver, so the selected entity becomes the giver
const giver = selectedEntity;
const recipient = this.createRecipientFromContext();
return { giver, recipient };
} else {
// We're selecting a recipient, so the selected entity becomes the recipient
const recipient = selectedEntity || {
did: "",
name: UNNAMED_ENTITY_NAME,
};
const giver = this.createGiverFromContext();
return { giver, recipient };
}
}
// Preserve the existing recipient from the context /**
if (this.recipientEntityType === "project") { * Creates recipient object from context (preserves existing recipient)
recipient = { */
did: this.recipientProjectHandleId, private createRecipientFromContext(): GiverReceiverInputInfo {
name: this.recipientProjectName, if (this.recipientEntityType === "project") {
image: this.recipientProjectImage, return {
handleId: this.recipientProjectHandleId, name: this.recipientProjectName,
}; image: this.recipientProjectImage,
} else { handleId: this.recipientProjectHandleId,
// Check if the preserved recipient was "You" or a regular contact };
if (this.recipientDid === this.activeDid) { } else {
// Recipient was "You" if (this.recipientDid === this.activeDid) {
recipient = { did: this.activeDid, name: "You" }; return { did: this.activeDid, name: "You" };
} else if (this.recipientDid) { } else if (this.recipientDid) {
// Recipient was a regular contact return {
recipient = { did: this.recipientDid,
did: this.recipientDid, name: this.recipientProjectName,
name: this.recipientProjectName || "Someone", };
};
} else {
// Fallback to "Unnamed"
recipient = { did: "", name: "Unnamed" };
}
}
} else { } else {
// We're selecting a recipient, so the contact becomes the recipient return { did: "", name: UNNAMED_ENTITY_NAME };
recipient = contact as GiverReceiverInputInfo; // Safe because we know contact is not "Unnamed" or undefined
// Preserve the existing giver from the context
if (this.giverEntityType === "project") {
giver = {
did: this.giverProjectHandleId,
name: this.giverProjectName,
image: this.giverProjectImage,
handleId: this.giverProjectHandleId,
};
} else {
// Check if the preserved giver was "You" or a regular contact
if (this.giverDid === this.activeDid) {
// Giver was "You"
giver = { did: this.activeDid, name: "You" };
} else if (this.giverDid) {
// Giver was a regular contact
giver = {
did: this.giverDid,
name: this.giverProjectName || "Someone",
};
} else {
// Fallback to "Unnamed"
giver = { did: "", name: "Unnamed" };
}
}
} }
}
}
(this.$refs.giftedDialog as GiftedDialog).open( /**
giver, * Creates giver object from context (preserves existing giver)
recipient, */
this.offerId, private createGiverFromContext(): GiverReceiverInputInfo {
this.prompt, if (this.giverEntityType === "project") {
this.description, return {
this.amountInput, name: this.giverProjectName,
this.unitCode, image: this.giverProjectImage,
); handleId: this.giverProjectHandleId,
};
} else {
if (this.giverDid === this.activeDid) {
return { did: this.activeDid, name: "You" };
} else if (this.giverDid) {
return {
did: this.giverDid,
name: this.giverProjectName,
};
} else {
return { did: "", name: UNNAMED_ENTITY_NAME };
}
}
}
// Move to Step 2 - entities are already set by the open() call /**
(this.$refs.giftedDialog as GiftedDialog).moveToStep2(); * Get the unnamed entity name constant
*/
get unnamedEntityName(): string {
return UNNAMED_ENTITY_NAME;
}
get shouldShowYouEntity(): boolean {
if (this.stepType === "giver") {
// When selecting a giver, show "You" if the current recipient is not "You"
// This prevents selecting yourself as both giver and recipient
if (this.recipientEntityType === "project") {
// If recipient is a project, we can select "You" as giver
return true;
} else {
// If recipient is a person, check if it's not "You"
return this.recipientDid !== this.activeDid;
}
} else {
// When selecting a recipient, show "You" if the current giver is not "You"
// This prevents selecting yourself as both giver and recipient
if (this.giverEntityType === "project") {
// If giver is a project, we can select "You" as recipient
return true;
} else {
// If giver is a person, check if it's not "You"
return this.giverDid !== this.activeDid;
}
} }
} }
} }

View File

@@ -220,21 +220,21 @@ export default class ContactQRScanFull extends Vue {
* Computed property for QR code container CSS classes * Computed property for QR code container CSS classes
*/ */
get qrContainerClasses(): string { get qrContainerClasses(): string {
return "block w-full max-w-[calc((100vh-env(safe-area-inset-top)-env(safe-area-inset-bottom))*0.4)] mx-auto mt-4"; return "block w-full max-w-[calc((100vh-max(env(safe-area-inset-top),var(--safe-area-inset-top,0px))-max(env(safe-area-inset-bottom),var(--safe-area-inset-bottom,0px)))*0.4)] mx-auto mt-4";
} }
/** /**
* Computed property for camera frame CSS classes * Computed property for camera frame CSS classes
*/ */
get cameraFrameClasses(): string { get cameraFrameClasses(): string {
return "relative w-full max-w-[calc((100vh-env(safe-area-inset-top)-env(safe-area-inset-bottom))*0.4)] mx-auto border border-dashed border-white mt-8 aspect-square"; return "relative w-full max-w-[calc((100vh-max(env(safe-area-inset-top),var(--safe-area-inset-top,0px))-max(env(safe-area-inset-bottom),var(--safe-area-inset-bottom,0px)))*0.4)] mx-auto border border-dashed border-white mt-8 aspect-square";
} }
/** /**
* Computed property for main content container CSS classes * Computed property for main content container CSS classes
*/ */
get mainContentClasses(): string { get mainContentClasses(): string {
return "p-6 bg-white w-full max-w-[calc((100vh-env(safe-area-inset-top)-env(safe-area-inset-bottom))*0.4)] mx-auto"; return "p-6 bg-white w-full max-w-[calc((100vh-max(env(safe-area-inset-top),var(--safe-area-inset-top,0px))-max(env(safe-area-inset-bottom),var(--safe-area-inset-bottom,0px)))*0.4)] mx-auto";
} }
/** /**

View File

@@ -257,11 +257,11 @@ export default class ContactQRScanShow extends Vue {
} }
get qrCodeContainerClasses(): string { get qrCodeContainerClasses(): string {
return "block w-[90vw] max-w-[calc((100vh-env(safe-area-inset-top)-env(safe-area-inset-bottom))*0.4)] mx-auto my-4"; return "block w-[90vw] max-w-[calc((100vh-max(env(safe-area-inset-top),var(--safe-area-inset-top,0px))-max(env(safe-area-inset-bottom),var(--safe-area-inset-bottom,0px)))*0.4)] mx-auto my-4";
} }
get scannerContainerClasses(): string { get scannerContainerClasses(): string {
return "relative aspect-square overflow-hidden bg-slate-800 w-[90vw] max-w-[calc((100vh-env(safe-area-inset-top)-env(safe-area-inset-bottom))*0.4)] mx-auto"; return "relative aspect-square overflow-hidden bg-slate-800 w-[90vw] max-w-[calc((100vh-max(env(safe-area-inset-top),var(--safe-area-inset-top,0px))-max(env(safe-area-inset-bottom),var(--safe-area-inset-bottom,0px)))*0.4)] mx-auto";
} }
get statusMessageClasses(): string { get statusMessageClasses(): string {

View File

@@ -290,6 +290,7 @@ import {
NOTIFY_SERVER_ACCESS_ERROR, NOTIFY_SERVER_ACCESS_ERROR,
NOTIFY_NO_IDENTITY_ERROR, NOTIFY_NO_IDENTITY_ERROR,
} from "@/constants/notifications"; } from "@/constants/notifications";
import { THAT_UNNAMED_PERSON } from "@/constants/entities";
/** /**
* DIDView Component * DIDView Component
@@ -551,7 +552,7 @@ export default class DIDView extends Vue {
contact.registered = true; contact.registered = true;
await this.$updateContact(contact.did, { registered: true }); await this.$updateContact(contact.did, { registered: true });
const name = contact.name || "That unnamed person"; const name = contact.name || THAT_UNNAMED_PERSON;
this.notify.success( this.notify.success(
`${name} ${NOTIFY_REGISTRATION_SUCCESS.message}`, `${name} ${NOTIFY_REGISTRATION_SUCCESS.message}`,
TIMEOUTS.LONG, TIMEOUTS.LONG,

View File

@@ -233,7 +233,7 @@
<div class="grow"> <div class="grow">
<h2 class="text-base font-semibold"> <h2 class="text-base font-semibold">
{{ project.name || "Unnamed Project" }} {{ project.name || unnamedProject }}
</h2> </h2>
<div class="text-sm"> <div class="text-sm">
<font-awesome <font-awesome
@@ -340,6 +340,7 @@ import {
NOTIFY_DISCOVER_LOCAL_SEARCH_ERROR, NOTIFY_DISCOVER_LOCAL_SEARCH_ERROR,
NOTIFY_DISCOVER_MAP_SEARCH_ERROR, NOTIFY_DISCOVER_MAP_SEARCH_ERROR,
} from "@/constants/notifications"; } from "@/constants/notifications";
import { UNNAMED_PROJECT } from "@/constants/entities";
interface Tile { interface Tile {
indexLat: number; indexLat: number;
indexLon: number; indexLon: number;
@@ -370,6 +371,13 @@ export default class DiscoverView extends Vue {
notify!: ReturnType<typeof createNotifyHelpers>; notify!: ReturnType<typeof createNotifyHelpers>;
/**
* Get the unnamed project constant
*/
get unnamedProject(): string {
return UNNAMED_PROJECT;
}
activeDid = ""; activeDid = "";
allContacts: Array<Contact> = []; allContacts: Array<Contact> = [];
allMyDids: Array<string> = []; allMyDids: Array<string> = [];

View File

@@ -200,7 +200,7 @@
</p> </p>
<p> <p>
Then you can record your appreciation for... whatever: select any contact on the home page Then you can record your appreciation for... whatever: select any contact on the home page
(or "Unnamed") and send it. The main goal is to record what people (or "{{ unnamedEntityName }}") and send it. The main goal is to record what people
have given you, to grow giving economies. You can also record your own have given you, to grow giving economies. You can also record your own
ideas for projects. Each claim is recorded on a ideas for projects. Each claim is recorded on a
custom ledger. custom ledger.
@@ -600,6 +600,7 @@ import QuickNav from "../components/QuickNav.vue";
import { APP_SERVER } from "../constants/app"; import { APP_SERVER } from "../constants/app";
import { PlatformServiceMixin } from "@/utils/PlatformServiceMixin"; import { PlatformServiceMixin } from "@/utils/PlatformServiceMixin";
import { QRNavigationService } from "@/services/QRNavigationService"; import { QRNavigationService } from "@/services/QRNavigationService";
import { UNNAMED_ENTITY_NAME } from "@/constants/entities";
/** /**
* HelpView.vue - Comprehensive Help System Component * HelpView.vue - Comprehensive Help System Component
@@ -647,6 +648,13 @@ export default class HelpView extends Vue {
APP_SERVER = APP_SERVER; APP_SERVER = APP_SERVER;
// Capacitor reference removed - using QRNavigationService instead // Capacitor reference removed - using QRNavigationService instead
/**
* Get the unnamed entity name constant
*/
get unnamedEntityName(): string {
return UNNAMED_ENTITY_NAME;
}
// Ideally, we put no functionality in here, especially in the setup, // Ideally, we put no functionality in here, especially in the setup,
// because we never want this page to have a chance of throwing an error. // because we never want this page to have a chance of throwing an error.

View File

@@ -282,6 +282,7 @@ import { PlatformServiceMixin } from "@/utils/PlatformServiceMixin";
import { createNotifyHelpers, TIMEOUTS } from "@/utils/notify"; import { createNotifyHelpers, TIMEOUTS } from "@/utils/notify";
import { NOTIFY_CONTACT_LOADING_ISSUE } from "@/constants/notifications"; import { NOTIFY_CONTACT_LOADING_ISSUE } from "@/constants/notifications";
import * as Package from "../../package.json"; import * as Package from "../../package.json";
import { UNNAMED_ENTITY_NAME } from "@/constants/entities";
// consolidate this with GiveActionClaim in src/interfaces/claims.ts // consolidate this with GiveActionClaim in src/interfaces/claims.ts
interface Claim { interface Claim {
@@ -1546,30 +1547,41 @@ export default class HomeView extends Vue {
* @param giver Optional contact info for giver * @param giver Optional contact info for giver
* @param description Optional gift description * @param description Optional gift description
*/ */
openDialog(giver?: GiverReceiverInputInfo | "Unnamed", prompt?: string) { openDialog(giver?: GiverReceiverInputInfo, prompt?: string) {
if (giver === "Unnamed") { // Determine the giver entity based on DID logic
// Special case: Pass undefined to trigger Step 1, but with "Unnamed" pre-selected const giverEntity = this.createGiverEntity(giver);
(this.$refs.giftedDialog as GiftedDialog).open(
undefined, (this.$refs.giftedDialog as GiftedDialog).open(
{ giverEntity,
did: this.activeDid, {
name: "You", did: this.activeDid,
} as GiverReceiverInputInfo, name: "You", // In HomeView, we always use "You" as the giver
undefined, } as GiverReceiverInputInfo,
prompt, undefined,
); prompt,
// Immediately select "Unnamed" and move to Step 2 );
(this.$refs.giftedDialog as GiftedDialog).selectGiver(); }
/**
* Creates giver entity using DID-based logic
*/
private createGiverEntity(
giver?: GiverReceiverInputInfo,
): GiverReceiverInputInfo | undefined {
if (!giver) {
return undefined;
}
// Handle GiverReceiverInputInfo object
if (giver.did === this.activeDid) {
// If DID matches active DID, create "You" entity
return { did: this.activeDid, name: "You" };
} else if (!giver.did || giver.did === "") {
// If DID is empty/null, create "Unnamed" entity
return { did: "", name: UNNAMED_ENTITY_NAME };
} else { } else {
(this.$refs.giftedDialog as GiftedDialog).open( // Return the giver as-is
giver, return giver;
{
did: this.activeDid,
name: "You",
} as GiverReceiverInputInfo,
undefined,
prompt,
);
} }
} }
@@ -1632,10 +1644,15 @@ export default class HomeView extends Vue {
this.isImageViewerOpen = true; this.isImageViewerOpen = true;
} }
openPersonDialog( private handleQRCodeClick() {
giver?: GiverReceiverInputInfo | "Unnamed", if (Capacitor.isNativePlatform()) {
prompt?: string, this.$router.push({ name: "contact-qr-scan-full" });
) { } else {
this.$router.push({ name: "contact-qr" });
}
}
openPersonDialog(giver?: GiverReceiverInputInfo, prompt?: string) {
this.showProjectsDialog = false; this.showProjectsDialog = false;
this.openDialog(giver, prompt); this.openDialog(giver, prompt);
} }

View File

@@ -183,7 +183,7 @@
class="text-blue-500" class="text-blue-500"
@click="onClickLoadProject(plan.handleId)" @click="onClickLoadProject(plan.handleId)"
> >
{{ plan.name || "Unnamed Project" }} {{ plan.name || unnamedProject }}
</button> </button>
</div> </div>
<div v-if="fulfillersToHitLimit" class="text-center"> <div v-if="fulfillersToHitLimit" class="text-center">
@@ -207,7 +207,7 @@
class="text-blue-500" class="text-blue-500"
@click="onClickLoadProject(fulfilledByThis.handleId)" @click="onClickLoadProject(fulfilledByThis.handleId)"
> >
{{ fulfilledByThis.name || "Unnamed Project" }} {{ fulfilledByThis.name || unnamedProject }}
</button> </button>
</div> </div>
</div> </div>
@@ -611,6 +611,7 @@ import { PlatformServiceMixin } from "@/utils/PlatformServiceMixin";
import { createNotifyHelpers, TIMEOUTS } from "@/utils/notify"; import { createNotifyHelpers, TIMEOUTS } from "@/utils/notify";
import { NOTIFY_CONFIRM_CLAIM } from "@/constants/notifications"; import { NOTIFY_CONFIRM_CLAIM } from "@/constants/notifications";
import { APP_SERVER } from "@/constants/app"; import { APP_SERVER } from "@/constants/app";
import { UNNAMED_PROJECT } from "@/constants/entities";
/** /**
* Project View Component * Project View Component
* @author Matthew Raymer * @author Matthew Raymer
@@ -664,6 +665,13 @@ export default class ProjectViewView extends Vue {
/** Notification helpers instance */ /** Notification helpers instance */
notify!: ReturnType<typeof createNotifyHelpers>; notify!: ReturnType<typeof createNotifyHelpers>;
/**
* Get the unnamed project constant
*/
get unnamedProject(): string {
return UNNAMED_PROJECT;
}
// Account and Settings State // Account and Settings State
/** Currently active DID */ /** Currently active DID */
activeDid = ""; activeDid = "";

View File

@@ -244,7 +244,7 @@
<div class="grow overflow-hidden"> <div class="grow overflow-hidden">
<h2 class="text-base font-semibold"> <h2 class="text-base font-semibold">
{{ project.name || "Unnamed Project" }} {{ project.name || unnamedProject }}
</h2> </h2>
<div class="text-sm truncate"> <div class="text-sm truncate">
{{ project.description }} {{ project.description }}
@@ -286,6 +286,7 @@ import {
NOTIFY_OFFERS_LOAD_ERROR, NOTIFY_OFFERS_LOAD_ERROR,
NOTIFY_OFFERS_FETCH_ERROR, NOTIFY_OFFERS_FETCH_ERROR,
} from "@/constants/notifications"; } from "@/constants/notifications";
import { UNNAMED_PROJECT } from "@/constants/entities";
/** /**
* Projects View Component * Projects View Component
@@ -324,6 +325,13 @@ export default class ProjectsView extends Vue {
notify!: ReturnType<typeof createNotifyHelpers>; notify!: ReturnType<typeof createNotifyHelpers>;
/**
* Get the unnamed project constant
*/
get unnamedProject(): string {
return UNNAMED_PROJECT;
}
// User account state // User account state
activeDid = ""; activeDid = "";
allContacts: Array<Contact> = []; allContacts: Array<Contact> = [];

View File

@@ -239,7 +239,8 @@ export default class QuickActionBvcEndView extends Vue {
} }
const eventStartDateObj = currentOrPreviousSat const eventStartDateObj = currentOrPreviousSat
.set({ weekday: 6 }) .set({ weekday: 6 })
.set({ hour: 9 }) .set({ hour: 8 })
.set({ minute: 30 }) // to catch if people put their claims 30 minutes early
.startOf("hour"); .startOf("hour");
// Hack, but full ISO pushes the length to 340 which crashes verifyJWT! // Hack, but full ISO pushes the length to 340 which crashes verifyJWT!

View File

@@ -69,6 +69,7 @@
*/ */
import { test, expect } from '@playwright/test'; import { test, expect } from '@playwright/test';
import { UNNAMED_ENTITY_NAME } from '../src/constants/entities';
import { deleteContact, generateAndRegisterEthrUser, importUser } from './testUtils'; import { deleteContact, generateAndRegisterEthrUser, importUser } from './testUtils';
test('Check activity feed - check that server is running', async ({ page }) => { test('Check activity feed - check that server is running', async ({ page }) => {
@@ -177,7 +178,7 @@ test('Check User 0 can register a random person', async ({ page }) => {
await page.goto('./'); await page.goto('./');
await page.getByTestId('closeOnboardingAndFinish').click(); await page.getByTestId('closeOnboardingAndFinish').click();
await page.getByRole('button', { name: 'Person' }).click(); await page.getByRole('button', { name: 'Person' }).click();
await page.getByRole('listitem').filter({ hasText: 'Unnamed' }).locator('svg').click(); await page.getByRole('listitem').filter({ hasText: UNNAMED_ENTITY_NAME }).locator('svg').click();
await page.getByPlaceholder('What was given').fill('Gave me access!'); await page.getByPlaceholder('What was given').fill('Gave me access!');
await page.getByRole('button', { name: 'Sign & Send' }).click(); await page.getByRole('button', { name: 'Sign & Send' }).click();
await expect(page.getByText('That gift was recorded.')).toBeVisible(); await expect(page.getByText('That gift was recorded.')).toBeVisible();

View File

@@ -79,6 +79,7 @@
* ``` * ```
*/ */
import { test, expect } from '@playwright/test'; import { test, expect } from '@playwright/test';
import { UNNAMED_ENTITY_NAME } from '../src/constants/entities';
import { importUser } from './testUtils'; import { importUser } from './testUtils';
test('Record something given', async ({ page }) => { test('Record something given', async ({ page }) => {
@@ -101,7 +102,7 @@ test('Record something given', async ({ page }) => {
await page.goto('./'); await page.goto('./');
await page.getByTestId('closeOnboardingAndFinish').click(); await page.getByTestId('closeOnboardingAndFinish').click();
await page.getByRole('button', { name: 'Person' }).click(); await page.getByRole('button', { name: 'Person' }).click();
await page.getByRole('listitem').filter({ hasText: 'Unnamed' }).locator('svg').click(); await page.getByRole('listitem').filter({ hasText: UNNAMED_ENTITY_NAME }).locator('svg').click();
await page.getByPlaceholder('What was given').fill(finalTitle); await page.getByPlaceholder('What was given').fill(finalTitle);
await page.getByRole('spinbutton').fill(randomNonZeroNumber.toString()); await page.getByRole('spinbutton').fill(randomNonZeroNumber.toString());
await page.getByRole('button', { name: 'Sign & Send' }).click(); await page.getByRole('button', { name: 'Sign & Send' }).click();

View File

@@ -85,6 +85,7 @@
*/ */
import { test, expect } from '@playwright/test'; import { test, expect } from '@playwright/test';
import { UNNAMED_ENTITY_NAME } from '../src/constants/entities';
import { importUser, createUniqueStringsArray, createRandomNumbersArray } from './testUtils'; import { importUser, createUniqueStringsArray, createRandomNumbersArray } from './testUtils';
test('Record 9 new gifts', async ({ page }) => { test('Record 9 new gifts', async ({ page }) => {
@@ -116,7 +117,7 @@ test('Record 9 new gifts', async ({ page }) => {
await page.getByTestId('closeOnboardingAndFinish').click(); await page.getByTestId('closeOnboardingAndFinish').click();
} }
await page.getByRole('button', { name: 'Person' }).click(); await page.getByRole('button', { name: 'Person' }).click();
await page.getByRole('listitem').filter({ hasText: 'Unnamed' }).locator('svg').click(); await page.getByRole('listitem').filter({ hasText: UNNAMED_ENTITY_NAME }).locator('svg').click();
await page.getByPlaceholder('What was given').fill(finalTitles[i]); await page.getByPlaceholder('What was given').fill(finalTitles[i]);
await page.getByRole('spinbutton').fill(finalNumbers[i].toString()); await page.getByRole('spinbutton').fill(finalNumbers[i].toString());
await page.getByRole('button', { name: 'Sign & Send' }).click(); await page.getByRole('button', { name: 'Sign & Send' }).click();