Compare commits

...

50 Commits

Author SHA1 Message Date
Matt Raymer
d402642db8 feat: add centralized git hash retrieval script
- Create scripts/get-git-hash.sh for consistent git hash logic
- Update build scripts to use new centralized git hash function
- Add comprehensive documentation and usage examples
- Ensure branch-aware hash retrieval across all build types

Provides single source of truth for git hash retrieval, preventing inconsistencies
when builds use wrong branch's commit hash.
2025-08-07 08:56:07 -04:00
Jose Olarte III
bf08e57ce7 Fix: re-organize entity type conditional logic in gifting flow
- Add conditional checks for person vs project entity types when setting DID fields
- Simplify project ID assignment logic by removing redundant entity type checks
- Preserve existing recipient context when selecting givers in ContactGiftingView, especially when dealing with "Unnamed" entity
2025-08-07 18:29:58 +08:00
Jose Olarte III
18e6aa5a9a Fix: gifting error messages
- Replaced error messages in GiftedDetailsView
- Maintained consistency between GiftedDialog and GiftedDetailsView (error message constants, amountInput)
2025-08-07 18:20:09 +08:00
Jose Olarte III
795df6a8fb Test "You" explicitly; fallback is "Unnamed" 2025-08-07 15:33:45 +08:00
Jose Olarte III
919b48e61f Remove unneeded comment 2025-08-07 15:08:57 +08:00
Matthew Raymer
3c37ead60d feat: add comprehensive Quick Start section and clean:all command
- Add Quick Start for Developers section with most common commands
- Add clean:all command to package.json for cleaning all platforms
- Update BUILDING.md with accurate command descriptions and workflow
- Reorganize sections to prioritize npm commands at the top
- Add comprehensive environment configuration documentation
- Update appendices A and B with current build system information
- Fix port number in development server documentation (8080)
- Add troubleshooting quick fixes and platform-specific guidance
- Document environment variable precedence and loading process
- Add comprehensive Vite configuration documentation in Appendix B
2025-08-07 05:29:49 +00:00
Matthew Raymer
6868a322f1 feat: switch ContactQRScanShowView to URL-based contact sharing
- Replace CSV QR value copying with URL generation for better UX
- Add generateEndorserJwtUrlForAccount import and Account type
- Implement proper error handling for URL generation failures
- Maintain consistency with ContactQRScanFullView behavior
- Add documentation for the URL solution implementation

Recipients can now click shared URLs to add contacts directly instead of
manually pasting CSV data into input fields.

addresses:  https://app.clickup.com/t/86b63xhz4
2025-08-07 03:14:45 +00:00
783ad6e122 fix: Fix data for gives that fulfill offers that are attached to projects (not to include person info). Also: commentary. 2025-08-06 20:55:24 -06:00
1f1739f00c fix: Fix messaging on the info-circles by givers & receivers in gifting details. 2025-08-06 20:54:34 -06:00
Matthew Raymer
ed0f49656d Simplify contactsToExportJson function
Remove unnecessary contact object mapping in contactsToExportJson function.
The contacts array can be used directly since it already contains all required
fields in the correct format.
2025-08-07 02:42:53 +00:00
Matthew Raymer
75e8b34e88 Merge branch 'build-improvement' of ssh://173.199.124.46:222/trent_larson/crowd-funder-for-time-pwa into build-improvement 2025-08-07 01:49:31 +00:00
Matthew Raymer
b267d1bc66 Fix contact backup export: contactMethods now exports as JSON arrays instead of strings
- Fixed contactsToExportJson to export contactMethods as proper arrays instead of stringified JSON
- Added JSON parsing for contactMethods in _mapColumnsToValues when retrieving from database
- Updated $insertContact to properly handle contactMethods field storage
- Removed unused ContactWithJsonStrings import
ref:  https://app.clickup.com/t/86b63ffpb
Resolves issue where contact backup exports showed contactMethods as "[]" strings instead of proper JSON arrays.
2025-08-07 01:49:22 +00:00
2a34d0e2d1 chore: Adjust some types and type commentary 2025-08-06 17:56:50 -06:00
Matthew Raymer
4480778a49 fix: export contactMethods as JSON arrays instead of strings
- Fixed contactsToExportJson to properly handle contactMethods as arrays
- Fixed contactToCsvLine to correctly stringify contactMethods arrays
- Removed unused parseJsonField function (consolidated with PlatformServiceMixin)
- Resolves issue where contact backup exports showed contactMethods as strings instead of JSON arrays
2025-08-06 06:33:57 +00:00
Matthew Raymer
607bb50a55 fix: Restore "Share Your Info" functionality with correct QR code format
- Fix navigation to use correct QR code routes (contact-qr/contact-qr-scan-full)
- Replace deep link generation with CSV format QR codes
- Remove unused imports and fix notification method calls
- Aligns with master branch behavior for contact sharing

Resolves issue where Share Your Info showed "not implemented" and generated
localhost deep links instead of proper CSV format QR codes.
2025-08-06 06:15:05 +00:00
Matthew Raymer
5ae0535935 fix: Restore "Get someone to onboard you" button functionality
Replace notify.confirm() with $notify() in ProjectsView to support complex modal
with custom Yes/No buttons and routing callbacks for non-registered users.
2025-08-06 05:40:33 +00:00
Matthew Raymer
c27caf8887 Fix build script to fail on TypeScript errors
Update measure_time function to properly handle command exit codes:
- Check command success/failure and return appropriate exit code
- Log failure messages with timing and exit code information
- Ensure TypeScript type checking failures stop the build process
- Maintains timing logs for both success and failure cases

This prevents deployment of code with TypeScript type errors by making
test and production builds fail fast when tsc --noEmit reports issues.
2025-08-06 03:10:17 +00:00
b17642fbcb fix: Toast messages were showing numbers. Removed unused function. Clarified tests. 2025-08-05 19:58:39 -06:00
Matthew Raymer
974d33b322 Document environment variable precedence and API configuration scheme
Add comprehensive documentation explaining the order of precedence for
environment variables in TimeSafari project. Covers shell script overrides,
platform-specific configurations, .env file usage, and API alignment between
claim and partner APIs. Includes troubleshooting guide and best practices
for maintaining consistent environment configuration across development,
test, and production environments.
2025-08-05 11:51:48 +00:00
Matthew Raymer
3b1a63468c Add iOS support for custom API IP configuration
Extend custom API IP feature to iOS platform with platform-appropriate defaults:
- Android: Defaults to 10.0.2.2 for emulator, custom IP for physical devices
- iOS: Uses localhost for simulator, custom IP for physical devices
- Added npm scripts for iOS custom IP builds (dev:custom, test:custom)
- Updated documentation to cover both platforms with examples
- Consistent --api-ip parameter across Android and iOS build scripts

Usage:
  ./scripts/build-ios.sh --dev                    # Default localhost
  ./scripts/build-ios.sh --dev --api-ip 192.168.1.100  # Custom IP
2025-08-05 10:43:51 +00:00
Matthew Raymer
1d6418b02c Add custom API IP support for Android physical device development
Implement --api-ip parameter for Android builds with smart defaults:
- Defaults to 10.0.2.2 for emulator development when no IP specified
- Supports custom IP for physical device development
- Added npm scripts for common use cases (dev:custom, test:custom)
- Updated help documentation with usage examples
- Created comprehensive documentation with troubleshooting guide

Usage:
  ./scripts/build-android.sh --dev                    # Default 10.0.2.2
  ./scripts/build-android.sh --dev --api-ip 192.168.1.100  # Custom IP
2025-08-05 10:26:38 +00:00
Matthew Raymer
b681905abd Upgrade Android API from 35 to 36
Update Android SDK configuration to target API 36 (Android 16):
- Update compileSdkVersion and targetSdkVersion from 35 to 36
- Update suppressUnsupportedCompileSdk from 34 to 36
- Maintains minSdkVersion at 22 for broad device compatibility
- Verified build system compatibility with Gradle 8.13
2025-08-05 09:42:53 +00:00
Matthew Raymer
32f589b866 Fix Android emulator API connectivity with cleaner build script approach
- Move Android-specific API server logic from common.sh to build-android.sh
- Remove unnecessary ANDROID_BUILD environment variable
- Set localhost:3000 as default in common.sh for all Capacitor builds
- Override to 10.0.2.2:3000 specifically in build-android.sh for Android development
- Fix execution order issue where common.sh ran before ANDROID_BUILD was set
- Maintain proper separation: Android emulator uses 10.0.2.2, iOS simulator uses localhost
2025-08-05 09:08:50 +00:00
938cf673fc fix: A 'back' from a DID view page goes back to the list (not to edit again) 2025-08-04 19:21:42 -06:00
984244117b chore: Remove duplicate checks for bad contact methods. 2025-08-04 09:02:24 -06:00
Matthew Raymer
0bd0e7c332 Fix contact methods JSON string/array duality in PlatformServiceMixin
- Add ContactMaybeWithJsonStrings type usage for internal database operations
- Implement $normalizeContacts() method to handle both JSON string and array formats
- Update $contacts(), $getContact(), and $getAllContacts() to use normalization
- Fix $updateContact() to properly convert contactMethods arrays to JSON strings
- Add validation to filter out malformed contact method objects
- Update ContactEditView to handle malformed data gracefully

Resolves issue where contactMethods could be stored as JSON strings in database
but expected as arrays in components, causing "Cannot create property 'label' on number '0'" errors.
2025-08-04 04:41:54 +00:00
Matthew Raymer
aed16ebe94 Remove PROD_SHARE_DOMAIN constant and unify domain configuration
- Remove hard-coded PROD_SHARE_DOMAIN from src/constants/app.ts
- Update all sharing functionality to use environment-specific APP_SERVER
- Rewrite domain configuration documentation to reflect unified approach
- Simplify domain management with single APP_SERVER constant
- Update README.md examples to use APP_SERVER for all URL generation

This change eliminates the complexity of separate constants for different
URL types and provides consistent environment-specific configuration for
all functionality including sharing.
2025-08-04 03:52:21 +00:00
Jose Olarte III
06f3a4c7c2 Refactor: simplify GiftedDialog with explicit entity type props
Replace complex updateEntityTypes() method with explicit giverEntityType and
recipientEntityType props. This makes the component more declarative and
maintainable by removing hidden logic and making entity type relationships
clear at the call site.

- Remove updateEntityTypes() method and related watchers
- Add explicit giverEntityType and recipientEntityType props with defaults
- Update all views to use inline logic for entity type determination
- Fix entity type preservation in navigation flows
- Enhance query parameter passing for better context preservation
- Fix recipient reset issue in ContactGiftingView
- Resolve entity type mismatch in HomeView project button flow

Files changed:
- GiftedDialog.vue: Remove complex logic, add explicit props
- EntitySelectionStep.vue: Enhanced query parameter handling
- ContactGiftingView.vue: Improved context preservation
- HomeView.vue, ProjectViewView.vue, ClaimView.vue, ContactsView.vue:
  Updated to use explicit entity type props
- Add refactoring documentation
2025-08-03 11:49:46 +08:00
Matthew Raymer
371cf763c8 Merge branch 'build-improvement' of ssh://173.199.124.46:222/trent_larson/crowd-funder-for-time-pwa into build-improvement 2025-08-02 08:19:52 +00:00
Matthew Raymer
3d38cb89a9 Fix HomeView registration status by using $accountSettings() instead of $settings()
- Change HomeView to use $accountSettings() method which returns correct isRegistered value
- Remove isRegistered: false default that was overriding database values
- Fix settings override issue where empty defaults were overriding activeDid
- Remove excessive settings tracing logs to clean up console output
- Ensure consistent registration status between HomeView and AccountViewView

The HomeView was incorrectly showing users as unregistered while AccountViewView showed them as registered due to using $settings() (returns null) instead of $accountSettings() (returns correct database value).
2025-08-02 08:19:41 +00:00
fb2ac963bd fix: Adjust VC types. 2025-08-02 01:00:01 -06:00
e5e01040b2 chore: Restore the dev setting for a variable. 2025-08-02 00:59:35 -06:00
197dea48c9 chore: remove comments that moved into ClickUp issues 2025-08-02 00:58:53 -06:00
Jose Olarte III
54bfaafbd0 Fix entity type matching in ClaimView
- Add recipientEntityTypeOverride prop to GiftedDialog component
- Add data-testid and data-recipient-entity-type-override attributes for testing
- Update updateEntityTypes() to respect recipientEntityTypeOverride when set
- Add watcher for recipientEntityTypeOverride prop changes
- Update ClaimView to pass recipient entity type override based on project context
- Improve recipient determination logic in ClaimView for person vs project recipients
2025-08-01 16:13:20 +08:00
Matthew Raymer
a63ccae9b1 Merge branch 'build-improvement' of ssh://173.199.124.46:222/trent_larson/crowd-funder-for-time-pwa into build-improvement 2025-08-01 05:47:53 +00:00
Matthew Raymer
c30b94dcc7 Integrate TypeScript type checking into build process with conditional execution
- Add type checking to build scripts for production/test builds only
- Fix TypeScript errors in migration service, router, and platform services
- Add electronAPI type declarations for Electron platform
- Remove type checking from development builds for faster hot reload
- Update tsconfig.node.json to resolve configuration conflicts
- Ensure type safety for production while maintaining fast development workflow
2025-08-01 05:47:43 +00:00
Jose Olarte III
e741790d70 Fix ClaimView affirm delivery action
- Add offer context support to gifting flow
- Add offerId prop to EntitySelectionStep for offer fulfillment context
- Pass offerId through GiftedDialog to EntitySelectionStep
- Update ContactGiftingView to handle offerId from route query parameters
- Extract offer details (description, amount, unitCode) for pre-population
2025-08-01 13:44:28 +08:00
Jose Olarte III
404a7cbc71 Add form field preservation in gifting flow
- Preserve description, amount, and unit code when navigating between gifting steps
- Add form field props to EntitySelectionStep and GiftedDialog components
- Update ContactGiftingView to handle form state persistence in URL parameters
2025-08-01 11:36:17 +08:00
Matthew Raymer
8b2c6714ec Merge branch 'build-improvement' of ssh://173.199.124.46:222/trent_larson/crowd-funder-for-time-pwa into build-improvement 2025-07-31 12:36:30 +00:00
Matthew Raymer
9cd4551bed docs: add comprehensive GiftedDialog architecture overview
Add detailed analysis of GiftedDialog component architecture including:
- DRY and SOLID principles compliance assessment
- Cognitive load analysis with before/after comparisons
- Architectural complexity remediation plan
- Phased implementation strategy (cognitive load → composable architecture)
- Component hierarchy and dependency analysis
- Performance and security considerations

The document provides actionable recommendations for reducing complexity
while maintaining functionality, with specific code examples and metrics.
2025-07-31 12:36:04 +00:00
Jose Olarte III
f4a7d437c8 Fix parameter passing in contact gift dialogs
- Replace @Emit decorator with direct $emit calls in ContactListItem
- Fix DID comparison from loose to strict equality in nameForDid function
- Resolve issue where giver/recipient names showed as "this unnamed user"

The @Emit decorator was not properly spreading array parameters, causing
the parent component to receive arrays instead of separate string parameters.
2025-07-31 20:13:55 +08:00
Jose Olarte III
433f3c1154 Fix GiftedDialog functionality
- Add description, amountInput, and unitCode parameters to GiftedDialog.open()
- Pass offer details to GiftedDialog in ProjectViewView
- Update ContactsView to handle new GiftedDialog parameters
2025-07-31 17:28:46 +08:00
Matthew Raymer
2a32903326 chore: more cursor rules updates 2025-07-31 09:22:32 +00:00
Jose Olarte III
0582954cfa Remove 'customTitle' variable 2025-07-31 15:53:37 +08:00
Jose Olarte III
6d28a7d8a3 Function name change 2025-07-31 15:29:46 +08:00
Jose Olarte III
12b43bf684 Ref name change 2025-07-31 14:53:32 +08:00
Matthew Raymer
1180ebd4ca chore: update cursor rulesets 2025-07-31 06:31:14 +00:00
cbdd54e383 undo port change because it may cause a timeout starting the tests (intermittently) 2025-07-30 20:44:22 -06:00
219a383015 change test port to 8081 (to not conflict with running server) and remove unused "logger" setting
I believe logger is not an option. https://playwright.dev/docs/api/class-testoptions
2025-07-30 20:36:29 -06:00
31711e2ea6 fix: usage of testRecursivelyOnStrings method to match new definition 2025-07-30 20:03:36 -06:00
63 changed files with 4860 additions and 1046 deletions

View File

@@ -8,28 +8,3 @@ alwaysApply: true
✅ remove whitespace at the end of lines
✅ use npm run lint-fix to check for warnings
✅ do not use npm run dev let me handle running and supplying feedback
✅ do not add or commit for the user; let him control that process
always preview changes and commit message to use and allow me to copy and paste
✅ Preferred Commit Message Format
Short summary in the first line (concise and high-level).
Avoid long commit bodies unless truly necessary.
✅ Valued Content in Commit Messages
Specific fixes or features.
Symptoms or problems that were fixed.
Notes about tests passing or TS/linting errors being resolved (briefly).
❌ Avoid in Commit Messages
Vague terms: “improved”, “enhanced”, “better” — especially from AI.
Minor changes: small doc tweaks, one-liners, cleanup, or lint fixes.
Redundant blurbs: repeated across files or too generic.
Multiple overlapping purposes in a single commit — prefer narrow, focused commits.
Long explanations of what can be deduced from good in-line code comments.
Guiding Principle
Let code and inline documentation speak for themselves. Use commits to highlight what isn't obvious from reading the code.

View File

@@ -0,0 +1,14 @@
---
alwaysApply: true
---
# Directive for Documentation Generation
1. Produce a **small, focused set of documents** rather than an overwhelming volume.
2. Ensure the content is **maintainable and worth preserving**, so that humans
are motivated to keep it up to date.
3. Prioritize **educational value**: the documents must clearly explain the
workings of the system.
4. Avoid **shallow, generic, or filler explanations** often found in
AI-generated documentation.
5. Aim for **clarity, depth, and usefulness**, so readers gain genuine understanding.
6. Always check the local system date to determine current date.

View File

@@ -312,3 +312,21 @@ Description of current situation or problem.
**Last Updated**: 2025-07-09
**Version**: 1.0
**Maintainer**: Matthew Raymer
### Heading Uniqueness
- **Rule**: No duplicate heading content at the same level
- **Scope**: Within a single document
- **Rationale**: Maintains clear document structure and navigation
- **Example**:
```markdown
## Features ✅
### Authentication
### Authorization
## Features ❌ (Duplicate heading)
### Security
### Performance
```

View File

