Compare commits
87 Commits
build-dev-
...
build-impr
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
de47829dc2 | ||
|
|
91e46f435e | ||
|
|
d086ab2f46 | ||
| ff61a0bdf3 | |||
| e0b9481be5 | |||
| a11ff04afa | |||
| e8bf8014b4 | |||
| c1713e1b0b | |||
|
|
0277b05fef | ||
|
|
d5db39878c | ||
|
|
778d00c2a4 | ||
|
|
4f9fb068c8 | ||
|
|
0eb8d3d50e | ||
|
|
f98d6c7020 | ||
|
|
bf08e57ce7 | ||
|
|
18e6aa5a9a | ||
|
|
795df6a8fb | ||
|
|
919b48e61f | ||
|
|
3c37ead60d | ||
|
|
6868a322f1 | ||
| 783ad6e122 | |||
| 1f1739f00c | |||
|
|
ed0f49656d | ||
|
|
75e8b34e88 | ||
|
|
b267d1bc66 | ||
| 2a34d0e2d1 | |||
|
|
4480778a49 | ||
|
|
607bb50a55 | ||
|
|
5ae0535935 | ||
|
|
c27caf8887 | ||
| b17642fbcb | |||
|
|
974d33b322 | ||
|
|
3b1a63468c | ||
|
|
1d6418b02c | ||
|
|
b681905abd | ||
|
|
32f589b866 | ||
| 938cf673fc | |||
| 984244117b | |||
|
|
0bd0e7c332 | ||
|
|
aed16ebe94 | ||
|
|
06f3a4c7c2 | ||
|
|
371cf763c8 | ||
|
|
3d38cb89a9 | ||
| fb2ac963bd | |||
| e5e01040b2 | |||
| 197dea48c9 | |||
|
|
54bfaafbd0 | ||
|
|
a63ccae9b1 | ||
|
|
c30b94dcc7 | ||
|
|
e741790d70 | ||
|
|
404a7cbc71 | ||
|
|
8b2c6714ec | ||
|
|
9cd4551bed | ||
|
|
f4a7d437c8 | ||
|
|
433f3c1154 | ||
|
|
2a32903326 | ||
|
|
0582954cfa | ||
|
|
6d28a7d8a3 | ||
|
|
12b43bf684 | ||
|
|
1180ebd4ca | ||
| cbdd54e383 | |||
| 219a383015 | |||
| 31711e2ea6 | |||
|
|
ca828d45a6 | ||
|
|
9067bec54a | ||
|
|
118e93b85a | ||
|
|
07c5c6fd31 | ||
|
|
9136a9c622 | ||
|
|
eb325871fa | ||
| 9562d3aa32 | |||
| 4e36612388 | |||
| 54e5657899 | |||
| 5971f0976a | |||
| bcbb80e034 | |||
| 64f24dc473 | |||
| 6ddde21a86 | |||
| fd0026ac2d | |||
| 3fce10ae98 | |||
| 002f240720 | |||
| ffe8d90161 | |||
| 6d6816d1a8 | |||
| c1477d0266 | |||
| 33ce6bdb72 | |||
| dc21e8dac3 | |||
| a9a8ba217c | |||
| b0d99e7c1e | |||
| 861408c7bc |
@@ -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.
|
||||
|
||||
14
.cursor/rules/documentation.mdc
Normal file
14
.cursor/rules/documentation.mdc
Normal 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.
|
||||
@@ -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
|
||||
```
|
||||
@@ -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
|
||||
|
||||
|
||||
122
.cursor/rules/version_control.mdc
Normal file
122
.cursor/rules/version_control.mdc
Normal file
@@ -0,0 +1,122 @@
|
||||
---
|
||||
alwaysApply: true
|
||||
---
|
||||
# Directive: Peaceful Co-Existence with Developers
|
||||
|
||||
## 1) Version-Control Ownership
|
||||
|
||||
* **MUST NOT** run `git add`, `git commit`, or any write action.
|
||||
* **MUST** leave staging/committing to the developer.
|
||||
|
||||
## 2) Source of Truth for Commit Text
|
||||
|
||||
* **MUST** derive messages **only** from:
|
||||
|
||||
* files **staged** for commit (primary), and
|
||||
* files **awaiting staging** (context).
|
||||
* **MUST** use the **diffs** to inform content.
|
||||
* **MUST NOT** invent changes or imply work not present in diffs.
|
||||
|
||||
## 3) Mandatory Preview Flow
|
||||
|
||||
* **ALWAYS** present, before any real commit:
|
||||
|
||||
* file list + brief per-file notes,
|
||||
* a **draft commit message** (copy-paste ready),
|
||||
* nothing auto-applied.
|
||||
|
||||
---
|
||||
|
||||
# Commit Message Format (Normative)
|
||||
|
||||
## A. Subject Line (required)
|
||||
|
||||
```
|
||||
<type>(<scope>)<!>: <summary>
|
||||
```
|
||||
|
||||
* **type** (lowercase, Conventional Commits): `feat|fix|refactor|perf|docs|test|build|chore|ci|revert`
|
||||
* **scope**: optional module/package/area (e.g., `api`, `ui/login`, `db`)
|
||||
* **!**: include when a breaking change is introduced
|
||||
* **summary**: imperative mood, ≤ 72 chars, no trailing period
|
||||
|
||||
**Examples**
|
||||
|
||||
* `fix(api): handle null token in refresh path`
|
||||
* `feat(ui/login)!: require OTP after 3 failed attempts`
|
||||
|
||||
## B. Body (optional, when it adds non-obvious value)
|
||||
|
||||
* One blank line after subject.
|
||||
* Wrap at \~72 chars.
|
||||
* Explain **what** and **why**, not line-by-line “how”.
|
||||
* Include brief notes like tests passing or TS/lint issues resolved **only if material**.
|
||||
|
||||
**Body checklist**
|
||||
|
||||
* [ ] Problem/symptom being addressed
|
||||
* [ ] High-level approach or rationale
|
||||
* [ ] Risks, tradeoffs, or follow-ups (if any)
|
||||
|
||||
## C. Footer (optional)
|
||||
|
||||
* Issue refs: `Closes #123`, `Refs #456`
|
||||
* Breaking change (alternative to `!`):
|
||||
`BREAKING CHANGE: <impact + migration note>`
|
||||
* Authors: `Co-authored-by: Name <email>`
|
||||
* Security: `CVE-XXXX-YYYY: <short note>` (if applicable)
|
||||
|
||||
---
|
||||
|
||||
## Content Guidance
|
||||
|
||||
### Include (when relevant)
|
||||
|
||||
* Specific fixes/features delivered
|
||||
* Symptoms/problems fixed
|
||||
* Brief note that tests passed or TS/lint errors resolved
|
||||
|
||||
### Avoid
|
||||
|
||||
* Vague: *improved, enhanced, better*
|
||||
* Trivialities: tiny docs, one-liners, pure lint cleanups (separate, focused commits if needed)
|
||||
* Redundancy: generic blurbs repeated across files
|
||||
* Multi-purpose dumps: keep commits **narrow and focused**
|
||||
* Long explanations that good inline code comments already cover
|
||||
|
||||
**Guiding Principle:** Let code and inline docs speak. Use commits to highlight what isn’t obvious.
|
||||
|
||||
---
|
||||
|
||||
# Copy-Paste Templates
|
||||
|
||||
## Minimal (no body)
|
||||
|
||||
```text
|
||||
<type>(<scope>): <summary>
|
||||
```
|
||||
|
||||
## Standard (with body & footer)
|
||||
|
||||
```text
|
||||
<type>(<scope>)<!>: <summary>
|
||||
|
||||
<why-this-change?>
|
||||
<what-it-does?>
|
||||
<risks-or-follow-ups?>
|
||||
|
||||
Closes #<id>
|
||||
BREAKING CHANGE: <impact + migration>
|
||||
Co-authored-by: <Name> <email>
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
# Assistant Output Checklist (before showing the draft)
|
||||
|
||||
* [ ] List changed files + 1–2 line notes per file
|
||||
* [ ] Provide **one** focused draft message (subject/body/footer)
|
||||
* [ ] Subject ≤ 72 chars, imperative mood, correct `type(scope)!` syntax
|
||||
* [ ] Body only if it adds non-obvious value
|
||||
* [ ] No invented changes; aligns strictly with diffs
|
||||
* [ ] Render as a single copy-paste block for the developer
|
||||
3
.gitignore
vendored
3
.gitignore
vendored
@@ -127,4 +127,5 @@ electron/out/
|
||||
|
||||
# Gradle cache files
|
||||
android/.gradle/file-system.probe
|
||||
android/.gradle/caches/
|
||||
android/.gradle/caches/
|
||||
coverage
|
||||
796
BUILDING.md
796
BUILDING.md
File diff suppressed because it is too large
Load Diff
20
CHANGELOG.md
20
CHANGELOG.md
@@ -12,9 +12,27 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
||||
- Deep link URLs (and other prod settings)
|
||||
- Error in BVC begin view
|
||||
|
||||
## [Unreleased]
|
||||
## [1.0.6] - 2025.08.09
|
||||
### Fixed
|
||||
- Deep link errors where none would validate
|
||||
|
||||
|
||||
## [1.0.5] - 2025.07.24
|
||||
### Fixed
|
||||
- Export & import of contacts corrupted contact methods
|
||||
|
||||
|
||||
## [1.0.4] - 2025.07.20 - 002f2407208d56cc59c0aa7c880535ae4cbace8b
|
||||
### Fixed
|
||||
- Deep link for invite-one-accept
|
||||
|
||||
|
||||
## [1.0.3] - 2025.07.12 - a9a8ba217cd6015321911e98e6843e988dc2c4ae
|
||||
### Changed
|
||||
- Photo is pinned to profile mode
|
||||
### Fixed
|
||||
- Deep link URLs (and other prod settings)
|
||||
- Error in BVC begin view
|
||||
|
||||
|
||||
## [1.0.2] - 2025.06.20 - 276e0a741bc327de3380c4e508cccb7fee58c06d
|
||||
|
||||
10
README.md
10
README.md
@@ -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
|
||||
|
||||
@@ -135,7 +137,7 @@ See [TESTING.md](test-playwright/TESTING.md) for detailed test instructions.
|
||||
|
||||
Application icons are in the `assets` directory, processed by the `capacitor-assets` command.
|
||||
|
||||
To add a Font Awesome icon, add to main.ts and reference with `font-awesome` element and `icon` attribute with the hyphenated name.
|
||||
To add a Font Awesome icon, add to fontawesome.ts and reference with `font-awesome` element and `icon` attribute with the hyphenated name.
|
||||
|
||||
## Other
|
||||
|
||||
|
||||
@@ -31,8 +31,8 @@ android {
|
||||
applicationId "app.timesafari.app"
|
||||
minSdkVersion rootProject.ext.minSdkVersion
|
||||
targetSdkVersion rootProject.ext.targetSdkVersion
|
||||
versionCode 35
|
||||
versionName "1.0.2"
|
||||
versionCode 39
|
||||
versionName "1.0.6"
|
||||
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
|
||||
aaptOptions {
|
||||
// Files and dirs to omit from the packaged assets dir, modified to accommodate modern web apps.
|
||||
@@ -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 {
|
||||
|
||||
@@ -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": {
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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'
|
||||
|
||||
@@ -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": {
|
||||
|
||||
338
docs/build-system/environment-variable-precedence.md
Normal file
338
docs/build-system/environment-variable-precedence.md
Normal 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)
|
||||
322
docs/build-system/platforms/android-custom-api-ip.md
Normal file
322
docs/build-system/platforms/android-custom-api-ip.md
Normal 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
|
||||
84
docs/contact-sharing-url-solution.md
Normal file
84
docs/contact-sharing-url-solution.md
Normal 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.
|
||||
@@ -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
|
||||
2160
docs/migration-templates/GiftedDialog-Architecture-Overview.md
Normal file
2160
docs/migration-templates/GiftedDialog-Architecture-Overview.md
Normal file
File diff suppressed because it is too large
Load Diff
113
docs/migration-templates/updateSettings-consolidation-plan.md
Normal file
113
docs/migration-templates/updateSettings-consolidation-plan.md
Normal file
@@ -0,0 +1,113 @@
|
||||
# $updateSettings to $saveSettings Consolidation Plan
|
||||
|
||||
## Overview
|
||||
Consolidate `$updateSettings` method into `$saveSettings` to eliminate code duplication and improve maintainability. The `$updateSettings` method is currently just a thin wrapper around `$saveSettings` and `$saveUserSettings`, providing no additional functionality.
|
||||
|
||||
## Current State Analysis
|
||||
|
||||
### Current Implementation
|
||||
```typescript
|
||||
// Current $updateSettings - just a wrapper
|
||||
async $updateSettings(changes: Partial<Settings>, did?: string): Promise<boolean> {
|
||||
try {
|
||||
if (did) {
|
||||
return await this.$saveUserSettings(did, changes);
|
||||
} else {
|
||||
return await this.$saveSettings(changes);
|
||||
}
|
||||
} catch (error) {
|
||||
logger.error("[PlatformServiceMixin] Error updating settings:", error);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Usage Statistics
|
||||
- **$updateSettings**: 42 references across codebase
|
||||
- **$saveSettings**: 38 references across codebase
|
||||
- **$saveUserSettings**: 12 references across codebase
|
||||
|
||||
## Migration Strategy
|
||||
|
||||
### Phase 1: Documentation and Planning ✅
|
||||
- [x] Document current usage patterns
|
||||
- [x] Identify all call sites
|
||||
- [x] Create migration plan
|
||||
|
||||
### Phase 2: Implementation
|
||||
- [ ] Update `$saveSettings` to accept optional `did` parameter
|
||||
- [ ] Add error handling to `$saveSettings` (currently missing)
|
||||
- [ ] Deprecate `$updateSettings` with migration notice
|
||||
- [ ] Update all call sites to use `$saveSettings` directly
|
||||
|
||||
### Phase 3: Cleanup
|
||||
- [ ] Remove `$updateSettings` method
|
||||
- [ ] Update documentation
|
||||
- [ ] Update tests
|
||||
|
||||
## Implementation Details
|
||||
|
||||
### Enhanced $saveSettings Method
|
||||
```typescript
|
||||
async $saveSettings(changes: Partial<Settings>, did?: string): Promise<boolean> {
|
||||
try {
|
||||
// Convert settings for database storage
|
||||
const convertedChanges = this._convertSettingsForStorage(changes);
|
||||
|
||||
if (did) {
|
||||
// User-specific settings
|
||||
return await this.$saveUserSettings(did, convertedChanges);
|
||||
} else {
|
||||
// Default settings
|
||||
return await this.$saveSettings(convertedChanges);
|
||||
}
|
||||
} catch (error) {
|
||||
logger.error("[PlatformServiceMixin] Error saving settings:", error);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Migration Benefits
|
||||
1. **Reduced Code Duplication**: Single method handles both use cases
|
||||
2. **Improved Maintainability**: One place to fix issues
|
||||
3. **Consistent Error Handling**: Unified error handling approach
|
||||
4. **Better Type Safety**: Single method signature to maintain
|
||||
|
||||
### Risk Assessment
|
||||
- **Low Risk**: `$updateSettings` is just a wrapper, no complex logic
|
||||
- **Backward Compatible**: Can maintain both methods during transition
|
||||
- **Testable**: Existing tests can be updated incrementally
|
||||
|
||||
## Call Site Migration Examples
|
||||
|
||||
### Before (using $updateSettings)
|
||||
```typescript
|
||||
await this.$updateSettings({ searchBoxes: [newSearchBox] });
|
||||
await this.$updateSettings({ filterFeedByNearby: false }, userDid);
|
||||
```
|
||||
|
||||
### After (using $saveSettings)
|
||||
```typescript
|
||||
await this.$saveSettings({ searchBoxes: [newSearchBox] });
|
||||
await this.$saveSettings({ filterFeedByNearby: false }, userDid);
|
||||
```
|
||||
|
||||
## Testing Strategy
|
||||
1. **Unit Tests**: Update existing tests to use `$saveSettings`
|
||||
2. **Integration Tests**: Verify both default and user-specific settings work
|
||||
3. **Migration Tests**: Ensure searchBoxes conversion still works
|
||||
4. **Performance Tests**: Verify no performance regression
|
||||
|
||||
## Timeline
|
||||
- **Phase 1**: ✅ Complete
|
||||
- **Phase 2**: 1-2 days
|
||||
- **Phase 3**: 1 day
|
||||
- **Total**: 2-3 days
|
||||
|
||||
## Success Criteria
|
||||
- [ ] All existing functionality preserved
|
||||
- [ ] No performance regression
|
||||
- [ ] All tests passing
|
||||
- [ ] Reduced code duplication
|
||||
- [ ] Improved maintainability
|
||||
207
docs/refactoring/GiftedDialog-EntityTypes-Refactoring.md
Normal file
207
docs/refactoring/GiftedDialog-EntityTypes-Refactoring.md
Normal 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
|
||||
@@ -403,7 +403,7 @@
|
||||
buildSettings = {
|
||||
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 37;
|
||||
CURRENT_PROJECT_VERSION = 39;
|
||||
DEVELOPMENT_TEAM = GM3FS5JQPH;
|
||||
ENABLE_APP_SANDBOX = NO;
|
||||
ENABLE_USER_SCRIPT_SANDBOXING = NO;
|
||||
@@ -413,7 +413,7 @@
|
||||
"$(inherited)",
|
||||
"@executable_path/Frameworks",
|
||||
);
|
||||
MARKETING_VERSION = 1.0.4;
|
||||
MARKETING_VERSION = 1.0.6;
|
||||
OTHER_SWIFT_FLAGS = "$(inherited) \"-D\" \"COCOAPODS\" \"-DDEBUG\"";
|
||||
PRODUCT_BUNDLE_IDENTIFIER = app.timesafari;
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
@@ -430,7 +430,7 @@
|
||||
buildSettings = {
|
||||
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 37;
|
||||
CURRENT_PROJECT_VERSION = 39;
|
||||
DEVELOPMENT_TEAM = GM3FS5JQPH;
|
||||
ENABLE_APP_SANDBOX = NO;
|
||||
ENABLE_USER_SCRIPT_SANDBOXING = NO;
|
||||
@@ -440,7 +440,7 @@
|
||||
"$(inherited)",
|
||||
"@executable_path/Frameworks",
|
||||
);
|
||||
MARKETING_VERSION = 1.0.4;
|
||||
MARKETING_VERSION = 1.0.6;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = app.timesafari;
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
SWIFT_ACTIVE_COMPILATION_CONDITIONS = "";
|
||||
|
||||
3316
package-lock.json
generated
3316
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
12
package.json
12
package.json
@@ -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",
|
||||
@@ -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",
|
||||
|
||||
@@ -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 */
|
||||
|
||||
@@ -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,
|
||||
// },
|
||||
});
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -186,8 +208,8 @@ clean_ios_build() {
|
||||
log_debug "Cleaned ios/App/DerivedData/"
|
||||
fi
|
||||
|
||||
# Clean Capacitor
|
||||
npx cap clean ios || true
|
||||
# Clean Capacitor (using npm script instead of invalid cap clean command)
|
||||
npm run clean:ios || true
|
||||
|
||||
log_success "iOS build cleaned"
|
||||
}
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -252,6 +252,25 @@ 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..."
|
||||
@@ -333,7 +352,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 +370,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
|
||||
|
||||
@@ -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
|
||||
@@ -197,20 +205,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
|
||||
|
||||
@@ -52,7 +52,7 @@
|
||||
<a
|
||||
class="cursor-pointer"
|
||||
data-testid="circle-info-link"
|
||||
@click="$emit('loadClaim', record.jwtId)"
|
||||
@click="emitLoadClaim(record.jwtId)"
|
||||
>
|
||||
<font-awesome icon="circle-info" class="fa-fw text-slate-500" />
|
||||
</a>
|
||||
@@ -67,7 +67,7 @@
|
||||
>
|
||||
<a
|
||||
class="block bg-slate-100/50 backdrop-blur-md px-6 py-4 cursor-pointer"
|
||||
@click="$emit('viewImage', record.image)"
|
||||
@click="emitViewImage(record.image)"
|
||||
>
|
||||
<img
|
||||
class="w-full h-auto max-w-lg max-h-96 object-contain mx-auto drop-shadow-md"
|
||||
@@ -80,7 +80,7 @@
|
||||
|
||||
<!-- Description -->
|
||||
<p class="font-medium">
|
||||
<a class="cursor-pointer" @click="$emit('loadClaim', record.jwtId)">
|
||||
<a class="cursor-pointer" @click="emitLoadClaim(record.jwtId)">
|
||||
{{ description }}
|
||||
</a>
|
||||
</p>
|
||||
@@ -248,11 +248,10 @@
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { Component, Prop, Vue } from "vue-facing-decorator";
|
||||
import { Component, Prop, Vue, Emit } from "vue-facing-decorator";
|
||||
import { GiveRecordWithContactInfo } from "@/interfaces/give";
|
||||
import EntityIcon from "./EntityIcon.vue";
|
||||
import { isGiveClaimType, notifyWhyCannotConfirm } from "../libs/util";
|
||||
import { containsHiddenDid, isHiddenDid } from "../libs/endorserServer";
|
||||
import { isHiddenDid } from "../libs/endorserServer";
|
||||
import ProjectIcon from "./ProjectIcon.vue";
|
||||
import { createNotifyHelpers } from "@/utils/notify";
|
||||
import {
|
||||
@@ -272,7 +271,6 @@ export default class ActivityListItem extends Vue {
|
||||
@Prop() lastViewedClaimId?: string;
|
||||
@Prop() isRegistered!: boolean;
|
||||
@Prop() activeDid!: string;
|
||||
@Prop() confirmerIdList?: string[];
|
||||
|
||||
/**
|
||||
* Function prop for handling image caching
|
||||
@@ -331,28 +329,15 @@ export default class ActivityListItem extends Vue {
|
||||
return unitCode === "HUR" ? (single ? "hour" : "hours") : unitCode;
|
||||
}
|
||||
|
||||
get canConfirm(): boolean {
|
||||
if (!this.isRegistered) return false;
|
||||
if (!isGiveClaimType(this.record.fullClaim?.["@type"])) return false;
|
||||
if (this.confirmerIdList?.includes(this.activeDid)) return false;
|
||||
if (this.record.issuerDid === this.activeDid) return false;
|
||||
if (containsHiddenDid(this.record.fullClaim)) return false;
|
||||
return true;
|
||||
// Emit methods using @Emit decorator
|
||||
@Emit("viewImage")
|
||||
emitViewImage(imageUrl: string) {
|
||||
return imageUrl;
|
||||
}
|
||||
|
||||
handleConfirmClick() {
|
||||
if (!this.canConfirm) {
|
||||
notifyWhyCannotConfirm(
|
||||
(msg, timeout) => this.notify.info(msg.text ?? "", timeout),
|
||||
this.isRegistered,
|
||||
this.record.fullClaim?.["@type"],
|
||||
this.record,
|
||||
this.activeDid,
|
||||
this.confirmerIdList,
|
||||
);
|
||||
return;
|
||||
}
|
||||
this.$emit("confirmClaim", this.record);
|
||||
@Emit("loadClaim")
|
||||
emitLoadClaim(jwtId: string) {
|
||||
return jwtId;
|
||||
}
|
||||
|
||||
get friendlyDate(): string {
|
||||
|
||||
@@ -6,13 +6,13 @@
|
||||
:checked="allContactsSelected"
|
||||
class="align-middle ml-2 h-6 w-6"
|
||||
data-testId="contactCheckAllBottom"
|
||||
@click="$emit('toggle-all-selection')"
|
||||
@click="emitToggleAllSelection"
|
||||
/>
|
||||
<button
|
||||
v-if="!showGiveNumbers"
|
||||
:class="copyButtonClass"
|
||||
:disabled="copyButtonDisabled"
|
||||
@click="$emit('copy-selected')"
|
||||
@click="emitCopySelected"
|
||||
>
|
||||
Copy
|
||||
</button>
|
||||
@@ -20,7 +20,7 @@
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { Component, Vue, Prop } from "vue-facing-decorator";
|
||||
import { Component, Vue, Prop, Emit } from "vue-facing-decorator";
|
||||
|
||||
/**
|
||||
* ContactBulkActions - Contact bulk actions component
|
||||
@@ -38,5 +38,16 @@ export default class ContactBulkActions extends Vue {
|
||||
@Prop({ required: true }) allContactsSelected!: boolean;
|
||||
@Prop({ required: true }) copyButtonClass!: string;
|
||||
@Prop({ required: true }) copyButtonDisabled!: boolean;
|
||||
|
||||
// Emit methods using @Emit decorator
|
||||
@Emit("toggle-all-selection")
|
||||
emitToggleAllSelection() {
|
||||
// No parameters needed
|
||||
}
|
||||
|
||||
@Emit("copy-selected")
|
||||
emitCopySelected() {
|
||||
// No parameters needed
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
@@ -64,7 +64,7 @@
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { Component, Vue, Prop } from "vue-facing-decorator";
|
||||
import { Component, Vue, Prop, Emit } from "vue-facing-decorator";
|
||||
|
||||
/**
|
||||
* ContactInputForm - Contact input form component
|
||||
@@ -165,9 +165,9 @@ export default class ContactInputForm extends Vue {
|
||||
* Handle QR scan button click
|
||||
* Emits qr-scan event for parent handling
|
||||
*/
|
||||
@Emit("qr-scan")
|
||||
private handleQRScan(): void {
|
||||
console.log("[ContactInputForm] QR scan button clicked");
|
||||
this.$emit("qr-scan");
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
@@ -8,21 +8,21 @@
|
||||
:checked="allContactsSelected"
|
||||
class="align-middle ml-2 h-6 w-6"
|
||||
data-testId="contactCheckAllTop"
|
||||
@click="$emit('toggle-all-selection')"
|
||||
@click="emitToggleAllSelection"
|
||||
/>
|
||||
<button
|
||||
v-if="!showGiveNumbers"
|
||||
:class="copyButtonClass"
|
||||
:disabled="copyButtonDisabled"
|
||||
data-testId="copySelectedContactsButtonTop"
|
||||
@click="$emit('copy-selected')"
|
||||
@click="emitCopySelected"
|
||||
>
|
||||
Copy
|
||||
</button>
|
||||
<font-awesome
|
||||
icon="circle-info"
|
||||
class="text-2xl text-blue-500 ml-2"
|
||||
@click="$emit('show-copy-info')"
|
||||
@click="emitShowCopyInfo"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
@@ -33,7 +33,7 @@
|
||||
v-if="showGiveNumbers"
|
||||
class="text-md bg-gradient-to-b shadow-[inset_0_-1px_0_0_rgba(0,0,0,0.5)] text-white px-3 py-1.5 rounded-md"
|
||||
:class="giveAmountsButtonClass"
|
||||
@click="$emit('toggle-give-totals')"
|
||||
@click="emitToggleGiveTotals"
|
||||
>
|
||||
{{ giveAmountsButtonText }}
|
||||
<font-awesome icon="left-right" class="fa-fw" />
|
||||
@@ -41,7 +41,7 @@
|
||||
|
||||
<button
|
||||
class="text-md bg-gradient-to-b from-slate-400 to-slate-700 shadow-[inset_0_-1px_0_0_rgba(0,0,0,0.5)] text-white px-3 py-1.5 rounded-md"
|
||||
@click="$emit('toggle-show-actions')"
|
||||
@click="emitToggleShowActions"
|
||||
>
|
||||
{{ showActionsButtonText }}
|
||||
</button>
|
||||
@@ -50,7 +50,7 @@
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { Component, Vue, Prop } from "vue-facing-decorator";
|
||||
import { Component, Vue, Prop, Emit } from "vue-facing-decorator";
|
||||
|
||||
/**
|
||||
* ContactListHeader - Contact list header component
|
||||
@@ -71,5 +71,31 @@ export default class ContactListHeader extends Vue {
|
||||
@Prop({ required: true }) giveAmountsButtonText!: string;
|
||||
@Prop({ required: true }) showActionsButtonText!: string;
|
||||
@Prop({ required: true }) giveAmountsButtonClass!: Record<string, boolean>;
|
||||
|
||||
// Emit methods using @Emit decorator
|
||||
@Emit("toggle-all-selection")
|
||||
emitToggleAllSelection() {
|
||||
// No parameters needed
|
||||
}
|
||||
|
||||
@Emit("copy-selected")
|
||||
emitCopySelected() {
|
||||
// No parameters needed
|
||||
}
|
||||
|
||||
@Emit("show-copy-info")
|
||||
emitShowCopyInfo() {
|
||||
// No parameters needed
|
||||
}
|
||||
|
||||
@Emit("toggle-give-totals")
|
||||
emitToggleGiveTotals() {
|
||||
// No parameters needed
|
||||
}
|
||||
|
||||
@Emit("toggle-show-actions")
|
||||
emitToggleShowActions() {
|
||||
// No parameters needed
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
@@ -9,14 +9,14 @@
|
||||
:checked="isSelected"
|
||||
class="ml-2 h-6 w-6 flex-shrink-0"
|
||||
data-testId="contactCheckOne"
|
||||
@click="$emit('toggle-selection', contact.did)"
|
||||
@click="emitToggleSelection(contact.did)"
|
||||
/>
|
||||
|
||||
<EntityIcon
|
||||
:contact="contact"
|
||||
:icon-size="48"
|
||||
class="shrink-0 align-text-bottom border border-slate-300 rounded cursor-pointer overflow-hidden"
|
||||
@click="$emit('show-identicon', contact)"
|
||||
@click="emitShowIdenticon(contact)"
|
||||
/>
|
||||
|
||||
<div class="overflow-hidden">
|
||||
@@ -63,7 +63,7 @@
|
||||
<button
|
||||
class="text-sm 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-2.5 py-1.5 rounded-l-md"
|
||||
:title="getGiveDescriptionForContact(contact.did, true)"
|
||||
@click="$emit('show-gifted-dialog', contact.did, activeDid)"
|
||||
@click="emitShowGiftedDialog(contact.did, activeDid)"
|
||||
>
|
||||
{{ getGiveAmountForContact(contact.did, true) }}
|
||||
</button>
|
||||
@@ -71,7 +71,7 @@
|
||||
<button
|
||||
class="text-sm 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-2.5 py-1.5 rounded-r-md border-l"
|
||||
:title="getGiveDescriptionForContact(contact.did, false)"
|
||||
@click="$emit('show-gifted-dialog', activeDid, contact.did)"
|
||||
@click="emitShowGiftedDialog(activeDid, contact.did)"
|
||||
>
|
||||
{{ getGiveAmountForContact(contact.did, false) }}
|
||||
</button>
|
||||
@@ -81,7 +81,7 @@
|
||||
<button
|
||||
class="text-sm 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-2 py-1.5 rounded-md"
|
||||
data-testId="offerButton"
|
||||
@click="$emit('open-offer-dialog', contact.did, contact.name)"
|
||||
@click="emitOpenOfferDialog(contact.did, contact.name)"
|
||||
>
|
||||
Offer
|
||||
</button>
|
||||
@@ -102,7 +102,7 @@
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { Component, Vue, Prop } from "vue-facing-decorator";
|
||||
import { Component, Vue, Prop, Emit } from "vue-facing-decorator";
|
||||
import EntityIcon from "./EntityIcon.vue";
|
||||
import { Contact } from "../db/tables/contacts";
|
||||
import { AppString } from "../constants/app";
|
||||
@@ -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;
|
||||
@@ -140,6 +146,25 @@ export default class ContactListItem extends Vue {
|
||||
// Constants
|
||||
AppString = AppString;
|
||||
|
||||
// Emit methods using @Emit decorator
|
||||
@Emit("toggle-selection")
|
||||
emitToggleSelection(did: string) {
|
||||
return did;
|
||||
}
|
||||
|
||||
@Emit("show-identicon")
|
||||
emitShowIdenticon(contact: Contact) {
|
||||
return contact;
|
||||
}
|
||||
|
||||
emitShowGiftedDialog(fromDid: string, toDid: string) {
|
||||
this.$emit("show-gifted-dialog", fromDid, toDid);
|
||||
}
|
||||
|
||||
emitOpenOfferDialog(did: string, name: string | undefined) {
|
||||
this.$emit("open-offer-dialog", did, name);
|
||||
}
|
||||
|
||||
/**
|
||||
* Format contact name with non-breaking spaces
|
||||
*/
|
||||
|
||||
@@ -54,7 +54,10 @@ messages * - Conditional UI based on platform capabilities * * @component *
|
||||
|
||||
<script lang="ts">
|
||||
import { Component, Prop, Vue } from "vue-facing-decorator";
|
||||
import * as R from "ramda";
|
||||
|
||||
import { AppString, NotificationIface } from "../constants/app";
|
||||
import { Contact } from "../db/tables/contacts";
|
||||
|
||||
import { logger } from "../utils/logger";
|
||||
import { contactsToExportJson } from "../libs/util";
|
||||
@@ -179,7 +182,19 @@ export default class DataExportSection extends Vue {
|
||||
const allContacts = await this.$contacts();
|
||||
|
||||
// Convert contacts to export format
|
||||
const exportData = contactsToExportJson(allContacts);
|
||||
const processedContacts: Contact[] = allContacts.map((contact) => {
|
||||
// first remove the contactMethods field, mostly to cast to a clear type (that will end up with JSON objects)
|
||||
const exContact: Contact = R.omit(["contactMethods"], contact);
|
||||
// now add contactMethods as a true array of ContactMethod objects
|
||||
exContact.contactMethods = contact.contactMethods
|
||||
? (typeof contact.contactMethods === 'string' && contact.contactMethods.trim() !== ''
|
||||
? JSON.parse(contact.contactMethods)
|
||||
: [])
|
||||
: [];
|
||||
return exContact;
|
||||
});
|
||||
|
||||
const exportData = contactsToExportJson(processedContacts);
|
||||
const jsonStr = JSON.stringify(exportData, null, 2);
|
||||
|
||||
// Use platform service to handle export (no platform-specific logic here!)
|
||||
|
||||
@@ -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 || "" : "",
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -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,
|
||||
},
|
||||
|
||||
@@ -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,
|
||||
};
|
||||
|
||||
@@ -55,10 +55,7 @@
|
||||
aria-label="Delete profile image"
|
||||
@click="deleteImage"
|
||||
>
|
||||
<font-awesome
|
||||
icon="trash-can"
|
||||
aria-hidden="true"
|
||||
/>
|
||||
<font-awesome icon="trash-can" aria-hidden="true" />
|
||||
</button>
|
||||
</span>
|
||||
<div v-else class="text-center">
|
||||
|
||||
@@ -25,7 +25,7 @@
|
||||
<div class="flex-1 flex items-center justify-center p-2">
|
||||
<div class="w-full h-full flex items-center justify-center">
|
||||
<img
|
||||
:src="transformedImageUrl"
|
||||
:src="imageUrl"
|
||||
class="max-h-[calc(100vh-5rem)] w-full h-full object-contain"
|
||||
alt="expanded shared content"
|
||||
@click="close"
|
||||
|
||||
@@ -7,14 +7,14 @@
|
||||
:contact="contact"
|
||||
:icon-size="512"
|
||||
class="flex w-11/12 max-w-sm mx-auto mb-3 overflow-hidden bg-white rounded-lg shadow-lg"
|
||||
@click="$emit('close')"
|
||||
@click="emitClose"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { Component, Vue, Prop } from "vue-facing-decorator";
|
||||
import { Component, Vue, Prop, Emit } from "vue-facing-decorator";
|
||||
import EntityIcon from "./EntityIcon.vue";
|
||||
import { Contact } from "../db/tables/contacts";
|
||||
|
||||
@@ -34,5 +34,11 @@ import { Contact } from "../db/tables/contacts";
|
||||
})
|
||||
export default class LargeIdenticonModal extends Vue {
|
||||
@Prop({ required: true }) contact!: Contact | undefined;
|
||||
|
||||
// Emit methods using @Emit decorator
|
||||
@Emit("close")
|
||||
emitClose() {
|
||||
// No parameters needed
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
@@ -159,26 +159,7 @@
|
||||
</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 } from "vue-facing-decorator";
|
||||
import { Component, Vue, Prop, Emit } from "vue-facing-decorator";
|
||||
|
||||
import {
|
||||
errorStringForLog,
|
||||
@@ -222,6 +203,12 @@ export default class MembersList extends Vue {
|
||||
@Prop({ required: true }) password!: string;
|
||||
@Prop({ default: false }) showOrganizerTools!: boolean;
|
||||
|
||||
// Emit methods using @Emit decorator
|
||||
@Emit("error")
|
||||
emitError(message: string) {
|
||||
return message;
|
||||
}
|
||||
|
||||
decryptedMembers: DecryptedMember[] = [];
|
||||
firstName = "";
|
||||
isLoading = true;
|
||||
@@ -262,10 +249,7 @@ export default class MembersList extends Vue {
|
||||
"Error fetching members: " + errorStringForLog(error),
|
||||
true,
|
||||
);
|
||||
this.$emit(
|
||||
"error",
|
||||
serverMessageForUser(error) || "Failed to fetch members.",
|
||||
);
|
||||
this.emitError(serverMessageForUser(error) || "Failed to fetch members.");
|
||||
} finally {
|
||||
this.isLoading = false;
|
||||
}
|
||||
@@ -478,8 +462,7 @@ export default class MembersList extends Vue {
|
||||
"Error toggling admission: " + errorStringForLog(error),
|
||||
true,
|
||||
);
|
||||
this.$emit(
|
||||
"error",
|
||||
this.emitError(
|
||||
serverMessageForUser(error) ||
|
||||
"Failed to update member admission status.",
|
||||
);
|
||||
|
||||
@@ -15,26 +15,25 @@ Raymer */
|
||||
class="block w-full rounded border border-slate-400 mb-2 px-3 py-2"
|
||||
placeholder="Description of what is offered"
|
||||
/>
|
||||
<div class="flex flex-row mt-2">
|
||||
<span :class="unitCodeDisplayClasses" @click="changeUnitCode()">
|
||||
{{ libsUtil.UNIT_SHORT[amountUnitCode] }}
|
||||
</span>
|
||||
<div
|
||||
v-if="showDecrementButton"
|
||||
:class="controlButtonClasses"
|
||||
@click="decrement()"
|
||||
>
|
||||
<font-awesome icon="chevron-left" />
|
||||
</div>
|
||||
<input
|
||||
v-model="amountInput"
|
||||
<div class="flex mb-4">
|
||||
<AmountInput
|
||||
:value="parseFloat(amountInput) || 0"
|
||||
:onUpdateValue="handleAmountUpdate"
|
||||
data-testId="inputOfferAmount"
|
||||
type="number"
|
||||
:class="amountInputClasses"
|
||||
/>
|
||||
<div :class="incrementButtonClasses" @click="increment()">
|
||||
<font-awesome icon="chevron-right" />
|
||||
</div>
|
||||
|
||||
<select
|
||||
v-model="amountUnitCode"
|
||||
class="flex-1 rounded border border-slate-400 ms-2 px-3 py-2"
|
||||
>
|
||||
<option
|
||||
v-for="(displayName, code) in unitOptions"
|
||||
:key="code"
|
||||
:value="code"
|
||||
>
|
||||
{{ displayName }}
|
||||
</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="mt-4 flex justify-center">
|
||||
<span>
|
||||
@@ -73,10 +72,15 @@ import {
|
||||
NOTIFY_OFFER_CREATION_ERROR,
|
||||
NOTIFY_OFFER_SUCCESS,
|
||||
NOTIFY_OFFER_SUBMISSION_ERROR,
|
||||
NOTIFY_OFFER_ERROR_NEGATIVE_AMOUNT,
|
||||
} from "@/constants/notifications";
|
||||
import AmountInput from "./AmountInput.vue";
|
||||
|
||||
@Component({
|
||||
mixins: [PlatformServiceMixin],
|
||||
components: {
|
||||
AmountInput,
|
||||
},
|
||||
})
|
||||
export default class OfferDialog extends Vue {
|
||||
@Prop projectId?: string;
|
||||
@@ -122,35 +126,10 @@ export default class OfferDialog extends Vue {
|
||||
}
|
||||
|
||||
/**
|
||||
* CSS classes for unit code selector and increment/decrement buttons
|
||||
* Reduces template complexity for repeated border and styling patterns
|
||||
* Computed property to get unit options for the select dropdown
|
||||
*/
|
||||
get controlButtonClasses(): string {
|
||||
return "border border-r-0 border-slate-400 bg-slate-200 px-4 py-2";
|
||||
}
|
||||
|
||||
/**
|
||||
* CSS classes for unit code display span
|
||||
* Reduces template complexity for unit code button styling
|
||||
*/
|
||||
get unitCodeDisplayClasses(): string {
|
||||
return "rounded-l border border-r-0 border-slate-400 bg-slate-200 w-1/3 text-center text-blue-500 px-2 py-2";
|
||||
}
|
||||
|
||||
/**
|
||||
* CSS classes for amount input field
|
||||
* Reduces template complexity for input styling
|
||||
*/
|
||||
get amountInputClasses(): string {
|
||||
return "w-full border border-r-0 border-slate-400 px-2 py-2 text-center";
|
||||
}
|
||||
|
||||
/**
|
||||
* CSS classes for the right-most increment button
|
||||
* Reduces template complexity for border styling
|
||||
*/
|
||||
get incrementButtonClasses(): string {
|
||||
return "rounded-r border border-slate-400 bg-slate-200 px-4 py-2";
|
||||
get unitOptions() {
|
||||
return this.libsUtil.UNIT_SHORT;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -173,13 +152,7 @@ export default class OfferDialog extends Vue {
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Whether the decrement button should be visible
|
||||
* Encapsulates conditional logic from template
|
||||
*/
|
||||
get showDecrementButton(): boolean {
|
||||
return this.amountInput !== "0";
|
||||
}
|
||||
|
||||
|
||||
// =================================================
|
||||
// COMPONENT METHODS
|
||||
@@ -226,30 +199,14 @@ export default class OfferDialog extends Vue {
|
||||
this.visible = false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Cycle through available unit codes
|
||||
*/
|
||||
changeUnitCode() {
|
||||
const units = Object.keys(this.libsUtil.UNIT_SHORT);
|
||||
const index = units.indexOf(this.amountUnitCode);
|
||||
this.amountUnitCode = units[(index + 1) % units.length];
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Increment the amount input
|
||||
* Handle amount updates from AmountInput component
|
||||
* @param value - New amount value
|
||||
*/
|
||||
increment() {
|
||||
this.amountInput = `${(parseFloat(this.amountInput) || 0) + 1}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Decrement the amount input
|
||||
*/
|
||||
decrement() {
|
||||
this.amountInput = `${Math.max(
|
||||
0,
|
||||
(parseFloat(this.amountInput) || 1) - 1,
|
||||
)}`;
|
||||
handleAmountUpdate(value: number) {
|
||||
this.amountInput = value.toString();
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -273,6 +230,28 @@ export default class OfferDialog extends Vue {
|
||||
* Confirm and submit the offer
|
||||
*/
|
||||
async confirm() {
|
||||
if (!this.activeDid) {
|
||||
this.notify.error(NOTIFY_OFFER_IDENTITY_REQUIRED.message, TIMEOUTS.LONG);
|
||||
return;
|
||||
}
|
||||
|
||||
if (parseFloat(this.amountInput) < 0) {
|
||||
this.notify.error(
|
||||
NOTIFY_OFFER_ERROR_NEGATIVE_AMOUNT.message,
|
||||
TIMEOUTS.SHORT,
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!this.description && !parseFloat(this.amountInput)) {
|
||||
const message = NOTIFY_OFFER_DESCRIPTION_REQUIRED.message.replace(
|
||||
"{unit}",
|
||||
this.libsUtil.UNIT_LONG[this.amountUnitCode],
|
||||
);
|
||||
this.notify.error(message, TIMEOUTS.SHORT);
|
||||
return;
|
||||
}
|
||||
|
||||
this.close();
|
||||
this.notify.toast(NOTIFY_OFFER_RECORDING.text, undefined, TIMEOUTS.BRIEF);
|
||||
|
||||
@@ -301,20 +280,6 @@ export default class OfferDialog extends Vue {
|
||||
unitCode: string = "HUR",
|
||||
expirationDateInput?: string,
|
||||
) {
|
||||
if (!this.activeDid) {
|
||||
this.notify.error(NOTIFY_OFFER_IDENTITY_REQUIRED.message, TIMEOUTS.LONG);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!description && !amount) {
|
||||
const message = NOTIFY_OFFER_DESCRIPTION_REQUIRED.message.replace(
|
||||
"{unit}",
|
||||
this.libsUtil.UNIT_LONG[unitCode],
|
||||
);
|
||||
this.notify.error(message, TIMEOUTS.MODAL);
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const result = await createAndSubmitOffer(
|
||||
this.axios,
|
||||
@@ -363,7 +328,7 @@ export default class OfferDialog extends Vue {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
z-index: 1000;
|
||||
z-index: 50;
|
||||
}
|
||||
|
||||
.dialog {
|
||||
|
||||
@@ -180,7 +180,7 @@
|
||||
>
|
||||
Let's go!
|
||||
<br />
|
||||
See & record gratitude.
|
||||
See & record things you've received.
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
|
||||
@@ -67,7 +67,7 @@
|
||||
out of <b>{{ imageLimits?.maxImagesPerWeek ?? "?" }}</b> for this week.
|
||||
Your image counter resets at
|
||||
<b class="whitespace-nowrap">{{
|
||||
readableDate(imageLimits?.nextWeekBeginDateTime || "")
|
||||
readableDate(imageLimits?.nextWeekBeginDateTime)
|
||||
}}</b>
|
||||
</p>
|
||||
</div>
|
||||
|
||||
@@ -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 =
|
||||
|
||||
@@ -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...",
|
||||
|
||||
@@ -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> {
|
||||
|
||||
@@ -6,7 +6,9 @@ export type ContactMethod = {
|
||||
|
||||
export type Contact = {
|
||||
//
|
||||
// When adding a property, consider whether it should be added when exporting & sharing contacts, eg. DataExportSection
|
||||
// When adding a property:
|
||||
// - Consider whether it should be added when exporting & sharing contacts, eg. DataExportSection
|
||||
// - If it's a boolean, it should be converted from a 0/1 integer in PlatformServiceMixin._mapColumnsToValues
|
||||
|
||||
did: string;
|
||||
contactMethods?: Array<ContactMethod>;
|
||||
@@ -30,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
|
||||
};
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -33,7 +33,6 @@ export const deepLinkSchemas = {
|
||||
id: z.string(),
|
||||
}),
|
||||
"claim-add-raw": z.object({
|
||||
id: z.string(),
|
||||
claim: z.string().optional(),
|
||||
claimJwtId: z.string().optional(),
|
||||
}),
|
||||
|
||||
@@ -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
|
||||
},
|
||||
};
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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,17 +34,7 @@ import { PlatformServiceFactory } from "../services/PlatformServiceFactory";
|
||||
import { IIdentifier } from "@veramo/core";
|
||||
import { DEFAULT_ROOT_DERIVATION_PATH } from "./crypto";
|
||||
|
||||
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>> {
|
||||
@@ -67,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 {
|
||||
@@ -190,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);
|
||||
};
|
||||
|
||||
@@ -805,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 = [
|
||||
@@ -903,31 +893,19 @@ export interface DatabaseExport {
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts an array of contacts to the standardized database export JSON format.
|
||||
* Converts an array of contacts to the export JSON format.
|
||||
* This format is used for data migration and backup purposes.
|
||||
*
|
||||
* @param contacts - Array of Contact objects to convert
|
||||
* @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,
|
||||
},
|
||||
],
|
||||
},
|
||||
@@ -973,28 +951,28 @@ export async function importFromMnemonic(
|
||||
if (isTestUser0) {
|
||||
// Set up Test User #0 specific settings with enhanced error handling
|
||||
const platformService = await getPlatformService();
|
||||
|
||||
|
||||
try {
|
||||
// First, ensure the DID-specific settings record exists
|
||||
await platformService.insertDidSpecificSettings(newId.did);
|
||||
|
||||
|
||||
// Then update with Test User #0 specific settings
|
||||
await platformService.updateDidSpecificSettings(newId.did, {
|
||||
firstName: "User Zero",
|
||||
isRegistered: true,
|
||||
});
|
||||
|
||||
|
||||
// Verify the settings were saved correctly
|
||||
const verificationResult = await platformService.dbQuery(
|
||||
"SELECT firstName, isRegistered FROM settings WHERE accountDid = ?",
|
||||
[newId.did],
|
||||
);
|
||||
|
||||
|
||||
if (verificationResult?.values?.length) {
|
||||
const settings = verificationResult.values[0];
|
||||
const firstName = settings[0];
|
||||
const isRegistered = settings[1];
|
||||
|
||||
|
||||
logger.info("[importFromMnemonic] Test User #0 settings verification", {
|
||||
did: newId.did,
|
||||
firstName,
|
||||
@@ -1002,40 +980,50 @@ export async function importFromMnemonic(
|
||||
expectedFirstName: "User Zero",
|
||||
expectedIsRegistered: true,
|
||||
});
|
||||
|
||||
|
||||
// If settings weren't saved correctly, try individual updates
|
||||
if (firstName !== "User Zero" || isRegistered !== 1) {
|
||||
logger.warn("[importFromMnemonic] Test User #0 settings not saved correctly, retrying with individual updates");
|
||||
|
||||
logger.warn(
|
||||
"[importFromMnemonic] Test User #0 settings not saved correctly, retrying with individual updates",
|
||||
);
|
||||
|
||||
await platformService.dbExec(
|
||||
"UPDATE settings SET firstName = ? WHERE accountDid = ?",
|
||||
["User Zero", newId.did],
|
||||
);
|
||||
|
||||
|
||||
await platformService.dbExec(
|
||||
"UPDATE settings SET isRegistered = ? WHERE accountDid = ?",
|
||||
[1, newId.did],
|
||||
);
|
||||
|
||||
|
||||
// Verify again
|
||||
const retryResult = await platformService.dbQuery(
|
||||
"SELECT firstName, isRegistered FROM settings WHERE accountDid = ?",
|
||||
[newId.did],
|
||||
);
|
||||
|
||||
|
||||
if (retryResult?.values?.length) {
|
||||
const retrySettings = retryResult.values[0];
|
||||
logger.info("[importFromMnemonic] Test User #0 settings after retry", {
|
||||
firstName: retrySettings[0],
|
||||
isRegistered: retrySettings[1],
|
||||
});
|
||||
logger.info(
|
||||
"[importFromMnemonic] Test User #0 settings after retry",
|
||||
{
|
||||
firstName: retrySettings[0],
|
||||
isRegistered: retrySettings[1],
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
logger.error("[importFromMnemonic] Failed to verify Test User #0 settings - no record found");
|
||||
logger.error(
|
||||
"[importFromMnemonic] Failed to verify Test User #0 settings - no record found",
|
||||
);
|
||||
}
|
||||
} catch (error) {
|
||||
logger.error("[importFromMnemonic] Error setting up Test User #0 settings:", error);
|
||||
logger.error(
|
||||
"[importFromMnemonic] Error setting up Test User #0 settings:",
|
||||
error,
|
||||
);
|
||||
// Don't throw - allow the import to continue even if settings fail
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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");
|
||||
|
||||
|
||||
@@ -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 = [
|
||||
|
||||
@@ -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`,
|
||||
);
|
||||
|
||||
@@ -103,9 +103,10 @@ export class ProfileService {
|
||||
{ headers },
|
||||
);
|
||||
|
||||
if (response.status === 200) {
|
||||
if (response.status === 201) {
|
||||
return true;
|
||||
} else {
|
||||
logger.error("Error saving profile:", response);
|
||||
throw new Error(ACCOUNT_VIEW_CONSTANTS.ERRORS.PROFILE_NOT_SAVED);
|
||||
}
|
||||
} catch (error) {
|
||||
|
||||
@@ -149,6 +149,10 @@ export class DeepLinkHandler {
|
||||
params[routeConfig.paramKey ?? "id"] = pathParams.join("/");
|
||||
}
|
||||
|
||||
// logConsoleAndDb(
|
||||
// `[DeepLink] Debug: Route Path: ${routePath} Path Params: ${JSON.stringify(params)} Query String: ${JSON.stringify(query)}`,
|
||||
// false,
|
||||
// );
|
||||
return { path: routePath, params, query };
|
||||
}
|
||||
|
||||
@@ -195,14 +199,14 @@ export class DeepLinkHandler {
|
||||
// Continue with parameter validation as before...
|
||||
const schema = deepLinkSchemas[path as keyof typeof deepLinkSchemas];
|
||||
|
||||
let validatedParams, validatedQuery;
|
||||
let validatedParams;
|
||||
try {
|
||||
validatedParams = await schema.parseAsync(params);
|
||||
validatedQuery = await schema.parseAsync(query);
|
||||
} catch (error) {
|
||||
// For parameter validation errors, provide specific error feedback
|
||||
console.error(
|
||||
logConsoleAndDb(
|
||||
`[DeepLink] Invalid parameters for route name ${routeName} for path: ${path}: ${JSON.stringify(error)} ... with params: ${JSON.stringify(params)} ... and query: ${JSON.stringify(query)}`,
|
||||
true,
|
||||
);
|
||||
await this.router.replace({
|
||||
name: "deep-link-error",
|
||||
@@ -223,11 +227,11 @@ export class DeepLinkHandler {
|
||||
await this.router.replace({
|
||||
name: routeName,
|
||||
params: validatedParams,
|
||||
query: validatedQuery,
|
||||
});
|
||||
} catch (error) {
|
||||
console.error(
|
||||
`[DeepLink] Error routing to route name ${routeName} for path: ${path}: ${JSON.stringify(error)} ... with validated params: ${JSON.stringify(validatedParams)} ... and validated query: ${JSON.stringify(validatedQuery)}`,
|
||||
logConsoleAndDb(
|
||||
`[DeepLink] Error routing to route name ${routeName} for path: ${path}: ${JSON.stringify(error)} ... with validated params: ${JSON.stringify(validatedParams)}`,
|
||||
true,
|
||||
);
|
||||
// For parameter validation errors, provide specific error feedback
|
||||
await this.router.replace({
|
||||
@@ -237,7 +241,6 @@ export class DeepLinkHandler {
|
||||
originalPath: path,
|
||||
errorCode: "ROUTING_ERROR",
|
||||
errorMessage: `Error routing to ${routeName}: ${JSON.stringify(error)}`,
|
||||
...validatedQuery,
|
||||
},
|
||||
});
|
||||
}
|
||||
@@ -260,8 +263,9 @@ export class DeepLinkHandler {
|
||||
await this.validateAndRoute(path, sanitizedParams, query);
|
||||
} catch (error) {
|
||||
const deepLinkError = error as DeepLinkError;
|
||||
console.error(
|
||||
logConsoleAndDb(
|
||||
`[DeepLink] Error (${deepLinkError.code}): ${deepLinkError.details}`,
|
||||
true,
|
||||
);
|
||||
|
||||
throw {
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -1,26 +1,91 @@
|
||||
<template>
|
||||
<div>
|
||||
<h2>PlatformServiceMixin Test</h2>
|
||||
<button @click="testInsert">Test Insert</button>
|
||||
<button @click="testUpdate">Test Update</button>
|
||||
<button
|
||||
:class="primaryButtonClasses"
|
||||
@click="testUserZeroSettings"
|
||||
>
|
||||
Test User #0 Settings
|
||||
</button>
|
||||
|
||||
<div v-if="userZeroTestResult" class="mt-4 p-4 border border-gray-300 rounded-md bg-gray-50">
|
||||
<h4 class="font-semibold mb-2">User #0 Settings Test Result:</h4>
|
||||
<pre class="text-sm">{{ JSON.stringify(userZeroTestResult, null, 2) }}</pre>
|
||||
<div class="space-y-2">
|
||||
<button
|
||||
:class="
|
||||
activeTest === 'insert'
|
||||
? 'bg-green-500 text-white'
|
||||
: 'bg-gray-200 hover:bg-gray-300'
|
||||
"
|
||||
class="px-4 py-2 rounded mr-2 transition-colors"
|
||||
@click="testInsert"
|
||||
>
|
||||
Test Contact Insert
|
||||
</button>
|
||||
<button
|
||||
:class="
|
||||
activeTest === 'update'
|
||||
? 'bg-green-500 text-white'
|
||||
: 'bg-gray-200 hover:bg-gray-300'
|
||||
"
|
||||
class="px-4 py-2 rounded mr-2 transition-colors"
|
||||
@click="testUpdate"
|
||||
>
|
||||
Test Contact Update
|
||||
</button>
|
||||
<button
|
||||
:class="
|
||||
activeTest === 'searchBoxes'
|
||||
? 'bg-green-500 text-white'
|
||||
: 'bg-gray-200 hover:bg-gray-300'
|
||||
"
|
||||
class="px-4 py-2 rounded mr-2 transition-colors"
|
||||
@click="testSearchBoxesConversion"
|
||||
>
|
||||
Test SearchBoxes Conversion
|
||||
</button>
|
||||
<button
|
||||
:class="
|
||||
activeTest === 'database'
|
||||
? 'bg-green-500 text-white'
|
||||
: 'bg-gray-200 hover:bg-gray-300'
|
||||
"
|
||||
class="px-4 py-2 rounded mr-2 transition-colors"
|
||||
@click="testDatabaseStorage"
|
||||
>
|
||||
Test SearchBox Database Storage -- Beware: Changes Your Search Box
|
||||
</button>
|
||||
<button
|
||||
:class="
|
||||
activeTest === 'userZero'
|
||||
? 'bg-green-500 text-white'
|
||||
: primaryButtonClasses
|
||||
"
|
||||
class="transition-colors"
|
||||
@click="testUserZeroSettings"
|
||||
>
|
||||
Test User #0 Settings
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div
|
||||
v-if="userZeroTestResult"
|
||||
class="mt-4 p-4 border border-gray-300 rounded-md bg-gray-50"
|
||||
>
|
||||
<h4 class="font-semibold mb-2">User #0 Settings Test Result:</h4>
|
||||
<pre class="text-sm">{{
|
||||
JSON.stringify(userZeroTestResult, null, 2)
|
||||
}}</pre>
|
||||
</div>
|
||||
|
||||
<div v-if="result" class="mt-4">
|
||||
<div class="p-4 border border-blue-300 rounded-md bg-blue-50">
|
||||
<h4 class="font-semibold mb-2 text-blue-800">Test Results:</h4>
|
||||
<div
|
||||
class="whitespace-pre-wrap text-sm font-mono bg-white p-3 rounded border"
|
||||
>
|
||||
{{ result }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<pre>{{ result }}</pre>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { Component, Vue } from "vue-facing-decorator";
|
||||
import { PlatformServiceMixin } from "@/utils/PlatformServiceMixin";
|
||||
import { MASTER_SETTINGS_KEY } from "@/db/tables/settings";
|
||||
|
||||
@Component({
|
||||
mixins: [PlatformServiceMixin],
|
||||
@@ -28,8 +93,15 @@ import { PlatformServiceMixin } from "@/utils/PlatformServiceMixin";
|
||||
export default class PlatformServiceMixinTest extends Vue {
|
||||
result: string = "";
|
||||
userZeroTestResult: any = null;
|
||||
activeTest: string = ""; // Track which test is currently active
|
||||
|
||||
// Add the missing computed property
|
||||
get primaryButtonClasses() {
|
||||
return "bg-blue-500 hover:bg-blue-700 text-white font-bold py-2 px-4 rounded";
|
||||
}
|
||||
|
||||
testInsert() {
|
||||
this.activeTest = "insert";
|
||||
const contact = {
|
||||
name: "Alice",
|
||||
age: 30,
|
||||
@@ -41,6 +113,7 @@ export default class PlatformServiceMixinTest extends Vue {
|
||||
}
|
||||
|
||||
testUpdate() {
|
||||
this.activeTest = "update";
|
||||
const changes = { name: "Bob", isActive: false };
|
||||
const { sql, params } = this.$generateUpdateStatement(
|
||||
changes,
|
||||
@@ -51,20 +124,138 @@ export default class PlatformServiceMixinTest extends Vue {
|
||||
this.result = `SQL: ${sql}\nParams: ${JSON.stringify(params)}`;
|
||||
}
|
||||
|
||||
testSearchBoxesConversion() {
|
||||
this.activeTest = "searchBoxes";
|
||||
// Test the _convertSettingsForStorage helper method
|
||||
const testSettings = {
|
||||
firstName: "John",
|
||||
searchBoxes: [
|
||||
{
|
||||
name: "Test Area",
|
||||
bbox: {
|
||||
eastLong: 1.0,
|
||||
maxLat: 1.0,
|
||||
minLat: 0.0,
|
||||
westLong: 0.0,
|
||||
},
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
const converted = (this as any)._convertSettingsForStorage(testSettings);
|
||||
|
||||
this.result = `# 🔄 SearchBoxes Conversion Test (Helper Method)
|
||||
|
||||
## 📥 Input Settings
|
||||
\`\`\`json
|
||||
{
|
||||
"firstName": "John",
|
||||
"searchBoxes": ${JSON.stringify(testSettings.searchBoxes, null, 2)}
|
||||
}
|
||||
\`\`\`
|
||||
|
||||
## 🔧 After _convertSettingsForStorage()
|
||||
\`\`\`json
|
||||
{
|
||||
"firstName": "John",
|
||||
"searchBoxes": "${converted.searchBoxes}"
|
||||
}
|
||||
\`\`\`
|
||||
|
||||
## ✅ Conversion Results
|
||||
- **Original Type**: \`${typeof testSettings.searchBoxes}\`
|
||||
- **Converted Type**: \`${typeof converted.searchBoxes}\`
|
||||
- **Conversion**: Array → JSON String ✅
|
||||
|
||||
## 📝 Note
|
||||
This tests the helper method only - no database interaction`;
|
||||
}
|
||||
|
||||
async testDatabaseStorage() {
|
||||
this.activeTest = "database";
|
||||
try {
|
||||
this.result = "🔄 Testing database storage format...";
|
||||
|
||||
// Create test settings with searchBoxes array
|
||||
const testSettings = {
|
||||
searchBoxes: [
|
||||
{
|
||||
name: "Test Area",
|
||||
bbox: {
|
||||
eastLong: 1.0,
|
||||
maxLat: 1.0,
|
||||
minLat: 0.0,
|
||||
westLong: 0.0,
|
||||
},
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
// Save to database using our fixed method
|
||||
const success = await this.$saveSettings(testSettings);
|
||||
|
||||
if (success) {
|
||||
// Now query the raw database to see how it's actually stored
|
||||
const rawResult = await this.$dbQuery(
|
||||
"SELECT searchBoxes FROM settings WHERE id = ?",
|
||||
[MASTER_SETTINGS_KEY],
|
||||
);
|
||||
|
||||
if (rawResult?.values?.length) {
|
||||
const rawSearchBoxes = rawResult.values[0][0]; // First column of first row
|
||||
|
||||
this.result = `# 🔧 Database Storage Format Test (Full Database Cycle)
|
||||
|
||||
## 📥 Input (JavaScript Array)
|
||||
\`\`\`json
|
||||
${JSON.stringify(testSettings.searchBoxes, null, 2)}
|
||||
\`\`\`
|
||||
|
||||
## 💾 Database Storage (JSON String)
|
||||
\`\`\`sql
|
||||
"${rawSearchBoxes}"
|
||||
\`\`\`
|
||||
|
||||
## ✅ Verification
|
||||
- **Type**: \`${typeof rawSearchBoxes}\`
|
||||
- **Is JSON String**: \`${typeof rawSearchBoxes === "string" && rawSearchBoxes.startsWith("[")}\`
|
||||
- **Conversion Working**: ✅ **YES** - Array converted to JSON string for database storage
|
||||
|
||||
## 🔄 Process Flow
|
||||
1. **Input**: JavaScript array with bounding box coordinates
|
||||
2. **Conversion**: \`_convertSettingsForStorage()\` converts array to JSON string
|
||||
3. **Storage**: JSON string saved to database using \`$saveSettings()\`
|
||||
4. **Retrieval**: JSON string parsed back to array for application use
|
||||
|
||||
## 📝 Note
|
||||
This tests the complete save → retrieve cycle with actual database interaction`;
|
||||
} else {
|
||||
this.result = "❌ No data found in database";
|
||||
}
|
||||
} else {
|
||||
this.result = "❌ Failed to save settings";
|
||||
}
|
||||
} catch (error) {
|
||||
this.result = `❌ Error: ${error}`;
|
||||
}
|
||||
}
|
||||
|
||||
async testUserZeroSettings() {
|
||||
this.activeTest = "userZero";
|
||||
try {
|
||||
// User #0's DID
|
||||
const userZeroDid = "did:ethr:0x0000694B58C2cC69658993A90D3840C560f2F51F";
|
||||
|
||||
|
||||
this.result = "Testing User #0 settings...";
|
||||
|
||||
|
||||
// Test the debug methods
|
||||
await this.$debugMergedSettings(userZeroDid);
|
||||
|
||||
|
||||
// Get the actual settings
|
||||
const didSettings = await this.$debugDidSettings(userZeroDid);
|
||||
const accountSettings = await this.$accountSettings(userZeroDid);
|
||||
|
||||
|
||||
this.userZeroTestResult = {
|
||||
didSettings,
|
||||
accountSettings,
|
||||
@@ -72,7 +263,7 @@ export default class PlatformServiceMixinTest extends Vue {
|
||||
firstName: accountSettings.firstName,
|
||||
timestamp: new Date().toISOString(),
|
||||
};
|
||||
|
||||
|
||||
this.result = `User #0 settings test completed. isRegistered: ${accountSettings.isRegistered}`;
|
||||
} catch (error) {
|
||||
this.result = `Error testing User #0 settings: ${error}`;
|
||||
|
||||
13
src/types/electron.d.ts
vendored
Normal file
13
src/types/electron.d.ts
vendored
Normal file
@@ -0,0 +1,13 @@
|
||||
declare global {
|
||||
interface Window {
|
||||
electronAPI?: {
|
||||
exportData: (fileName: string, content: string) => Promise<{
|
||||
success: boolean;
|
||||
path?: string;
|
||||
error?: string;
|
||||
}>;
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
export {};
|
||||
83
src/types/global.d.ts
vendored
83
src/types/global.d.ts
vendored
@@ -1,83 +0,0 @@
|
||||
import type { QueryExecResult, SqlValue } from "./database";
|
||||
|
||||
declare module '@jlongster/sql.js' {
|
||||
interface SQL {
|
||||
Database: new (path: string, options?: { filename: boolean }) => Database;
|
||||
FS: {
|
||||
mkdir: (path: string) => void;
|
||||
mount: (fs: any, options: any, path: string) => void;
|
||||
open: (path: string, flags: string) => any;
|
||||
close: (stream: any) => void;
|
||||
};
|
||||
register_for_idb: (fs: any) => void;
|
||||
}
|
||||
|
||||
interface Database {
|
||||
exec: (sql: string, params?: unknown[]) => Promise<QueryExecResult[]>;
|
||||
run: (sql: string, params?: unknown[]) => Promise<{ changes: number; lastId?: number }>;
|
||||
get: (sql: string, params?: unknown[]) => Promise<SqlValue[]>;
|
||||
all: (sql: string, params?: unknown[]) => Promise<SqlValue[][]>;
|
||||
prepare: (sql: string) => Promise<Statement>;
|
||||
close: () => void;
|
||||
}
|
||||
|
||||
interface Statement {
|
||||
run: (params?: unknown[]) => Promise<{ changes: number; lastId?: number }>;
|
||||
get: (params?: unknown[]) => Promise<SqlValue[]>;
|
||||
all: (params?: unknown[]) => Promise<SqlValue[][]>;
|
||||
finalize: () => void;
|
||||
}
|
||||
|
||||
const initSqlJs: (options?: {
|
||||
locateFile?: (file: string) => string;
|
||||
}) => Promise<SQL>;
|
||||
|
||||
export default initSqlJs;
|
||||
}
|
||||
|
||||
/**
|
||||
* Electron API types for the main world context bridge.
|
||||
*
|
||||
* These types define the secure IPC APIs exposed by the preload script
|
||||
* to the renderer process for native Electron functionality.
|
||||
*/
|
||||
interface ElectronAPI {
|
||||
/**
|
||||
* Export data to the user's Downloads folder.
|
||||
*
|
||||
* @param fileName - The name of the file to save (e.g., 'backup-2025-07-06.json')
|
||||
* @param data - The content to write to the file (string)
|
||||
* @returns Promise with success status, file path, or error message
|
||||
*/
|
||||
exportData: (fileName: string, data: string) => Promise<{
|
||||
success: boolean;
|
||||
path?: string;
|
||||
error?: string;
|
||||
}>;
|
||||
}
|
||||
|
||||
/**
|
||||
* Global window interface extension for Electron APIs.
|
||||
*
|
||||
* This makes the electronAPI available on the window object
|
||||
* in TypeScript without type errors.
|
||||
*/
|
||||
declare global {
|
||||
interface Window {
|
||||
electronAPI: ElectronAPI;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Vue instance interface extension for global properties.
|
||||
*
|
||||
* This makes global properties available on Vue instances
|
||||
* in TypeScript without type errors.
|
||||
*/
|
||||
declare module 'vue' {
|
||||
interface ComponentCustomProperties {
|
||||
$notify: (notification: any, timeout?: number) => void;
|
||||
$route: import('vue-router').RouteLocationNormalizedLoaded;
|
||||
$router: import('vue-router').Router;
|
||||
}
|
||||
}
|
||||
67
src/types/modules.d.ts
vendored
67
src/types/modules.d.ts
vendored
@@ -1,67 +0,0 @@
|
||||
import type { QueryExecResult, SqlValue } from "./database";
|
||||
|
||||
declare module '@jlongster/sql.js' {
|
||||
interface SQL {
|
||||
Database: new (path: string, options?: { filename: boolean }) => Database;
|
||||
FS: {
|
||||
mkdir: (path: string) => void;
|
||||
mount: (fs: any, options: any, path: string) => void;
|
||||
open: (path: string, flags: string) => any;
|
||||
close: (stream: any) => void;
|
||||
};
|
||||
register_for_idb: (fs: any) => void;
|
||||
}
|
||||
|
||||
interface Database {
|
||||
exec: (sql: string, params?: unknown[]) => Promise<QueryExecResult[]>;
|
||||
run: (sql: string, params?: unknown[]) => Promise<{ changes: number; lastId?: number }>;
|
||||
get: (sql: string, params?: unknown[]) => Promise<SqlValue[]>;
|
||||
all: (sql: string, params?: unknown[]) => Promise<SqlValue[][]>;
|
||||
prepare: (sql: string) => Promise<Statement>;
|
||||
close: () => void;
|
||||
}
|
||||
|
||||
interface Statement {
|
||||
run: (params?: unknown[]) => Promise<{ changes: number; lastId?: number }>;
|
||||
get: (params?: unknown[]) => Promise<SqlValue[]>;
|
||||
all: (params?: unknown[]) => Promise<SqlValue[][]>;
|
||||
finalize: () => void;
|
||||
}
|
||||
|
||||
const initSqlJs: (options?: {
|
||||
locateFile?: (file: string) => string;
|
||||
}) => Promise<SQL>;
|
||||
|
||||
export default initSqlJs;
|
||||
}
|
||||
|
||||
declare module 'absurd-sql' {
|
||||
import type { SQL } from '@jlongster/sql.js';
|
||||
export class SQLiteFS {
|
||||
constructor(fs: any, backend: any);
|
||||
}
|
||||
}
|
||||
|
||||
declare module 'absurd-sql/dist/indexeddb-backend' {
|
||||
export default class IndexedDBBackend {
|
||||
constructor();
|
||||
}
|
||||
}
|
||||
|
||||
declare module 'absurd-sql/dist/indexeddb-main-thread' {
|
||||
import type { QueryExecResult } from './database';
|
||||
export interface SQLiteOptions {
|
||||
filename?: string;
|
||||
autoLoad?: boolean;
|
||||
debug?: boolean;
|
||||
}
|
||||
|
||||
export interface SQLiteDatabase {
|
||||
exec: (sql: string, params?: unknown[]) => Promise<QueryExecResult[]>;
|
||||
close: () => Promise<void>;
|
||||
}
|
||||
|
||||
export function initSqlJs(options?: any): Promise<any>;
|
||||
export function createDatabase(options?: SQLiteOptions): Promise<SQLiteDatabase>;
|
||||
export function openDatabase(options?: SQLiteOptions): Promise<SQLiteDatabase>;
|
||||
}
|
||||
@@ -44,9 +44,13 @@ import type {
|
||||
PlatformService,
|
||||
PlatformCapabilities,
|
||||
} from "@/services/PlatformService";
|
||||
import { MASTER_SETTINGS_KEY, type Settings } from "@/db/tables/settings";
|
||||
import {
|
||||
MASTER_SETTINGS_KEY,
|
||||
type Settings,
|
||||
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";
|
||||
@@ -59,14 +63,14 @@ import {
|
||||
// TYPESCRIPT INTERFACES
|
||||
// =================================================
|
||||
|
||||
/**
|
||||
* Cache entry interface for storing data with TTL
|
||||
*/
|
||||
interface CacheEntry<T> {
|
||||
data: T;
|
||||
timestamp: number;
|
||||
ttl: number; // milliseconds
|
||||
}
|
||||
// /**
|
||||
// * Cache entry interface for storing data with TTL
|
||||
// */
|
||||
// interface CacheEntry<T> {
|
||||
// data: T;
|
||||
// timestamp: number;
|
||||
// ttl: number; // milliseconds
|
||||
// }
|
||||
|
||||
/**
|
||||
* Vue component interface that uses the PlatformServiceMixin
|
||||
@@ -79,21 +83,21 @@ interface VueComponentWithMixin {
|
||||
platformService(): PlatformService;
|
||||
}
|
||||
|
||||
/**
|
||||
* Global cache store for mixin instances
|
||||
* Uses WeakMap to avoid memory leaks when components are destroyed
|
||||
*/
|
||||
const componentCaches = new WeakMap<
|
||||
VueComponentWithMixin,
|
||||
Map<string, CacheEntry<unknown>>
|
||||
>();
|
||||
|
||||
/**
|
||||
* Cache configuration constants
|
||||
*/
|
||||
const CACHE_DEFAULTS = {
|
||||
default: 15000, // 15 seconds default TTL
|
||||
} as const;
|
||||
// /**
|
||||
// * Global cache store for mixin instances
|
||||
// * Uses WeakMap to avoid memory leaks when components are destroyed
|
||||
// */
|
||||
// const componentCaches = new WeakMap<
|
||||
// VueComponentWithMixin,
|
||||
// Map<string, CacheEntry<unknown>>
|
||||
// >();
|
||||
//
|
||||
// /**
|
||||
// * Cache configuration constants
|
||||
// */
|
||||
// const CACHE_DEFAULTS = {
|
||||
// default: 15000, // 15 seconds default TTL
|
||||
// } as const;
|
||||
|
||||
const _memoryLogs: string[] = [];
|
||||
|
||||
@@ -178,8 +182,8 @@ export const PlatformServiceMixin = {
|
||||
logger.debug(
|
||||
`[PlatformServiceMixin] ActiveDid changed from ${oldDid} to ${newDid}`,
|
||||
);
|
||||
// Clear caches that might be affected by the change
|
||||
(this as any).$clearAllCaches();
|
||||
// // Clear caches that might be affected by the change
|
||||
// (this as any).$clearAllCaches();
|
||||
}
|
||||
},
|
||||
immediate: true,
|
||||
@@ -203,8 +207,8 @@ export const PlatformServiceMixin = {
|
||||
logger.debug(
|
||||
`[PlatformServiceMixin] ActiveDid updated from ${oldDid} to ${newDid}`,
|
||||
);
|
||||
// Clear caches that might be affected by the change
|
||||
this.$clearAllCaches();
|
||||
// // Clear caches that might be affected by the change
|
||||
// this.$clearAllCaches();
|
||||
}
|
||||
},
|
||||
|
||||
@@ -220,13 +224,20 @@ export const PlatformServiceMixin = {
|
||||
const obj: Record<string, unknown> = {};
|
||||
columns.forEach((column, index) => {
|
||||
let value = row[index];
|
||||
|
||||
|
||||
// Convert SQLite integer booleans to JavaScript booleans
|
||||
if (column === 'isRegistered' || column === 'finishedOnboarding' ||
|
||||
column === 'filterFeedByVisible' || column === 'filterFeedByNearby' ||
|
||||
column === 'hideRegisterPromptOnNewContact' || column === 'showContactGivesInline' ||
|
||||
column === 'showGeneralAdvanced' || column === 'showShortcutBvc' ||
|
||||
column === 'warnIfProdServer' || column === 'warnIfTestServer') {
|
||||
if (
|
||||
column === "isRegistered" ||
|
||||
column === "finishedOnboarding" ||
|
||||
column === "filterFeedByVisible" ||
|
||||
column === "filterFeedByNearby" ||
|
||||
column === "hideRegisterPromptOnNewContact" ||
|
||||
column === "showContactGivesInline" ||
|
||||
column === "showGeneralAdvanced" ||
|
||||
column === "showShortcutBvc" ||
|
||||
column === "warnIfProdServer" ||
|
||||
column === "warnIfTestServer"
|
||||
) {
|
||||
if (value === 1) {
|
||||
value = true;
|
||||
} else if (value === 0) {
|
||||
@@ -234,7 +245,16 @@ 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;
|
||||
@@ -244,6 +264,8 @@ export const PlatformServiceMixin = {
|
||||
/**
|
||||
* Self-contained implementation of parseJsonField
|
||||
* Safely parses JSON strings with fallback to default value
|
||||
*
|
||||
* Consolidate this with src/libs/util.ts parseJsonField
|
||||
*/
|
||||
_parseJsonField<T>(value: unknown, defaultValue: T): T {
|
||||
if (typeof value === "string") {
|
||||
@@ -256,71 +278,92 @@ export const PlatformServiceMixin = {
|
||||
return (value as T) || defaultValue;
|
||||
},
|
||||
|
||||
// =================================================
|
||||
// CACHING UTILITY METHODS
|
||||
// =================================================
|
||||
|
||||
/**
|
||||
* Get or initialize cache for this component instance
|
||||
* Convert Settings object to SettingsWithJsonStrings for database storage
|
||||
* Handles conversion of complex objects like searchBoxes to JSON strings
|
||||
* @param settings Settings object to convert
|
||||
* @returns SettingsWithJsonStrings object ready for database storage
|
||||
*/
|
||||
_getCache(): Map<string, CacheEntry<unknown>> {
|
||||
let cache = componentCaches.get(this as unknown as VueComponentWithMixin);
|
||||
if (!cache) {
|
||||
cache = new Map();
|
||||
componentCaches.set(this as unknown as VueComponentWithMixin, cache);
|
||||
_convertSettingsForStorage(
|
||||
settings: Partial<Settings>,
|
||||
): Partial<SettingsWithJsonStrings> {
|
||||
const converted = { ...settings } as Partial<SettingsWithJsonStrings>;
|
||||
|
||||
// Convert searchBoxes array to JSON string if present
|
||||
if (settings.searchBoxes !== undefined) {
|
||||
(converted as any).searchBoxes = Array.isArray(settings.searchBoxes)
|
||||
? JSON.stringify(settings.searchBoxes)
|
||||
: String(settings.searchBoxes);
|
||||
}
|
||||
return cache;
|
||||
|
||||
return converted;
|
||||
},
|
||||
|
||||
/**
|
||||
* Check if cache entry is valid (not expired)
|
||||
*/
|
||||
_isCacheValid(entry: CacheEntry<unknown>): boolean {
|
||||
return Date.now() - entry.timestamp < entry.ttl;
|
||||
},
|
||||
// // =================================================
|
||||
// // CACHING UTILITY METHODS
|
||||
// // =================================================
|
||||
|
||||
/**
|
||||
* Get data from cache if valid, otherwise return null
|
||||
*/
|
||||
_getCached<T>(key: string): T | null {
|
||||
const cache = this._getCache();
|
||||
const entry = cache.get(key);
|
||||
if (entry && this._isCacheValid(entry)) {
|
||||
return entry.data as T;
|
||||
}
|
||||
cache.delete(key); // Clean up expired entries
|
||||
return null;
|
||||
},
|
||||
// /**
|
||||
// * Get or initialize cache for this component instance
|
||||
// */
|
||||
// _getCache(): Map<string, CacheEntry<unknown>> {
|
||||
// let cache = componentCaches.get(this as unknown as VueComponentWithMixin);
|
||||
// if (!cache) {
|
||||
// cache = new Map();
|
||||
// componentCaches.set(this as unknown as VueComponentWithMixin, cache);
|
||||
// }
|
||||
// return cache;
|
||||
// },
|
||||
|
||||
/**
|
||||
* Store data in cache with TTL
|
||||
*/
|
||||
_setCached<T>(key: string, data: T, ttl?: number): T {
|
||||
const cache = this._getCache();
|
||||
const actualTtl = ttl || CACHE_DEFAULTS.default;
|
||||
cache.set(key, {
|
||||
data,
|
||||
timestamp: Date.now(),
|
||||
ttl: actualTtl,
|
||||
});
|
||||
return data;
|
||||
},
|
||||
// /**
|
||||
// * Check if cache entry is valid (not expired)
|
||||
// */
|
||||
// _isCacheValid(entry: CacheEntry<unknown>): boolean {
|
||||
// return Date.now() - entry.timestamp < entry.ttl;
|
||||
// },
|
||||
|
||||
/**
|
||||
* Invalidate specific cache entry
|
||||
*/
|
||||
_invalidateCache(key: string): void {
|
||||
const cache = this._getCache();
|
||||
cache.delete(key);
|
||||
},
|
||||
// /**
|
||||
// * Get data from cache if valid, otherwise return null
|
||||
// */
|
||||
// _getCached<T>(key: string): T | null {
|
||||
// const cache = this._getCache();
|
||||
// const entry = cache.get(key);
|
||||
// if (entry && this._isCacheValid(entry)) {
|
||||
// return entry.data as T;
|
||||
// }
|
||||
// cache.delete(key); // Clean up expired entries
|
||||
// return null;
|
||||
// },
|
||||
|
||||
/**
|
||||
* Clear all cache entries for this component
|
||||
*/
|
||||
_clearCache(): void {
|
||||
const cache = this._getCache();
|
||||
cache.clear();
|
||||
},
|
||||
// /**
|
||||
// * Store data in cache with TTL
|
||||
// */
|
||||
// _setCached<T>(key: string, data: T, ttl?: number): T {
|
||||
// const cache = this._getCache();
|
||||
// const actualTtl = ttl || CACHE_DEFAULTS.default;
|
||||
// cache.set(key, {
|
||||
// data,
|
||||
// timestamp: Date.now(),
|
||||
// ttl: actualTtl,
|
||||
// });
|
||||
// return data;
|
||||
// },
|
||||
|
||||
// /**
|
||||
// * Invalidate specific cache entry
|
||||
// */
|
||||
// _invalidateCache(key: string): void {
|
||||
// const cache = this._getCache();
|
||||
// cache.delete(key);
|
||||
// },
|
||||
|
||||
// /**
|
||||
// * Clear all cache entries for this component
|
||||
// */
|
||||
// _clearCache(): void {
|
||||
// const cache = this._getCache();
|
||||
// cache.clear();
|
||||
// },
|
||||
|
||||
// =================================================
|
||||
// ENHANCED DATABASE METHODS (with error handling)
|
||||
@@ -425,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;
|
||||
}
|
||||
},
|
||||
@@ -499,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;
|
||||
}
|
||||
},
|
||||
@@ -614,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);
|
||||
},
|
||||
|
||||
/**
|
||||
@@ -714,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;
|
||||
@@ -746,6 +844,9 @@ export const PlatformServiceMixin = {
|
||||
/**
|
||||
* Save default settings - $saveSettings()
|
||||
* Ultra-concise shortcut for updateDefaultSettings
|
||||
*
|
||||
* ✅ KEEP: This method will be the primary settings save method after consolidation
|
||||
*
|
||||
* @param changes Settings changes to save
|
||||
* @returns Promise<boolean> Success status
|
||||
*/
|
||||
@@ -760,10 +861,13 @@ export const PlatformServiceMixin = {
|
||||
|
||||
if (Object.keys(safeChanges).length === 0) return true;
|
||||
|
||||
// Convert settings for database storage (handles searchBoxes conversion)
|
||||
const convertedChanges = this._convertSettingsForStorage(safeChanges);
|
||||
|
||||
const setParts: string[] = [];
|
||||
const params: unknown[] = [];
|
||||
|
||||
Object.entries(safeChanges).forEach(([key, value]) => {
|
||||
Object.entries(convertedChanges).forEach(([key, value]) => {
|
||||
if (value !== undefined) {
|
||||
setParts.push(`${key} = ?`);
|
||||
params.push(value);
|
||||
@@ -810,10 +914,13 @@ export const PlatformServiceMixin = {
|
||||
|
||||
if (Object.keys(safeChanges).length === 0) return true;
|
||||
|
||||
// Convert settings for database storage (handles searchBoxes conversion)
|
||||
const convertedChanges = this._convertSettingsForStorage(safeChanges);
|
||||
|
||||
const setParts: string[] = [];
|
||||
const params: unknown[] = [];
|
||||
|
||||
Object.entries(safeChanges).forEach(([key, value]) => {
|
||||
Object.entries(convertedChanges).forEach(([key, value]) => {
|
||||
if (value !== undefined) {
|
||||
setParts.push(`${key} = ?`);
|
||||
params.push(value);
|
||||
@@ -872,13 +979,13 @@ export const PlatformServiceMixin = {
|
||||
return await this.$contacts();
|
||||
},
|
||||
|
||||
/**
|
||||
* Clear all caches for this component - $clearAllCaches()
|
||||
* Useful for manual cache management
|
||||
*/
|
||||
$clearAllCaches(): void {
|
||||
this._clearCache();
|
||||
},
|
||||
// /**
|
||||
// * Clear all caches for this component - $clearAllCaches()
|
||||
// * Useful for manual cache management
|
||||
// */
|
||||
// $clearAllCaches(): void {
|
||||
// this._clearCache();
|
||||
// },
|
||||
|
||||
// =================================================
|
||||
// HIGH-LEVEL ENTITY OPERATIONS (eliminate verbose SQL patterns)
|
||||
@@ -953,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,
|
||||
@@ -967,6 +1080,7 @@ export const PlatformServiceMixin = {
|
||||
safeContact.registered,
|
||||
safeContact.nextPubKeyHashB64,
|
||||
safeContact.profileImageUrl,
|
||||
safeContact.contactMethods,
|
||||
],
|
||||
);
|
||||
return true;
|
||||
@@ -994,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);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
@@ -1016,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];
|
||||
},
|
||||
|
||||
/**
|
||||
@@ -1184,6 +1295,17 @@ export const PlatformServiceMixin = {
|
||||
* @param did Optional DID for user-specific settings
|
||||
* @returns Promise<boolean> Success status
|
||||
*/
|
||||
/**
|
||||
* Update settings - $updateSettings()
|
||||
* Ultra-concise shortcut for updating settings (default or user-specific)
|
||||
*
|
||||
* ⚠️ DEPRECATED: This method will be removed in favor of $saveSettings()
|
||||
* Use $saveSettings(changes, did?) instead for better consistency
|
||||
*
|
||||
* @param changes Settings changes to save
|
||||
* @param did Optional DID for user-specific settings
|
||||
* @returns Promise<boolean> Success status
|
||||
*/
|
||||
async $updateSettings(
|
||||
changes: Partial<Settings>,
|
||||
did?: string,
|
||||
@@ -1401,7 +1523,9 @@ export const PlatformServiceMixin = {
|
||||
);
|
||||
|
||||
if (!result?.values?.length) {
|
||||
logger.warn(`[PlatformServiceMixin] No settings found for DID: ${did}`);
|
||||
logger.warn(
|
||||
`[PlatformServiceMixin] No settings found for DID: ${did}`,
|
||||
);
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -1411,7 +1535,9 @@ export const PlatformServiceMixin = {
|
||||
);
|
||||
|
||||
if (!mappedResults.length) {
|
||||
logger.warn(`[PlatformServiceMixin] Failed to map settings for DID: ${did}`);
|
||||
logger.warn(
|
||||
`[PlatformServiceMixin] Failed to map settings for DID: ${did}`,
|
||||
);
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -1426,7 +1552,10 @@ export const PlatformServiceMixin = {
|
||||
|
||||
return settings;
|
||||
} catch (error) {
|
||||
logger.error(`[PlatformServiceMixin] Error debugging settings for DID ${did}:`, error);
|
||||
logger.error(
|
||||
`[PlatformServiceMixin] Error debugging settings for DID ${did}:`,
|
||||
error,
|
||||
);
|
||||
return null;
|
||||
}
|
||||
},
|
||||
@@ -1440,14 +1569,24 @@ export const PlatformServiceMixin = {
|
||||
async $debugMergedSettings(did: string): Promise<void> {
|
||||
try {
|
||||
// Get default settings
|
||||
const defaultSettings = await this.$getSettings(MASTER_SETTINGS_KEY, {});
|
||||
logger.info(`[PlatformServiceMixin] Default settings:`, defaultSettings);
|
||||
const defaultSettings = await this.$getSettings(
|
||||
MASTER_SETTINGS_KEY,
|
||||
{},
|
||||
);
|
||||
logger.info(
|
||||
`[PlatformServiceMixin] Default settings:`,
|
||||
defaultSettings,
|
||||
);
|
||||
|
||||
// Get DID-specific settings
|
||||
const didSettings = await this.$debugDidSettings(did);
|
||||
|
||||
// Get merged settings
|
||||
const mergedSettings = await this.$getMergedSettings(MASTER_SETTINGS_KEY, did, defaultSettings || {});
|
||||
const mergedSettings = await this.$getMergedSettings(
|
||||
MASTER_SETTINGS_KEY,
|
||||
did,
|
||||
defaultSettings || {},
|
||||
);
|
||||
|
||||
logger.info(`[PlatformServiceMixin] Merged settings for ${did}:`, {
|
||||
defaultSettings,
|
||||
@@ -1456,7 +1595,10 @@ export const PlatformServiceMixin = {
|
||||
isRegistered: mergedSettings.isRegistered,
|
||||
});
|
||||
} catch (error) {
|
||||
logger.error(`[PlatformServiceMixin] Error debugging merged settings for DID ${did}:`, error);
|
||||
logger.error(
|
||||
`[PlatformServiceMixin] Error debugging merged settings for DID ${did}:`,
|
||||
error,
|
||||
);
|
||||
}
|
||||
},
|
||||
},
|
||||
@@ -1618,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>;
|
||||
@@ -1630,7 +1773,7 @@ declare module "@vue/runtime-core" {
|
||||
// Cache management methods
|
||||
$refreshSettings(): Promise<Settings>;
|
||||
$refreshContacts(): Promise<Contact[]>;
|
||||
$clearAllCaches(): void;
|
||||
// $clearAllCaches(): void;
|
||||
|
||||
// High-level entity operations (eliminate verbose SQL patterns)
|
||||
$mapResults<T>(
|
||||
|
||||
@@ -60,8 +60,10 @@
|
||||
@share-info="onShareInfo"
|
||||
/>
|
||||
|
||||
<!-- Notifications -->
|
||||
<!-- Currently disabled because it doesn't work, even on Chrome. If restored, make sure it works or doesn't show on mobile/electron. -->
|
||||
<section
|
||||
v-if="isRegistered"
|
||||
v-if="false"
|
||||
id="sectionNotifications"
|
||||
class="bg-slate-100 rounded-md overflow-hidden px-4 py-4 mt-8 mb-8"
|
||||
aria-labelledby="notificationsHeading"
|
||||
@@ -1396,10 +1398,6 @@ export default class AccountViewView extends Vue {
|
||||
if (imageResp.status === 200) {
|
||||
this.imageLimits = imageResp.data;
|
||||
} else {
|
||||
await this.$saveSettings({
|
||||
profileImageUrl: "",
|
||||
});
|
||||
this.profileImageUrl = "";
|
||||
this.limitsMessage = ACCOUNT_VIEW_CONSTANTS.LIMITS.NO_IMAGE_ACCESS;
|
||||
this.notify.warning(ACCOUNT_VIEW_CONSTANTS.LIMITS.CANNOT_UPLOAD_IMAGES);
|
||||
return;
|
||||
@@ -1414,17 +1412,14 @@ export default class AccountViewView extends Vue {
|
||||
if (endorserResp.status === 200) {
|
||||
this.endorserLimits = endorserResp.data;
|
||||
} else {
|
||||
await this.$saveSettings({
|
||||
profileImageUrl: "",
|
||||
});
|
||||
this.profileImageUrl = "";
|
||||
this.limitsMessage = ACCOUNT_VIEW_CONSTANTS.LIMITS.NO_LIMITS_FOUND;
|
||||
this.notify.warning(ACCOUNT_VIEW_CONSTANTS.LIMITS.BAD_SERVER_RESPONSE);
|
||||
return;
|
||||
}
|
||||
} catch (error) {
|
||||
this.limitsMessage = ACCOUNT_VIEW_CONSTANTS.LIMITS.ERROR_RETRIEVING_LIMITS;
|
||||
console.log("error: ", error);
|
||||
this.limitsMessage =
|
||||
ACCOUNT_VIEW_CONSTANTS.LIMITS.ERROR_RETRIEVING_LIMITS;
|
||||
logger.error("Error retrieving limits: ", error);
|
||||
// this.notify.error(this.limitsMessage, TIMEOUTS.STANDARD);
|
||||
} finally {
|
||||
this.loadingLimits = false;
|
||||
@@ -1483,7 +1478,7 @@ export default class AccountViewView extends Vue {
|
||||
async deleteImage(): Promise<void> {
|
||||
try {
|
||||
// Extract the image ID from the full URL
|
||||
const imageId = this.profileImageUrl?.split('/').pop();
|
||||
const imageId = this.profileImageUrl?.split("/").pop();
|
||||
if (!imageId) {
|
||||
this.notify.error("Invalid image URL");
|
||||
return;
|
||||
@@ -1668,14 +1663,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() {
|
||||
|
||||
@@ -52,7 +52,6 @@ function isApiResponse(response: unknown): response is AxiosResponse {
|
||||
);
|
||||
}
|
||||
|
||||
// TODO: Testing Required - Database Operations + Logging Migration to PlatformServiceMixin
|
||||
// Priority: High | Migrated: 2025-07-06 | Author: Matthew Raymer
|
||||
//
|
||||
// MIGRATION DETAILS: Migrated from legacy database utilities + logging to PlatformServiceMixin
|
||||
|
||||
@@ -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,
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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],
|
||||
|
||||
@@ -553,11 +553,6 @@ export default class ContactQRScanFull extends Vue {
|
||||
return;
|
||||
}
|
||||
|
||||
// Add new contact
|
||||
// @ts-expect-error because we're just using the value to store to the DB
|
||||
contact.contactMethods = JSON.stringify(
|
||||
(this as any)._parseJsonField(contact.contactMethods, []),
|
||||
);
|
||||
await this.$insertContact(contact);
|
||||
|
||||
if (this.activeDid) {
|
||||
|
||||
@@ -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;
|
||||
@@ -213,15 +213,11 @@ export default class ContactQRScanShow extends Vue {
|
||||
$router!: Router;
|
||||
|
||||
// Notification helper system
|
||||
private notify = createNotifyHelpers(this.$notify);
|
||||
notify!: ReturnType<typeof createNotifyHelpers>;
|
||||
|
||||
activeDid = "";
|
||||
apiServer = "";
|
||||
|
||||
// Axios instance for API calls
|
||||
get axios() {
|
||||
return (this as any).$platformService.axios;
|
||||
}
|
||||
givenName = "";
|
||||
hideRegisterPromptOnNewContact = false;
|
||||
isRegistered = false;
|
||||
@@ -288,6 +284,8 @@ export default class ContactQRScanShow extends Vue {
|
||||
}
|
||||
|
||||
async created() {
|
||||
this.notify = createNotifyHelpers(this.$notify);
|
||||
|
||||
try {
|
||||
const settings = await this.$accountSettings();
|
||||
this.activeDid = settings.activeDid || "";
|
||||
@@ -547,6 +545,7 @@ export default class ContactQRScanShow extends Vue {
|
||||
name: contact.name,
|
||||
});
|
||||
this.notify.toast(
|
||||
"Submitted",
|
||||
NOTIFY_QR_REGISTRATION_SUBMITTED.message,
|
||||
QR_TIMEOUT_SHORT,
|
||||
);
|
||||
@@ -611,21 +610,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() {
|
||||
|
||||
@@ -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,
|
||||
);
|
||||
|
||||
@@ -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>
|
||||
@@ -273,6 +273,7 @@ import {
|
||||
displayAmount,
|
||||
getHeaders,
|
||||
register,
|
||||
setVisibilityUtil,
|
||||
} from "../libs/endorserServer";
|
||||
import * as libsUtil from "../libs/util";
|
||||
import EntityIcon from "../components/EntityIcon.vue";
|
||||
@@ -324,6 +325,7 @@ export default class DIDView extends Vue {
|
||||
apiServer = "";
|
||||
claims: Array<GenericCredWrapper<GenericVerifiableCredential>> = [];
|
||||
contactFromDid?: Contact;
|
||||
|
||||
contactYaml = "";
|
||||
hitEnd = false;
|
||||
isLoading = false;
|
||||
@@ -722,18 +724,31 @@ export default class DIDView extends Vue {
|
||||
visibility: boolean,
|
||||
showSuccessAlert: boolean,
|
||||
) {
|
||||
// Update contact visibility using mixin method
|
||||
await this.$updateContact(contact.did, { seesMe: visibility });
|
||||
const result = await setVisibilityUtil(
|
||||
this.activeDid,
|
||||
this.apiServer,
|
||||
this.axios,
|
||||
contact,
|
||||
visibility,
|
||||
);
|
||||
|
||||
if (showSuccessAlert) {
|
||||
if (result.success) {
|
||||
if (showSuccessAlert) {
|
||||
const message =
|
||||
(contact.name || "That user") +
|
||||
" can " +
|
||||
(visibility ? "" : "not ") +
|
||||
"see your activity.";
|
||||
this.notify.success(message, TIMEOUTS.SHORT);
|
||||
}
|
||||
return true;
|
||||
} else {
|
||||
logger.error("Got strange result from setting visibility:", result);
|
||||
const message =
|
||||
(contact.name || "That user") +
|
||||
" can " +
|
||||
(visibility ? "" : "not ") +
|
||||
"see your activity.";
|
||||
this.notify.success(message, TIMEOUTS.SHORT);
|
||||
(result.error as string) || "Could not set visibility on the server.";
|
||||
this.notify.error(message, TIMEOUTS.LONG);
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -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) {
|
||||
@@ -594,10 +588,20 @@ export default class GiftedDetails extends Vue {
|
||||
this.imageUrl = "";
|
||||
} catch (error) {
|
||||
logger.error("Error deleting image:", error);
|
||||
this.notify.error(
|
||||
NOTIFY_GIFTED_DETAILS_DELETE_IMAGE_ERROR.message,
|
||||
TIMEOUTS.LONG,
|
||||
);
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
if ((error as any)?.response?.status === 404) {
|
||||
logger.log("Weird: the image was already deleted.", error);
|
||||
|
||||
localStorage.removeItem("imageUrl");
|
||||
this.imageUrl = "";
|
||||
|
||||
// it already doesn't exist so we won't say anything to the user
|
||||
} else {
|
||||
this.notify.error(
|
||||
NOTIFY_GIFTED_DETAILS_DELETE_IMAGE_ERROR.message,
|
||||
TIMEOUTS.LONG,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -611,14 +615,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 +633,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 +642,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 +677,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 +693,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,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -62,59 +62,6 @@
|
||||
|
||||
<h2 class="text-xl font-semibold">I want to know more because...</h2>
|
||||
<ul class="list-disc list-outside ml-4">
|
||||
<li class="p-2">
|
||||
<div class="text-blue-500" @click="toggleAlpha">... I'm a member of Alpha chat.</div>
|
||||
<div v-if="showAlpha">
|
||||
<p>
|
||||
This is a project for public benefit. You are invited to add your gratitude
|
||||
and propose projects on a distributable ledger.
|
||||
</p>
|
||||
<p>
|
||||
The underlying data is on a merkle tree with each verifiable claim, signature and all.
|
||||
The chain includes individual IDs for discovery & visibility, so not all data is distributed -- yet.
|
||||
The goal is to eventually distribute the data on people's devices with their chosen network,
|
||||
where anyone could host their own chain of provenance if they choose.
|
||||
The formats follow standard schemas (eg. schema.org) to encourage interoperability.
|
||||
We're currently at the beginning phase where we're trusting the server to keep IDs private.
|
||||
It's all open-source, and we expect to have a professional audit someday.
|
||||
</p>
|
||||
<p>
|
||||
A person's network of contacts is similar: the server currently knows some of the links between people
|
||||
to allow discovery and visibility. However, even that will be manageable on personal devices someday.
|
||||
</p>
|
||||
<p>
|
||||
There are no tokens to maintain the chain: the purpose is to create software that communities
|
||||
and activists can easily join and use. We're betting that this is a case where network
|
||||
participants have the motivation to run the software. The protocol is meant to be lightweight enough that
|
||||
non-technical people can run it on inexpensive devices they already own. There may be cases for
|
||||
MPC or ZKP in the future when they are more widespread and standard,
|
||||
but our preference is to engineer as simply as possible with "white-magic" cryptography
|
||||
over those "black-magic" functions.
|
||||
</p>
|
||||
<p>
|
||||
Let's make real distributed computing and shared data happen, starting with our own small networks.
|
||||
</p>
|
||||
<p>
|
||||
... and exemplify the fun along the way.
|
||||
</p>
|
||||
</div>
|
||||
</li>
|
||||
<li class="p-2">
|
||||
<div class="text-blue-500" @click="toggleGroup">... I want to find a group I'll enjoy working with.</div>
|
||||
<div v-if="showGroup">
|
||||
<p>
|
||||
This app encourages people to offer small bits of time to one another. It's a way to
|
||||
run experiments with other people... tests of working together, which can start small
|
||||
and easy but build into cooperation with people who are like-minded and who work well together.
|
||||
</p>
|
||||
<p>
|
||||
Search the projects and place an offer on an interesting one
|
||||
-- or create your own project and see who offers to help.
|
||||
After your first experiment, you can give and get confirmation about the work, which you might choose
|
||||
to show to future contacts.
|
||||
</p>
|
||||
</div>
|
||||
</li>
|
||||
<li class="p-2">
|
||||
<div class="text-blue-500" @click="toggleCommunity">... I want to participate in community projects.</div>
|
||||
<div v-if="showCommunity">
|
||||
@@ -188,6 +135,59 @@
|
||||
</p>
|
||||
</div>
|
||||
</li>
|
||||
<li class="p-2">
|
||||
<div class="text-blue-500" @click="toggleGroup">... I want to find a group I'll enjoy working with.</div>
|
||||
<div v-if="showGroup">
|
||||
<p>
|
||||
This app encourages people to offer small bits of time to one another. It's a way to
|
||||
run experiments with other people... tests of working together, which can start small
|
||||
and easy but build into cooperation with people who are like-minded and who work well together.
|
||||
</p>
|
||||
<p>
|
||||
Search the projects and place an offer on an interesting one
|
||||
-- or create your own project and see who offers to help.
|
||||
After your first experiment, you can give and get confirmation about the work, which you might choose
|
||||
to show to future contacts.
|
||||
</p>
|
||||
</div>
|
||||
</li>
|
||||
<li class="p-2">
|
||||
<div class="text-blue-500" @click="toggleAlpha">... I'm a member of Alpha chat.</div>
|
||||
<div v-if="showAlpha">
|
||||
<p>
|
||||
This is a project for public benefit. You are invited to add your gratitude
|
||||
and propose projects on a distributable ledger.
|
||||
</p>
|
||||
<p>
|
||||
The underlying data is on a merkle tree with each verifiable claim, signature and all.
|
||||
The chain includes individual IDs for discovery & visibility, so not all data is distributed -- yet.
|
||||
The goal is to eventually distribute the data on people's devices with their chosen network,
|
||||
where anyone could host their own chain of provenance if they choose.
|
||||
The formats follow standard schemas (eg. schema.org) to encourage interoperability.
|
||||
We're currently at the beginning phase where we're trusting the server to keep IDs private.
|
||||
It's all open-source, and we expect to have a professional audit someday.
|
||||
</p>
|
||||
<p>
|
||||
A person's network of contacts is similar: the server currently knows some of the links between people
|
||||
to allow discovery and visibility. However, even that will be manageable on personal devices someday.
|
||||
</p>
|
||||
<p>
|
||||
There are no tokens to maintain the chain: the purpose is to create software that communities
|
||||
and activists can easily join and use. We're betting that this is a case where network
|
||||
participants have the motivation to run the software. The protocol is meant to be lightweight enough that
|
||||
non-technical people can run it on inexpensive devices they already own. There may be cases for
|
||||
MPC or ZKP in the future when they are more widespread and standard,
|
||||
but our preference is to engineer as simply as possible with "white-magic" cryptography
|
||||
over those "black-magic" functions.
|
||||
</p>
|
||||
<p>
|
||||
Let's make real distributed computing and shared data happen, starting with our own small networks.
|
||||
</p>
|
||||
<p>
|
||||
... and exemplify the fun along the way.
|
||||
</p>
|
||||
</div>
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
<h2 class="text-xl font-semibold">How do I get started?</h2>
|
||||
|
||||
@@ -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" />
|
||||
|
||||
@@ -230,11 +234,9 @@ Raymer * @version 1.0.0 */
|
||||
:last-viewed-claim-id="feedLastViewedClaimId"
|
||||
:is-registered="isRegistered"
|
||||
:active-did="activeDid"
|
||||
:confirmer-id-list="record.confirmerIdList"
|
||||
:on-image-cache="cacheImageData"
|
||||
@load-claim="onClickLoadClaim"
|
||||
@view-image="openImageViewer"
|
||||
@confirm-claim="confirmClaim"
|
||||
/>
|
||||
</ul>
|
||||
</InfiniteScroll>
|
||||
@@ -302,18 +304,14 @@ import {
|
||||
OnboardPage,
|
||||
} from "../libs/util";
|
||||
import { GiveSummaryRecord } from "../interfaces/records";
|
||||
import * as serverUtil from "../libs/endorserServer";
|
||||
import { logger } from "../utils/logger";
|
||||
import { GiveRecordWithContactInfo } from "../interfaces/give";
|
||||
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 { NOTIFY_CONTACT_LOADING_ISSUE } from "@/constants/notifications";
|
||||
import * as Package from "../../package.json";
|
||||
|
||||
// consolidate this with GiveActionClaim in src/interfaces/claims.ts
|
||||
interface Claim {
|
||||
claim?: Claim; // For nested claims in Verifiable Credentials
|
||||
agent?: {
|
||||
@@ -484,6 +482,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",
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -526,11 +528,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}`,
|
||||
@@ -598,65 +596,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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -680,44 +634,6 @@ 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
|
||||
*
|
||||
* @internal
|
||||
* Called by mounted() and reloadFeedOnChange()
|
||||
*/
|
||||
private async loadSettings() {
|
||||
// Use the current activeDid (set in initializeIdentity) to get user-specific settings
|
||||
const settings = await this.$accountSettings(this.activeDid, {
|
||||
apiServer: "",
|
||||
activeDid: "",
|
||||
filterFeedByVisible: false,
|
||||
filterFeedByNearby: false,
|
||||
isRegistered: false,
|
||||
});
|
||||
|
||||
this.apiServer = settings.apiServer || "";
|
||||
|
||||
// **CRITICAL**: Ensure correct API server for platform
|
||||
await this.ensureCorrectApiServer();
|
||||
|
||||
this.activeDid = settings.activeDid || "";
|
||||
this.feedLastViewedClaimId = settings.lastViewedClaimId;
|
||||
this.givenName = settings.firstName || "";
|
||||
this.isFeedFilteredByVisible = !!settings.filterFeedByVisible;
|
||||
this.isFeedFilteredByNearby = !!settings.filterFeedByNearby;
|
||||
this.isRegistered = !!settings.isRegistered;
|
||||
this.lastAckedOfferToUserJwtId = settings.lastAckedOfferToUserJwtId;
|
||||
this.lastAckedOfferToUserProjectsJwtId =
|
||||
settings.lastAckedOfferToUserProjectsJwtId;
|
||||
this.searchBoxes = settings.searchBoxes || [];
|
||||
this.showShortcutBvc = !!settings.showShortcutBvc;
|
||||
this.isAnyFeedFilterOn = checkIsAnyFeedFilterOn(settings);
|
||||
}
|
||||
|
||||
/**
|
||||
* Loads user contacts from database using ultra-concise mixin
|
||||
* Used for displaying contact info in feed and actions
|
||||
@@ -732,36 +648,6 @@ export default class HomeView extends Vue {
|
||||
.map((c) => c.did);
|
||||
}
|
||||
|
||||
/**
|
||||
* Verifies user registration status with endorser service
|
||||
* - Checks if unregistered user can access API
|
||||
* - Updates registration status if successful
|
||||
* - Preserves unregistered state on failure
|
||||
*
|
||||
* @internal
|
||||
* Called by mounted() and initializeIdentity()
|
||||
*/
|
||||
private async checkRegistrationStatus() {
|
||||
if (!this.isRegistered && this.activeDid) {
|
||||
try {
|
||||
const resp = await fetchEndorserRateLimits(
|
||||
this.apiServer,
|
||||
this.axios,
|
||||
this.activeDid,
|
||||
);
|
||||
if (resp.status === 200) {
|
||||
// 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 (e) {
|
||||
// ignore the error... just keep us unregistered
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Initializes feed data
|
||||
* Triggers updateAllFeed() to populate activity feed
|
||||
@@ -814,7 +700,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);
|
||||
}
|
||||
@@ -1590,37 +1476,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,
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1657,17 +1541,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
|
||||
*
|
||||
@@ -1771,53 +1644,6 @@ export default class HomeView extends Vue {
|
||||
this.isImageViewerOpen = true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles claim confirmation
|
||||
*
|
||||
* @public
|
||||
* Called by ActivityListItem component
|
||||
* @param record Record to confirm
|
||||
*/
|
||||
async confirmClaim(record: GiveRecordWithContactInfo) {
|
||||
this.notify.confirm(
|
||||
"Do you personally confirm that this is true?",
|
||||
async () => {
|
||||
const goodClaim = serverUtil.removeSchemaContext(
|
||||
serverUtil.removeVisibleToDids(
|
||||
serverUtil.addLastClaimOrHandleAsIdIfMissing(
|
||||
record.fullClaim,
|
||||
record.jwtId,
|
||||
record.handleId,
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
const confirmationClaim = {
|
||||
"@context": "https://schema.org",
|
||||
"@type": "AgreeAction",
|
||||
object: goodClaim,
|
||||
};
|
||||
|
||||
const result = await serverUtil.createAndSubmitClaim(
|
||||
confirmationClaim,
|
||||
this.activeDid,
|
||||
this.apiServer,
|
||||
this.axios,
|
||||
);
|
||||
|
||||
if (result.success) {
|
||||
this.notify.confirmationSubmitted();
|
||||
|
||||
// Refresh the feed to show updated confirmation status
|
||||
await this.updateAllFeed();
|
||||
} else {
|
||||
logger.error("Error submitting confirmation:", result);
|
||||
this.notify.error(NOTIFY_CONFIRMATION_ERROR.message, TIMEOUTS.LONG);
|
||||
}
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
private handleQRCodeClick() {
|
||||
if (Capacitor.isNativePlatform()) {
|
||||
this.$router.push({ name: "contact-qr-scan-full" });
|
||||
@@ -1826,17 +1652,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();
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -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" });
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -115,6 +115,8 @@ import {
|
||||
serverMessageForUser,
|
||||
} from "../libs/endorserServer";
|
||||
import { PlatformServiceMixin } from "@/utils/PlatformServiceMixin";
|
||||
import { createNotifyHelpers, TIMEOUTS } from "@/utils/notify";
|
||||
import { NotificationIface } from "@/constants/app";
|
||||
|
||||
interface Meeting {
|
||||
name: string;
|
||||
@@ -129,19 +131,11 @@ interface Meeting {
|
||||
mixins: [PlatformServiceMixin],
|
||||
})
|
||||
export default class OnboardMeetingListView extends Vue {
|
||||
$notify!: (
|
||||
notification: {
|
||||
group: string;
|
||||
type: string;
|
||||
title: string;
|
||||
text: string;
|
||||
onYes?: () => void;
|
||||
yesText?: string;
|
||||
},
|
||||
timeout?: number,
|
||||
) => void;
|
||||
$notify!: (notification: NotificationIface, timeout?: number) => void;
|
||||
$router!: Router;
|
||||
|
||||
notify!: ReturnType<typeof createNotifyHelpers>;
|
||||
|
||||
activeDid = "";
|
||||
apiServer = "";
|
||||
attendingMeeting: Meeting | null = null;
|
||||
@@ -153,30 +147,66 @@ export default class OnboardMeetingListView extends Vue {
|
||||
selectedMeeting: Meeting | null = null;
|
||||
showPasswordDialog = false;
|
||||
|
||||
/**
|
||||
* Vue lifecycle hook - component initialization
|
||||
*
|
||||
* Initializes the component by loading user settings and fetching available
|
||||
* onboarding meetings. This method is called when the component is created
|
||||
* and sets up all necessary data for the meeting list interface.
|
||||
*
|
||||
* Workflow:
|
||||
* 1. Initialize notification system using createNotifyHelpers
|
||||
* 2. Load user account settings (DID, API server, registration status)
|
||||
* 3. Fetch available onboarding meetings from the server
|
||||
*
|
||||
* Dependencies:
|
||||
* - PlatformServiceMixin for settings access ($accountSettings)
|
||||
* - Server API for meeting data (fetchMeetings)
|
||||
*
|
||||
* Error Handling:
|
||||
* - Server errors during meeting fetch are handled in fetchMeetings()
|
||||
*
|
||||
* @author Matthew Raymer
|
||||
*/
|
||||
async created() {
|
||||
const settings = await this.$accountSettings();
|
||||
this.notify = createNotifyHelpers(this.$notify);
|
||||
|
||||
if (settings?.activeDid) {
|
||||
try {
|
||||
// Verify database settings are accessible
|
||||
await this.$query("SELECT * FROM settings WHERE accountDid = ?", [
|
||||
settings.activeDid,
|
||||
]);
|
||||
} catch (error) {
|
||||
logger.error("Error checking database settings:", error);
|
||||
}
|
||||
}
|
||||
// Load user account settings
|
||||
const settings = await this.$accountSettings();
|
||||
|
||||
this.activeDid = settings?.activeDid || "";
|
||||
this.apiServer = settings?.apiServer || "";
|
||||
this.firstName = settings?.firstName || "";
|
||||
this.isRegistered = !!settings?.isRegistered;
|
||||
|
||||
if (this.isRegistered) {
|
||||
await this.fetchMeetings();
|
||||
}
|
||||
await this.fetchMeetings();
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetches available onboarding meetings from the server
|
||||
*
|
||||
* This method retrieves the list of onboarding meetings that the user can join.
|
||||
* It first checks if the user is already attending a meeting, and if so,
|
||||
* displays that meeting instead of the full list.
|
||||
*
|
||||
* Workflow:
|
||||
* 1. Check if user is already attending a meeting (groupOnboardMember endpoint)
|
||||
* 2. If attending: Fetch meeting details and display single meeting view
|
||||
* 3. If not attending: Fetch all available meetings (groupsOnboarding endpoint)
|
||||
* 4. Handle loading states and error conditions
|
||||
*
|
||||
* API Endpoints Used:
|
||||
* - GET /api/partner/groupOnboardMember - Check current attendance
|
||||
* - GET /api/partner/groupOnboard/{id} - Get meeting details
|
||||
* - GET /api/partner/groupsOnboarding - Get all available meetings
|
||||
*
|
||||
* State Management:
|
||||
* - Sets isLoading flag during API calls
|
||||
* - Updates attendingMeeting or meetings array
|
||||
* - Handles error states with user notifications
|
||||
*
|
||||
* @author Matthew Raymer
|
||||
*/
|
||||
async fetchMeetings() {
|
||||
this.isLoading = true;
|
||||
try {
|
||||
@@ -226,20 +256,36 @@ export default class OnboardMeetingListView extends Vue {
|
||||
"Error fetching meetings: " + errorStringForLog(error),
|
||||
true,
|
||||
);
|
||||
this.$notify(
|
||||
{
|
||||
group: "alert",
|
||||
type: "danger",
|
||||
title: "Error",
|
||||
text: serverMessageForUser(error) || "Failed to fetch meetings.",
|
||||
},
|
||||
5000,
|
||||
this.notify.error(
|
||||
serverMessageForUser(error) || "There was a problem fetching meetings.",
|
||||
TIMEOUTS.LONG,
|
||||
);
|
||||
} finally {
|
||||
this.isLoading = false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Opens the password dialog for joining a meeting
|
||||
*
|
||||
* This method initiates the process of joining an onboarding meeting by
|
||||
* opening a modal dialog that prompts the user for the meeting password.
|
||||
* The dialog is focused and ready for input when displayed.
|
||||
*
|
||||
* Workflow:
|
||||
* 1. Clear any previous password input
|
||||
* 2. Store the selected meeting for later use
|
||||
* 3. Show the password dialog modal
|
||||
* 4. Focus the password input field for immediate typing
|
||||
*
|
||||
* UI State Changes:
|
||||
* - Sets showPasswordDialog to true (shows modal)
|
||||
* - Clears password field for fresh input
|
||||
* - Stores selectedMeeting for password submission
|
||||
*
|
||||
* @param meeting - The meeting object the user wants to join
|
||||
* @author Matthew Raymer
|
||||
*/
|
||||
promptPassword(meeting: Meeting) {
|
||||
this.password = "";
|
||||
this.selectedMeeting = meeting;
|
||||
@@ -252,12 +298,61 @@ export default class OnboardMeetingListView extends Vue {
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Cancels the password dialog and resets state
|
||||
*
|
||||
* This method handles the cancellation of the meeting password dialog.
|
||||
* It cleans up the dialog state and resets all related variables to
|
||||
* their initial state, ensuring a clean slate for future dialog interactions.
|
||||
*
|
||||
* State Cleanup:
|
||||
* - Clears password input field
|
||||
* - Removes selected meeting reference
|
||||
* - Hides password dialog modal
|
||||
*
|
||||
* This ensures that if the user reopens the dialog, they start with
|
||||
* a fresh state without any leftover data from previous attempts.
|
||||
*
|
||||
* @author Matthew Raymer
|
||||
*/
|
||||
cancelPasswordDialog() {
|
||||
this.password = "";
|
||||
this.selectedMeeting = null;
|
||||
this.showPasswordDialog = false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Submits the password and joins the selected meeting
|
||||
*
|
||||
* This method handles the complete workflow of joining an onboarding meeting.
|
||||
* It encrypts the user's member data with the provided password and sends
|
||||
* it to the server to register the user as a meeting participant.
|
||||
*
|
||||
* Workflow:
|
||||
* 1. Validate that a meeting is selected (safety check)
|
||||
* 2. Create member data object with user information
|
||||
* 3. Encrypt member data using the meeting password
|
||||
* 4. Send encrypted data to server via groupOnboardMember endpoint
|
||||
* 5. On success: Navigate to meeting members view with credentials
|
||||
* 6. On failure: Show error notification to user
|
||||
*
|
||||
* Data Encryption:
|
||||
* - Member data includes: name, DID, registration status
|
||||
* - Data is encrypted using the meeting password for security
|
||||
* - Encrypted data is sent to server for verification
|
||||
*
|
||||
* Navigation:
|
||||
* - On successful join: Redirects to onboard-meeting-members view
|
||||
* - Passes groupId, password, and memberId as route parameters
|
||||
* - Allows user to see other meeting participants
|
||||
*
|
||||
* Error Handling:
|
||||
* - Invalid passwords result in server rejection
|
||||
* - Network errors are caught and displayed to user
|
||||
* - All errors are logged for debugging purposes
|
||||
*
|
||||
* @author Matthew Raymer
|
||||
*/
|
||||
async submitPassword() {
|
||||
if (!this.selectedMeeting) {
|
||||
// this should never happen
|
||||
@@ -316,69 +411,95 @@ export default class OnboardMeetingListView extends Vue {
|
||||
"Error joining meeting: " + errorStringForLog(error),
|
||||
true,
|
||||
);
|
||||
this.$notify(
|
||||
{
|
||||
group: "alert",
|
||||
type: "danger",
|
||||
title: "Error",
|
||||
text:
|
||||
serverMessageForUser(error) || "You failed to join the meeting.",
|
||||
},
|
||||
5000,
|
||||
this.notify.error(
|
||||
serverMessageForUser(error) ||
|
||||
"There was a problem joining the meeting.",
|
||||
TIMEOUTS.LONG,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Prompts user to confirm leaving the current meeting
|
||||
*
|
||||
* This method initiates the process of leaving an onboarding meeting.
|
||||
* It shows a confirmation dialog to prevent accidental departures,
|
||||
* then handles the server-side removal and UI updates.
|
||||
*
|
||||
* Workflow:
|
||||
* 1. Display confirmation dialog asking user to confirm departure
|
||||
* 2. On confirmation: Send DELETE request to groupOnboardMember endpoint
|
||||
* 3. On success: Clear attending meeting state and refresh meeting list
|
||||
* 4. Show success notification to user
|
||||
* 5. On failure: Show error notification with details
|
||||
*
|
||||
* Server Interaction:
|
||||
* - DELETE /api/partner/groupOnboardMember - Removes user from meeting
|
||||
* - Requires authentication headers for user verification
|
||||
* - Server handles the actual removal from meeting database
|
||||
*
|
||||
* State Management:
|
||||
* - Clears attendingMeeting when successfully left
|
||||
* - Refreshes meetings list to show updated availability
|
||||
* - Updates UI to show meeting list instead of single meeting
|
||||
*
|
||||
* User Experience:
|
||||
* - Confirmation prevents accidental departures
|
||||
* - Clear feedback on success/failure
|
||||
* - Seamless transition back to meeting list
|
||||
*
|
||||
* @author Matthew Raymer
|
||||
*/
|
||||
async leaveMeeting() {
|
||||
this.$notify(
|
||||
{
|
||||
group: "modal",
|
||||
type: "confirm",
|
||||
title: "Leave Meeting",
|
||||
text: "Are you sure you want to leave this meeting?",
|
||||
onYes: async () => {
|
||||
try {
|
||||
const headers = await getHeaders(this.activeDid);
|
||||
await this.axios.delete(
|
||||
this.apiServer + "/api/partner/groupOnboardMember",
|
||||
{ headers },
|
||||
);
|
||||
this.notify.confirm(
|
||||
"Are you sure you want to leave this meeting?",
|
||||
async () => {
|
||||
try {
|
||||
const headers = await getHeaders(this.activeDid);
|
||||
await this.axios.delete(
|
||||
this.apiServer + "/api/partner/groupOnboardMember",
|
||||
{ headers },
|
||||
);
|
||||
|
||||
this.attendingMeeting = null;
|
||||
await this.fetchMeetings();
|
||||
this.attendingMeeting = null;
|
||||
await this.fetchMeetings();
|
||||
|
||||
this.$notify(
|
||||
{
|
||||
group: "alert",
|
||||
type: "success",
|
||||
title: "Success",
|
||||
text: "You left the meeting.",
|
||||
},
|
||||
5000,
|
||||
);
|
||||
} catch (error) {
|
||||
this.$logAndConsole(
|
||||
"Error leaving meeting: " + errorStringForLog(error),
|
||||
true,
|
||||
);
|
||||
this.$notify(
|
||||
{
|
||||
group: "alert",
|
||||
type: "danger",
|
||||
title: "Error",
|
||||
text:
|
||||
serverMessageForUser(error) ||
|
||||
"You failed to leave the meeting.",
|
||||
},
|
||||
5000,
|
||||
);
|
||||
}
|
||||
},
|
||||
this.notify.success("You left the meeting.", TIMEOUTS.LONG);
|
||||
} catch (error) {
|
||||
this.$logAndConsole(
|
||||
"Error leaving meeting: " + errorStringForLog(error),
|
||||
true,
|
||||
);
|
||||
this.notify.error(
|
||||
serverMessageForUser(error) ||
|
||||
"There was a problem leaving the meeting.",
|
||||
TIMEOUTS.LONG,
|
||||
);
|
||||
}
|
||||
},
|
||||
-1,
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Navigates to the meeting creation page
|
||||
*
|
||||
* This method handles the navigation to the meeting setup page where
|
||||
* registered users can create new onboarding meetings. It's only
|
||||
* available to users who are registered in the system.
|
||||
*
|
||||
* Navigation:
|
||||
* - Routes to onboard-meeting-setup view
|
||||
* - Allows user to configure new meeting settings
|
||||
* - Only accessible to registered users (controlled by template)
|
||||
*
|
||||
* User Flow:
|
||||
* - User clicks "Create Meeting" button
|
||||
* - System navigates to setup page
|
||||
* - User can configure meeting name, password, etc.
|
||||
* - New meeting becomes available to other users
|
||||
*
|
||||
* @author Matthew Raymer
|
||||
*/
|
||||
createMeeting() {
|
||||
this.$router.push({ name: "onboard-meeting-setup" });
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -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,
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -310,10 +310,9 @@ export default class SearchAreaView extends Vue {
|
||||
},
|
||||
};
|
||||
|
||||
const searchBoxes = JSON.stringify([newSearchBox]);
|
||||
|
||||
// Store search box configuration using platform service
|
||||
await this.$updateSettings({ searchBoxes: searchBoxes as any });
|
||||
// searchBoxes will be automatically converted to JSON string by $updateSettings
|
||||
await this.$updateSettings({ searchBoxes: [newSearchBox] });
|
||||
|
||||
this.searchBox = newSearchBox;
|
||||
this.isChoosingSearchBox = false;
|
||||
@@ -347,7 +346,7 @@ export default class SearchAreaView extends Vue {
|
||||
try {
|
||||
// Clear search box settings and disable nearby filtering
|
||||
await this.$updateSettings({
|
||||
searchBoxes: "[]" as any,
|
||||
searchBoxes: [],
|
||||
filterFeedByNearby: false,
|
||||
});
|
||||
|
||||
|
||||
@@ -75,15 +75,9 @@ export default class ShareMyContactInfoView extends Vue {
|
||||
isLoading = false;
|
||||
|
||||
async mounted() {
|
||||
// Debug logging for test diagnosis
|
||||
const settings = await this.$settings();
|
||||
|
||||
const activeDid = settings?.activeDid;
|
||||
// @ts-expect-error - Debug property for testing contact sharing functionality
|
||||
window.__SHARE_CONTACT_DEBUG__ = { settings, activeDid };
|
||||
// eslint-disable-next-line no-console
|
||||
if (!activeDid) {
|
||||
// eslint-disable-next-line no-console
|
||||
this.$router.push({ name: "home" });
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,9 +4,7 @@
|
||||
"skipLibCheck": true,
|
||||
"module": "ESNext",
|
||||
"moduleResolution": "bundler",
|
||||
"allowSyntheticDefaultImports": true,
|
||||
"allowImportingTsExtensions": true,
|
||||
"noEmit": true
|
||||
"allowSyntheticDefaultImports": true
|
||||
},
|
||||
"include": ["vite.config.*"]
|
||||
}
|
||||
@@ -6,7 +6,9 @@ import path from "path";
|
||||
import { fileURLToPath } from 'url';
|
||||
|
||||
// Load environment variables
|
||||
dotenv.config();
|
||||
console.log('NODE_ENV:', process.env.NODE_ENV)
|
||||
dotenv.config({ path: `.env.${process.env.NODE_ENV}` })
|
||||
|
||||
|
||||
const __filename = fileURLToPath(import.meta.url);
|
||||
const __dirname = path.dirname(__filename);
|
||||
|
||||
Reference in New Issue
Block a user