@@ -1,70 +1,96 @@
---
description:
globs:
alwaysApply: true
---
---
description:
globs:
description:
globs:
alwaysApply: true
---
# Time Safari Context
## Project Overview
Time Safari is an application designed to foster community building through gifts, gratitude, and collaborative projects. The app should make it extremely easy and intuitive for users of any age and capability to recognize contributions, build trust networks, and organize collective action. It is built on services that preserve privacy and data sovereignty.
Time Safari is an application designed to foster community building through gifts,
gratitude, and collaborative projects. The app should make it extremely easy and
intuitive for users of any age and capability to recognize contributions, build
trust networks, and organize collective action. It is built on services that
preserve privacy and data sovereignty.
The ultimate goals of Time Safari are two-fold:
1. **Connect** Make it easy, rewarding, and non-threatening for people to connect with others who have similar interests, and to initiate activities together. This helps people accomplish and learn from other individuals in less-structured environments; moreover, it helps them discover who they want to continue to support and with whom they want to maintain relationships.
1. **Connect** Make it easy, rewarding, and non-threatening for people to
connect with others who have similar interests, and to initiate activities
together. This helps people accomplish and learn from other individuals in
less-structured environments; moreover, it helps them discover who they want
to continue to support and with whom they want to maintain relationships.
2. **Reveal** Widely advertise the great support and rewards that are being given and accepted freely, especially non-monetary ones. Using visuals and text, display the kind of impact that gifts are making in the lives of others. Also show useful and engaging reports of project statistics and personal accomplishments.
2. **Reveal** Widely advertise the great support and rewards that are being
given and accepted freely, especially non-monetary ones. Using visuals and text,
display the kind of impact that gifts are making in the lives of others. Also
show useful and engaging reports of project statistics and personal accomplishments.
## Core Approaches
Time Safari should help everyday users build meaningful connections and organize collective efforts by:
Time Safari should help everyday users build meaningful connections and organize
collective efforts by:
1. **Recognizing Contributions**: Creating permanent, verifiable records of gifts and contributions people give to each other and their communities.
1. **Recognizing Contributions**: Creating permanent, verifiable records of gifts
and contributions people give to each other and their communities.
2. **Facilitating Collaboration**: Making it ridiculously easy for people to ask for or propose help on projects and interests that matter to them.
2. **Facilitating Collaboration**: Making it ridiculously easy for people to ask
for or propose help on projects and interests that matter to them.
3. **Building Trust Networks**: Enabling users to maintain their network and activity visibility. Developing reputation through verified contributions and references, which can be selectively shown to others outside the network.
3. **Building Trust Networks**: Enabling users to maintain their network and activity
visibility. Developing reputation through verified contributions and references,
which can be selectively shown to others outside the network.
4. **Preserving Privacy**: Ensuring personal identifiers are only shared with explicitly authorized contacts, allowing private individuals including children to participate safely.
4. **Preserving Privacy**: Ensuring personal identifiers are only shared with
explicitly authorized contacts, allowing private individuals including children
to participate safely.
5. **Engaging Content**: Displaying people's records in compelling stories, and highlighting those projects that are lifting people's lives long-term, both in physical support and in emotional-spiritual-creative thriving.
5. **Engaging Content**: Displaying people's records in compelling stories, and
highlighting those projects that are lifting people's lives long-term, both in
physical support and in emotional-spiritual-creative thriving.
## Technical Foundation
This application is built on a privacy-preserving claims architecture (via endorser.ch) with these key characteristics:
This application is built on a privacy-preserving claims architecture (via
endorser.ch) with these key characteristics:
- **Decentralized Identifiers (DIDs)**: User identities are based on public/private key pairs stored on their devices
- **Cryptographic Verification**: All claims and confirmations are cryptographically signed
- **User-Controlled Visibility**: Users explicitly control who can see their identifiers and data
- **Merkle-Chained Claims**: Claims are cryptographically chained for verification and integrity
- **Native and Web App**: Works on Capacitor (iOS, Android), Desktop (Electron and CEFPython), and web browsers
- **Decentralized Identifiers (DIDs)**: User identities are based on public/private
key pairs stored on their devices
- **Cryptographic Verification**: All claims and confirmations are
cryptographically signed
- **User-Controlled Visibility**: Users explicitly control who can see their
identifiers and data
- **Merkle-Chained Claims**: Claims are cryptographically chained for verification
and integrity
- **Native and Web App**: Works on Capacitor (iOS, Android), Desktop (Electron
and CEFPython), and web browsers
## User Journey
The typical progression of usage follows these stages:
1. **Gratitude & Recognition**: Users begin by expressing and recording gratitude for gifts received, building a foundation of acknowledgment.
1. **Gratitude & Recognition**: Users begin by expressing and recording gratitude
for gifts received, building a foundation of acknowledgment.
2. **Project Proposals**: Users propose projects and ideas, reaching out to connect with others who share similar interests.
2. **Project Proposals**: Users propose projects and ideas, reaching out to connect
with others who share similar interests.
3. **Action Triggers**: Offers of help serve as triggers and motivations to execute proposed projects, moving from ideas to action.
3. **Action Triggers**: Offers of help serve as triggers and motivations to execute
proposed projects, moving from ideas to action.
## Context for LLM Development
When developing new functionality for Time Safari, consider these design principles:
1. **Accessibility First**: Features should be usable by non-technical users with minimal learning curve.
1. **Accessibility First**: Features should be usable by non-technical users with
minimal learning curve.
2. **Privacy by Design**: All features must respect user privacy and data sovereignty.
3. **Progressive Enhancement**: Core functionality should work across all devices, with richer experiences where supported.
3. **Progressive Enhancement**: Core functionality should work across all devices,
with richer experiences where supported.
4. **Voluntary Collaboration**: The system should enable but never coerce participation.
@@ -72,31 +98,40 @@ When developing new functionality for Time Safari, consider these design princip
6. **Network Effects**: Consider how features scale as more users join the platform.
7. **Low Resource Requirements**: The system should be lightweight enough to run on inexpensive devices users already own.
7. **Low Resource Requirements**: The system should be lightweight enough to run
on inexpensive devices users already own.
## Use Cases to Support
LLM development should focus on enhancing these key use cases:
1. **Community Building**: Tools that help people find others with shared interests and values.
1. **Community Building**: Tools that help people find others with shared
interests and values.
2. **Project Coordination**: Features that make it easy to propose collaborative projects and to submit suggestions and offers to existing ones.
2. **Project Coordination**: Features that make it easy to propose collaborative
projects and to submit suggestions and offers to existing ones.
3. **Reputation Building**: Methods for users to showcase their contributions and reliability, in contexts where they explicitly reveal that information.
3. **Reputation Building**: Methods for users to showcase their contributions
and reliability, in contexts where they explicitly reveal that information.
4. **Governance Experimentation**: Features that facilitate decision-making and collective governance.
4. **Governance Experimentation**: Features that facilitate decision-making and
collective governance.
## Constraints
When developing new features, be mindful of these constraints:
1. **Privacy Preservation**: User identifiers must remain private except when explicitly shared.
1. **Privacy Preservation**: User identifiers must remain private except when
explicitly shared.
2. **Platform Limitations**: Features must work within the constraints of the target app platforms, while aiming to leverage the best platform technology available.
2. **Platform Limitations**: Features must work within the constraints of the target
app platforms, while aiming to leverage the best platform technology available.
3. **Endorser API Limitations**: Backend features are constrained by the endorser.ch API capabilities.
3. **Endorser API Limitations**: Backend features are constrained by the endorser.ch
API capabilities.
4. **Performance on Low-End Devices**: The application should remain performant on older/simpler devices.
4. **Performance on Low-End Devices**: The application should remain performant
on older/simpler devices.
5. **Offline-First When Possible**: Key functionality should work offline when feasible.
@@ -116,12 +151,14 @@ When developing new features, be mindful of these constraints:
## Project Architecture
- The application must work on web browser, PWA (Progressive Web Application), desktop via Electron, and mobile via Capacitor
- The application must work on web browser, PWA (Progressive Web Application),
desktop via Electron, and mobile via Capacitor
- Building for each platform is managed via Vite
## Core Development Principles
### DRY development
- **Code Reuse**
- Extract common functionality into utility functions
- Create reusable components for UI patterns
@@ -177,14 +214,24 @@ When developing new features, be mindful of these constraints:
- Use shared test configurations
- Create reusable test helpers
- Implement consistent test patterns
- F.I.R.S.T. (for Unit Tests)
F Fast
I Independent
R Repeatable
S Self-validating
T Timely
### SOLID Principles
- **Single Responsibility**: Each class/component should have only one reason to change
- **Single Responsibility**: Each class/component should have only one reason to
change
- Components should focus on one specific feature (e.g., QR scanning, DID management)
- Services should handle one type of functionality (e.g., platform services, crypto services)
- Services should handle one type of functionality (e.g., platform services,
crypto services)
- Utilities should provide focused helper functions
- **Open/Closed**: Software entities should be open for extension but closed for modification
- **Open/Closed**: Software entities should be open for extension but closed for
modification
- Use interfaces for service definitions
- Implement plugin architecture for platform-specific features
- Allow component behavior extension through props and events
@@ -205,6 +252,7 @@ When developing new features, be mindful of these constraints:
- Implement factory patterns for component creation
### Law of Demeter
- Components should only communicate with immediate dependencies
- Avoid chaining method calls (e.g., `this.service.getUser().getProfile().getName()`)
- Use mediator patterns for complex component interactions
@@ -212,6 +260,7 @@ When developing new features, be mindful of these constraints:
- Keep component communication through defined events and props
### Composition over Inheritance
- Prefer building components through composition
- Use mixins for shared functionality
- Implement feature toggles through props
@@ -219,6 +268,7 @@ When developing new features, be mindful of these constraints:
- Use service composition for complex features
### Interface Segregation
- Define clear interfaces for services
- Keep component APIs minimal and focused
- Split large interfaces into smaller, specific ones
@@ -226,6 +276,7 @@ When developing new features, be mindful of these constraints:
- Implement role-based interfaces for different use cases
### Fail Fast
- Validate inputs early in the process
- Use TypeScript strict mode
- Implement comprehensive error handling
@@ -233,6 +284,7 @@ When developing new features, be mindful of these constraints:
- Use assertions for development-time validation
### Principle of Least Astonishment
- Follow Vue.js conventions consistently
- Use familiar naming patterns
- Implement predictable component behaviors
@@ -240,6 +292,7 @@ When developing new features, be mindful of these constraints:
- Keep UI interactions intuitive
### Information Hiding
- Encapsulate implementation details
- Use private class members
- Implement proper access modifiers
@@ -247,6 +300,7 @@ When developing new features, be mindful of these constraints:
- Use TypeScript's access modifiers effectively
### Single Source of Truth
- Use Pinia for state management
- Maintain one source for user data
- Centralize configuration management
@@ -254,23 +308,9 @@ When developing new features, be mindful of these constraints:
- Implement proper state synchronization
### Principle of Least Privilege
- Implement proper access control
- Use minimal required permissions
- Follow privacy-by-design principles
- Restrict component access to necessary data
- Implement proper authentication/authorization
### Continuous Integration/Continuous Deployment (CI/CD)
- Automated testing on every commit
- Consistent build process across platforms
- Automated deployment pipelines
- Quality gates for code merging
- Environment-specific configurations
This expanded documentation provides:
1. Clear principles for development
2. Practical implementation guidelines
3. Real-world examples
4. TypeScript integration
5. Best practices for Time Safari

View File

@@ -0,0 +1,34 @@
---
alwaysApply: true
---
# Rules for peaceful co-existence with developers
do not add or commit for the user; let him control that process
the content of commit messages should be from the files awaiting staging
and those which have been staged. use the differences in those files
to inform the content of the commit message
always preview changes and commit message to use and allow me to copy and paste
✅ Preferred Commit Message Format
Short summary in the first line (concise and high-level).
Avoid long commit bodies unless truly necessary.
✅ Valued Content in Commit Messages
Specific fixes or features.
Symptoms or problems that were fixed.
Notes about tests passing or TS/linting errors being resolved (briefly).
❌ Avoid in Commit Messages
Vague terms: “improved”, “enhanced”, “better” — especially from AI.
Minor changes: small doc tweaks, one-liners, cleanup, or lint fixes.
Redundant blurbs: repeated across files or too generic.
Multiple overlapping purposes in a single commit — prefer narrow, focused commits.
Long explanations of what can be deduced from good in-line code comments.
Guiding Principle
Let code and inline documentation speak for themselves. Use commits to highlight what isn't obvious from reading the code.

File diff suppressed because it is too large Load Diff

View File

@@ -113,10 +113,11 @@ appearing in shared links during development.
-**Type-Safe Configuration**: Full TypeScript support
### Quick Reference
```typescript
// For sharing functionality (always production)
import { PROD_SHARE_DOMAIN } from "@/constants/app";
const shareLink = `${PROD_SHARE_DOMAIN}/deep-link/claim/123`;
// For sharing functionality (environment-specific)
import { APP_SERVER } from "@/constants/app";
const shareLink = `${APP_SERVER}/deep-link/claim/123`;
// For internal operations (environment-specific)
import { APP_SERVER } from "@/constants/app";
@@ -124,6 +125,7 @@ const apiUrl = `${APP_SERVER}/api/claim/123`;
```
### Documentation
- [Domain Configuration System](docs/domain-configuration.md) - Complete guide
- [Constants and Configuration](src/constants/app.ts) - Core constants

View File

@@ -64,6 +64,14 @@ android {
}
}
}
packagingOptions {
jniLibs {
pickFirsts += ['**/lib/x86_64/libbarhopper_v3.so', '**/lib/x86_64/libimage_processing_util_jni.so', '**/lib/x86_64/libsqlcipher.so']
}
}
// Configure for 16 KB page size compatibility
// Enable bundle builds (without which it doesn't work right for bundleDebug vs bundleRelease)
bundle {

View File

@@ -57,13 +57,14 @@
]
},
"android": {
"allowMixedContent": false,
"allowMixedContent": true,
"captureInput": true,
"webContentsDebuggingEnabled": false,
"allowNavigation": [
"*.timesafari.app",
"*.jsdelivr.net",
"api.endorser.ch"
"api.endorser.ch",
"10.0.2.2:3000"
]
},
"electron": {

View File

@@ -7,7 +7,7 @@ buildscript {
mavenCentral()
}
dependencies {
classpath 'com.android.tools.build:gradle:8.11.1'
classpath 'com.android.tools.build:gradle:8.12.0'
classpath 'com.google.gms:google-services:4.4.0'
// NOTE: Do not place your application dependencies here; they belong

View File

@@ -20,4 +20,4 @@ org.gradle.jvmargs=-Xmx1536m
# Android operating system, and which are packaged with your app's APK
# https://developer.android.com/topic/libraries/support-library/androidx-rn
android.useAndroidX=true
android.suppressUnsupportedCompileSdk=34
android.suppressUnsupportedCompileSdk=36

View File

@@ -1,7 +1,7 @@
ext {
minSdkVersion = 22
compileSdkVersion = 34
targetSdkVersion = 34
compileSdkVersion = 36
targetSdkVersion = 36
androidxActivityVersion = '1.8.0'
androidxAppCompatVersion = '1.6.1'
androidxCoordinatorLayoutVersion = '1.2.0'

View File

@@ -57,13 +57,14 @@
]
},
"android": {
"allowMixedContent": false,
"allowMixedContent": true,
"captureInput": true,
"webContentsDebuggingEnabled": false,
"allowNavigation": [
"*.timesafari.app",
"*.jsdelivr.net",
"api.endorser.ch"
"api.endorser.ch",
"10.0.2.2:3000"
]
},
"electron": {

View File

@@ -0,0 +1,338 @@
# Environment Variable Precedence and API Configuration
**Date:** August 4, 2025
**Author:** Matthew Raymer
## Overview
This document explains the order of precedence for environment variables in the
TimeSafari project, how `.env` files are used, and the API configuration scheme
for different environments.
## Order of Precedence (Highest to Lowest)
### 1. Shell Script Overrides (Highest Priority)
Shell scripts can override environment variables for platform-specific needs:
```bash
# scripts/common.sh - setup_build_env()
if [ "$BUILD_MODE" = "development" ]; then
export VITE_DEFAULT_ENDORSER_API_SERVER="http://localhost:3000"
export VITE_DEFAULT_PARTNER_API_SERVER="http://localhost:3000"
fi
```
### 2. Platform-Specific Overrides (High Priority)
Platform-specific build scripts can override for mobile development:
```bash
# scripts/build-android.sh
if [ "$BUILD_MODE" = "development" ]; then
export VITE_DEFAULT_ENDORSER_API_SERVER="http://10.0.2.2:3000"
export VITE_DEFAULT_PARTNER_API_SERVER="http://10.0.2.2:3000"
fi
```
### 3. Environment-Specific .env Files (Medium Priority)
Environment-specific `.env` files provide environment-specific defaults:
```bash
# .env.development, .env.test, .env.production
VITE_DEFAULT_ENDORSER_API_SERVER=http://localhost:3000
VITE_DEFAULT_PARTNER_API_SERVER=http://localhost:3000
```
### 4. Fallback .env File (Low Priority)
General `.env` file provides project-wide defaults:
```bash
# .env (if exists)
VITE_DEFAULT_ENDORSER_API_SERVER=http://localhost:3000
```
### 5. app.ts Constants (Lowest Priority - Fallback)
Hardcoded constants in `src/constants/app.ts` provide safety nets:
```typescript
export const DEFAULT_ENDORSER_API_SERVER =
import.meta.env.VITE_DEFAULT_ENDORSER_API_SERVER ||
AppString.PROD_ENDORSER_API_SERVER;
```
## Build Process Flow
### 1. Shell Scripts Set Base Values
```bash
# scripts/common.sh
setup_build_env() {
if [ "$BUILD_MODE" = "development" ]; then
export VITE_DEFAULT_ENDORSER_API_SERVER="http://localhost:3000"
export VITE_DEFAULT_PARTNER_API_SERVER="http://localhost:3000"
fi
}
```
### 2. Platform-Specific Overrides
```bash
# scripts/build-android.sh
if [ "$BUILD_MODE" = "development" ]; then
export VITE_DEFAULT_ENDORSER_API_SERVER="http://10.0.2.2:3000"
export VITE_DEFAULT_PARTNER_API_SERVER="http://10.0.2.2:3000"
fi
```
### 3. Load .env Files
```bash
# scripts/build-web.sh
local env_file=".env.$BUILD_MODE" # .env.development, .env.test, .env.production
if [ -f "$env_file" ]; then
load_env_file "$env_file"
fi
# Fallback to .env
if [ -f ".env" ]; then
load_env_file ".env"
fi
```
### 4. Vite Processes Environment
```typescript
// vite.config.common.mts
dotenv.config(); // Loads .env files
```
### 5. Application Uses Values
```typescript
// src/constants/app.ts
export const DEFAULT_ENDORSER_API_SERVER =
import.meta.env.VITE_DEFAULT_ENDORSER_API_SERVER ||
AppString.PROD_ENDORSER_API_SERVER;
```
## API Configuration Scheme
### Environment Configuration Summary
| Environment | Endorser API (Claims) | Partner API | Image API |
|-------------|----------------------|-------------|-----------|
| **Development** | `http://localhost:3000` | `http://localhost:3000` | `https://image-api.timesafari.app` |
| **Test** | `https://test-api.endorser.ch` | `https://test-partner-api.endorser.ch` | `https://image-api.timesafari.app` |
| **Production** | `https://api.endorser.ch` | `https://partner-api.endorser.ch` | `https://image-api.timesafari.app` |
### Mobile Development Overrides
#### Android Development
- **Emulator**: `http://10.0.2.2:3000` (Android emulator default)
- **Physical Device**: `http://{CUSTOM_IP}:3000` (Custom IP for physical device)
#### iOS Development
- **Simulator**: `http://localhost:3000` (iOS simulator default)
- **Physical Device**: `http://{CUSTOM_IP}:3000` (Custom IP for physical device)
## .env File Structure
### .env.development
```bash
# ==========================================
# DEVELOPMENT ENVIRONMENT CONFIGURATION
# ==========================================
# API Server Configuration:
# - Endorser API (Claims): Local development server
# - Partner API: Local development server (aligned with claims)
# - Image API: Test server (shared for development)
# ==========================================
# Only the variables that start with VITE_ are seen in the application import.meta.env in Vue.
# iOS doesn't like spaces in the app title.
TIME_SAFARI_APP_TITLE="TimeSafari_Dev"
VITE_APP_SERVER=http://localhost:8080
# This is the claim ID for actions in the BVC project, with the JWT ID on this environment (not production).
VITE_BVC_MEETUPS_PROJECT_CLAIM_ID=https://endorser.ch/entity/01HWE8FWHQ1YGP7GFZYYPS272F
# API Servers (Development - Local)
VITE_DEFAULT_ENDORSER_API_SERVER=http://localhost:3000
VITE_DEFAULT_PARTNER_API_SERVER=http://localhost:3000
# Image API (Test server for development)
VITE_DEFAULT_IMAGE_API_SERVER=https://test-image-api.timesafari.app
# Push Server (disabled for localhost)
#VITE_DEFAULT_PUSH_SERVER... can't be set up with localhost domain
# Feature Flags
VITE_PASSKEYS_ENABLED=true
```
### .env.test
```bash
# ==========================================
# TEST ENVIRONMENT CONFIGURATION
# ==========================================
# API Server Configuration:
# - Endorser API (Claims): Test server
# - Partner API: Test server (aligned with claims)
# - Image API: Test server
# ==========================================
# Only the variables that start with VITE_ are seen in the application import.meta.env in Vue.
# iOS doesn't like spaces in the app title.
TIME_SAFARI_APP_TITLE="TimeSafari_Test"
VITE_APP_SERVER=https://test.timesafari.app
# This is the claim ID for actions in the BVC project, with the JWT ID on this environment (not production).
VITE_BVC_MEETUPS_PROJECT_CLAIM_ID=https://endorser.ch/entity/01HWE8FWHQ1YGP7GFZYYPS272F
# API Servers (Test Environment)
VITE_DEFAULT_ENDORSER_API_SERVER=https://test-api.endorser.ch
VITE_DEFAULT_PARTNER_API_SERVER=https://test-partner-api.endorser.ch
# Image API (Test server)
VITE_DEFAULT_IMAGE_API_SERVER=https://test-image-api.timesafari.app
# Push Server (Test)
VITE_DEFAULT_PUSH_SERVER=https://test.timesafari.app
# Feature Flags
VITE_PASSKEYS_ENABLED=true
```
### .env.production
```bash
# ==========================================
# PRODUCTION ENVIRONMENT CONFIGURATION
# ==========================================
# API Server Configuration:
# - Endorser API (Claims): Production server
# - Partner API: Production server (aligned with claims)
# - Image API: Production server
# ==========================================
# Only the variables that start with VITE_ are seen in the application import.meta.env in Vue.
# App Server
VITE_APP_SERVER=https://timesafari.app
# This is the claim ID for actions in the BVC project.
VITE_BVC_MEETUPS_PROJECT_CLAIM_ID=https://endorser.ch/entity/01GXYPFF7FA03NXKPYY142PY4H
# API Servers (Production Environment)
VITE_DEFAULT_ENDORSER_API_SERVER=https://api.endorser.ch
VITE_DEFAULT_PARTNER_API_SERVER=https://partner-api.endorser.ch
# Image API (Production server)
VITE_DEFAULT_IMAGE_API_SERVER=https://image-api.timesafari.app
# Push Server (Production)
VITE_DEFAULT_PUSH_SERVER=https://timesafari.app
```
## Key Principles
### 1. API Alignment
- **Partner API** values follow the same pattern as **Claim API** (Endorser API)
- Both APIs use the same environment-specific endpoints
- This ensures consistency across the application
### 2. Platform Flexibility
- Shell scripts can override for platform-specific needs
- Android emulator uses `10.0.2.2:3000`
- iOS simulator uses `localhost:3000`
- Physical devices use custom IP addresses
### 3. Environment Isolation
- Each environment has its own `.env` file
- Test environment uses test APIs
- Development environment uses local APIs
- Production environment uses production APIs
### 4. Safety Nets
- Hardcoded constants in `app.ts` provide fallbacks
- Multiple layers of configuration prevent failures
- Clear precedence order ensures predictable behavior
## Usage Examples
### Development Build
```bash
# Uses .env.development + shell script overrides
npm run build:web -- --mode development
```
### Test Build
```bash
# Uses .env.test + shell script overrides
npm run build:web -- --mode test
```
### Production Build
```bash
# Uses .env.production + shell script overrides
npm run build:web -- --mode production
```
### Android Development
```bash
# Uses .env.development + Android-specific overrides
./scripts/build-android.sh --dev
```
### iOS Development
```bash
# Uses .env.development + iOS-specific overrides
./scripts/build-ios.sh --dev
```
## Troubleshooting
### Environment Variable Debugging
```bash
# Show current environment variables
./scripts/build-web.sh --env
# Check specific variable
echo $VITE_DEFAULT_ENDORSER_API_SERVER
```
### Common Issues
1. **Wrong API Server**: Check if shell script overrides are correct
2. **Missing .env File**: Ensure environment-specific .env file exists
3. **Platform-Specific Issues**: Verify platform overrides in build scripts
4. **Vite Not Loading**: Check if `dotenv.config()` is called
### Validation
```bash
# Validate environment configuration
npm run test-env
```
## Best Practices
1. **Always use environment-specific .env files** for different environments
2. **Keep shell script overrides minimal** and platform-specific
3. **Document API alignment** in .env file headers
4. **Use hardcoded fallbacks** in `app.ts` for safety
5. **Test all environments** before deployment
6. **Validate configuration** with test scripts
## Related Documentation
- [Build System Overview](../build-system/README.md)
- [Android Custom API IP](../platforms/android-custom-api-ip.md)
- [API Configuration](../api-configuration.md)
- [Environment Setup](../environment-setup.md)

View File

@@ -0,0 +1,322 @@
# Mobile Custom API IP Configuration
**Author**: Matthew Raymer
**Date**: 2025-01-27
**Status**: ✅ **COMPLETE** - Custom API IP support for physical device development
## Overview
When deploying TimeSafari to physical Android devices during development, you may need to specify a custom IP address for the claim API server. This is necessary because physical devices cannot access `localhost` or `10.0.2.2` (Android emulator IP) to reach your local development server.
## Problem
During mobile development:
- **Android Emulator**: Uses `10.0.2.2:3000` to access host machine's localhost (Android emulator default)
- **iOS Simulator**: Uses `localhost:3000` to access host machine's localhost (iOS simulator default)
- **Physical Devices**: Cannot access `localhost` or `10.0.2.2` - needs actual IP address for network access
## Solution
The mobile build system uses platform-appropriate defaults and supports specifying a custom IP address for the claim API server when building for physical devices:
- **Android**: Defaults to `10.0.2.2:3000` for emulator development
- **iOS**: Uses Capacitor default (`localhost:3000`) for simulator development
## Usage
### Command Line Usage
```bash
# Android - Default behavior (uses 10.0.2.2 for emulator)
./scripts/build-android.sh --dev
# Android - Custom IP for physical device
./scripts/build-android.sh --dev --api-ip 192.168.1.100
# iOS - Default behavior (uses localhost for simulator)
./scripts/build-ios.sh --dev
# iOS - Custom IP for physical device
./scripts/build-ios.sh --dev --api-ip 192.168.1.100
# Test environment with custom IP
./scripts/build-android.sh --test --api-ip 192.168.1.100
./scripts/build-ios.sh --test --api-ip 192.168.1.100
# Build and auto-run with custom IP
./scripts/build-android.sh --dev --api-ip 192.168.1.100 --auto-run
./scripts/build-ios.sh --dev --api-ip 192.168.1.100 --auto-run
```
### NPM Scripts
```bash
# Android - Default development build (uses 10.0.2.2 for emulator)
npm run build:android:dev
# Android - Development build with custom IP (requires IP parameter)
npm run build:android:dev:custom 192.168.1.100
# iOS - Default development build (uses localhost for simulator)
npm run build:ios:dev
# iOS - Development build with custom IP (requires IP parameter)
npm run build:ios:dev:custom 192.168.1.100
# Test builds with custom IP (requires IP parameter)
npm run build:android:test:custom 192.168.1.100
npm run build:ios:test:custom 192.168.1.100
# Development build + auto-run with custom IP
npm run build:android:dev:run:custom 192.168.1.100
npm run build:ios:dev:run:custom 192.168.1.100
# Test build + auto-run with custom IP
npm run build:android:test:run:custom 192.168.1.100
npm run build:ios:test:run:custom 192.168.1.100
```
## Examples
### Scenario 1: Development on Simulator/Emulator (Default)
```bash
# Android - Default behavior - uses 10.0.2.2 for emulator
npm run build:android:dev
# iOS - Default behavior - uses localhost for simulator
npm run build:ios:dev
# Build and immediately run on simulator/emulator
npm run build:android:dev:run
npm run build:ios:dev:run
```
### Scenario 2: Development on Physical Device
```bash
# Your development server is running on 192.168.1.50:3000
npm run build:android:dev:custom 192.168.1.50
npm run build:ios:dev:custom 192.168.1.50
# Build and immediately run on device
npm run build:android:dev:run:custom 192.168.1.50
npm run build:ios:dev:run:custom 192.168.1.50
```
### Scenario 3: Testing on Physical Device
```bash
# Your test server is running on 192.168.1.75:3000
npm run build:android:test:custom 192.168.1.75
npm run build:ios:test:custom 192.168.1.75
# Build and immediately run on device
npm run build:android:test:run:custom 192.168.1.75
npm run build:ios:test:run:custom 192.168.1.75
```
### Scenario 4: Direct Script Usage
```bash
# Default behavior (uses platform-appropriate defaults)
./scripts/build-android.sh --dev --studio
./scripts/build-ios.sh --dev --studio
# Custom IP for physical device
./scripts/build-android.sh --dev --api-ip 192.168.1.100 --studio
./scripts/build-ios.sh --dev --api-ip 192.168.1.100 --studio
```
## How It Works
### Environment Variable Override
The build system handles API server configuration as follows:
1. **Android default**: Uses Android emulator default (`http://10.0.2.2:3000`)
2. **iOS default**: Uses Capacitor default (`http://localhost:3000`)
3. **Custom IP specified**: Overrides with `http://<custom-ip>:3000` for physical device development
4. **Maintains other APIs**: Image and Partner APIs remain at production URLs
5. **Logs the configuration**: Shows which IP is being used in build logs
### Build Process
```bash
# Development mode with Android emulator default (10.0.2.2)
export VITE_DEFAULT_ENDORSER_API_SERVER="http://10.0.2.2:3000"
export VITE_DEFAULT_PARTNER_API_SERVER="http://10.0.2.2:3000"
npm run build:capacitor -- --mode development
# Development mode with iOS simulator default (localhost)
export VITE_DEFAULT_ENDORSER_API_SERVER="http://localhost:3000"
export VITE_DEFAULT_PARTNER_API_SERVER="http://localhost:3000"
npm run build:capacitor -- --mode development
# Development mode with custom IP
export VITE_DEFAULT_ENDORSER_API_SERVER="http://192.168.1.100:3000"
export VITE_DEFAULT_PARTNER_API_SERVER="http://192.168.1.100:3000"
npm run build:capacitor -- --mode development
```
### Default Behavior
- **Android (no `--api-ip`)**: Uses Android emulator default (`10.0.2.2:3000`)
- **iOS (no `--api-ip`)**: Uses Capacitor default (`localhost:3000`)
- **Custom IP specified**: Uses provided IP address for physical device development
- **Invalid IP format**: Build will fail with clear error message
- **Network unreachable**: App will show connection errors at runtime
## Finding Your IP Address
### On Linux/macOS
```bash
# Find your local IP address
ifconfig | grep "inet " | grep -v 127.0.0.1
# or
ip addr show | grep "inet " | grep -v 127.0.0.1
```
### On Windows
```bash
# Find your local IP address
ipconfig | findstr "IPv4"
```
### Common Network Patterns
- **Home WiFi**: Usually `192.168.1.x` or `192.168.0.x`
- **Office Network**: May be `10.x.x.x` or `172.16.x.x`
- **Mobile Hotspot**: Often `192.168.43.x`
## Troubleshooting
### Common Issues
#### 1. Device Cannot Connect to API
```bash
# Check if your IP is accessible
ping 192.168.1.100
# Check if port 3000 is open
telnet 192.168.1.100 3000
```
#### 2. Build Fails with Invalid IP
```bash
# Ensure IP format is correct
./scripts/build-android.sh --dev --api-ip 192.168.1.100 # ✅ Correct
./scripts/build-android.sh --dev --api-ip localhost # ❌ Wrong
```
#### 3. Firewall Blocking Connection
```bash
# Check firewall settings
sudo ufw status # Ubuntu/Debian
sudo firewall-cmd --list-all # CentOS/RHEL
```
### Debug Mode
```bash
# Enable verbose logging
./scripts/build-android.sh --dev --api-ip 192.168.1.100 --verbose
```
## Best Practices
### 1. Use Consistent IP Addresses
```bash
# Create aliases for common development scenarios
alias build-dev="npm run build:android:dev:custom 192.168.1.100"
alias build-test="npm run build:android:test:custom 192.168.1.100"
```
### 2. Document Your Setup
```bash
# Create a development setup file
echo "DEV_API_IP=192.168.1.100" > .env.development
echo "TEST_API_IP=192.168.1.100" >> .env.development
```
### 3. Network Security
- Ensure your development server is only accessible on your local network
- Use HTTPS in production environments
- Consider VPN for remote development scenarios
### 4. Team Development
```bash
# Share IP configuration with team
# Add to .env.example
DEV_API_IP=192.168.1.100
TEST_API_IP=192.168.1.100
```
## Integration with CI/CD
### Environment Variables
```yaml
# Example CI/CD configuration
variables:
DEV_API_IP: "192.168.1.100"
TEST_API_IP: "192.168.1.100"
build:
script:
- npm run build:android:dev:custom $DEV_API_IP
```
### Automated Testing
```bash
# Test with different IP configurations
npm run build:android:test:custom 192.168.1.100
npm run build:android:test:custom 10.0.0.100
```
## Migration from Legacy
### Previous Workarounds
Before this feature, developers had to:
1. Manually edit environment files
2. Use different build configurations
3. Modify source code for IP addresses
### New Approach
```bash
# Simple one-liner
npm run build:android:dev:custom 192.168.1.100
```
## Future Enhancements
### Planned Features
1. **IP Validation**: Automatic IP format validation
2. **Network Discovery**: Auto-detect available IP addresses
3. **Port Configuration**: Support for custom ports
4. **Multiple APIs**: Support for custom IPs for all API endpoints
### Integration Opportunities
1. **Docker Integration**: Automatic IP detection in containerized environments
2. **Network Profiles**: Save and reuse common network configurations
3. **Hot Reload**: Automatic rebuild when IP changes
---
**Status**: Complete and ready for production use
**Last Updated**: 2025-01-27
**Version**: 1.0
**Maintainer**: Matthew Raymer

View File

@@ -0,0 +1,84 @@
# Contact Sharing - URL Solution
## Overview
Simple implementation to switch ContactQRScanShowView from copying QR value (CSV) to copying a URL for better user experience.
## Problem
The ContactQRScanShowView was copying QR value (CSV content) to clipboard instead of a URL, making contact sharing less user-friendly.
## Solution
Updated the `onCopyUrlToClipboard()` method in ContactQRScanShowView.vue to generate and copy a URL instead of the QR value.
## Changes Made
### ContactQRScanShowView.vue
**Added Imports:**
```typescript
import { generateEndorserJwtUrlForAccount } from "../libs/endorserServer";
import { Account } from "@/db/tables/accounts";
```
**Updated Method:**
```typescript
async onCopyUrlToClipboard() {
try {
// Generate URL for sharing
const account = (await libsUtil.retrieveFullyDecryptedAccount(
this.activeDid,
)) as Account;
const jwtUrl = await generateEndorserJwtUrlForAccount(
account,
this.isRegistered,
this.givenName,
this.profileImageUrl,
true,
);
// Copy the URL to clipboard
useClipboard()
.copy(jwtUrl)
.then(() => {
this.notify.toast(
"Copied",
NOTIFY_QR_URL_COPIED.message,
QR_TIMEOUT_MEDIUM,
);
});
} catch (error) {
logger.error("Failed to generate contact URL:", error);
this.notify.error("Failed to generate contact URL. Please try again.");
}
}
```
## Benefits
1. **Better UX**: Recipients can click the URL to add contact directly
2. **Consistency**: Both ContactQRScanShowView and ContactQRScanFullView now use URL format
3. **Error Handling**: Graceful fallback if URL generation fails
4. **Simple**: Minimal changes, no new components needed
## User Experience
**Before:**
- Click QR code → Copy CSV data to clipboard
- Recipient must paste CSV into input field
**After:**
- Click QR code → Copy URL to clipboard
- Recipient clicks URL → Contact added automatically
## Testing
- ✅ Linting passes
- ✅ Error handling implemented
- ✅ Consistent with ContactQRScanFullView behavior
- ✅ Maintains existing notification system
## Deployment
Ready for deployment. No breaking changes, maintains backward compatibility.

View File

@@ -2,33 +2,30 @@
**Author**: Matthew Raymer
**Date**: 2025-01-27
**Status**: ✅ **COMPLETE** - Domain configuration system implemented
**Status**: ✅ **UPDATED** - Simplified to use APP_SERVER for all functionality
## Overview
TimeSafari uses a centralized domain configuration system to ensure consistent
URL generation across all environments. This system prevents localhost URLs from
appearing in shared links during development and provides a single point of
control for domain changes.
URL generation across all environments. This system provides a single point of
control for domain changes and uses environment-specific configuration for all
functionality including sharing.
## Problem Solved
### Issue: Localhost URLs in Shared Links
### Issue: Inconsistent Domain Usage
Previously, copy link buttons and deep link generation used the environment-
specific `APP_SERVER` constant, which resulted in:
Previously, the system used separate constants for different types of URLs:
- **Development**: `http://localhost:8080/deep-link/claim/123`
- **Test**: `https://test.timesafari.app/deep-link/claim/123`
- **Production**: `https://timesafari.app/deep-link/claim/123`
- **Internal Operations**: Used `APP_SERVER` (environment-specific)
- **Sharing**: Used separate constants (removed)
This caused problems when users in development mode shared links, as the
localhost URLs wouldn't work for other users.
This created complexity and confusion about when to use which constant.
### Solution: Production Domain for Sharing
### Solution: Unified Domain Configuration
All sharing functionality now uses the `PROD_SHARE_DOMAIN` constant, which
always points to the production domain regardless of the current environment.
All functionality now uses the `APP_SERVER` constant, which provides
environment-specific URLs that can be configured per environment.
## Implementation
@@ -43,27 +40,28 @@ export enum AppString {
// ... other constants ...
}
// Production domain for sharing links (always use production URL for sharing)
export const PROD_SHARE_DOMAIN = AppString.PROD_PUSH_SERVER;
// Environment-specific server URL for all functionality
export const APP_SERVER =
import.meta.env.VITE_APP_SERVER || "https://timesafari.app";
```
### Usage Pattern
All components that generate shareable links follow this pattern:
All components that generate URLs follow this pattern:
```typescript
import { PROD_SHARE_DOMAIN } from "@/constants/app";
import { APP_SERVER } from "@/constants/app";
// In component class
PROD_SHARE_DOMAIN = PROD_SHARE_DOMAIN;
APP_SERVER = APP_SERVER;
// In methods
const deepLink = `${PROD_SHARE_DOMAIN}/deep-link/claim/${claimId}`;
const deepLink = `${APP_SERVER}/deep-link/claim/${claimId}`;
```
### Components Updated
The following components and services were updated to use `PROD_SHARE_DOMAIN`:
The following components and services use `APP_SERVER`:
#### Views
- `ClaimView.vue` - Claim and certificate links
@@ -82,17 +80,28 @@ The following components and services were updated to use `PROD_SHARE_DOMAIN`:
## Configuration Management
### Changing the Production Domain
### Environment-Specific Configuration
To change the production domain for all sharing functionality:
The system uses environment variables to configure domains:
1. **Update the constant** in `src/constants/app.ts`:
```typescript
export enum AppString {
// ... other constants ...
PROD_PUSH_SERVER = "https://your-new-domain.com",
// ... other constants ...
}
```bash
# Development
VITE_APP_SERVER=http://localhost:8080
# Test
VITE_APP_SERVER=https://test.timesafari.app
# Production
VITE_APP_SERVER=https://timesafari.app
```
### Changing the Domain
To change the domain for all functionality:
1. **Update environment variables** for the target environment:
```bash
VITE_APP_SERVER=https://your-new-domain.com
```
2. **Rebuild the application** for all platforms:
@@ -102,46 +111,32 @@ To change the production domain for all sharing functionality:
npm run build:electron
```
### Environment-Specific Configuration
The system maintains environment-specific configuration for internal operations
while using production domains for sharing:
```typescript
// Internal operations use environment-specific URLs
export const APP_SERVER =
import.meta.env.VITE_APP_SERVER || "https://timesafari.app";
// Sharing always uses production URLs
export const PROD_SHARE_DOMAIN = AppString.PROD_PUSH_SERVER;
```
## Benefits
### ✅ Consistent User Experience
### ✅ Simplified Configuration
- All shared links work for all users regardless of environment
- No more broken localhost links in development
- Consistent behavior across all platforms
- Single constant for all URL generation
- No confusion about which constant to use
- Consistent behavior across all functionality
### ✅ Environment Flexibility
- Easy to configure different domains per environment
- Support for development, test, and production environments
- Environment-specific sharing URLs when needed
### ✅ Maintainability
- Single source of truth for production domain
- Single source of truth for domain configuration
- Easy to change domain across entire application
- Clear separation between internal and sharing URLs
- Clear pattern for implementing new URL functionality
### ✅ Developer Experience
- No need to remember which environment URLs work for sharing
- Clear pattern for implementing new sharing functionality
- Simple, consistent pattern for URL generation
- Clear documentation and examples
- Type-safe configuration with TypeScript
### ✅ Security
- No accidental exposure of internal development URLs
- Controlled domain configuration
- Clear audit trail for domain changes
## Testing
### Manual Testing
@@ -150,7 +145,7 @@ export const PROD_SHARE_DOMAIN = AppString.PROD_PUSH_SERVER;
```bash
npm run dev
# Navigate to any page with copy link buttons
# Verify links use production domain, not localhost
# Verify links use configured domain
```
2. **Production Build**:
@@ -164,27 +159,19 @@ export const PROD_SHARE_DOMAIN = AppString.PROD_PUSH_SERVER;
The implementation includes comprehensive linting to ensure:
- All components properly import `PROD_SHARE_DOMAIN`
- No hardcoded URLs in sharing functionality
- All components properly import `APP_SERVER`
- No hardcoded URLs in functionality
- Consistent usage patterns across the codebase
## Migration Notes
## Implementation Pattern
### Before Implementation
### Current Approach
```typescript
// ❌ Hardcoded URLs
const deepLink = "https://timesafari.app/deep-link/claim/123";
// ❌ Environment-specific URLs
const deepLink = `${APP_SERVER}/deep-link/claim/123`;
```
### After Implementation
```typescript
// ✅ Configurable production URLs
const deepLink = `${PROD_SHARE_DOMAIN}/deep-link/claim/123`;
// ✅ Single constant for all functionality
import { APP_SERVER } from "@/constants/app";
const shareLink = `${APP_SERVER}/deep-link/claim/123`;
const apiUrl = `${APP_SERVER}/api/claim/123`;
```
## Future Enhancements
@@ -208,6 +195,7 @@ const deepLink = `${PROD_SHARE_DOMAIN}/deep-link/claim/123`;
```
3. **Platform-Specific Domains**:
```typescript
export const getPlatformShareDomain = () => {
const platform = process.env.VITE_PLATFORM;
@@ -229,5 +217,5 @@ const deepLink = `${PROD_SHARE_DOMAIN}/deep-link/claim/123`;
---
**Last Updated**: 2025-01-27
**Version**: 1.0
**Version**: 2.0
**Maintainer**: Matthew Raymer

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,207 @@
# GiftedDialog Entity Types Refactoring
## Overview
This refactoring simplifies the `GiftedDialog` component by replacing the complex `updateEntityTypes()` method with explicit props for entity types. This makes the component more declarative, reusable, and easier to understand.
## Problem
The original `updateEntityTypes()` method used multiple props (`showProjects`, `fromProjectId`, `toProjectId`, `recipientEntityTypeOverride`) to determine entity types through complex conditional logic:
```typescript
updateEntityTypes() {
// Reset and set entity types based on current context
this.giverEntityType = "person";
this.recipientEntityType = "person";
// If recipient entity type is explicitly overridden, use that
if (this.recipientEntityTypeOverride) {
this.recipientEntityType = this.recipientEntityTypeOverride;
}
// Determine entity types based on current context
if (this.showProjects) {
// HomeView "Project" button or ProjectViewView "Given by This"
this.giverEntityType = "project";
// Only override recipient if not already set by recipientEntityTypeOverride
if (!this.recipientEntityTypeOverride) {
this.recipientEntityType = "person";
}
} else if (this.fromProjectId) {
// ProjectViewView "Given by This" button (project is giver)
this.giverEntityType = "project";
// Only override recipient if not already set by recipientEntityTypeOverride
if (!this.recipientEntityTypeOverride) {
this.recipientEntityType = "person";
}
} else if (this.toProjectId) {
// ProjectViewView "Given to This" button (project is recipient)
this.giverEntityType = "person";
// Only override recipient if not already set by recipientEntityTypeOverride
if (!this.recipientEntityTypeOverride) {
this.recipientEntityType = "project";
}
} else {
// HomeView "Person" button
this.giverEntityType = "person";
// Only override recipient if not already set by recipientEntityTypeOverride
if (!this.recipientEntityTypeOverride) {
this.recipientEntityType = "person";
}
}
}
```
### Issues with the Original Approach
1. **Complex Logic**: Nested conditionals that were hard to follow
2. **Tight Coupling**: Views needed to understand internal logic to set the right props
3. **Inflexible**: Adding new entity type combinations required modifying the method
4. **Unclear Intent**: The relationship between props and entity types was not obvious
## Solution
### 1. Explicit Props
Replace the complex logic with explicit props:
```typescript
@Prop({ default: "person" }) giverEntityType = "person" as "person" | "project";
@Prop({ default: "person" }) recipientEntityType = "person" as "person" | "project";
```
### 2. Simple Inline Logic
Views now use simple inline logic to determine entity types:
```vue
<!-- HomeView -->
<GiftedDialog
ref="giftedDialog"
:giver-entity-type="showProjectsDialog ? 'project' : 'person'"
:recipient-entity-type="'person'"
/>
<!-- ProjectViewView -->
<GiftedDialog
ref="giveDialogToThis"
:giver-entity-type="'person'"
:recipient-entity-type="'project'"
:to-project-id="projectId"
:is-from-project-view="true"
/>
<!-- ClaimView -->
<GiftedDialog
ref="customGiveDialog"
:giver-entity-type="'person'"
:recipient-entity-type="projectInfo ? 'project' : 'person'"
:to-project-id="..."
/>
```
## Benefits
### 1. **Declarative**
- Entity types are explicitly declared in the template
- No hidden logic in watchers or complex methods
- Clear intent at the call site
### 2. **Reusable**
- Views can easily specify any combination of entity types
- No need to understand internal logic
- Simple inline logic is easy to understand
### 3. **Maintainable**
- Adding new entity type combinations is straightforward
- Logic is visible directly in the template
- No additional files to maintain
### 4. **Testable**
- Entity type logic is visible and predictable
- No complex state management to test
- Template logic can be easily verified
### 5. **Type Safe**
- TypeScript ensures correct entity type values
- Compile-time validation of entity type combinations
## Migration Guide
### For Views Using GiftedDialog
Simply update the template to use explicit entity type props:
```vue
<!-- Before -->
<GiftedDialog :show-projects="showProjects" />
<!-- After -->
<GiftedDialog
:giver-entity-type="showProjects ? 'project' : 'person'"
:recipient-entity-type="'person'"
/>
```
### Common Patterns
1. **Person-to-Person**: `giver-entity-type="'person'" recipient-entity-type="'person'"`
2. **Project-to-Person**: `giver-entity-type="'project'" recipient-entity-type="'person'"`
3. **Person-to-Project**: `giver-entity-type="'person'" recipient-entity-type="'project'"`
4. **Conditional Project**: `recipient-entity-type="hasProject ? 'project' : 'person'"`
## Files Changed
### Core Changes
- `src/components/GiftedDialog.vue` - Removed `updateEntityTypes()` method, added explicit props
### View Updates
- `src/views/HomeView.vue` - Updated to use inline logic
- `src/views/ProjectViewView.vue` - Updated to use inline logic
- `src/views/ClaimView.vue` - Updated to use inline logic
- `src/views/ContactGiftingView.vue` - Updated to use inline logic
- `src/views/ContactsView.vue` - Updated to use inline logic
## Backward Compatibility
The refactoring maintains backward compatibility by:
- Keeping all existing props that are still needed (`fromProjectId`, `toProjectId`, `isFromProjectView`)
- Preserving the same component API for the `open()` method
- Maintaining the same template structure
## Future Enhancements
1. **Validation**: Add runtime validation for entity type combinations
2. **Documentation**: Add JSDoc comments to the component props
3. **Testing**: Add unit tests for the component with different entity type combinations
## Conclusion
This refactoring transforms `GiftedDialog` from a component with complex internal logic to a declarative, reusable component. The explicit entity type props make the component's behavior clear and predictable, while the simple inline logic keeps the code straightforward and maintainable.
## Bug Fixes
### Issue 1: Entity Type Preservation in Navigation
**Problem**: When navigating from HomeView with `showProjects = true` to ContactGiftingView via "Show All", the entity type information was lost because `showAllQueryParams` returned an empty object for project contexts.
**Solution**: Modified `EntitySelectionStep.vue` to always pass entity type information in the query parameters, even for project contexts.
### Issue 2: Recipient Reset in ContactGiftingView
**Problem**: When selecting a giver in ContactGiftingView, the recipient was always reset to "You" instead of preserving the current recipient.
**Solution**: Updated ContactGiftingView to preserve the existing recipient from the context when selecting a giver, and enhanced the query parameter passing to include both giver and recipient information for better context preservation.
### Issue 3: HomeView Project Button Entity Type Mismatch
**Problem**: When navigating from HomeView Project button → change recipient → Show All → ContactGifting, the giver entity type was incorrectly set to "person" instead of "project".
**Root Cause**: ContactGiftingView was inferring entity types from `fromProjectId` and `toProjectId` instead of using the explicitly passed `giverEntityType` and `recipientEntityType` from the query parameters.
**Solution**: Updated ContactGiftingView to use the explicitly passed entity types from query parameters instead of inferring them from project IDs.
### Files Modified for Bug Fixes
- `src/components/EntitySelectionStep.vue` - Enhanced query parameter passing
- `src/views/ContactGiftingView.vue` - Improved context preservation logic and entity type handling

View File

@@ -8,6 +8,7 @@
"scripts": {
"lint": "eslint --ext .js,.ts,.vue --ignore-path .gitignore src",
"lint-fix": "eslint --ext .js,.ts,.vue --ignore-path .gitignore --fix src",
"type-check": "tsc --noEmit",
"prebuild": "eslint --ext .js,.ts,.vue --ignore-path .gitignore src && node sw_combine.js && node scripts/copy-wasm.js",
"test:prerequisites": "node scripts/check-prerequisites.js",
"test:web": "npx playwright test -c playwright.config-local.ts --trace on",
@@ -22,7 +23,7 @@
"auto-run:ios": "./scripts/auto-run.sh --platform=ios",
"auto-run:android": "./scripts/auto-run.sh --platform=android",
"auto-run:electron": "./scripts/auto-run.sh --platform=electron",
"build:capacitor": "VITE_GIT_HASH=`git log -1 --pretty=format:%h` vite build --mode capacitor --config vite.config.capacitor.mts",
"build:capacitor": "VITE_GIT_HASH=$(./scripts/get-git-hash.sh) vite build --mode capacitor --config vite.config.capacitor.mts",
"build:capacitor:sync": "npm run build:capacitor && npx cap sync",
"build:ios": "./scripts/build-ios.sh",
"build:ios:dev": "./scripts/build-ios.sh --dev",
@@ -40,6 +41,10 @@
"build:ios:sync": "./scripts/build-ios.sh --sync",
"build:ios:assets": "./scripts/build-ios.sh --assets",
"build:ios:deploy": "./scripts/build-ios.sh --deploy",
"build:ios:dev:custom": "./scripts/build-ios.sh --dev --api-ip",
"build:ios:test:custom": "./scripts/build-ios.sh --test --api-ip",
"build:ios:dev:run:custom": "./scripts/build-ios.sh --dev --api-ip --auto-run",
"build:ios:test:run:custom": "./scripts/build-ios.sh --test --api-ip --auto-run",
"build:web": "./scripts/build-web.sh",
"build:web:dev": "./scripts/build-web.sh --dev",
"build:web:test": "./scripts/build-web.sh --test",
@@ -87,6 +92,7 @@
"clean:android": "adb uninstall app.timesafari.app || true",
"clean:ios": "rm -rf ios/App/build ios/App/Pods ios/App/output ios/App/App/public ios/DerivedData ios/capacitor-cordova-ios-plugins ios/App/App/capacitor.config.json ios/App/App/config.xml || true",
"clean:electron": "./scripts/build-electron.sh --clean",
"clean:all": "npm run clean:ios && npm run clean:android && npm run clean:electron",
"build:android": "./scripts/build-android.sh",
"build:android:dev": "./scripts/build-android.sh --dev",
"build:android:test": "./scripts/build-android.sh --test",
@@ -103,7 +109,11 @@
"build:android:clean": "./scripts/build-android.sh --clean",
"build:android:sync": "./scripts/build-android.sh --sync",
"build:android:assets": "./scripts/build-android.sh --assets",
"build:android:deploy": "./scripts/build-android.sh --deploy"
"build:android:deploy": "./scripts/build-android.sh --deploy",
"build:android:dev:custom": "./scripts/build-android.sh --dev --api-ip",
"build:android:test:custom": "./scripts/build-android.sh --test --api-ip",
"build:android:dev:run:custom": "./scripts/build-android.sh --dev --api-ip --auto-run",
"build:android:test:run:custom": "./scripts/build-android.sh --test --api-ip --auto-run"
},
"dependencies": {
"@capacitor-community/electron": "^5.0.1",

View File

@@ -35,13 +35,7 @@ export default defineConfig({
baseURL: "http://localhost:8080",
/* Collect trace when retrying the failed test. See https://playwright.dev/docs/trace-viewer */
trace: "retain-on-failure",
// Add request logging
logger: {
isEnabled: (name, severity) => severity === 'error' || name === 'api',
log: (name, severity, message, args) => console.log(`${severity}: ${message}`, args)
}
trace: "retain-on-failure"
},
/* Configure projects for major browsers */

View File

@@ -1,81 +0,0 @@
import { defineConfig, devices } from '@playwright/test';
/**
* Read environment variables from file.
* https://github.com/motdotla/dotenv
*/
// import dotenv from 'dotenv';
// dotenv.config({ path: path.resolve(__dirname, '.env') });
/**
* See https://playwright.dev/docs/test-configuration.
*/
export default defineConfig({
testDir: './test-playwright',
/* Run tests in files in parallel */
fullyParallel: true,
/* Fail the build on CI if you accidentally left test.only in the source code. */
forbidOnly: !!process.env.CI,
/* Retry on CI only */
retries: process.env.CI ? 2 : 0,
/* Opt out of parallel tests on CI. */
workers: process.env.CI ? 1 : undefined,
/* Reporter to use. See https://playwright.dev/docs/test-reporters */
reporter: 'html',
/* Shared settings for all the projects below. See https://playwright.dev/docs/api/class-testoptions. */
use: {
/* Base URL to use in actions like `await page.goto('/')`. */
baseURL: 'https://test.timesafari.app',
/* Collect trace when retrying the failed test. See https://playwright.dev/docs/trace-viewer */
trace: 'on-first-retry',
},
/* Configure projects for major browsers */
projects: [
{
name: 'chromium',
use: {
...devices['Desktop Chrome'],
permissions: ["clipboard-read"],
},
},
{
name: 'firefox',
use: { ...devices['Desktop Firefox'] },
},
// {
// name: 'webkit',
// use: { ...devices['Desktop Safari'] },
// },
/* Test against mobile viewports. */
// {
// name: 'Mobile Chrome',
// use: { ...devices['Pixel 5'] },
// },
// {
// name: 'Mobile Safari',
// use: { ...devices['iPhone 12'] },
// },
/* Test against branded browsers. */
// {
// name: 'Microsoft Edge',
// use: { ...devices['Desktop Edge'], channel: 'msedge' },
// },
// {
// name: 'Google Chrome',
// use: { ...devices['Desktop Chrome'], channel: 'chrome' },
// },
],
/* Run your local dev server before starting the tests */
// webServer: {
// command:
// "VITE_PASSKEYS_ENABLED=true VITE_DEFAULT_ENDORSER_API_SERVER=http://localhost:3000 npm run dev",
// url: "http://localhost:8080",
// reuseExistingServer: !process.env.CI,
// },
});

View File

@@ -60,12 +60,16 @@ SYNC_ONLY=false
ASSETS_ONLY=false
DEPLOY_APP=false
AUTO_RUN=false
CUSTOM_API_IP=""
# Function to parse Android-specific arguments
parse_android_args() {
local args=("$@")
local i=0
for arg in "${args[@]}"; do
while [ $i -lt ${#args[@]} ]; do
local arg="${args[$i]}"
case $arg in
--dev|--development)
BUILD_MODE="development"
@@ -106,6 +110,18 @@ parse_android_args() {
--auto-run)
AUTO_RUN=true
;;
--api-ip)
if [ $((i + 1)) -lt ${#args[@]} ]; then
CUSTOM_API_IP="${args[$((i + 1))]}"
i=$((i + 1)) # Skip the next argument
else
log_error "Error: --api-ip requires an IP address"
exit 1
fi
;;
--api-ip=*)
CUSTOM_API_IP="${arg#*=}"
;;
-h|--help)
print_android_usage
exit 0
@@ -117,6 +133,7 @@ parse_android_args() {
log_warn "Unknown argument: $arg"
;;
esac
i=$((i + 1))
done
}
@@ -138,6 +155,7 @@ print_android_usage() {
echo " --assets Generate assets only"
echo " --deploy Deploy APK to connected device"
echo " --auto-run Auto-run app after build"
echo " --api-ip <ip> Custom IP address for claim API (defaults to 10.0.2.2)"
echo ""
echo "Common Options:"
echo " -h, --help Show this help message"
@@ -151,6 +169,8 @@ print_android_usage() {
echo " $0 --clean # Clean only"
echo " $0 --sync # Sync only"
echo " $0 --deploy # Build and deploy to device"
echo " $0 --dev # Dev build with default 10.0.2.2"
echo " $0 --dev --api-ip 192.168.1.100 # Dev build with custom API IP"
echo ""
}
@@ -166,6 +186,21 @@ log_info "Build type: $BUILD_TYPE"
# Setup environment for Capacitor build
setup_build_env "capacitor"
# Override API servers for Android development
if [ "$BUILD_MODE" = "development" ]; then
if [ -n "$CUSTOM_API_IP" ]; then
# Use custom IP for physical device development
export VITE_DEFAULT_ENDORSER_API_SERVER="http://${CUSTOM_API_IP}:3000"
export VITE_DEFAULT_PARTNER_API_SERVER="http://${CUSTOM_API_IP}:3000"
log_info "Android development mode: Using custom IP ${CUSTOM_API_IP} for physical device"
else
# Use Android emulator IP (10.0.2.2) for Android development
export VITE_DEFAULT_ENDORSER_API_SERVER="http://10.0.2.2:3000"
export VITE_DEFAULT_PARTNER_API_SERVER="http://10.0.2.2:3000"
log_debug "Android development mode: Using 10.0.2.2 for emulator"
fi
fi
# Setup application directories
setup_app_directories

View File

@@ -168,7 +168,11 @@ build_web_assets() {
local mode=$1
log_info "Building web assets for Electron (mode: $mode)"
safe_execute "Building web assets" "VITE_GIT_HASH=\$(git log -1 --pretty=format:%h) vite build --mode $mode --config vite.config.electron.mts"
# Get git hash using the improved function from common.sh
local git_hash=$(get_git_hash)
log_debug "Using git hash: $git_hash"
safe_execute "Building web assets" "VITE_GIT_HASH=$git_hash vite build --mode $mode --config vite.config.electron.mts"
}
# Sync with Capacitor

View File

@@ -22,6 +22,7 @@ SYNC_ONLY=false
ASSETS_ONLY=false
DEPLOY_APP=false
AUTO_RUN=false
CUSTOM_API_IP=""
# Function to print iOS-specific usage
print_ios_usage() {
@@ -41,6 +42,7 @@ print_ios_usage() {
echo " --assets Generate assets only"
echo " --deploy Deploy app to connected device"
echo " --auto-run Auto-run app after build"
echo " --api-ip <ip> Custom IP address for claim API (uses Capacitor default)"
echo ""
echo "Common Options:"
echo " -h, --help Show this help message"
@@ -54,12 +56,19 @@ print_ios_usage() {
echo " $0 --clean # Clean only"
echo " $0 --sync # Sync only"
echo " $0 --deploy # Build and deploy to device"
echo " $0 --dev # Dev build with Capacitor default"
echo " $0 --dev --api-ip 192.168.1.100 # Dev build with custom API IP"
echo ""
}
# Function to parse iOS-specific arguments
parse_ios_args() {
for arg in "$@"; do
local args=("$@")
local i=0
while [ $i -lt ${#args[@]} ]; do
local arg="${args[$i]}"
case $arg in
--dev|--development)
BUILD_MODE="development"
@@ -100,6 +109,18 @@ parse_ios_args() {
--auto-run)
AUTO_RUN=true
;;
--api-ip)
if [ $((i + 1)) -lt ${#args[@]} ]; then
CUSTOM_API_IP="${args[$((i + 1))]}"
i=$((i + 1)) # Skip the next argument
else
log_error "Error: --api-ip requires an IP address"
exit 1
fi
;;
--api-ip=*)
CUSTOM_API_IP="${arg#*=}"
;;
-h|--help)
print_ios_usage
exit 0
@@ -111,6 +132,7 @@ parse_ios_args() {
log_warn "Unknown argument: $arg"
;;
esac
i=$((i + 1))
done
}
@@ -291,6 +313,14 @@ log_info "Build type: $BUILD_TYPE"
# Setup environment for Capacitor build
setup_build_env "capacitor"
# Override API servers for iOS development when custom IP is specified
if [ "$BUILD_MODE" = "development" ] && [ -n "$CUSTOM_API_IP" ]; then
# Use custom IP for physical device development
export VITE_DEFAULT_ENDORSER_API_SERVER="http://${CUSTOM_API_IP}:3000"
export VITE_DEFAULT_PARTNER_API_SERVER="http://${CUSTOM_API_IP}:3000"
log_info "iOS development mode: Using custom IP ${CUSTOM_API_IP} for physical device"
fi
# Setup application directories
setup_app_directories

View File

@@ -203,8 +203,12 @@ execute_vite_build() {
local mode="$1"
log_info "Executing Vite build for $mode mode..."
# Get git hash using the improved function from common.sh
local git_hash=$(get_git_hash)
log_debug "Using git hash: $git_hash"
# Construct Vite build command
local vite_cmd="VITE_GIT_HASH=\$(git log -1 --pretty=format:%h) npx vite build --config vite.config.web.mts"
local vite_cmd="VITE_GIT_HASH=$git_hash npx vite build --config vite.config.web.mts"
# Add mode if not development (development is default)
if [ "$mode" != "development" ]; then
@@ -252,12 +256,35 @@ execute_docker_build() {
log_info "Docker image available as: $image_tag:$mode"
}
# Function to run type checking based on build mode
run_type_checking() {
local mode="$1"
# Only run type checking for production and test builds
if [ "$mode" = "production" ] || [ "$mode" = "test" ]; then
log_info "Running TypeScript type checking for $mode mode..."
if ! measure_time npm run type-check; then
log_error "TypeScript type checking failed for $mode mode!"
exit 2
fi
log_success "TypeScript type checking completed for $mode mode"
else
log_debug "Skipping TypeScript type checking for development mode"
fi
}
# Function to start Vite development server
start_dev_server() {
log_info "Starting Vite development server..."
# Get git hash using the improved function from common.sh
local git_hash=$(get_git_hash)
log_debug "Using git hash: $git_hash"
# Construct Vite dev server command
local vite_cmd="VITE_GIT_HASH=\$(git log -1 --pretty=format:%h) npx vite --config vite.config.web.mts"
local vite_cmd="VITE_GIT_HASH=$git_hash npx vite --config vite.config.web.mts"
# Add mode if specified (though development is default)
if [ "$BUILD_MODE" != "development" ]; then
@@ -333,7 +360,10 @@ elif [ "$SERVE_BUILD" = true ]; then
log_info "Cleaning dist directory..."
clean_build_artifacts "dist"
# Step 2: Execute Vite build
# Step 2: Run type checking (for production/test builds)
safe_execute "Type checking for $BUILD_MODE mode" "run_type_checking $BUILD_MODE" || exit 2
# Step 3: Execute Vite build
safe_execute "Vite build for $BUILD_MODE mode" "execute_vite_build $BUILD_MODE" || exit 3
# Step 3: Serve the build
@@ -348,7 +378,10 @@ else
log_info "Cleaning dist directory..."
clean_build_artifacts "dist"
# Step 2: Execute Vite build
# Step 2: Run type checking (for production/test builds)
safe_execute "Type checking for $BUILD_MODE mode" "run_type_checking $BUILD_MODE" || exit 2
# Step 3: Execute Vite build
safe_execute "Vite build for $BUILD_MODE mode" "execute_vite_build $BUILD_MODE" || exit 3
# Step 3: Execute Docker build if requested

View File

@@ -51,10 +51,18 @@ log_step() {
# Function to measure and log execution time
measure_time() {
local start_time=$(date +%s)
"$@"
local end_time=$(date +%s)
local duration=$((end_time - start_time))
log_success "Completed in ${duration} seconds"
if "$@"; then
local end_time=$(date +%s)
local duration=$((end_time - start_time))
log_success "Completed in ${duration} seconds"
return 0
else
local exit_code=$?
local end_time=$(date +%s)
local duration=$((end_time - start_time))
log_error "Failed after ${duration} seconds (exit code: ${exit_code})"
return $exit_code
fi
}
# Function to print section headers
@@ -126,10 +134,25 @@ check_venv() {
# Function to get git hash for versioning
get_git_hash() {
if command -v git &> /dev/null; then
git log -1 --pretty=format:%h 2>/dev/null || echo "unknown"
# Use the dedicated git hash script for consistency
if [ -f "$(dirname "$0")/get-git-hash.sh" ]; then
"$(dirname "$0")/get-git-hash.sh"
else
echo "unknown"
# Fallback to direct git command if script not found
if command -v git &> /dev/null; then
# Get the current branch name
local current_branch=$(git branch --show-current 2>/dev/null || git rev-parse --abbrev-ref HEAD 2>/dev/null)
# If we're in a detached HEAD state or no branch, use HEAD
if [ -z "$current_branch" ] || [ "$current_branch" = "HEAD" ]; then
git log -1 --pretty=format:%h 2>/dev/null || echo "unknown"
else
# Use the current branch explicitly
git log -1 --pretty=format:%h "$current_branch" 2>/dev/null || echo "unknown"
fi
else
echo "unknown"
fi
fi
}
@@ -197,20 +220,22 @@ setup_build_env() {
# Set API server environment variables based on build mode
if [ "$BUILD_MODE" = "development" ]; then
# For Capacitor development, use localhost by default
# Android builds will override this in build-android.sh
export VITE_DEFAULT_ENDORSER_API_SERVER="http://localhost:3000"
export VITE_DEFAULT_PARTNER_API_SERVER="http://localhost:3000"
log_debug "Development mode: Using localhost for Endorser and Partner APIs"
export VITE_DEFAULT_IMAGE_API_SERVER="https://image-api.timesafari.app"
export VITE_DEFAULT_PARTNER_API_SERVER="https://partner-api.endorser.ch"
log_debug "Development mode: Using localhost for Endorser API, production for Image/Partner APIs"
elif [ "$BUILD_MODE" = "test" ]; then
export VITE_DEFAULT_ENDORSER_API_SERVER="https://test-api.endorser.ch"
export VITE_DEFAULT_PARTNER_API_SERVER="https://test-partner-api.endorser.ch"
log_debug "Test mode: Using test Endorser and Partner APIs"
export VITE_DEFAULT_IMAGE_API_SERVER="https://image-api.timesafari.app"
export VITE_DEFAULT_PARTNER_API_SERVER="https://partner-api.endorser.ch"
log_debug "Test mode: Using test Endorser API, production for Image/Partner APIs"
elif [ "$BUILD_MODE" = "production" ]; then
export VITE_DEFAULT_ENDORSER_API_SERVER="https://api.endorser.ch"
export VITE_DEFAULT_IMAGE_API_SERVER="https://image-api.timesafari.app"
export VITE_DEFAULT_PARTNER_API_SERVER="https://partner-api.endorser.ch"
log_debug "Production mode: Using production API servers"
export VITE_DEFAULT_IMAGE_API_SERVER="https://image-api.timesafari.app"
fi
# Log environment setup

105
scripts/get-git-hash.sh Executable file
View File

@@ -0,0 +1,105 @@
#!/bin/bash
# TimeSafari Git Hash Retrieval Script
# Author: Matthew Raymer
# Description: Retrieves the current git commit hash for the active branch
#
# This script ensures that the correct git hash is retrieved regardless of
# the current branch or git state. It handles edge cases like detached HEAD
# and provides fallbacks for when git is not available.
#
# ARCHITECTURAL BENEFITS:
# - Centralized Logic: Single source of truth for git hash retrieval across all build scripts
# - Consistent Behavior: Ensures all builds use the same git hash logic and format
# - Maintainability: Changes to git hash logic only need to be made in one place
# - Robust Error Handling: Handles edge cases that could cause build failures
# - Branch-Aware: Explicitly uses current branch, preventing default branch fallback issues
#
# USAGE PATTERNS:
# # Direct usage
# ./scripts/get-git-hash.sh
#
# # In build scripts (recommended)
# VITE_GIT_HASH=$(./scripts/get-git-hash.sh) npm run build
#
# # In shell scripts
# git_hash=$(./scripts/get-git-hash.sh)
# echo "Current commit: $git_hash"
#
# # In package.json scripts
# "build:capacitor": "VITE_GIT_HASH=$(./scripts/get-git-hash.sh) vite build --mode capacitor --config vite.config.capacitor.mts"
#
# OUTPUT:
# - Git commit hash (7 characters) if available (e.g., "bf08e57c")
# - "unknown" if git is not available or no repository found
#
# EXIT CODES:
# 0 - Success (hash retrieved or "unknown" returned)
# 1 - Error (should not occur in normal operation)
#
# EDGE CASES HANDLED:
# - Detached HEAD state: Falls back to HEAD commit
# - No git repository: Returns "unknown"
# - Git not installed: Returns "unknown"
# - No commits: Returns "unknown"
# - Branch detection failure: Falls back to HEAD commit
#
# INTEGRATION POINTS:
# - scripts/common.sh: Primary usage via get_git_hash() function
# - package.json: Direct usage in build:capacitor script
# - Build scripts: Used by build-web.sh, build-electron.sh, etc.
# - Docker builds: Ensures consistent git hashes in containerized builds
#
# VALUE PROPOSITION:
# This script was created to solve git hash inconsistencies across different
# build environments and branch states. It provides a reliable, consistent
# interface for git hash retrieval that works regardless of the current
# git state or environment. This prevents issues like:
# - Builds using wrong branch's commit hash
# - Inconsistent versioning across different build types
# - Build failures due to git state issues
# - Manual git hash management in multiple scripts
#
# MAINTENANCE:
# - Update this script if git hash retrieval logic needs to change
# - All build scripts automatically benefit from improvements
# - Test with various git states (detached HEAD, different branches, etc.)
# - Ensure compatibility with CI/CD environments
set -euo pipefail
# Function to get git hash for versioning
get_git_hash() {
# Check if git is available
if ! command -v git &> /dev/null; then
echo "unknown"
return 0
fi
# Check if we're in a git repository
if ! git rev-parse --git-dir &> /dev/null; then
echo "unknown"
return 0
fi
# Get the current branch name
local current_branch
current_branch=$(git branch --show-current 2>/dev/null || git rev-parse --abbrev-ref HEAD 2>/dev/null || echo "")
# If we're in a detached HEAD state or no branch, use HEAD
if [ -z "$current_branch" ] || [ "$current_branch" = "HEAD" ]; then
git log -1 --pretty=format:%h 2>/dev/null || echo "unknown"
else
# Use the current branch explicitly
git log -1 --pretty=format:%h "$current_branch" 2>/dev/null || echo "unknown"
fi
}
# Main execution
main() {
local git_hash
git_hash=$(get_git_hash)
echo "$git_hash"
}
# Run main function
main "$@"

View File

@@ -121,6 +121,12 @@ import { AppString } from "../constants/app";
components: {
EntityIcon,
},
emits: [
"toggle-selection",
"show-identicon",
"show-gifted-dialog",
"open-offer-dialog",
],
})
export default class ContactListItem extends Vue {
@Prop({ required: true }) contact!: Contact;
@@ -151,14 +157,12 @@ export default class ContactListItem extends Vue {
return contact;
}
@Emit("show-gifted-dialog")
emitShowGiftedDialog(fromDid: string, toDid: string) {
return { fromDid, toDid };
this.$emit("show-gifted-dialog", fromDid, toDid);
}
@Emit("open-offer-dialog")
emitOpenOfferDialog(did: string, name: string | undefined) {
return { did, name };
this.$emit("open-offer-dialog", did, name);
}
/**

View File

@@ -136,6 +136,20 @@ export default class EntitySelectionStep extends Vue {
@Prop()
receiver?: EntityData | null;
/** Form field values to preserve when navigating to "Show All" */
@Prop({ default: "" })
description!: string;
@Prop({ default: "0" })
amountInput!: string;
@Prop({ default: "HUR" })
unitCode!: string;
/** Offer ID for context when fulfilling an offer */
@Prop({ default: "" })
offerId!: string;
/** Notification function from parent component */
@Prop()
notify?: (notification: NotificationIface, timeout?: number) => void;
@@ -220,34 +234,41 @@ export default class EntitySelectionStep extends Vue {
* Query parameters for "Show All" navigation
*/
get showAllQueryParams(): Record<string, string> {
if (this.shouldShowProjects) {
return {};
}
return {
const baseParams = {
stepType: this.stepType,
giverEntityType: this.giverEntityType,
recipientEntityType: this.recipientEntityType,
...(this.stepType === "giver"
? {
recipientProjectId: this.toProjectId || "",
recipientProjectName: this.receiver?.name || "",
recipientProjectImage: this.receiver?.image || "",
recipientProjectHandleId: this.receiver?.handleId || "",
recipientDid: this.receiver?.did || "",
}
: {
giverProjectId: this.fromProjectId || "",
giverProjectName: this.giver?.name || "",
giverProjectImage: this.giver?.image || "",
giverProjectHandleId: this.giver?.handleId || "",
giverDid: this.giver?.did || "",
}),
// Form field values to preserve
description: this.description,
amountInput: this.amountInput,
unitCode: this.unitCode,
offerId: this.offerId,
fromProjectId: this.fromProjectId,
toProjectId: this.toProjectId,
showProjects: this.showProjects.toString(),
isFromProjectView: this.isFromProjectView.toString(),
};
if (this.shouldShowProjects) {
// For project contexts, still pass entity type information
return baseParams;
}
return {
...baseParams,
// Always pass both giver and recipient info for context preservation
giverProjectId: this.fromProjectId || "",
giverProjectName: this.giver?.name || "",
giverProjectImage: this.giver?.image || "",
giverProjectHandleId: this.giver?.handleId || "",
giverDid: this.giverEntityType === "person" ? this.giver?.did || "" : "",
recipientProjectId: this.toProjectId || "",
recipientProjectName: this.receiver?.name || "",
recipientProjectImage: this.receiver?.image || "",
recipientProjectHandleId: this.receiver?.handleId || "",
recipientDid:
this.recipientEntityType === "person" ? this.receiver?.did || "" : "",
};
}
/**

View File

@@ -315,16 +315,15 @@ export default class GiftDetailsStep extends Vue {
giverName: this.giver?.name,
offerId: this.offerId,
fulfillsProjectId:
this.giverEntityType === "person" &&
this.recipientEntityType === "project"
? this.toProjectId
: undefined,
this.recipientEntityType === "project" ? this.toProjectId : undefined,
providerProjectId:
this.giverEntityType === "project" &&
this.recipientEntityType === "person"
this.giverEntityType === "project"
? this.giver?.handleId
: this.fromProjectId,
recipientDid: this.receiver?.did,
recipientDid:
this.recipientEntityType === "person"
? this.receiver?.did
: undefined,
recipientName: this.receiver?.name,
unitCode: this.localUnitCode,
},

View File

@@ -1,13 +1,19 @@
<template>
<div v-if="visible" class="dialog-overlay">
<div class="dialog">
<div
class="dialog"
data-testid="gifted-dialog"
:data-recipient-entity-type="recipientEntityType"
>
<!-- Step 1: Entity Selection -->
<EntitySelectionStep
v-show="firstStep"
:step-type="stepType"
:giver-entity-type="giverEntityType"
:recipient-entity-type="recipientEntityType"
:show-projects="showProjects"
:show-projects="
giverEntityType === 'project' || recipientEntityType === 'project'
"
:is-from-project-view="isFromProjectView"
:projects="projects"
:all-contacts="allContacts"
@@ -18,6 +24,10 @@
:to-project-id="toProjectId"
:giver="giver"
:receiver="receiver"
:description="description"
:amount-input="amountInput"
:unit-code="unitCode"
:offer-id="offerId"
:notify="$notify"
@entity-selected="handleEntitySelected"
@cancel="cancel"
@@ -52,7 +62,7 @@
</template>
<script lang="ts">
import { Vue, Component, Prop, Watch } from "vue-facing-decorator";
import { Vue, Component, Prop } from "vue-facing-decorator";
import {
createAndSubmitGive,
@@ -71,6 +81,12 @@ import GiftDetailsStep from "../components/GiftDetailsStep.vue";
import { PlanData } from "../interfaces/records";
import { PlatformServiceMixin } from "@/utils/PlatformServiceMixin";
import { createNotifyHelpers, TIMEOUTS } from "@/utils/notify";
import {
NOTIFY_GIFT_ERROR_NEGATIVE_AMOUNT,
NOTIFY_GIFT_ERROR_NO_DESCRIPTION,
NOTIFY_GIFTED_DETAILS_NO_IDENTIFIER,
NOTIFY_GIFTED_DETAILS_RECORDING_GIVE,
} from "@/constants/notifications";
@Component({
components: {
@@ -97,23 +113,13 @@ export default class GiftedDialog extends Vue {
@Prop() fromProjectId = "";
@Prop() toProjectId = "";
@Prop({ default: false }) showProjects = false;
@Prop() isFromProjectView = false;
@Watch("showProjects")
onShowProjectsChange() {
this.updateEntityTypes();
}
@Watch("fromProjectId")
onFromProjectIdChange() {
this.updateEntityTypes();
}
@Watch("toProjectId")
onToProjectIdChange() {
this.updateEntityTypes();
}
@Prop({ default: "person" }) giverEntityType = "person" as
| "person"
| "project";
@Prop({ default: "person" }) recipientEntityType = "person" as
| "person"
| "project";
activeDid = "";
allContacts: Array<Contact> = [];
@@ -122,20 +128,19 @@ export default class GiftedDialog extends Vue {
amountInput = "0";
callbackOnSuccess?: (amount: number) => void = () => {};
customTitle?: string;
description = "";
firstStep = true; // true = Step 1 (giver/recipient selection), false = Step 2 (amount/description)
giver?: libsUtil.GiverReceiverInputInfo; // undefined means no identified giver agent
offerId = "";
projects: PlanData[] = [];
prompt = "";
receiver?: libsUtil.GiverReceiverInputInfo;
stepType = "giver";
unitCode = "HUR";
visible = false;
libsUtil = libsUtil;
projects: PlanData[] = [];
didInfo = didInfo;
// Computed property to help debug template logic
@@ -189,56 +194,27 @@ export default class GiftedDialog extends Vue {
return false;
}
stepType = "giver";
giverEntityType = "person" as "person" | "project";
recipientEntityType = "person" as "person" | "project";
updateEntityTypes() {
// Reset and set entity types based on current context
this.giverEntityType = "person";
this.recipientEntityType = "person";
// Determine entity types based on current context
if (this.showProjects) {
// HomeView "Project" button or ProjectViewView "Given by This"
this.giverEntityType = "project";
this.recipientEntityType = "person";
} else if (this.fromProjectId) {
// ProjectViewView "Given by This" button (project is giver)
this.giverEntityType = "project";
this.recipientEntityType = "person";
} else if (this.toProjectId) {
// ProjectViewView "Given to This" button (project is recipient)
this.giverEntityType = "person";
this.recipientEntityType = "project";
} else {
// HomeView "Person" button
this.giverEntityType = "person";
this.recipientEntityType = "person";
}
}
async open(
giver?: libsUtil.GiverReceiverInputInfo,
receiver?: libsUtil.GiverReceiverInputInfo,
offerId?: string,
customTitle?: string,
prompt?: string,
description?: string,
amountInput?: string,
unitCode?: string,
callbackOnSuccess: (amount: number) => void = () => {},
) {
this.customTitle = customTitle;
this.giver = giver;
this.prompt = prompt || "";
this.receiver = receiver;
this.amountInput = "0";
this.callbackOnSuccess = callbackOnSuccess;
this.offerId = offerId || "";
this.prompt = prompt || "";
this.description = description || "";
this.amountInput = amountInput || "0";
this.unitCode = unitCode || "HUR";
this.callbackOnSuccess = callbackOnSuccess;
this.firstStep = !giver;
this.stepType = "giver";
// Update entity types based on current props
this.updateEntityTypes();
try {
const settings = await this.$settings();
this.apiServer = settings.apiServer || "";
@@ -318,23 +294,24 @@ export default class GiftedDialog extends Vue {
async confirm() {
if (!this.activeDid) {
this.safeNotify.error(
"You must select an identifier before you can record a give.",
TIMEOUTS.STANDARD,
NOTIFY_GIFTED_DETAILS_NO_IDENTIFIER.message,
TIMEOUTS.SHORT,
);
return;
}
if (parseFloat(this.amountInput) < 0) {
this.safeNotify.error(
"You may not send a negative number.",
NOTIFY_GIFT_ERROR_NEGATIVE_AMOUNT.message,
TIMEOUTS.SHORT,
);
return;
}
if (!this.description && !parseFloat(this.amountInput)) {
this.safeNotify.error(
`You must enter a description or some number of ${
this.libsUtil.UNIT_LONG[this.unitCode]
}.`,
NOTIFY_GIFT_ERROR_NO_DESCRIPTION.message.replace(
"{unit}",
this.libsUtil.UNIT_SHORT[this.unitCode] || this.unitCode,
),
TIMEOUTS.SHORT,
);
return;
@@ -350,7 +327,11 @@ export default class GiftedDialog extends Vue {
}
this.close();
this.safeNotify.toast("Recording the give...", undefined, TIMEOUTS.BRIEF);
this.safeNotify.toast(
NOTIFY_GIFTED_DETAILS_RECORDING_GIVE.message,
undefined,
TIMEOUTS.BRIEF,
);
// this is asynchronous, but we don't need to wait for it to complete
await this.recordGive(
(this.giver?.did as string) || null,
@@ -477,10 +458,13 @@ export default class GiftedDialog extends Vue {
name: contact.name || contact.did,
};
} else {
this.giver = {
did: "",
name: "Unnamed",
};
// Only set to "Unnamed" if no giver is currently set
if (!this.giver || !this.giver.did) {
this.giver = {
did: "",
name: "Unnamed",
};
}
}
this.firstStep = false;
}
@@ -490,6 +474,10 @@ export default class GiftedDialog extends Vue {
this.firstStep = true;
}
moveToStep2() {
this.firstStep = false;
}
async loadProjects() {
try {
const response = await fetch(this.apiServer + "/api/v2/report/plans", {
@@ -532,10 +520,13 @@ export default class GiftedDialog extends Vue {
name: contact.name || contact.did,
};
} else {
this.receiver = {
did: "",
name: "Unnamed",
};
// Only set to "Unnamed" if no receiver is currently set
if (!this.receiver || !this.receiver.did) {
this.receiver = {
did: "",
name: "Unnamed",
};
}
}
this.firstStep = false;
}
@@ -559,16 +550,13 @@ export default class GiftedDialog extends Vue {
giverName: this.giver?.name,
offerId: this.offerId,
fulfillsProjectId:
this.giverEntityType === "person" &&
this.recipientEntityType === "project"
? this.toProjectId
: undefined,
this.recipientEntityType === "project" ? this.toProjectId : undefined,
providerProjectId:
this.giverEntityType === "project" &&
this.recipientEntityType === "person"
this.giverEntityType === "project"
? this.giver?.handleId
: this.fromProjectId,
recipientDid: this.receiver?.did,
recipientDid:
this.recipientEntityType === "person" ? this.receiver?.did : undefined,
recipientName: this.receiver?.name,
unitCode: this.unitCode,
};

View File

@@ -159,25 +159,6 @@
</template>
<script lang="ts">
/* TODO: Human Testing Required - PlatformServiceMixin Migration */
// Priority: High | Migrated: 2025-07-06 | Author: Matthew Raymer
//
// TESTING NEEDED: Component migrated from legacy logConsoleAndDb to PlatformServiceMixin
// but requires human validation due to meeting component accessibility limitations.
//
// Test Scenarios Required:
// 1. Load members list with valid meeting password
// 2. Test member admission toggle (organizer role)
// 3. Test adding member as contact
// 4. Test error scenarios: network failure, invalid password, server errors
// 5. Verify error logging appears in console and database
// 6. Cross-platform testing: web, mobile, desktop
//
// Reference: docs/migration-testing/migration-checklist-MembersList.md
// Migration Details: Replaced 3 logConsoleAndDb() calls with this.$logAndConsole()
// Validation: Passes lint checks and TypeScript compilation
// Navigation: Contacts → Chair Icon → Start/Join Meeting → Members List
import { Component, Vue, Prop, Emit } from "vue-facing-decorator";
import {

View File

@@ -19,7 +19,8 @@ export enum AppString {
PROD_PARTNER_API_SERVER = "https://partner-api.endorser.ch",
TEST_PARTNER_API_SERVER = "https://test-partner-api.endorser.ch",
LOCAL_PARTNER_API_SERVER = "http://127.0.0.1:3002",
// eslint-disable-next-line @typescript-eslint/no-duplicate-enum-values
LOCAL_PARTNER_API_SERVER = "http://127.0.0.1:3000",
PROD_PUSH_SERVER = "https://timesafari.app",
TEST1_PUSH_SERVER = "https://test.timesafari.app",
@@ -46,9 +47,6 @@ export const DEFAULT_PARTNER_API_SERVER =
export const DEFAULT_PUSH_SERVER =
import.meta.env.VITE_DEFAULT_PUSH_SERVER || AppString.PROD_PUSH_SERVER;
// Production domain for sharing links (always use production URL for sharing)
export const PROD_SHARE_DOMAIN = AppString.PROD_PUSH_SERVER;
export const IMAGE_TYPE_PROFILE = "profile";
export const PASSKEYS_ENABLED =

View File

@@ -1191,17 +1191,6 @@ export const NOTIFY_GIFTED_DETAILS_NO_IDENTIFIER = {
message: "You must select an identifier before you can record a give.",
};
export const NOTIFY_GIFTED_DETAILS_PROJECT_PROVIDER_INFO = {
title: "Project Provider Info",
message:
"To select a project as a provider, you must open this page through a project.",
};
export const NOTIFY_GIFTED_DETAILS_BOTH_PROVIDER_SELECTED = {
title: "Invalid Selection",
message: "You cannot select both a giving project and person.",
};
export const NOTIFY_GIFTED_DETAILS_RECORDING_GIVE = {
title: "",
message: "Recording the give...",

View File

@@ -131,7 +131,7 @@ const MIGRATIONS = [
* @param extractMigrationNames - A function that extracts the names (string array) from "select name from migrations"
*/
export async function runMigrations<T>(
sqlExec: (sql: string, params?: unknown[]) => Promise<unknown>,
sqlExec: (sql: string, params?: unknown[]) => Promise<void>,
sqlQuery: (sql: string, params?: unknown[]) => Promise<T>,
extractMigrationNames: (result: T) => Set<string>,
): Promise<void> {

View File

@@ -32,6 +32,17 @@ export type ContactWithJsonStrings = Omit<Contact, "contactMethods"> & {
contactMethods?: string;
};
/**
* This is for those cases (eg. with a DB) where field values may be all primitives or may be JSON values.
* See src/db/databaseUtil.ts parseJsonField for more details.
*
* This is so that we can reuse most of the type and don't have to maintain another copy.
* Another approach uses typescript conditionals: https://chatgpt.com/share/6855cdc3-ab5c-8007-8525-726612016eb2
*/
export type ContactMaybeWithJsonStrings = Omit<Contact, "contactMethods"> & {
contactMethods?: string | Array<ContactMethod>;
};
export const ContactSchema = {
contacts: "&did, name", // no need to key by other things
};

View File

@@ -10,6 +10,8 @@ export type BoundingBox = {
/**
* Settings type encompasses user-specific configuration details.
*
* New entries that are boolean should also be added to PlatformServiceMixin._mapColumnsToValues
*/
export type Settings = {
// default entry is keyed with MASTER_SETTINGS_KEY; other entries are linked to an account with account ID

View File

@@ -1,10 +1,7 @@
// similar to VerifiableCredentialSubject... maybe rename this
export interface GenericVerifiableCredential {
"@context"?: string;
"@type": string;
name?: string;
description?: string;
agent?: string | { identifier: string };
"@type"?: string;
[key: string]: unknown;
}
@@ -47,7 +44,7 @@ export interface KeyMetaWithPrivate extends KeyMeta {
}
export interface QuantitativeValue extends GenericVerifiableCredential {
"@type": "QuantitativeValue";
"@type"?: "QuantitativeValue";
"@context"?: string;
amountOfThisGood: number;
unitCode: string;
@@ -97,8 +94,7 @@ export interface ClaimObject {
export interface VerifiableCredentialClaim {
"@context"?: string;
"@type": string;
type: string[];
"@type"?: string;
credentialSubject: ClaimObject;
[key: string]: unknown;
}

View File

@@ -212,13 +212,13 @@ const testRecursivelyOnStrings = (
// eslint-disable-next-line @typescript-eslint/no-explicit-any
export function containsHiddenDid(obj: any) {
return testRecursivelyOnStrings(isHiddenDid, obj);
return testRecursivelyOnStrings(obj, isHiddenDid);
}
// eslint-disable-next-line @typescript-eslint/no-explicit-any
export const containsNonHiddenDid = (obj: any) => {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
return testRecursivelyOnStrings((s: any) => isDid(s) && !isHiddenDid(s), obj);
return testRecursivelyOnStrings(obj, (s: any) => isDid(s) && !isHiddenDid(s));
};
export function stripEndorserPrefix(claimId: string) {
@@ -697,7 +697,6 @@ export function hydrateGive(
if (amount && !isNaN(amount)) {
const quantitativeValue: QuantitativeValue = {
"@type": "QuantitativeValue",
amountOfThisGood: amount,
unitCode: unitCode || "HUR",
};
@@ -1342,7 +1341,6 @@ export async function createEndorserJwtVcFromClaim(
vc: {
"@context": "https://www.w3.org/2018/credentials/v1",
"@type": "VerifiableCredential",
type: ["VerifiableCredential"],
credentialSubject: claim,
},
};
@@ -1380,7 +1378,6 @@ export async function createInviteJwt(
vc: {
"@context": "https://www.w3.org/2018/credentials/v1",
"@type": "VerifiableCredential",
type: ["VerifiableCredential"],
credentialSubject: vcClaim as unknown as ClaimObject, // Type assertion needed due to object being string
},
};

View File

@@ -4,6 +4,6 @@ export interface UserProfile {
locLon?: number;
locLat2?: number;
locLon2?: number;
issuerDid?: string;
issuerDid: string;
rowId?: string; // set on profile retrieved from server
}

View File

@@ -7,7 +7,7 @@ import { useClipboard } from "@vueuse/core";
import { DEFAULT_PUSH_SERVER, NotificationIface } from "../constants/app";
import { Account, AccountEncrypted } from "../db/tables/accounts";
import { Contact, ContactWithJsonStrings } from "../db/tables/contacts";
import { Contact } from "../db/tables/contacts";
import { DEFAULT_PASSKEY_EXPIRATION_MINUTES } from "../db/tables/settings";
import {
arrayBufferToBase64,
@@ -34,18 +34,7 @@ import { PlatformServiceFactory } from "../services/PlatformServiceFactory";
import { IIdentifier } from "@veramo/core";
import { DEFAULT_ROOT_DERIVATION_PATH } from "./crypto";
// Consolidate this with src/utils/PlatformServiceMixin._parseJsonField
function parseJsonField<T>(value: unknown, defaultValue: T): T {
if (typeof value === "string") {
try {
return JSON.parse(value);
} catch {
return defaultValue;
}
}
return (value as T) || defaultValue;
}
// Consolidate this with src/utils/PlatformServiceMixin.mapQueryResultToValues
function mapQueryResultToValues(
record: { columns: string[]; values: unknown[][] } | undefined,
): Array<Record<string, unknown>> {
@@ -68,10 +57,10 @@ async function getPlatformService() {
}
export interface GiverReceiverInputInfo {
did?: string;
did?: string; // only for people
name?: string;
image?: string;
handleId?: string;
handleId?: string; // only for projects
}
export enum OnboardPage {
@@ -191,9 +180,9 @@ export const nameForDid = (
did: string,
): string => {
if (did === activeDid) {
return "you";
return "You";
}
const contact = R.find((con) => con.did == did, contacts);
const contact = R.find((con) => con.did === did, contacts);
return nameForContact(contact);
};
@@ -806,7 +795,7 @@ export const contactToCsvLine = (contact: Contact): string => {
// Handle contactMethods array by stringifying it
const contactMethodsStr = contact.contactMethods
? escapeField(JSON.stringify(parseJsonField(contact.contactMethods, [])))
? escapeField(JSON.stringify(contact.contactMethods))
: "";
const fields = [
@@ -911,24 +900,12 @@ export interface DatabaseExport {
* @returns DatabaseExport object in the standardized format
*/
export const contactsToExportJson = (contacts: Contact[]): DatabaseExport => {
// Convert each contact to a plain object and ensure all fields are included
const rows = contacts.map((contact) => {
const exContact: ContactWithJsonStrings = R.omit(
["contactMethods"],
contact,
);
exContact.contactMethods = contact.contactMethods
? JSON.stringify(parseJsonField(contact.contactMethods, []))
: undefined;
return exContact;
});
return {
data: {
data: [
{
tableName: "contacts",
rows,
rows: contacts,
},
],
},

View File

@@ -79,7 +79,7 @@ window.addEventListener("unhandledrejection", (event) => {
});
// Electron-specific initialization
if (typeof window !== "undefined" && window.require) {
if (typeof window !== "undefined" && typeof window.require === "function") {
// We're in an Electron renderer process
logger.log("[Electron] Detected Electron renderer process");

View File

@@ -338,7 +338,7 @@ router.onError(errorHandler); // Assign the error handler to the router instance
* @param from - Source route
* @param next - Navigation function
*/
router.beforeEach(async (to, from, next) => {
router.beforeEach(async (to, _from, next) => {
try {
// Skip identity check for routes that handle identity creation manually
const skipIdentityRoutes = [

View File

@@ -134,7 +134,11 @@ class AbsurdSqlDatabaseService implements DatabaseService {
// An error is thrown without this pragma: "File has invalid page size. (the first block of a new file must be written first)"
await this.db.exec(`PRAGMA journal_mode=MEMORY;`);
const sqlExec = this.db.run.bind(this.db);
// Create wrapper functions that match the expected signatures
const sqlExec = async (sql: string, params?: unknown[]): Promise<void> => {
await this.db!.run(sql, params);
};
const sqlQuery = this.db.exec.bind(this.db);
// Extract the migration names for the absurd-sql format
@@ -178,14 +182,6 @@ class AbsurdSqlDatabaseService implements DatabaseService {
}
operation.resolve(result);
} catch (error) {
// logger.error( // DISABLED
// "Error while processing SQL queue:",
// error,
// " ... for sql:",
// operation.sql,
// " ... with params:",
// operation.params,
// );
logger.error(
"Error while processing SQL queue:",
error,
@@ -238,9 +234,6 @@ class AbsurdSqlDatabaseService implements DatabaseService {
// If initialized but no db, something went wrong
if (!this.db) {
// logger.error( // DISABLED
// `Database not properly initialized after await waitForInitialization() - initialized flag is true but db is null`,
// );
logger.error(
`Database not properly initialized after await waitForInitialization() - initialized flag is true but db is null`,
);

View File

@@ -11,7 +11,6 @@ import {
SQLiteConnection,
SQLiteDBConnection,
CapacitorSQLite,
capSQLiteChanges,
DBSQLiteValues,
} from "@capacitor-community/sqlite";
@@ -493,22 +492,17 @@ export class CapacitorPlatformService implements PlatformService {
* @param params - Optional parameters for prepared statements
* @returns Promise resolving to execution results
*/
const sqlExec = async (
sql: string,
params?: unknown[],
): Promise<capSQLiteChanges> => {
const sqlExec = async (sql: string, params?: unknown[]): Promise<void> => {
logger.debug(`🔧 [CapacitorMigration] Executing SQL:`, sql);
if (params && params.length > 0) {
// Use run method for parameterized queries (prepared statements)
// This is essential for proper parameter binding and SQL injection prevention
const result = await this.db!.run(sql, params);
return result;
await this.db!.run(sql, params);
} else {
// Use execute method for non-parameterized queries
// This is more efficient for simple DDL statements
const result = await this.db!.execute(sql);
return result;
await this.db!.execute(sql);
}
};

View File

@@ -11,7 +11,7 @@
class="px-4 py-2 rounded mr-2 transition-colors"
@click="testInsert"
>
Test Insert
Test Contact Insert
</button>
<button
:class="
@@ -22,7 +22,7 @@
class="px-4 py-2 rounded mr-2 transition-colors"
@click="testUpdate"
>
Test Update
Test Contact Update
</button>
<button
:class="
@@ -44,7 +44,7 @@
class="px-4 py-2 rounded mr-2 transition-colors"
@click="testDatabaseStorage"
>
Test Database Storage Format
Test SearchBox Database Storage -- Beware: Changes Your Search Box
</button>
<button
:class="

13
src/types/electron.d.ts vendored Normal file
View File

@@ -0,0 +1,13 @@
declare global {
interface Window {
electronAPI?: {
exportData: (fileName: string, content: string) => Promise<{
success: boolean;
path?: string;
error?: string;
}>;
};
}
}
export {};

View File

@@ -50,7 +50,7 @@ import {
type SettingsWithJsonStrings,
} from "@/db/tables/settings";
import { logger } from "@/utils/logger";
import { Contact } from "@/db/tables/contacts";
import { Contact, ContactMaybeWithJsonStrings } from "@/db/tables/contacts";
import { Account } from "@/db/tables/accounts";
import { Temp } from "@/db/tables/temp";
import { QueryExecResult, DatabaseExecResult } from "@/interfaces/database";
@@ -246,6 +246,15 @@ export const PlatformServiceMixin = {
// Keep null values as null
}
// Handle JSON fields like contactMethods
if (column === "contactMethods" && typeof value === "string") {
try {
value = JSON.parse(value);
} catch {
value = [];
}
}
obj[column] = value;
});
return obj;
@@ -459,13 +468,10 @@ export const PlatformServiceMixin = {
return settings;
} catch (error) {
logger.error(
`[${(this as unknown as VueComponentWithMixin).$options.name}] Failed to get settings:`,
{
key,
error,
},
);
logger.error(`[Settings Trace] ❌ Failed to get settings:`, {
key,
error,
});
return fallback;
}
},
@@ -533,14 +539,11 @@ export const PlatformServiceMixin = {
return mergedSettings;
} catch (error) {
logger.error(
`[${(this as unknown as VueComponentWithMixin).$options.name}] Failed to get merged settings:`,
{
defaultKey,
accountDid,
error,
},
);
logger.error(`[Settings Trace] ❌ Failed to get merged settings:`, {
defaultKey,
accountDid,
error,
});
return defaultFallback;
}
},
@@ -648,15 +651,81 @@ export const PlatformServiceMixin = {
// CACHED SPECIALIZED SHORTCUTS (massive performance boost)
// =================================================
/**
* Normalize contact data by parsing JSON strings into proper objects
* Handles the contactMethods field which can be either a JSON string or an array
* @param rawContacts Raw contact data from database
* @returns Normalized Contact[] array
*/
$normalizeContacts(rawContacts: ContactMaybeWithJsonStrings[]): Contact[] {
return rawContacts.map((contact) => {
// Create a new contact object with proper typing
const normalizedContact: Contact = {
did: contact.did,
iViewContent: contact.iViewContent,
name: contact.name,
nextPubKeyHashB64: contact.nextPubKeyHashB64,
notes: contact.notes,
profileImageUrl: contact.profileImageUrl,
publicKeyBase64: contact.publicKeyBase64,
seesMe: contact.seesMe,
registered: contact.registered,
};
// Handle contactMethods field which can be a JSON string or an array
if (contact.contactMethods !== undefined) {
if (typeof contact.contactMethods === "string") {
// Parse JSON string into array
normalizedContact.contactMethods = this._parseJsonField(
contact.contactMethods,
[],
);
} else if (Array.isArray(contact.contactMethods)) {
// Validate that each item in the array is a proper ContactMethod object
normalizedContact.contactMethods = contact.contactMethods.filter(
(method) => {
const isValid =
method &&
typeof method === "object" &&
typeof method.label === "string" &&
typeof method.type === "string" &&
typeof method.value === "string";
if (!isValid && method !== undefined) {
console.warn(
"[ContactNormalization] Invalid contact method:",
method,
);
}
return isValid;
},
);
} else {
// Invalid data, use empty array
normalizedContact.contactMethods = [];
}
} else {
// No contactMethods, use empty array
normalizedContact.contactMethods = [];
}
return normalizedContact;
});
},
/**
* Load all contacts (always fresh) - $contacts()
* Always fetches fresh data from database for consistency
* @returns Promise<Contact[]> Array of contact objects
* Handles JSON string/object duality for contactMethods field
* @returns Promise<Contact[]> Array of normalized contact objects
*/
async $contacts(): Promise<Contact[]> {
return (await this.$query(
const rawContacts = (await this.$query(
"SELECT * FROM contacts ORDER BY name",
)) as Contact[];
)) as ContactMaybeWithJsonStrings[];
return this.$normalizeContacts(rawContacts);
},
/**
@@ -748,25 +817,20 @@ export const PlatformServiceMixin = {
);
mergedSettings.apiServer = DEFAULT_ENDORSER_API_SERVER;
logger.debug(
`[Electron Settings] Forced API server to: ${DEFAULT_ENDORSER_API_SERVER}`,
);
}
// Merge with any provided defaults (these take highest precedence)
const finalSettings = { ...mergedSettings, ...defaults };
console.log(
"[PlatformServiceMixin] $accountSettings",
JSON.stringify(finalSettings, null, 2),
// Filter out undefined and empty string values to prevent overriding real settings
const filteredDefaults = Object.fromEntries(
Object.entries(defaults).filter(
([_, value]) => value !== undefined && value !== "",
),
);
const finalSettings = { ...mergedSettings, ...filteredDefaults };
return finalSettings;
} catch (error) {
logger.error(
"[PlatformServiceMixin] Error in $accountSettings:",
error,
);
logger.error("[Settings Trace] ❌ Error in $accountSettings:", error);
// Fallback to defaults on error
return defaults;
@@ -996,12 +1060,18 @@ export const PlatformServiceMixin = {
contact.profileImageUrl !== undefined
? contact.profileImageUrl
: null,
contactMethods:
contact.contactMethods !== undefined
? Array.isArray(contact.contactMethods)
? JSON.stringify(contact.contactMethods)
: contact.contactMethods
: null,
};
await this.$dbExec(
`INSERT OR REPLACE INTO contacts
(did, name, publicKeyBase64, seesMe, registered, nextPubKeyHashB64, profileImageUrl)
VALUES (?, ?, ?, ?, ?, ?, ?)`,
(did, name, publicKeyBase64, seesMe, registered, nextPubKeyHashB64, profileImageUrl, contactMethods)
VALUES (?, ?, ?, ?, ?, ?, ?, ?)`,
[
safeContact.did,
safeContact.name,
@@ -1010,6 +1080,7 @@ export const PlatformServiceMixin = {
safeContact.registered,
safeContact.nextPubKeyHashB64,
safeContact.profileImageUrl,
safeContact.contactMethods,
],
);
return true;
@@ -1037,7 +1108,13 @@ export const PlatformServiceMixin = {
Object.entries(changes).forEach(([key, value]) => {
if (value !== undefined) {
setParts.push(`${key} = ?`);
params.push(value);
// Handle contactMethods field - convert array to JSON string
if (key === "contactMethods" && Array.isArray(value)) {
params.push(JSON.stringify(value));
} else {
params.push(value);
}
}
});
@@ -1059,45 +1136,36 @@ export const PlatformServiceMixin = {
/**
* Get all contacts as typed objects - $getAllContacts()
* Eliminates verbose query + mapping patterns
* @returns Promise<Contact[]> Array of contact objects
* Handles JSON string/object duality for contactMethods field
* @returns Promise<Contact[]> Array of normalized contact objects
*/
async $getAllContacts(): Promise<Contact[]> {
const results = await this.$dbQuery(
"SELECT did, name, publicKeyBase64, seesMe, registered, nextPubKeyHashB64, profileImageUrl FROM contacts ORDER BY name",
);
const rawContacts = (await this.$query(
"SELECT * FROM contacts ORDER BY name",
)) as ContactMaybeWithJsonStrings[];
return this.$mapResults(results, (row: unknown[]) => ({
did: row[0] as string,
name: row[1] as string,
publicKeyBase64: row[2] as string,
seesMe: Boolean(row[3]),
registered: Boolean(row[4]),
nextPubKeyHashB64: row[5] as string,
profileImageUrl: row[6] as string,
}));
return this.$normalizeContacts(rawContacts);
},
/**
* Get single contact by DID - $getContact()
* Eliminates verbose single contact query patterns
* Handles JSON string/object duality for contactMethods field
* @param did Contact DID to retrieve
* @returns Promise<Contact | null> Contact object or null if not found
* @returns Promise<Contact | null> Normalized contact object or null if not found
*/
async $getContact(did: string): Promise<Contact | null> {
const results = await this.$dbQuery(
const rawContacts = (await this.$query(
"SELECT * FROM contacts WHERE did = ?",
[did],
);
)) as ContactMaybeWithJsonStrings[];
if (!results || !results.values || results.values.length === 0) {
if (rawContacts.length === 0) {
return null;
}
const contactData = this._mapColumnsToValues(
results.columns,
results.values,
);
return contactData.length > 0 ? (contactData[0] as Contact) : null;
const normalizedContacts = this.$normalizeContacts(rawContacts);
return normalizedContacts[0];
},
/**
@@ -1692,6 +1760,7 @@ declare module "@vue/runtime-core" {
$contactCount(): Promise<number>;
$settings(defaults?: Settings): Promise<Settings>;
$accountSettings(did?: string, defaults?: Settings): Promise<Settings>;
$normalizeContacts(rawContacts: ContactMaybeWithJsonStrings[]): Contact[];
// Settings update shortcuts (eliminate 90% boilerplate)
$saveSettings(changes: Partial<Settings>): Promise<boolean>;

View File

@@ -1661,14 +1661,12 @@ export default class AccountViewView extends Vue {
}
onShareInfo() {
// Call the existing logic for sharing info, e.g., open the share dialog
this.openShareDialog();
}
// Placeholder for share dialog logic
openShareDialog() {
// TODO: Implement share dialog logic
this.notify.info("Share dialog not yet implemented.");
// Navigate to QR code sharing page - mobile uses full scan, web uses basic
if (Capacitor.isNativePlatform()) {
this.$router.push({ name: "contact-qr-scan-full" });
} else {
this.$router.push({ name: "contact-qr" });
}
}
onRecheckLimits() {

View File

@@ -199,7 +199,16 @@
/>
</button>
</div>
<GiftedDialog ref="customGiveDialog" />
<GiftedDialog
ref="customGiveDialog"
:giver-entity-type="'person'"
:recipient-entity-type="projectInfo ? 'project' : 'person'"
:to-project-id="
detailsForGive?.fulfillsPlanHandleId ||
detailsForOffer?.fulfillsPlanHandleId ||
''
"
/>
<div v-if="libsUtil.isGiveAction(veriClaim)">
<div class="flex columns-3">
@@ -549,6 +558,12 @@ export default class ClaimView extends Vue {
fulfillsHandleId?: string;
} | null = null;
detailsForOffer: { fulfillsPlanHandleId?: string } | null = null;
// Project information for fulfillsPlanHandleId
projectInfo: {
name: string;
imageUrl?: string;
issuer: string;
} | null = null;
fullClaim = null;
fullClaimDump = "";
fullClaimMessage = "";
@@ -674,6 +689,7 @@ export default class ClaimView extends Vue {
this.confsVisibleToIdList = [];
this.detailsForGive = null;
this.detailsForOffer = null;
this.projectInfo = null;
this.fullClaim = null;
this.fullClaimDump = "";
this.fullClaimMessage = "";
@@ -851,6 +867,14 @@ export default class ClaimView extends Vue {
}
}
// Load project information if there's a fulfillsPlanHandleId
const planHandleId =
this.detailsForGive?.fulfillsPlanHandleId ||
this.detailsForOffer?.fulfillsPlanHandleId;
if (planHandleId) {
await this.loadProjectInfo(planHandleId, userDid);
}
// retrieve the list of confirmers
const confirmerInfo = await libsUtil.retrieveConfirmerIdList(
this.apiServer,
@@ -878,6 +902,33 @@ export default class ClaimView extends Vue {
}
}
async loadProjectInfo(planHandleId: string, userDid: string) {
const url =
this.apiServer +
"/api/claim/byHandle/" +
encodeURIComponent(planHandleId);
const headers = await serverUtil.getHeaders(userDid);
try {
const resp = await this.axios.get(url, { headers });
if (resp.status === 200) {
this.projectInfo = {
name: resp.data.claim?.name || "(no name)",
imageUrl: resp.data.claim?.image,
issuer: resp.data.issuer,
};
} else {
await this.$logError(
"Error getting project info: " + JSON.stringify(resp),
);
}
} catch (error: unknown) {
await this.$logError(
"Error retrieving project info: " + JSON.stringify(error),
);
}
}
async showFullClaim(claimId: string) {
const url =
this.apiServer + "/api/claim/full/" + encodeURIComponent(claimId);
@@ -997,11 +1048,52 @@ export default class ClaimView extends Vue {
this.veriClaim as GenericCredWrapper<OfferClaim>,
),
};
// Determine recipient based on whether it's a project or person
let recipient: libsUtil.GiverReceiverInputInfo | undefined;
if (this.projectInfo) {
// Recipient is a project
recipient = {
name: this.projectInfo.name,
handleId: this.detailsForOffer?.fulfillsPlanHandleId,
image: this.projectInfo.imageUrl,
};
} else {
// Recipient is a person - we need to determine who that person is
// For offers, the recipient is typically the person who made the offer
const offerClaim = this.veriClaim.claim as OfferClaim;
const recipientDid =
offerClaim.recipient?.identifier || this.veriClaim.issuer;
if (recipientDid) {
const recipientContact = serverUtil.contactForDid(
recipientDid,
this.allContacts,
);
recipient = {
did: recipientDid,
name: recipientContact?.name || recipientDid,
};
}
}
// Extract offer information from the claim
const offerClaim = this.veriClaim.claim as OfferClaim;
const description =
offerClaim.itemOffered?.description || offerClaim.description;
const amount =
offerClaim.includesObject?.amountOfThisGood?.toString() || "0";
const unitCode = offerClaim.includesObject?.unitCode || "HUR";
(this.$refs.customGiveDialog as GiftedDialog).open(
giver,
undefined,
recipient,
this.veriClaim.handleId,
"Offer fulfilled by " + (giver?.name || "someone not named"),
undefined, // prompt
description,
amount,
unitCode,
);
}

View File

@@ -66,10 +66,11 @@
</ul>
<GiftedDialog
ref="customDialog"
ref="giftedDialog"
:giver-entity-type="giverEntityType"
:recipient-entity-type="recipientEntityType"
:from-project-id="fromProjectId"
:to-project-id="toProjectId"
:show-projects="showProjects"
:is-from-project-view="isFromProjectView"
/>
</section>
@@ -102,9 +103,11 @@ export default class ContactGiftingView extends Vue {
activeDid = "";
allContacts: Array<Contact> = [];
apiServer = "";
description = "";
projectId = "";
prompt = "";
description = "";
amountInput = "0";
unitCode = "HUR";
recipientProjectName = "";
recipientProjectImage = "";
recipientProjectHandleId = "";
@@ -123,6 +126,7 @@ export default class ContactGiftingView extends Vue {
toProjectId = "";
showProjects = false;
isFromProjectView = false;
offerId = "";
async created() {
this.notify = createNotifyHelpers(this.$notify);
@@ -143,6 +147,9 @@ export default class ContactGiftingView extends Vue {
this.recipientProjectHandleId =
(this.$route.query["recipientProjectHandleId"] as string) || "";
this.prompt = (this.$route.query["prompt"] as string) ?? this.prompt;
this.description = (this.$route.query["description"] as string) || "";
this.amountInput = (this.$route.query["amountInput"] as string) || "0";
this.unitCode = (this.$route.query["unitCode"] as string) || "HUR";
// Read new context parameters
this.stepType = (this.$route.query["stepType"] as string) || "giver";
@@ -168,6 +175,7 @@ export default class ContactGiftingView extends Vue {
(this.$route.query["showProjects"] as string) === "true";
this.isFromProjectView =
(this.$route.query["isFromProjectView"] as string) === "true";
this.offerId = (this.$route.query["offerId"] as string) || "";
// eslint-disable-next-line @typescript-eslint/no-explicit-any
} catch (err: any) {
@@ -182,12 +190,12 @@ export default class ContactGiftingView extends Vue {
openDialog(contact?: GiverReceiverInputInfo | "Unnamed") {
if (contact === "Unnamed") {
// Special case: Pass undefined to trigger Step 1, but with "Unnamed" pre-selected
// Special case: Handle "Unnamed" contacts for both givers and recipients
let recipient: GiverReceiverInputInfo;
let giver: GiverReceiverInputInfo | undefined;
if (this.stepType === "giver") {
// We're selecting a giver, so recipient is either a project or the current user
// We're selecting a giver, so preserve the existing recipient from context
if (this.recipientEntityType === "project") {
recipient = {
did: this.recipientProjectHandleId,
@@ -196,64 +204,26 @@ export default class ContactGiftingView extends Vue {
handleId: this.recipientProjectHandleId,
};
} else {
recipient = { did: this.activeDid, name: "You" };
// 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
if (this.giverEntityType === "project") {
giver = {
// no did, because it's a project
name: this.giverProjectName,
image: this.giverProjectImage,
handleId: this.giverProjectHandleId,
};
} else if (this.giverDid) {
giver = {
did: this.giverDid,
name: this.giverProjectName || "Someone",
};
} else {
giver = { did: this.activeDid, name: "You" };
}
}
(this.$refs.customDialog as GiftedDialog).open(
giver,
recipient,
undefined,
this.stepType === "giver" ? "Given by Unnamed" : "Given to Unnamed",
this.prompt,
);
// Immediately select "Unnamed" and move to Step 2
(this.$refs.customDialog as GiftedDialog).selectGiver();
} else {
// Regular case: contact is a GiverReceiverInputInfo
let giver: GiverReceiverInputInfo;
let recipient: GiverReceiverInputInfo;
if (this.stepType === "giver") {
// We're selecting a giver, so the contact becomes the giver
giver = contact as GiverReceiverInputInfo; // Safe because we know contact is not "Unnamed" or undefined
// Recipient is either a project or the current user
if (this.recipientEntityType === "project") {
recipient = {
did: this.recipientProjectHandleId,
name: this.recipientProjectName,
image: this.recipientProjectImage,
handleId: this.recipientProjectHandleId,
};
} else {
recipient = { did: this.activeDid, name: "You" };
}
} else {
// We're selecting a recipient, so the contact becomes the recipient
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 = {
@@ -272,15 +242,93 @@ export default class ContactGiftingView extends Vue {
}
}
(this.$refs.customDialog as GiftedDialog).open(
(this.$refs.giftedDialog as GiftedDialog).open(
giver,
recipient,
undefined,
this.stepType === "giver"
? "Given by " + (contact?.name || "someone not named")
: "Given to " + (contact?.name || "someone not named"),
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();
} else {
// Regular case: contact is a GiverReceiverInputInfo
let giver: GiverReceiverInputInfo;
let recipient: GiverReceiverInputInfo;
if (this.stepType === "giver") {
// We're selecting a giver, so the contact becomes the giver
giver = contact as GiverReceiverInputInfo; // Safe because we know contact is not "Unnamed" or undefined
// Preserve the existing recipient from the context
if (this.recipientEntityType === "project") {
recipient = {
did: this.recipientProjectHandleId,
name: this.recipientProjectName,
image: this.recipientProjectImage,
handleId: this.recipientProjectHandleId,
};
} else {
// Check if the preserved recipient was "You" or a regular contact
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 "Unnamed"
recipient = { did: "", name: "Unnamed" };
}
}
} else {
// We're selecting a recipient, so the contact becomes the recipient
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,
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();
}
}
}

View File

@@ -253,22 +253,6 @@ import { createNotifyHelpers, TIMEOUTS } from "@/utils/notify";
*
* @component
*/
// TODO: Testing Required - Database Operations + Logging Migration to PlatformServiceMixin
// Priority: Medium | Migrated: 2025-07-06 | Author: Matthew Raymer
//
//
// TESTING NEEDED: Contact import functionality
// 1. Test contact import via URL: /contact-import?contacts=[{"did":"did:example:123","name":"Alice"}]
// 2. Test JWT import via URL path: /contact-import/[JWT_TOKEN]
// 3. Test manual JWT input via textarea
// 4. Test duplicate contact detection and field comparison
// 5. Test error scenarios: invalid JWT, malformed data, network issues
// 6. Verify error logging appears correctly
//
// Test URLs:
// /contact-import (manual input)
// /contact-import?contacts=[{"did":"did:test:123","name":"Test User"}]
@Component({
components: { EntityIcon, OfferDialog, QuickNav },
mixins: [PlatformServiceMixin],

View File

@@ -151,9 +151,9 @@ import { getContactJwtFromJwtUrl } from "../libs/crypto";
import {
CONTACT_CSV_HEADER,
CONTACT_IMPORT_CONFIRM_URL_PATH_TIME_SAFARI,
generateEndorserJwtUrlForAccount,
register,
setVisibilityUtil,
generateEndorserJwtUrlForAccount,
} from "../libs/endorserServer";
import { decodeEndorserJwt, ETHR_DID_PREFIX } from "../libs/crypto/vc";
import * as libsUtil from "../libs/util";
@@ -162,7 +162,6 @@ import { logger } from "../utils/logger";
import { QRScannerFactory } from "@/services/QRScanner/QRScannerFactory";
import { CameraState } from "@/services/QRScanner/types";
import { PlatformServiceMixin } from "@/utils/PlatformServiceMixin";
import { Account } from "@/db/tables/accounts";
import { createNotifyHelpers } from "@/utils/notify";
import {
NOTIFY_QR_INITIALIZATION_ERROR,
@@ -189,6 +188,7 @@ import {
QR_TIMEOUT_STANDARD,
QR_TIMEOUT_LONG,
} from "@/constants/notifications";
import { Account } from "@/db/tables/accounts";
interface QRScanResult {
rawValue?: string;
@@ -547,6 +547,7 @@ export default class ContactQRScanShow extends Vue {
name: contact.name,
});
this.notify.toast(
"Submitted",
NOTIFY_QR_REGISTRATION_SUBMITTED.message,
QR_TIMEOUT_SHORT,
);
@@ -611,21 +612,33 @@ export default class ContactQRScanShow extends Vue {
}
async onCopyUrlToClipboard() {
const account = (await libsUtil.retrieveFullyDecryptedAccount(
this.activeDid,
)) as Account;
const jwtUrl = await generateEndorserJwtUrlForAccount(
account,
this.isRegistered,
this.givenName,
this.profileImageUrl,
true,
);
useClipboard()
.copy(jwtUrl)
.then(() => {
this.notify.toast(NOTIFY_QR_URL_COPIED.message, QR_TIMEOUT_MEDIUM);
});
try {
// Generate URL for sharing
const account = (await libsUtil.retrieveFullyDecryptedAccount(
this.activeDid,
)) as Account;
const jwtUrl = await generateEndorserJwtUrlForAccount(
account,
this.isRegistered,
this.givenName,
this.profileImageUrl,
true,
);
// Copy the URL to clipboard
useClipboard()
.copy(jwtUrl)
.then(() => {
this.notify.toast(
"Copied",
NOTIFY_QR_URL_COPIED.message,
QR_TIMEOUT_MEDIUM,
);
});
} catch (error) {
logger.error("Failed to generate contact URL:", error);
this.notify.error("Failed to generate contact URL. Please try again.");
}
}
toastQRCodeHelp() {

View File

@@ -107,7 +107,11 @@
@copy-selected="copySelectedContacts"
/>
<GiftedDialog ref="customGivenDialog" />
<GiftedDialog
ref="customGivenDialog"
:giver-entity-type="'person'"
:recipient-entity-type="'person'"
/>
<OfferDialog ref="customOfferDialog" />
<ContactNameDialog ref="contactNameDialog" />
@@ -1049,7 +1053,6 @@ export default class ContactsView extends Vue {
}
let callback: (amount: number) => void;
let customTitle = "";
// choose whether to open dialog to user or from user
if (giverDid == this.activeDid) {
callback = (amount: number) => {
@@ -1057,7 +1060,6 @@ export default class ContactsView extends Vue {
newList[recipientDid] = (newList[recipientDid] || 0) + amount;
this.givenByMeUnconfirmed = newList;
};
customTitle = "Given to " + (receiver?.name || "Someone Unnamed");
} else {
// must be (recipientDid == this.activeDid)
callback = (amount: number) => {
@@ -1065,13 +1067,14 @@ export default class ContactsView extends Vue {
newList[giverDid] = (newList[giverDid] || 0) + amount;
this.givenToMeUnconfirmed = newList;
};
customTitle = "Received from " + (giver?.name || "Someone Unnamed");
}
(this.$refs.customGivenDialog as GiftedDialog).open(
giver,
receiver,
undefined as unknown as string,
customTitle,
undefined as unknown as string,
undefined as unknown as string,
undefined as unknown as string,
undefined as unknown as string,
callback,
);

View File

@@ -7,13 +7,13 @@
<!-- Breadcrumb -->
<div id="ViewBreadcrumb" class="mb-8">
<h1 id="ViewHeading" class="text-lg text-center font-light relative px-7">
<!-- Back -->
<button
<!-- Go to 'contacts' instead of just 'back' because they could get here from an edit page (and going back there is annoying). -->
<router-link
:to="{ name: 'contacts' }"
class="text-lg text-center px-2 py-1 absolute -left-2 -top-1"
@click="goBack"
>
<font-awesome icon="chevron-left" class="fa-fw"></font-awesome>
</button>
</router-link>
Identifier Details
</h1>
</div>

View File

@@ -48,24 +48,12 @@
placeholder="What was received"
/>
<div class="flex mb-4">
<button
class="rounded-s border border-e-0 border-slate-400 bg-slate-200 px-4 py-2"
@click="amountInput === '0' ? null : decrement()"
>
<font-awesome icon="chevron-left" />
</button>
<input
id="inputGivenAmount"
v-model="amountInput"
type="number"
class="flex-1 border border-e-0 border-slate-400 px-2 py-2 text-center w-[1px]"
<AmountInput
:value="parseFloat(amountInput) || 0"
:min="0"
input-id="inputGivenAmount"
:on-update-value="handleAmountChange"
/>
<button
class="rounded-e border border-slate-400 bg-slate-200 px-4 py-2"
@click="increment()"
>
<font-awesome icon="chevron-right" />
</button>
<select
v-model="unitCode"
@@ -189,7 +177,7 @@
{{
recipientDid
? "This was given to " + recipientName + "."
: "No individual benefitted."
: "No named individual benefitted."
}}
</label>
<font-awesome
@@ -275,6 +263,7 @@ import { RouteLocationNormalizedLoaded, Router } from "vue-router";
import ImageMethodDialog from "../components/ImageMethodDialog.vue";
import QuickNav from "../components/QuickNav.vue";
import TopMessage from "../components/TopMessage.vue";
import AmountInput from "../components/AmountInput.vue";
import { DEFAULT_IMAGE_API_SERVER, NotificationIface } from "../constants/app";
import { GenericCredWrapper, GiveActionClaim } from "../interfaces";
import {
@@ -296,11 +285,11 @@ import {
NOTIFY_GIFTED_DETAILS_DELETE_IMAGE_CONFIRM,
NOTIFY_GIFTED_DETAILS_DELETE_IMAGE_ERROR,
NOTIFY_GIFTED_DETAILS_NO_IDENTIFIER,
NOTIFY_GIFTED_DETAILS_PROJECT_PROVIDER_INFO,
NOTIFY_GIFTED_DETAILS_BOTH_PROVIDER_SELECTED,
NOTIFY_GIFT_ERROR_NEGATIVE_AMOUNT,
NOTIFY_GIFTED_DETAILS_RECORDING_GIVE,
NOTIFY_GIFTED_DETAILS_CREATE_GIVE_ERROR,
NOTIFY_GIFTED_DETAILS_GIFT_RECORDED,
NOTIFY_GIFT_ERROR_NO_DESCRIPTION,
} from "@/constants/notifications";
@Component({
@@ -308,6 +297,7 @@ import {
ImageMethodDialog,
QuickNav,
TopMessage,
AmountInput,
},
mixins: [PlatformServiceMixin],
})
@@ -530,6 +520,10 @@ export default class GiftedDetails extends Vue {
)}`;
}
handleAmountChange(value: number): void {
this.amountInput = value.toString();
}
cancel() {
this.deleteImage(); // not awaiting, so they'll go back immediately
if (this.destinationPathAfter) {
@@ -611,14 +605,17 @@ export default class GiftedDetails extends Vue {
}
if (parseFloat(this.amountInput) < 0) {
this.notify.error(
NOTIFY_GIFTED_DETAILS_NO_IDENTIFIER.message,
NOTIFY_GIFT_ERROR_NEGATIVE_AMOUNT.message,
TIMEOUTS.SHORT,
);
return;
}
if (!this.description && !parseFloat(this.amountInput)) {
this.notify.error(
NOTIFY_GIFTED_DETAILS_NO_IDENTIFIER.message,
NOTIFY_GIFT_ERROR_NO_DESCRIPTION.message.replace(
"{unit}",
this.libsUtil.UNIT_SHORT[this.unitCode] || this.unitCode,
),
TIMEOUTS.SHORT,
);
return;
@@ -626,7 +623,8 @@ export default class GiftedDetails extends Vue {
this.notify.toast(
NOTIFY_GIFTED_DETAILS_RECORDING_GIVE.message,
TIMEOUTS.SHORT,
undefined,
TIMEOUTS.BRIEF,
);
// this is asynchronous, but we don't need to wait for it to complete
@@ -634,29 +632,32 @@ export default class GiftedDetails extends Vue {
}
notifyUserOfGiver() {
// there's no individual giver or there's a provider project
if (!this.giverDid) {
this.notify.warning(
NOTIFY_GIFTED_DETAILS_NO_IDENTIFIER.message,
"To assign a giver, you must choose a person in a previous step.",
TIMEOUTS.SHORT,
);
} else {
// must be because providedByProject is true
this.notify.warning(
NOTIFY_GIFTED_DETAILS_BOTH_PROVIDER_SELECTED.message,
"You cannot assign both a giver and a project.",
TIMEOUTS.SHORT,
);
}
}
notifyUserOfRecipient() {
// there's no individual recipient or there's a fulfills project
if (!this.recipientDid) {
this.notify.warning(
NOTIFY_GIFTED_DETAILS_NO_IDENTIFIER.message,
"To assign a recipient, you must choose a person in a previous step.",
TIMEOUTS.SHORT,
);
} else {
// must be because givenToProject is true
this.notify.warning(
NOTIFY_GIFTED_DETAILS_BOTH_PROVIDER_SELECTED.message,
"You cannot assign both to a recipient and to a project.",
TIMEOUTS.SHORT,
);
}
@@ -666,13 +667,13 @@ export default class GiftedDetails extends Vue {
// we're here because they clicked and either there is no provider project or there is a giver chosen
if (!this.providerProjectId) {
this.notify.warning(
NOTIFY_GIFTED_DETAILS_PROJECT_PROVIDER_INFO.message,
"To select a project as a provider, you must choose a project in a previous step.",
TIMEOUTS.SHORT,
);
} else {
// no providing project was chosen
// no providing project was chosen, so there must be an individual giver
this.notify.warning(
NOTIFY_GIFTED_DETAILS_BOTH_PROVIDER_SELECTED.message,
"You cannot select both a giving project and person.",
TIMEOUTS.SHORT,
);
}
@@ -682,13 +683,13 @@ export default class GiftedDetails extends Vue {
// we're here because they clicked and either there is no fulfills project or there is a recipient chosen
if (!this.fulfillsProjectId) {
this.notify.warning(
NOTIFY_GIFTED_DETAILS_PROJECT_PROVIDER_INFO.message,
"To assign a project as a recipient, you must choose a project in a previous step.",
TIMEOUTS.SHORT,
);
} else {
// no fulfills project was chosen
// no fulfills project was chosen, so there must be an individual recipient
this.notify.warning(
NOTIFY_GIFTED_DETAILS_BOTH_PROVIDER_SELECTED.message,
"You cannot select both a receiving project and person.",
TIMEOUTS.SHORT,
);
}

View File

@@ -132,7 +132,7 @@ Raymer * @version 1.0.0 */
<button
type="button"
class="text-center text-base 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-2 rounded-lg"
@click="openDialogPerson()"
@click="openPersonDialog()"
>
<font-awesome icon="user" />
Person
@@ -151,7 +151,11 @@ Raymer * @version 1.0.0 */
</div>
</div>
<GiftedDialog ref="customDialog" :show-projects="showProjectsDialog" />
<GiftedDialog
ref="giftedDialog"
:giver-entity-type="showProjectsDialog ? 'project' : 'person'"
:recipient-entity-type="'person'"
/>
<GiftedPrompts ref="giftedPrompts" />
<FeedFilters ref="feedFilters" />
@@ -309,7 +313,6 @@ import { PlatformServiceMixin } from "@/utils/PlatformServiceMixin";
import { createNotifyHelpers, TIMEOUTS } from "@/utils/notify";
import {
NOTIFY_CONTACT_LOADING_ISSUE,
NOTIFY_FEED_LOADING_ISSUE,
NOTIFY_CONFIRMATION_ERROR,
} from "@/constants/notifications";
import * as Package from "../../package.json";
@@ -485,6 +488,10 @@ export default class HomeView extends Vue {
if (newDid !== oldDid) {
// Re-initialize identity with new settings (loads settings internally)
await this.initializeIdentity();
} else {
logger.info(
"[HomeView Settings Trace] 📍 DID unchanged, skipping re-initialization",
);
}
}
@@ -527,11 +534,7 @@ export default class HomeView extends Vue {
// Load settings with better error context using ultra-concise mixin
let settings;
try {
settings = await this.$settings({
apiServer: "",
activeDid: "",
isRegistered: false,
});
settings = await this.$accountSettings();
} catch (error) {
this.$logAndConsole(
`[HomeView] Failed to retrieve settings: ${error}`,
@@ -599,65 +602,21 @@ export default class HomeView extends Vue {
// Ultra-concise settings update with automatic cache invalidation!
await this.$saveMySettings({ isRegistered: true });
this.isRegistered = true;
// Force Vue to re-render the template
await this.$nextTick();
}
} catch (error) {
// Consolidate logging: Only log unexpected errors, not expected 400s
const axiosError = error as any;
if (axiosError?.response?.status !== 400) {
this.$logAndConsole(
`[HomeView] Registration check failed: ${error}`,
true,
);
}
// Continue as unregistered - this is expected for new users
}
}
// Initialize feed and offers
try {
// Start feed update in background
this.updateAllFeed().catch((error) => {
this.$logAndConsole(
`[HomeView] Background feed update failed: ${error}`,
true,
logger.warn(
"[HomeView Settings Trace] ⚠️ Registration check failed",
{
error: error instanceof Error ? error.message : String(error),
},
);
});
// Load new offers if we have an active DID
if (this.activeDid) {
const [offersToUser, offersToProjects] = await Promise.all([
getNewOffersToUser(
this.axios,
this.apiServer,
this.activeDid,
this.lastAckedOfferToUserJwtId,
),
getNewOffersToUserProjects(
this.axios,
this.apiServer,
this.activeDid,
this.lastAckedOfferToUserProjectsJwtId,
),
]);
this.numNewOffersToUser = offersToUser.data.length;
this.newOffersToUserHitLimit = offersToUser.hitLimit;
this.numNewOffersToUserProjects = offersToProjects.data.length;
this.newOffersToUserProjectsHitLimit = offersToProjects.hitLimit;
}
} catch (error) {
this.$logAndConsole(
`[HomeView] Failed to initialize feed/offers: ${error}`,
true,
);
// Don't throw - we can continue with empty feed
this.notify.warning(NOTIFY_FEED_LOADING_ISSUE.message, TIMEOUTS.LONG);
}
} catch (error) {
this.handleError(error);
throw error; // Re-throw to be caught by mounted()
} catch (err: unknown) {
logger.error("[HomeView Settings Trace] ❌ initializeIdentity() failed", {
error: err instanceof Error ? err.message : String(err),
});
throw err;
}
}
@@ -682,10 +641,8 @@ export default class HomeView extends Vue {
}
/**
* Loads user settings from storage using ultra-concise mixin utilities
* Sets component state for:
* - API server, Active DID, Feed filters and view settings
* - Registration status, Notification acknowledgments
* Loads user settings from database using ultra-concise mixin
* Used for displaying settings in feed and actions
*
* @internal
* Called by mounted() and reloadFeedOnChange()
@@ -815,7 +772,7 @@ export default class HomeView extends Vue {
* Called by mounted()
*/
private async checkOnboarding() {
const settings = await this.$settings();
const settings = await this.$accountSettings();
if (!settings.finishedOnboarding) {
(this.$refs.onboardingDialog as OnboardingDialog).open(OnboardPage.Home);
}
@@ -1591,37 +1548,35 @@ export default class HomeView extends Vue {
* openGiftedPrompts() -> openDialog()
*
* @requires
* - this.$refs.customDialog
* - this.$refs.giftedDialog
* - this.activeDid
*
* @param giver Optional contact info for giver
* @param description Optional gift description
*/
openDialog(giver?: GiverReceiverInputInfo | "Unnamed", description?: string) {
openDialog(giver?: GiverReceiverInputInfo | "Unnamed", prompt?: string) {
if (giver === "Unnamed") {
// Special case: Pass undefined to trigger Step 1, but with "Unnamed" pre-selected
(this.$refs.customDialog as GiftedDialog).open(
(this.$refs.giftedDialog as GiftedDialog).open(
undefined,
{
did: this.activeDid,
name: "You",
} as GiverReceiverInputInfo,
undefined,
"Given by Unnamed",
description,
prompt,
);
// Immediately select "Unnamed" and move to Step 2
(this.$refs.customDialog as GiftedDialog).selectGiver();
(this.$refs.giftedDialog as GiftedDialog).selectGiver();
} else {
(this.$refs.customDialog as GiftedDialog).open(
(this.$refs.giftedDialog as GiftedDialog).open(
giver,
{
did: this.activeDid,
name: "You",
} as GiverReceiverInputInfo,
undefined,
"Given by " + (giver?.name || "someone not named"),
description,
prompt,
);
}
}
@@ -1658,17 +1613,6 @@ export default class HomeView extends Vue {
(this.$refs.feedFilters as FeedFilters).open(this.reloadFeedOnChange);
}
/**
* Shows toast notification to user
*
* @internal
* Used for various user notifications
* @param message Message to display
*/
toastUser(message: string) {
this.notify.toast("FYI", message, TIMEOUTS.SHORT);
}
/**
* Computes CSS classes for known person icons
*
@@ -1827,17 +1771,17 @@ export default class HomeView extends Vue {
}
}
openDialogPerson(
openPersonDialog(
giver?: GiverReceiverInputInfo | "Unnamed",
description?: string,
prompt?: string,
) {
this.showProjectsDialog = false;
this.openDialog(giver, description);
this.openDialog(giver, prompt);
}
openProjectDialog() {
this.showProjectsDialog = true;
(this.$refs.customDialog as GiftedDialog).open();
(this.$refs.giftedDialog as GiftedDialog).open();
}
/**

View File

@@ -214,7 +214,10 @@ export default class IdentitySwitcherView extends Vue {
}
} catch (err) {
this.notify.error(NOTIFY_ERROR_LOADING_ACCOUNTS.message, TIMEOUTS.LONG);
logger.error("Telling user to clear cache at page create because:", err);
logger.error(
"[IdentitySwitcher Settings Trace] ❌ Error loading accounts:",
err,
);
}
}
@@ -225,12 +228,35 @@ export default class IdentitySwitcherView extends Vue {
// Check if we need to load user-specific settings for the new DID
if (did) {
try {
await this.$accountSettings(did);
const newSettings = await this.$accountSettings(did);
logger.info(
"[IdentitySwitcher Settings Trace] ✅ New account settings loaded",
{
did,
settingsKeys: Object.keys(newSettings).filter(
(k) => (newSettings as any)[k] !== undefined,
),
},
);
} catch (error) {
logger.warn(
"[IdentitySwitcher Settings Trace] ⚠️ Error loading new account settings",
{
did,
error: error instanceof Error ? error.message : String(error),
},
);
// Handle error silently - user settings will be loaded when needed
}
}
logger.info(
"[IdentitySwitcher Settings Trace] 🔄 Navigating to home to trigger watcher",
{
newDid: did,
},
);
// Navigate to home page to trigger the watcher
this.$router.push({ name: "home" });
}

View File

@@ -166,10 +166,8 @@ export default class ImportAccountView extends Vue {
] as string;
const newDerivPath = nextDerivationPath(maxDerivPath);
const mne = selectedArray[0].mnemonic as string;
const [address, privateHex, publicHex] = deriveAddress(mne, newDerivPath);
const newId = newIdentifier(address, publicHex, privateHex, newDerivPath);
try {
@@ -187,7 +185,10 @@ export default class ImportAccountView extends Vue {
);
this.$router.push({ name: "account" });
} catch (err) {
logger.error("Error saving mnemonic & updating settings:", err);
logger.error(
"[ImportDerived Settings Trace] ❌ Error saving mnemonic & updating settings:",
err,
);
this.notify.error(NOTIFY_ACCOUNT_DERIVATION_ERROR.message, TIMEOUTS.LONG);
}
}

View File

@@ -216,6 +216,8 @@
<GiftedDialog
ref="giveDialogToThis"
:giver-entity-type="'person'"
:recipient-entity-type="'project'"
:to-project-id="projectId"
:is-from-project-view="true"
/>
@@ -486,8 +488,9 @@
</div>
<GiftedDialog
ref="giveDialogFromThis"
:giver-entity-type="'project'"
:recipient-entity-type="'person'"
:from-project-id="projectId"
:show-projects="true"
:is-from-project-view="true"
/>
@@ -1155,7 +1158,6 @@ export default class ProjectViewView extends Vue {
undefined,
undefined,
undefined,
"Given by Unnamed to this project",
);
// Immediately select "Unnamed" and move to Step 2
(this.$refs.giveDialogToThis as GiftedDialog).selectGiver();
@@ -1173,7 +1175,6 @@ export default class ProjectViewView extends Vue {
image: this.imageUrl,
},
undefined,
`Given to ${this.name}`,
);
}
}
@@ -1189,7 +1190,6 @@ export default class ProjectViewView extends Vue {
},
{ did: this.activeDid, name: "You" },
undefined,
`${this.name} gave to you`,
undefined,
undefined,
);
@@ -1237,9 +1237,17 @@ export default class ProjectViewView extends Vue {
};
(this.$refs.giveDialogToThis as GiftedDialog).open(
giver,
undefined,
{
did: offer.issuerDid,
name: this.name,
handleId: this.projectId,
image: this.imageUrl,
},
offer.handleId,
"Given by " + (giver?.name || "someone not named"),
undefined,
offer.objectDescription,
offer.amount.toString(),
offer.unit,
);
}

View File

@@ -638,16 +638,18 @@ export default class ProjectsView extends Vue {
* - Alternative sharing methods for remote users
*/
promptForShareMethod() {
this.notify.confirm(
NOTIFY_CAMERA_SHARE_METHOD.title,
NOTIFY_CAMERA_SHARE_METHOD.text,
this.$notify(
{
group: "modal",
type: "confirm",
title: NOTIFY_CAMERA_SHARE_METHOD.title,
text: NOTIFY_CAMERA_SHARE_METHOD.text,
onYes: () => this.handleQRCodeClick(),
onNo: () => this.$router.push({ name: "share-my-contact-info" }),
yesText: NOTIFY_CAMERA_SHARE_METHOD.yesText,
noText: NOTIFY_CAMERA_SHARE_METHOD.noText,
timeout: TIMEOUTS.MODAL,
},
TIMEOUTS.MODAL,
);
}

View File

@@ -4,9 +4,7 @@
"skipLibCheck": true,
"module": "ESNext",
"moduleResolution": "bundler",
"allowSyntheticDefaultImports": true,
"allowImportingTsExtensions": true,
"noEmit": true
"allowSyntheticDefaultImports": true
},
"include": ["vite.config.*"]
}