Compare commits
82 Commits
onboard-al
...
activedid_
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
9c8e8d573d | ||
| 43e7bc1c12 | |||
|
|
1a77dfb750 | ||
|
|
1365adad92 | ||
|
|
baccb962cf | ||
| aa346a9abd | |||
| 9ea2f96106 | |||
| 623bf12ecd | |||
|
|
427660d686 | ||
|
|
643f31c43a | ||
| 2c6b787fa2 | |||
|
|
ec53452220 | ||
|
|
ec326495b2 | ||
|
|
cc50c38d13 | ||
| 3969167d92 | |||
|
|
9dfb2fda27 | ||
|
|
d3aa2e40a0 | ||
|
|
08cda50f13 | ||
| 716a23e76b | |||
| 7f499a0fc0 | |||
| 1b343b598c | |||
|
|
e588e223bc | ||
|
|
02eead5609 | ||
| 1e203da9bb | |||
|
|
504f26190f | ||
|
|
1893c2af1b | ||
|
|
df49c80199 | ||
|
|
4d89042997 | ||
|
|
0c9ede9fc9 | ||
|
|
dc857f9119 | ||
| 580a485573 | |||
|
|
3a8652fd8d | ||
|
|
7231ad18a6 | ||
|
|
135023d17b | ||
|
|
c2949c4dbf | ||
|
|
28c541e682 | ||
|
|
4ba58145d0 | ||
|
|
4ea72162ec | ||
|
|
a6a461d358 | ||
|
|
6c1c109cbd | ||
|
|
cf41665629 | ||
|
|
63024f6e89 | ||
|
|
c2f2ef4a09 | ||
|
|
80a76dadb7 | ||
|
|
bdac9e0da3 | ||
|
|
0277b65caa | ||
|
|
453c791036 | ||
|
|
552196c18f | ||
|
|
17951d8cb8 | ||
|
|
09e6a7107a | ||
|
|
e172faaaf2 | ||
|
|
c3534b54ae | ||
|
|
211de332db | ||
|
|
628469b1bb | ||
|
|
4a63ff6838 | ||
|
|
6013b8e167 | ||
|
|
b2e678dc2f | ||
|
|
8f5111d100 | ||
|
|
4391cb2881 | ||
| 0b9c243969 | |||
|
|
74c70c7fa0 | ||
|
|
e3cc22245c | ||
|
|
f31eb5f6c9 | ||
|
|
9f976f011a | ||
|
|
eb44e7b51e | ||
|
|
ca8d72e1c9 | ||
|
|
5a4bc9efc5 | ||
|
|
909d6ff561 | ||
|
|
a4528c5703 | ||
|
|
6acebb66ef | ||
|
|
6d0f4d910f | ||
|
|
87ebe4ffae | ||
|
|
47509b1482 | ||
| b3f7026afe | |||
|
|
ec1a725832 | ||
|
|
6d316c2b3f | ||
|
|
24f6730572 | ||
|
|
0fc44b31bf | ||
|
|
bed2c7106a | ||
|
|
48856d4fce | ||
|
|
c3bd22fb83 | ||
|
|
c18a6b334f |
207
.cursor/rules/harbor_pilot_universal.mdc
Normal file
207
.cursor/rules/harbor_pilot_universal.mdc
Normal file
@@ -0,0 +1,207 @@
|
||||
---
|
||||
alwaysApply: true
|
||||
inherits: base_context.mdc
|
||||
---
|
||||
```json
|
||||
{
|
||||
"coaching_level": "standard",
|
||||
"socratic_max_questions": 2,
|
||||
"verbosity": "concise",
|
||||
"timebox_minutes": 10,
|
||||
"format_enforcement": "strict"
|
||||
}
|
||||
```
|
||||
|
||||
# Harbor Pilot — Universal Directive for Human-Facing Technical Guides
|
||||
|
||||
**Author**: System/Shared
|
||||
**Date**: 2025-08-21 (UTC)
|
||||
**Status**: 🚢 ACTIVE — General ruleset extending *Base Context — Human Competence First*
|
||||
|
||||
> **Alignment with Base Context**
|
||||
> - **Purpose fit**: Prioritizes human competence and collaboration while delivering reproducible artifacts.
|
||||
> - **Output Contract**: This directive **adds universal constraints** for any technical topic while **inheriting** the Base Context contract sections.
|
||||
> - **Toggles honored**: Uses the same toggle semantics; defaults above can be overridden by the caller.
|
||||
|
||||
---
|
||||
|
||||
## Objective
|
||||
Produce a **developer-grade, reproducible guide** for any technical topic that onboards a competent practitioner **without meta narration** and **with evidence-backed steps**.
|
||||
|
||||
## Scope & Constraints
|
||||
- **One Markdown document** as the deliverable.
|
||||
- Use **absolute dates** in **UTC** (e.g., `2025-08-21T14:22Z`) — avoid “today/yesterday”.
|
||||
- Include at least **one diagram** (Mermaid preferred). Choose the most fitting type:
|
||||
- `sequenceDiagram` (protocols/flows), `flowchart`, `stateDiagram`, `gantt` (timelines), or `classDiagram` (schemas).
|
||||
- Provide runnable examples where applicable:
|
||||
- **APIs**: `curl` + one client library (e.g., `httpx` for Python).
|
||||
- **CLIs**: literal command blocks and expected output snippets.
|
||||
- **Code**: minimal, self-contained samples (language appropriate).
|
||||
- Cite **evidence** for *Works/Doesn’t* items (timestamps, filenames, line numbers, IDs/status codes, or logs).
|
||||
- If something is unknown, output `TODO:<missing>` — **never invent**.
|
||||
|
||||
## Required Sections (extends Base Output Contract)
|
||||
Follow this exact order **after** the Base Contract’s **Objective → Result → Use/Run** headers:
|
||||
|
||||
1. **Context & Scope**
|
||||
- Problem statement, audience, in/out-of-scope bullets.
|
||||
2. **Artifacts & Links**
|
||||
- Repos/PRs, design docs, datasets/HARs/pcaps, scripts/tools, dashboards.
|
||||
3. **Environment & Preconditions**
|
||||
- OS/runtime, versions/build IDs, services/endpoints/URLs, credentials/auth mode (describe acquisition, do not expose secrets).
|
||||
4. **Architecture / Process Overview**
|
||||
- Short prose + **one diagram** selected from the list above.
|
||||
5. **Interfaces & Contracts (choose one)**
|
||||
- **API-based**: Endpoint table (*Step, Method, Path/URL, Auth, Key Headers/Params, Sample Req/Resp ref*).
|
||||
- **Data/Files**: I/O contract table (*Source, Format, Schema/Columns, Size, Validation rules*).
|
||||
- **Systems/Hardware**: Interfaces table (*Port/Bus, Protocol, Voltage/Timing, Constraints*).
|
||||
6. **Repro: End-to-End Procedure**
|
||||
- Minimal copy-paste steps with code/commands and **expected outputs**.
|
||||
7. **What Works (with Evidence)**
|
||||
- Each item: **Time (UTC)** • **Artifact/Req IDs** • **Status/Result** • **Where to verify**.
|
||||
8. **What Doesn’t (Evidence & Hypotheses)**
|
||||
- Each failure: locus (file/endpoint/module), evidence snippet; short hypothesis and **next probe**.
|
||||
9. **Risks, Limits, Assumptions**
|
||||
- SLOs/limits, rate/size caps, security boundaries (CORS/CSRF/ACLs), retries/backoff/idempotency patterns.
|
||||
10. **Next Steps (Owner • Exit Criteria • Target Date)**
|
||||
- Actionable, assigned, and time-bound.
|
||||
11. **References**
|
||||
- Canonical docs, specs, tickets, prior analyses.
|
||||
|
||||
> **Competence Hooks (per Base Context; keep lightweight):**
|
||||
> - *Why this works* (≤3 bullets) — core invariants or guarantees.
|
||||
> - *Common pitfalls* (≤3 bullets) — the traps we saw in evidence.
|
||||
> - *Next skill unlock* (1 line) — the next capability to implement/learn.
|
||||
> - *Teach-back* (1 line) — prompt the reader to restate the flow/architecture.
|
||||
|
||||
> **Collaboration Hooks (per Base Context):**
|
||||
> - Name reviewers for **Interfaces & Contracts** and the **diagram**.
|
||||
> - Short **sign-off checklist** before merging/publishing the guide.
|
||||
|
||||
## Do / Don’t (Base-aligned)
|
||||
- **Do** quantify progress only against a defined scope with acceptance criteria.
|
||||
- **Do** include minimal sample payloads/headers or I/O schemas; redact sensitive values.
|
||||
- **Do** keep commentary lean; if timeboxed, move depth to **Deferred for depth**.
|
||||
- **Don’t** use marketing language or meta narration (“Perfect!”, “tool called”, “new chat”).
|
||||
- **Don’t** include IDE-specific chatter or internal rules unrelated to the task.
|
||||
|
||||
## Validation Checklist (self-check before returning)
|
||||
- [ ] All Required Sections present and ordered.
|
||||
- [ ] Diagram compiles (basic Mermaid syntax) and fits the problem.
|
||||
- [ ] If API-based, **Auth** and **Key Headers/Params** are listed for each endpoint.
|
||||
- [ ] Repro section includes commands/code **and expected outputs**.
|
||||
- [ ] Every Works/Doesn’t item has **UTC timestamp**, **status/result**, and **verifiable evidence**.
|
||||
- [ ] Next Steps include **Owner**, **Exit Criteria**, **Target Date**.
|
||||
- [ ] Unknowns are `TODO:<missing>` — no fabrication.
|
||||
- [ ] Base **Output Contract** sections satisfied (Objective/Result/Use/Run/Competence/Collaboration/Assumptions/References).
|
||||
|
||||
## Universal Template (fill-in)
|
||||
```markdown
|
||||
# <Title> — Working Notes (As of YYYY-MM-DDTHH:MMZ)
|
||||
|
||||
## Objective
|
||||
<one line>
|
||||
|
||||
## Result
|
||||
<link to the produced guide file or say “this document”>
|
||||
|
||||
## Use/Run
|
||||
<how to apply/test and where to run samples>
|
||||
|
||||
## Context & Scope
|
||||
- Audience: <role(s)>
|
||||
- In scope: <bullets>
|
||||
- Out of scope: <bullets>
|
||||
|
||||
## Artifacts & Links
|
||||
- Repo/PR: <link>
|
||||
- Data/Logs: <paths or links>
|
||||
- Scripts/Tools: <paths>
|
||||
- Dashboards: <links>
|
||||
|
||||
## Environment & Preconditions
|
||||
- OS/Runtime: <details>
|
||||
- Versions/Builds: <list>
|
||||
- Services/Endpoints: <list>
|
||||
- Auth mode: <Bearer/Session/Keys + how acquired>
|
||||
|
||||
## Architecture / Process Overview
|
||||
<short prose>
|
||||
```mermaid
|
||||
<one suitable diagram: sequenceDiagram | flowchart | stateDiagram | gantt | classDiagram>
|
||||
```
|
||||
|
||||
## Interfaces & Contracts
|
||||
### If API-based
|
||||
| Step | Method | Path/URL | Auth | Key Headers/Params | Sample |
|
||||
|---|---|---|---|---|---|
|
||||
| <…> | <…> | <…> | <…> | <…> | below |
|
||||
|
||||
### If Data/Files
|
||||
| Source | Format | Schema/Columns | Size | Validation |
|
||||
|---|---|---|---|---|
|
||||
| <…> | <…> | <…> | <…> | <…> |
|
||||
|
||||
### If Systems/Hardware
|
||||
| Interface | Protocol | Timing/Voltage | Constraints | Notes |
|
||||
|---|---|---|---|---|
|
||||
| <…> | <…> | <…> | <…> | <…> |
|
||||
|
||||
## Repro: End-to-End Procedure
|
||||
```bash
|
||||
# commands / curl examples (redacted where necessary)
|
||||
```
|
||||
```python
|
||||
# minimal client library example (language appropriate)
|
||||
```
|
||||
> Expected output: <snippet/checks>
|
||||
|
||||
## What Works (Evidence)
|
||||
- ✅ <short statement>
|
||||
- **Time**: <YYYY-MM-DDTHH:MMZ>
|
||||
- **Evidence**: file/line/log or request id/status
|
||||
- **Verify at**: <where>
|
||||
|
||||
## What Doesn’t (Evidence & Hypotheses)
|
||||
- ❌ <short failure> at `<component/endpoint/file>`
|
||||
- **Time**: <YYYY-MM-DDTHH:MMZ>
|
||||
- **Evidence**: <snippet/id/status>
|
||||
- **Hypothesis**: <short>
|
||||
- **Next probe**: <short>
|
||||
|
||||
## Risks, Limits, Assumptions
|
||||
<bullets: limits, security boundaries, retries/backoff, idempotency, SLOs>
|
||||
|
||||
## Next Steps
|
||||
| Owner | Task | Exit Criteria | Target Date (UTC) |
|
||||
|---|---|---|---|
|
||||
| <name> | <action> | <measurable outcome> | <YYYY-MM-DD> |
|
||||
|
||||
## References
|
||||
<links/titles>
|
||||
|
||||
## Competence Hooks
|
||||
- *Why this works*: <≤3 bullets>
|
||||
- *Common pitfalls*: <≤3 bullets>
|
||||
- *Next skill unlock*: <1 line>
|
||||
- *Teach-back*: <1 line>
|
||||
|
||||
## Collaboration Hooks
|
||||
- Reviewers: <names/roles>
|
||||
- Sign-off checklist: <≤5 checks>
|
||||
|
||||
## Assumptions & Limits
|
||||
<bullets>
|
||||
|
||||
## Deferred for depth
|
||||
<park deeper material here to respect timeboxing>
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
**Notes for Implementers:**
|
||||
- Respect Base *Do-Not* (no filler, no invented facts, no censorship).
|
||||
- Prefer clarity over completeness when timeboxed; capture unknowns explicitly.
|
||||
- Apply historical comment management rules (see `.cursor/rules/historical_comment_management.mdc`)
|
||||
- Apply realistic time estimation rules (see `.cursor/rules/realistic_time_estimation.mdc`)
|
||||
- Apply Playwright test investigation rules (see `.cursor/rules/playwright_test_investigation.mdc`)
|
||||
356
.cursor/rules/playwright-test-investigation.mdc
Normal file
356
.cursor/rules/playwright-test-investigation.mdc
Normal file
@@ -0,0 +1,356 @@
|
||||
---
|
||||
description: when working with playwright tests either generating them or using them to test code
|
||||
alwaysApply: false
|
||||
---
|
||||
# Playwright Test Investigation — Harbor Pilot Directive
|
||||
|
||||
**Author**: Matthew Raymer
|
||||
**Date**: 2025-08-21T14:22Z
|
||||
**Status**: 🎯 **ACTIVE** - Playwright test debugging guidelines
|
||||
|
||||
## Objective
|
||||
Provide systematic approach for investigating Playwright test failures with focus on UI element conflicts, timing issues, and selector ambiguity.
|
||||
|
||||
## Context & Scope
|
||||
- **Audience**: Developers debugging Playwright test failures
|
||||
- **In scope**: Test failure analysis, selector conflicts, UI state investigation, timing issues
|
||||
- **Out of scope**: Test writing best practices, CI/CD configuration
|
||||
|
||||
## Artifacts & Links
|
||||
- Test results: `test-results/` directory
|
||||
- Error context: `error-context.md` files with page snapshots
|
||||
- Trace files: `trace.zip` files for failed tests
|
||||
- HTML reports: Interactive test reports with screenshots
|
||||
|
||||
## Environment & Preconditions
|
||||
- OS/Runtime: Linux/Windows/macOS with Node.js
|
||||
- Versions: Playwright test framework, browser drivers
|
||||
- Services: Local test server (localhost:8080), test data setup
|
||||
- Auth mode: None required for test investigation
|
||||
|
||||
## Architecture / Process Overview
|
||||
Playwright test investigation follows a systematic diagnostic workflow that leverages built-in debugging tools and error context analysis.
|
||||
|
||||
```mermaid
|
||||
flowchart TD
|
||||
A[Test Failure] --> B[Check Error Context]
|
||||
B --> C[Analyze Page Snapshot]
|
||||
C --> D[Identify UI Conflicts]
|
||||
D --> E[Check Trace Files]
|
||||
E --> F[Verify Selector Uniqueness]
|
||||
F --> G[Test Selector Fixes]
|
||||
G --> H[Document Root Cause]
|
||||
|
||||
B --> I[Check Test Results Directory]
|
||||
I --> J[Locate Failed Test Results]
|
||||
J --> K[Extract Error Details]
|
||||
|
||||
D --> L[Multiple Alerts?]
|
||||
L --> M[Button Text Conflicts?]
|
||||
M --> N[Timing Issues?]
|
||||
|
||||
E --> O[Use Trace Viewer]
|
||||
O --> P[Analyze Action Sequence]
|
||||
P --> Q[Identify Failure Point]
|
||||
```
|
||||
|
||||
## Interfaces & Contracts
|
||||
|
||||
### Test Results Structure
|
||||
| Component | Format | Content | Validation |
|
||||
|---|---|---|---|
|
||||
| Error Context | Markdown | Page snapshot in YAML | Verify DOM state matches test expectations |
|
||||
| Trace Files | ZIP archive | Detailed execution trace | Use `npx playwright show-trace` |
|
||||
| HTML Reports | Interactive HTML | Screenshots, traces, logs | Check browser for full report |
|
||||
| JSON Results | JSON | Machine-readable results | Parse for automated analysis |
|
||||
|
||||
### Investigation Commands
|
||||
| Step | Command | Expected Output | Notes |
|
||||
|---|---|---|---|
|
||||
| Locate failed tests | `find test-results -name "*test-name*"` | Test result directories | Use exact test name patterns |
|
||||
| Check error context | `cat test-results/*/error-context.md` | Page snapshots | Look for UI state conflicts |
|
||||
| View traces | `npx playwright show-trace trace.zip` | Interactive trace viewer | Analyze exact failure sequence |
|
||||
|
||||
## Repro: End-to-End Investigation Procedure
|
||||
|
||||
### 1. Locate Failed Test Results
|
||||
```bash
|
||||
# Find all results for a specific test
|
||||
find test-results -name "*test-name*" -type d
|
||||
|
||||
# Check for error context files
|
||||
find test-results -name "error-context.md" | head -5
|
||||
```
|
||||
|
||||
### 2. Analyze Error Context
|
||||
```bash
|
||||
# Read error context for specific test
|
||||
cat test-results/test-name-test-description-browser/error-context.md
|
||||
|
||||
# Look for UI conflicts in page snapshot
|
||||
grep -A 10 -B 5 "button.*Yes\|button.*No" test-results/*/error-context.md
|
||||
```
|
||||
|
||||
### 3. Check Trace Files
|
||||
```bash
|
||||
# List available trace files
|
||||
find test-results -name "*.zip" | grep trace
|
||||
|
||||
# View trace in browser
|
||||
npx playwright show-trace test-results/test-name/trace.zip
|
||||
```
|
||||
|
||||
### 4. Investigate Selector Issues
|
||||
```typescript
|
||||
// Check for multiple elements with same text
|
||||
await page.locator('button:has-text("Yes")').count(); // Should be 1
|
||||
|
||||
// Use more specific selectors
|
||||
await page.locator('div[role="alert"]:has-text("Register") button:has-text("Yes")').click();
|
||||
```
|
||||
|
||||
## What Works (Evidence)
|
||||
- ✅ **Error context files** provide page snapshots showing exact DOM state at failure
|
||||
- **Time**: 2025-08-21T14:22Z
|
||||
- **Evidence**: `test-results/60-new-activity-New-offers-for-another-user-chromium/error-context.md` shows both alerts visible
|
||||
- **Verify at**: Error context files in test results directory
|
||||
|
||||
- ✅ **Trace files** capture detailed execution sequence for failed tests
|
||||
- **Time**: 2025-08-21T14:22Z
|
||||
- **Evidence**: `trace.zip` files available for all failed tests
|
||||
- **Verify at**: Use `npx playwright show-trace <filename>`
|
||||
|
||||
- ✅ **Page snapshots** reveal UI conflicts like multiple alerts with duplicate button text
|
||||
- **Time**: 2025-08-21T14:22Z
|
||||
- **Evidence**: YAML snapshots show registration + export alerts simultaneously
|
||||
- **Verify at**: Error context markdown files
|
||||
|
||||
## What Doesn't (Evidence & Hypotheses)
|
||||
- ❌ **Generic selectors** fail with multiple similar elements at `test-playwright/testUtils.ts:161`
|
||||
- **Time**: 2025-08-21T14:22Z
|
||||
- **Evidence**: `button:has-text("Yes")` matches both "Yes" and "Yes, Export Data"
|
||||
- **Hypothesis**: Selector ambiguity due to multiple alerts with conflicting button text
|
||||
- **Next probe**: Use more specific selectors or dismiss alerts sequentially
|
||||
|
||||
- ❌ **Timing-dependent tests** fail due to alert stacking at `src/views/ContactsView.vue:860,1283`
|
||||
- **Time**: 2025-08-21T14:22Z
|
||||
- **Evidence**: Both alerts use identical 1000ms delays, ensuring simultaneous display
|
||||
- **Hypothesis**: Race condition between alert displays creates UI conflicts
|
||||
- **Next probe**: Implement alert queuing or prevent overlapping alerts
|
||||
|
||||
## Risks, Limits, Assumptions
|
||||
- **Trace file size**: Large trace files may impact storage and analysis time
|
||||
- **Browser compatibility**: Trace viewer requires specific browser support
|
||||
- **Test isolation**: Shared state between tests may affect investigation results
|
||||
- **Timing sensitivity**: Tests may pass/fail based on system performance
|
||||
|
||||
## Next Steps
|
||||
| Owner | Task | Exit Criteria | Target Date (UTC) |
|
||||
|---|---|---|---|
|
||||
| Development Team | Fix test selectors for multiple alerts | All tests pass consistently | 2025-08-22 |
|
||||
| Development Team | Implement alert queuing system | No overlapping alerts with conflicting buttons | 2025-08-25 |
|
||||
| Development Team | Add test IDs to alert buttons | Unique selectors for all UI elements | 2025-08-28 |
|
||||
|
||||
## References
|
||||
- [Playwright Trace Viewer Documentation](https://playwright.dev/docs/trace-viewer)
|
||||
- [Playwright Test Results](https://playwright.dev/docs/test-reporters)
|
||||
- [Test Investigation Workflow](./research_diagnostic.mdc)
|
||||
|
||||
## Competence Hooks
|
||||
- **Why this works**: Systematic investigation leverages Playwright's built-in debugging tools to identify root causes
|
||||
- **Common pitfalls**: Generic selectors fail with multiple similar elements; timing issues create race conditions; alert stacking causes UI conflicts
|
||||
- **Next skill unlock**: Implement unique test IDs and handle alert dismissal order in test flows
|
||||
- **Teach-back**: "How would you investigate a Playwright test failure using error context, trace files, and page snapshots?"
|
||||
|
||||
## Collaboration Hooks
|
||||
- **Reviewers**: QA team, test automation engineers
|
||||
- **Sign-off checklist**: Error context analyzed, trace files reviewed, root cause identified, fix implemented and tested
|
||||
|
||||
## Assumptions & Limits
|
||||
- Test results directory structure follows Playwright conventions
|
||||
- Trace files are enabled in configuration (`trace: "retain-on-failure"`)
|
||||
- Error context files contain valid YAML page snapshots
|
||||
- Browser environment supports trace viewer functionality
|
||||
|
||||
---
|
||||
|
||||
**Status**: Active investigation directive
|
||||
**Priority**: High
|
||||
**Maintainer**: Development team
|
||||
**Next Review**: 2025-09-21
|
||||
# Playwright Test Investigation — Harbor Pilot Directive
|
||||
|
||||
**Author**: Matthew Raymer
|
||||
**Date**: 2025-08-21T14:22Z
|
||||
**Status**: 🎯 **ACTIVE** - Playwright test debugging guidelines
|
||||
|
||||
## Objective
|
||||
Provide systematic approach for investigating Playwright test failures with focus on UI element conflicts, timing issues, and selector ambiguity.
|
||||
|
||||
## Context & Scope
|
||||
- **Audience**: Developers debugging Playwright test failures
|
||||
- **In scope**: Test failure analysis, selector conflicts, UI state investigation, timing issues
|
||||
- **Out of scope**: Test writing best practices, CI/CD configuration
|
||||
|
||||
## Artifacts & Links
|
||||
- Test results: `test-results/` directory
|
||||
- Error context: `error-context.md` files with page snapshots
|
||||
- Trace files: `trace.zip` files for failed tests
|
||||
- HTML reports: Interactive test reports with screenshots
|
||||
|
||||
## Environment & Preconditions
|
||||
- OS/Runtime: Linux/Windows/macOS with Node.js
|
||||
- Versions: Playwright test framework, browser drivers
|
||||
- Services: Local test server (localhost:8080), test data setup
|
||||
- Auth mode: None required for test investigation
|
||||
|
||||
## Architecture / Process Overview
|
||||
Playwright test investigation follows a systematic diagnostic workflow that leverages built-in debugging tools and error context analysis.
|
||||
|
||||
```mermaid
|
||||
flowchart TD
|
||||
A[Test Failure] --> B[Check Error Context]
|
||||
B --> C[Analyze Page Snapshot]
|
||||
C --> D[Identify UI Conflicts]
|
||||
D --> E[Check Trace Files]
|
||||
E --> F[Verify Selector Uniqueness]
|
||||
F --> G[Test Selector Fixes]
|
||||
G --> H[Document Root Cause]
|
||||
|
||||
B --> I[Check Test Results Directory]
|
||||
I --> J[Locate Failed Test Results]
|
||||
J --> K[Extract Error Details]
|
||||
|
||||
D --> L[Multiple Alerts?]
|
||||
L --> M[Button Text Conflicts?]
|
||||
M --> N[Timing Issues?]
|
||||
|
||||
E --> O[Use Trace Viewer]
|
||||
O --> P[Analyze Action Sequence]
|
||||
P --> Q[Identify Failure Point]
|
||||
```
|
||||
|
||||
## Interfaces & Contracts
|
||||
|
||||
### Test Results Structure
|
||||
| Component | Format | Content | Validation |
|
||||
|---|---|---|---|
|
||||
| Error Context | Markdown | Page snapshot in YAML | Verify DOM state matches test expectations |
|
||||
| Trace Files | ZIP archive | Detailed execution trace | Use `npx playwright show-trace` |
|
||||
| HTML Reports | Interactive HTML | Screenshots, traces, logs | Check browser for full report |
|
||||
| JSON Results | JSON | Machine-readable results | Parse for automated analysis |
|
||||
|
||||
### Investigation Commands
|
||||
| Step | Command | Expected Output | Notes |
|
||||
|---|---|---|---|
|
||||
| Locate failed tests | `find test-results -name "*test-name*"` | Test result directories | Use exact test name patterns |
|
||||
| Check error context | `cat test-results/*/error-context.md` | Page snapshots | Look for UI state conflicts |
|
||||
| View traces | `npx playwright show-trace trace.zip` | Interactive trace viewer | Analyze exact failure sequence |
|
||||
|
||||
## Repro: End-to-End Investigation Procedure
|
||||
|
||||
### 1. Locate Failed Test Results
|
||||
```bash
|
||||
# Find all results for a specific test
|
||||
find test-results -name "*test-name*" -type d
|
||||
|
||||
# Check for error context files
|
||||
find test-results -name "error-context.md" | head -5
|
||||
```
|
||||
|
||||
### 2. Analyze Error Context
|
||||
```bash
|
||||
# Read error context for specific test
|
||||
cat test-results/test-name-test-description-browser/error-context.md
|
||||
|
||||
# Look for UI conflicts in page snapshot
|
||||
grep -A 10 -B 5 "button.*Yes\|button.*No" test-results/*/error-context.md
|
||||
```
|
||||
|
||||
### 3. Check Trace Files
|
||||
```bash
|
||||
# List available trace files
|
||||
find test-results -name "*.zip" | grep trace
|
||||
|
||||
# View trace in browser
|
||||
npx playwright show-trace test-results/test-name/trace.zip
|
||||
```
|
||||
|
||||
### 4. Investigate Selector Issues
|
||||
```typescript
|
||||
// Check for multiple elements with same text
|
||||
await page.locator('button:has-text("Yes")').count(); // Should be 1
|
||||
|
||||
// Use more specific selectors
|
||||
await page.locator('div[role="alert"]:has-text("Register") button:has-text("Yes")').click();
|
||||
```
|
||||
|
||||
## What Works (Evidence)
|
||||
- ✅ **Error context files** provide page snapshots showing exact DOM state at failure
|
||||
- **Time**: 2025-08-21T14:22Z
|
||||
- **Evidence**: `test-results/60-new-activity-New-offers-for-another-user-chromium/error-context.md` shows both alerts visible
|
||||
- **Verify at**: Error context files in test results directory
|
||||
|
||||
- ✅ **Trace files** capture detailed execution sequence for failed tests
|
||||
- **Time**: 2025-08-21T14:22Z
|
||||
- **Evidence**: `trace.zip` files available for all failed tests
|
||||
- **Verify at**: Use `npx playwright show-trace <filename>`
|
||||
|
||||
- ✅ **Page snapshots** reveal UI conflicts like multiple alerts with duplicate button text
|
||||
- **Time**: 2025-08-21T14:22Z
|
||||
- **Evidence**: YAML snapshots show registration + export alerts simultaneously
|
||||
- **Verify at**: Error context markdown files
|
||||
|
||||
## What Doesn't (Evidence & Hypotheses)
|
||||
- ❌ **Generic selectors** fail with multiple similar elements at `test-playwright/testUtils.ts:161`
|
||||
- **Time**: 2025-08-21T14:22Z
|
||||
- **Evidence**: `button:has-text("Yes")` matches both "Yes" and "Yes, Export Data"
|
||||
- **Hypothesis**: Selector ambiguity due to multiple alerts with conflicting button text
|
||||
- **Next probe**: Use more specific selectors or dismiss alerts sequentially
|
||||
|
||||
- ❌ **Timing-dependent tests** fail due to alert stacking at `src/views/ContactsView.vue:860,1283`
|
||||
- **Time**: 2025-08-21T14:22Z
|
||||
- **Evidence**: Both alerts use identical 1000ms delays, ensuring simultaneous display
|
||||
- **Hypothesis**: Race condition between alert displays creates UI conflicts
|
||||
- **Next probe**: Implement alert queuing or prevent overlapping alerts
|
||||
|
||||
## Risks, Limits, Assumptions
|
||||
- **Trace file size**: Large trace files may impact storage and analysis time
|
||||
- **Browser compatibility**: Trace viewer requires specific browser support
|
||||
- **Test isolation**: Shared state between tests may affect investigation results
|
||||
- **Timing sensitivity**: Tests may pass/fail based on system performance
|
||||
|
||||
## Next Steps
|
||||
| Owner | Task | Exit Criteria | Target Date (UTC) |
|
||||
|---|---|---|---|
|
||||
| Development Team | Fix test selectors for multiple alerts | All tests pass consistently | 2025-08-22 |
|
||||
| Development Team | Implement alert queuing system | No overlapping alerts with conflicting buttons | 2025-08-25 |
|
||||
| Development Team | Add test IDs to alert buttons | Unique selectors for all UI elements | 2025-08-28 |
|
||||
|
||||
## References
|
||||
- [Playwright Trace Viewer Documentation](https://playwright.dev/docs/trace-viewer)
|
||||
- [Playwright Test Results](https://playwright.dev/docs/test-reporters)
|
||||
- [Test Investigation Workflow](./research_diagnostic.mdc)
|
||||
|
||||
## Competence Hooks
|
||||
- **Why this works**: Systematic investigation leverages Playwright's built-in debugging tools to identify root causes
|
||||
- **Common pitfalls**: Generic selectors fail with multiple similar elements; timing issues create race conditions; alert stacking causes UI conflicts
|
||||
- **Next skill unlock**: Implement unique test IDs and handle alert dismissal order in test flows
|
||||
- **Teach-back**: "How would you investigate a Playwright test failure using error context, trace files, and page snapshots?"
|
||||
|
||||
## Collaboration Hooks
|
||||
- **Reviewers**: QA team, test automation engineers
|
||||
- **Sign-off checklist**: Error context analyzed, trace files reviewed, root cause identified, fix implemented and tested
|
||||
|
||||
## Assumptions & Limits
|
||||
- Test results directory structure follows Playwright conventions
|
||||
- Trace files are enabled in configuration (`trace: "retain-on-failure"`)
|
||||
- Error context files contain valid YAML page snapshots
|
||||
- Browser environment supports trace viewer functionality
|
||||
|
||||
---
|
||||
|
||||
**Status**: Active investigation directive
|
||||
**Priority**: High
|
||||
**Maintainer**: Development team
|
||||
**Next Review**: 2025-09-21
|
||||
56
BUILDING.md
56
BUILDING.md
@@ -251,7 +251,7 @@ npm run build:web:dev # Start development server with hot reload
|
||||
npm run build:web # Development build (starts dev server with hot reload)
|
||||
npm run build:web:test # Test environment build (optimized for testing)
|
||||
npm run build:web:prod # Production build (optimized for production)
|
||||
npm run build:web:serve # Build and serve locally (builds then serves)
|
||||
npm run build:web:serve # Build and serve locally for production testing
|
||||
|
||||
# Docker builds
|
||||
npm run build:web:docker # Development build with Docker containerization
|
||||
@@ -269,6 +269,12 @@ Start the development server using `npm run build:web:dev` or `npm run build:web
|
||||
2. The built files will be in the `dist` directory
|
||||
3. To test the production build locally, use `npm run build:web:serve` (builds then serves)
|
||||
|
||||
**Why Use `serve`?**
|
||||
- **Production Testing**: Test your optimized production build locally before deployment
|
||||
- **SPA Routing Validation**: Verify deep linking and navigation work correctly (handles routes like `/discover`, `/account`)
|
||||
- **Performance Testing**: Test the minified and optimized build locally
|
||||
- **Deployment Validation**: Ensure built files work correctly when served by a real HTTP server
|
||||
|
||||
You'll likely want to use test locations for the Endorser & image & partner servers; see "DEFAULT_ENDORSER_API_SERVER" & "DEFAULT_IMAGE_API_SERVER" & "DEFAULT_PARTNER_API_SERVER" below.
|
||||
|
||||
### Web Build Script Details
|
||||
@@ -288,7 +294,7 @@ All web build commands use the `./scripts/build-web.sh` script, which provides:
|
||||
- **Clean Build**: Removes previous `dist/` directory
|
||||
- **Vite Build**: Executes `npx vite build --config vite.config.web.mts`
|
||||
- **Docker Support**: Optional Docker containerization
|
||||
- **Local Serving**: Built-in HTTP server for testing builds
|
||||
- **Local Serving**: Built-in HTTP server for testing builds with SPA routing support
|
||||
|
||||
**Direct Script Usage:**
|
||||
|
||||
@@ -324,6 +330,25 @@ All web build commands use the `./scripts/build-web.sh` script, which provides:
|
||||
- `5` - Serve command failed
|
||||
- `6` - Invalid build mode
|
||||
|
||||
### Local Serving with `serve`
|
||||
|
||||
The `serve` functionality provides a local HTTP server for testing production builds:
|
||||
|
||||
**What It Does:**
|
||||
1. **Builds** the application using Vite
|
||||
2. **Serves** the built files from the `dist/` directory
|
||||
3. **Handles SPA Routing** - serves `index.html` for all routes (fixes 404s on `/discover`, `/account`, etc.)
|
||||
|
||||
**Server Options:**
|
||||
- **Primary**: `npx serve -s dist -l 8080` (recommended - full SPA support)
|
||||
- **Fallback**: Python HTTP server (limited SPA routing support)
|
||||
|
||||
**Use Cases:**
|
||||
- Testing production builds before deployment
|
||||
- Validating SPA routing behavior
|
||||
- Performance testing of optimized builds
|
||||
- Debugging production build issues locally
|
||||
|
||||
### Compile and minify for test & production
|
||||
|
||||
- If there are DB changes: before updating the test server, open browser(s) with
|
||||
@@ -2716,6 +2741,33 @@ configuration files in the repository.
|
||||
|
||||
---
|
||||
|
||||
### 2025-08-21 - Commitlint Configuration Refinement
|
||||
|
||||
#### Commit Message Validation Improvements
|
||||
- **Modified**: Commitlint configuration moved from `package.json` to dedicated `commitlint.config.js`
|
||||
- **Enhanced**: Strict validation rules downgraded from errors to warnings
|
||||
- **Before**: `subject-case` and `subject-full-stop` rules caused red error messages
|
||||
- **After**: Same rules now show yellow warnings without blocking commits
|
||||
- **Benefit**: Eliminates confusing red error messages while maintaining commit quality guidance
|
||||
|
||||
#### Configuration Structure
|
||||
- **File**: `commitlint.config.js` - Dedicated commitlint configuration
|
||||
- **Extends**: `@commitlint/config-conventional` - Standard conventional commit rules
|
||||
- **Custom Rules**:
|
||||
- `subject-case: [1, 'never', ['sentence-case', 'start-case', 'pascal-case', 'upper-case']]`
|
||||
- `subject-full-stop: [1, 'never', '.']`
|
||||
- **Levels**:
|
||||
- `0` = Disabled, `1` = Warning, `2` = Error
|
||||
- Current: Problematic rules set to warning level (1)
|
||||
|
||||
#### User Experience Impact
|
||||
- **Before**: Red error messages on every push with strict commit rules
|
||||
- **After**: Yellow warning messages that provide guidance without disruption
|
||||
- **Workflow**: Commits and pushes continue to work while maintaining quality standards
|
||||
- **Feedback**: Developers still receive helpful commit message guidance
|
||||
|
||||
---
|
||||
|
||||
**Note**: This documentation is maintained alongside the build system. For the
|
||||
most up-to-date information, refer to the actual script files and Vite
|
||||
configuration files in the repository.
|
||||
|
||||
@@ -18,7 +18,7 @@ npm install
|
||||
npm run build:web:serve -- --test
|
||||
```
|
||||
|
||||
To be able to make submissions: go to "profile" (bottom left), go to the bottom and expand "Show Advanced Settings", go to the bottom and to the "Test Page", and finally "Become User 0" to see all the functionality.
|
||||
To be able to take action on the platform: go to [the test page](http://localhost:8080/test) and click "Become User 0".
|
||||
|
||||
See [BUILDING.md](BUILDING.md) for comprehensive build instructions for all platforms (Web, Electron, iOS, Android, Docker).
|
||||
|
||||
|
||||
@@ -13,8 +13,10 @@ dependencies {
|
||||
implementation project(':capacitor-mlkit-barcode-scanning')
|
||||
implementation project(':capacitor-app')
|
||||
implementation project(':capacitor-camera')
|
||||
implementation project(':capacitor-clipboard')
|
||||
implementation project(':capacitor-filesystem')
|
||||
implementation project(':capacitor-share')
|
||||
implementation project(':capacitor-status-bar')
|
||||
implementation project(':capawesome-capacitor-file-picker')
|
||||
|
||||
}
|
||||
|
||||
@@ -14,6 +14,7 @@
|
||||
android:label="@string/title_activity_main"
|
||||
android:launchMode="singleTask"
|
||||
android:screenOrientation="portrait"
|
||||
android:windowSoftInputMode="adjustResize"
|
||||
android:theme="@style/AppTheme.NoActionBarLaunch">
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.MAIN" />
|
||||
|
||||
@@ -15,6 +15,10 @@
|
||||
"pkg": "@capacitor/camera",
|
||||
"classpath": "com.capacitorjs.plugins.camera.CameraPlugin"
|
||||
},
|
||||
{
|
||||
"pkg": "@capacitor/clipboard",
|
||||
"classpath": "com.capacitorjs.plugins.clipboard.ClipboardPlugin"
|
||||
},
|
||||
{
|
||||
"pkg": "@capacitor/filesystem",
|
||||
"classpath": "com.capacitorjs.plugins.filesystem.FilesystemPlugin"
|
||||
@@ -23,6 +27,10 @@
|
||||
"pkg": "@capacitor/share",
|
||||
"classpath": "com.capacitorjs.plugins.share.SharePlugin"
|
||||
},
|
||||
{
|
||||
"pkg": "@capacitor/status-bar",
|
||||
"classpath": "com.capacitorjs.plugins.statusbar.StatusBarPlugin"
|
||||
},
|
||||
{
|
||||
"pkg": "@capawesome/capacitor-file-picker",
|
||||
"classpath": "io.capawesome.capacitorjs.plugins.filepicker.FilePickerPlugin"
|
||||
|
||||
@@ -1,7 +1,16 @@
|
||||
package app.timesafari;
|
||||
|
||||
import android.os.Bundle;
|
||||
import android.view.View;
|
||||
import android.view.WindowManager;
|
||||
import android.view.WindowInsetsController;
|
||||
import android.view.WindowInsets;
|
||||
import android.os.Build;
|
||||
import android.webkit.WebView;
|
||||
import android.webkit.WebSettings;
|
||||
import android.webkit.WebViewClient;
|
||||
import com.getcapacitor.BridgeActivity;
|
||||
import app.timesafari.safearea.SafeAreaPlugin;
|
||||
//import com.getcapacitor.community.sqlite.SQLite;
|
||||
|
||||
public class MainActivity extends BridgeActivity {
|
||||
@@ -9,7 +18,39 @@ public class MainActivity extends BridgeActivity {
|
||||
public void onCreate(Bundle savedInstanceState) {
|
||||
super.onCreate(savedInstanceState);
|
||||
|
||||
// Enable edge-to-edge display for modern Android
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
|
||||
// Android 11+ (API 30+)
|
||||
getWindow().setDecorFitsSystemWindows(false);
|
||||
|
||||
// Set up system UI visibility for edge-to-edge
|
||||
WindowInsetsController controller = getWindow().getInsetsController();
|
||||
if (controller != null) {
|
||||
controller.setSystemBarsAppearance(
|
||||
WindowInsetsController.APPEARANCE_LIGHT_STATUS_BARS |
|
||||
WindowInsetsController.APPEARANCE_LIGHT_NAVIGATION_BARS,
|
||||
WindowInsetsController.APPEARANCE_LIGHT_STATUS_BARS |
|
||||
WindowInsetsController.APPEARANCE_LIGHT_NAVIGATION_BARS
|
||||
);
|
||||
controller.setSystemBarsBehavior(WindowInsetsController.BEHAVIOR_SHOW_TRANSIENT_BARS_BY_SWIPE);
|
||||
}
|
||||
} else {
|
||||
// Legacy Android (API 21-29)
|
||||
getWindow().getDecorView().setSystemUiVisibility(
|
||||
View.SYSTEM_UI_FLAG_LAYOUT_STABLE |
|
||||
View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION |
|
||||
View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN |
|
||||
View.SYSTEM_UI_FLAG_LIGHT_STATUS_BAR |
|
||||
View.SYSTEM_UI_FLAG_LIGHT_NAVIGATION_BAR
|
||||
);
|
||||
}
|
||||
|
||||
// Register SafeArea plugin
|
||||
registerPlugin(SafeAreaPlugin.class);
|
||||
|
||||
// Initialize SQLite
|
||||
//registerPlugin(SQLite.class);
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
package app.timesafari.safearea;
|
||||
|
||||
import android.os.Build;
|
||||
import android.view.WindowInsets;
|
||||
import com.getcapacitor.JSObject;
|
||||
import com.getcapacitor.Plugin;
|
||||
import com.getcapacitor.PluginCall;
|
||||
import com.getcapacitor.PluginMethod;
|
||||
import com.getcapacitor.annotation.CapacitorPlugin;
|
||||
|
||||
@CapacitorPlugin(name = "SafeArea")
|
||||
public class SafeAreaPlugin extends Plugin {
|
||||
|
||||
@PluginMethod
|
||||
public void getSafeAreaInsets(PluginCall call) {
|
||||
JSObject result = new JSObject();
|
||||
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
|
||||
WindowInsets insets = getActivity().getWindow().getDecorView().getRootWindowInsets();
|
||||
if (insets != null) {
|
||||
int top = insets.getInsets(WindowInsets.Type.statusBars()).top;
|
||||
int bottom = insets.getInsets(WindowInsets.Type.navigationBars()).bottom;
|
||||
int left = insets.getInsets(WindowInsets.Type.systemBars()).left;
|
||||
int right = insets.getInsets(WindowInsets.Type.systemBars()).right;
|
||||
|
||||
result.put("top", top);
|
||||
result.put("bottom", bottom);
|
||||
result.put("left", left);
|
||||
result.put("right", right);
|
||||
|
||||
call.resolve(result);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// Fallback values
|
||||
result.put("top", 0);
|
||||
result.put("bottom", 0);
|
||||
result.put("left", 0);
|
||||
result.put("right", 0);
|
||||
|
||||
call.resolve(result);
|
||||
}
|
||||
}
|
||||
@@ -18,5 +18,14 @@
|
||||
|
||||
<style name="AppTheme.NoActionBarLaunch" parent="Theme.SplashScreen">
|
||||
<item name="android:background">@drawable/splash</item>
|
||||
<item name="android:windowTranslucentStatus">false</item>
|
||||
<item name="android:windowTranslucentNavigation">false</item>
|
||||
<item name="android:windowDrawsSystemBarBackgrounds">true</item>
|
||||
<item name="android:statusBarColor">@android:color/transparent</item>
|
||||
<item name="android:navigationBarColor">@android:color/transparent</item>
|
||||
<item name="android:windowLightStatusBar">true</item>
|
||||
<item name="android:windowLightNavigationBar">true</item>
|
||||
<item name="android:enforceStatusBarContrast">false</item>
|
||||
<item name="android:enforceNavigationBarContrast">false</item>
|
||||
</style>
|
||||
</resources>
|
||||
@@ -14,11 +14,17 @@ project(':capacitor-app').projectDir = new File('../node_modules/@capacitor/app/
|
||||
include ':capacitor-camera'
|
||||
project(':capacitor-camera').projectDir = new File('../node_modules/@capacitor/camera/android')
|
||||
|
||||
include ':capacitor-clipboard'
|
||||
project(':capacitor-clipboard').projectDir = new File('../node_modules/@capacitor/clipboard/android')
|
||||
|
||||
include ':capacitor-filesystem'
|
||||
project(':capacitor-filesystem').projectDir = new File('../node_modules/@capacitor/filesystem/android')
|
||||
|
||||
include ':capacitor-share'
|
||||
project(':capacitor-share').projectDir = new File('../node_modules/@capacitor/share/android')
|
||||
|
||||
include ':capacitor-status-bar'
|
||||
project(':capacitor-status-bar').projectDir = new File('../node_modules/@capacitor/status-bar/android')
|
||||
|
||||
include ':capawesome-capacitor-file-picker'
|
||||
project(':capawesome-capacitor-file-picker').projectDir = new File('../node_modules/@capawesome/capacitor-file-picker/android')
|
||||
|
||||
9
commitlint.config.js
Normal file
9
commitlint.config.js
Normal file
@@ -0,0 +1,9 @@
|
||||
module.exports = {
|
||||
extends: ['@commitlint/config-conventional'],
|
||||
rules: {
|
||||
// Downgrade strict case rules to warnings (level 1) instead of errors (level 2)
|
||||
// This eliminates red error messages while maintaining helpful guidance
|
||||
'subject-case': [1, 'never', ['sentence-case', 'start-case', 'pascal-case', 'upper-case']],
|
||||
'subject-full-stop': [1, 'never', '.'],
|
||||
}
|
||||
};
|
||||
343
doc/active-identity-implementation-overview.md
Normal file
343
doc/active-identity-implementation-overview.md
Normal file
@@ -0,0 +1,343 @@
|
||||
# Active Identity Implementation Overview
|
||||
|
||||
**Author**: Matthew Raymer
|
||||
**Date**: 2025-08-21T13:40Z
|
||||
**Status**: 🚧 **IN PROGRESS** - Implementation Complete, Testing Pending
|
||||
|
||||
## Objective
|
||||
|
||||
Separate the `activeDid` field from the monolithic `settings` table into a
|
||||
dedicated `active_identity` table to achieve:
|
||||
|
||||
- **Data normalization** and reduced cache drift
|
||||
- **Simplified identity management** with dedicated table
|
||||
- **Zero breaking API surface** for existing components
|
||||
- **Phased migration** with rollback capability
|
||||
|
||||
## Result
|
||||
|
||||
This document provides a comprehensive overview of the implemented Active
|
||||
Identity table separation system, including architecture, migration strategy,
|
||||
and component integration.
|
||||
|
||||
## Use/Run
|
||||
|
||||
The implementation is ready for testing. Components can immediately use the new
|
||||
façade methods while maintaining backward compatibility through dual-write
|
||||
triggers.
|
||||
|
||||
## Context & Scope
|
||||
|
||||
- **Audience**: Developers working with identity management and database
|
||||
migrations
|
||||
- **In scope**: Active DID management, database schema evolution, Vue component
|
||||
integration
|
||||
- **Out of scope**: Multi-profile support beyond basic scope framework, complex
|
||||
identity hierarchies
|
||||
|
||||
## Artifacts & Links
|
||||
|
||||
- **Implementation**: `src/db/tables/activeIdentity.ts`,
|
||||
`src/utils/PlatformServiceMixin.ts`
|
||||
- **Migrations**: `src/db-sql/migration.ts` (migrations 003 & 004)
|
||||
- **Configuration**: `src/config/featureFlags.ts`
|
||||
- **Documentation**: This document and progress tracking
|
||||
|
||||
## Environment & Preconditions
|
||||
|
||||
- **Database**: SQLite (Absurd-SQL for Web, Capacitor SQLite for Mobile)
|
||||
- **Framework**: Vue.js with PlatformServiceMixin
|
||||
- **Migration System**: Built-in migrationService.ts with automatic execution
|
||||
|
||||
## Architecture / Process Overview
|
||||
|
||||
The Active Identity separation follows a **phased migration pattern** with
|
||||
dual-write triggers to ensure zero downtime and backward compatibility.
|
||||
|
||||
```mermaid
|
||||
flowchart TD
|
||||
A[Legacy State] --> B[Phase A: Dual-Write]
|
||||
B --> C[Phase B: Component Cutover]
|
||||
C --> D[Phase C: Legacy Cleanup]
|
||||
|
||||
A --> A1[settings.activeDid]
|
||||
B --> B1[active_identity table]
|
||||
B --> B2[Dual-write trigger]
|
||||
B --> B3[Fallback support]
|
||||
C --> C1[Components use façade]
|
||||
C --> C2[Legacy fallback disabled]
|
||||
D --> D1[Drop activeDid column]
|
||||
D --> D2[Remove triggers]
|
||||
```
|
||||
|
||||
## Interfaces & Contracts
|
||||
|
||||
### Database Schema
|
||||
|
||||
| Table | Purpose | Key Fields | Constraints |
|
||||
|-------|---------|------------|-------------|
|
||||
| `active_identity` | Store active DID | `id`, `active_did`, | FK to accounts.did |
|
||||
| | | `updated_at` | |
|
||||
|
||||
### Service Façade API
|
||||
|
||||
| Method | Purpose | Parameters | Returns |
|
||||
|--------|---------|------------|---------|
|
||||
| `$getActiveDid()` | Retrieve active DID | None | `Promise<string \| null>` |
|
||||
| `$setActiveDid(did)` | Set active DID | `did` | `Promise<void>` |
|
||||
| `$switchActiveIdentity(did)` | Switch to different DID | `did` | `Promise<void>` |
|
||||
| `$getActiveIdentityScopes()` | Get available scopes | None | `Promise<string[]>` (always returns `["default"]`) |
|
||||
|
||||
## Repro: End-to-End Procedure
|
||||
|
||||
### 1. Database Migration Execution
|
||||
|
||||
```bash
|
||||
# Migrations run automatically on app startup
|
||||
# Migration 003: Creates active_identity table
|
||||
# Migration 004: Drops settings.activeDid column (Phase C)
|
||||
```
|
||||
|
||||
### 2. Component Usage
|
||||
|
||||
```typescript
|
||||
// Before (legacy)
|
||||
const activeDid = settings.activeDid || "";
|
||||
await this.$saveSettings({ activeDid: newDid });
|
||||
|
||||
// After (new façade)
|
||||
const activeDid = await this.$getActiveDid() || "";
|
||||
await this.$setActiveDid(newDid);
|
||||
```
|
||||
|
||||
### 3. Feature Flag Control
|
||||
|
||||
```typescript
|
||||
// Enable/disable migration phases
|
||||
FLAGS.USE_ACTIVE_IDENTITY_ONLY = false; // Allow legacy fallback
|
||||
FLAGS.DROP_SETTINGS_ACTIVEDID = false; // Keep legacy column
|
||||
FLAGS.LOG_ACTIVE_ID_FALLBACK = true; // Log fallback usage
|
||||
```
|
||||
|
||||
## What Works (Evidence)
|
||||
|
||||
- ✅ **Migration Infrastructure**: Migrations 003 and 004 integrated into
|
||||
`migrationService.ts`
|
||||
- ✅ **Table Creation**: `active_identity` table schema with proper constraints
|
||||
and indexes
|
||||
- ✅ **Service Façade**: PlatformServiceMixin extended with all required methods
|
||||
- ✅ **Feature Flags**: Comprehensive flag system for controlling rollout phases
|
||||
- ✅ **Dual-Write Support**: One-way trigger from `settings.activeDid` →
|
||||
`active_identity.active_did`
|
||||
- ✅ **Validation**: DID existence validation before setting as active
|
||||
- ✅ **Error Handling**: Comprehensive error handling with logging
|
||||
|
||||
## What Doesn't (Evidence & Hypotheses)
|
||||
|
||||
- ❌ **Component Migration**: No components yet updated to use new façade
|
||||
methods
|
||||
- ❌ **Testing**: No automated tests for new functionality
|
||||
- ❌ **Performance Validation**: No benchmarks for read/write performance
|
||||
- ❌ **Cross-Platform Validation**: Not tested on mobile platforms yet
|
||||
|
||||
## Risks, Limits, Assumptions
|
||||
|
||||
### **Migration Risks**
|
||||
|
||||
- **Data Loss**: If migration fails mid-process, could lose active DID state
|
||||
- **Rollback Complexity**: Phase C (column drop) requires table rebuild, not
|
||||
easily reversible
|
||||
- **Trigger Dependencies**: Dual-write trigger could fail if `active_identity`
|
||||
table is corrupted
|
||||
|
||||
### **Performance Limits**
|
||||
|
||||
- **Dual-Write Overhead**: Each `activeDid` change triggers additional
|
||||
database operations
|
||||
- **Fallback Queries**: Legacy fallback requires additional database queries
|
||||
- **Transaction Scope**: Active DID changes wrapped in transactions for
|
||||
consistency
|
||||
|
||||
### **Security Boundaries**
|
||||
|
||||
- **DID Validation**: Only validates DID exists in accounts table, not
|
||||
ownership
|
||||
- **Scope Isolation**: No current scope separation enforcement beyond table
|
||||
constraints
|
||||
- **Access Control**: No row-level security on `active_identity` table
|
||||
|
||||
## Next Steps
|
||||
|
||||
| Owner | Task | Exit Criteria | Target Date (UTC) |
|
||||
|-------|------|---------------|-------------------|
|
||||
| Developer | Test migrations | Migrations execute without errors | 2025-08-21 |
|
||||
| Developer | Update components | All components use new façade | 2025-08-22 |
|
||||
| | | methods | |
|
||||
| Developer | Performance testing | Read/write performance meets | 2025-08-23 |
|
||||
| | | requirements | |
|
||||
| Developer | Phase C activation | Feature flag enables column | 2025-08-24 |
|
||||
| | | removal | |
|
||||
|
||||
## References
|
||||
|
||||
- [Database Migration Guide](../database-migration-guide.md)
|
||||
- [PlatformServiceMixin Documentation](../component-communication-guide.md)
|
||||
- [Feature Flags Configuration](../feature-flags.md)
|
||||
|
||||
## Competence Hooks
|
||||
|
||||
- **Why this works**: Phased migration with dual-write triggers ensures zero
|
||||
downtime while maintaining data consistency through foreign key constraints
|
||||
and validation
|
||||
- **Common pitfalls**: Forgetting to update components before enabling
|
||||
`USE_ACTIVE_IDENTITY_ONLY`, not testing rollback scenarios, ignoring
|
||||
cross-platform compatibility
|
||||
- **Next skill unlock**: Implement automated component migration using codemods
|
||||
and ESLint rules
|
||||
- **Teach-back**: Explain how the dual-write trigger prevents data divergence
|
||||
during the transition phase
|
||||
|
||||
## Collaboration Hooks
|
||||
|
||||
- **Reviewers**: Database team for migration logic, Vue team for component
|
||||
integration, DevOps for deployment strategy
|
||||
- **Sign-off checklist**: Migrations tested in staging, components updated,
|
||||
performance validated, rollback plan documented
|
||||
|
||||
## Assumptions & Limits
|
||||
|
||||
- **Single User Focus**: Current implementation assumes single-user mode with
|
||||
'default' scope
|
||||
- **Vue Compatibility**: Assumes `vue-facing-decorator` compatibility (needs
|
||||
validation)
|
||||
- **Migration Timing**: Assumes migrations run on app startup (automatic
|
||||
execution)
|
||||
- **Platform Support**: Assumes same behavior across Web (Absurd-SQL) and
|
||||
Mobile (Capacitor SQLite)
|
||||
|
||||
## Implementation Details
|
||||
|
||||
### **Migration 003: Table Creation**
|
||||
|
||||
Creates the `active_identity` table with:
|
||||
|
||||
- **Primary Key**: Auto-incrementing ID
|
||||
- **Scope Field**: For future multi-profile support (currently 'default')
|
||||
- **Active DID**: Foreign key to accounts.did with CASCADE UPDATE
|
||||
- **Timestamps**: ISO format timestamps for audit trail
|
||||
- **Indexes**: Performance optimization for scope and DID lookups
|
||||
|
||||
### **Migration 004: Column Removal**
|
||||
|
||||
Implements Phase C by:
|
||||
|
||||
- **Table Rebuild**: Creates new settings table without activeDid column
|
||||
- **Data Preservation**: Copies all other data from legacy table
|
||||
- **Index Recreation**: Rebuilds necessary indexes
|
||||
- **Trigger Cleanup**: Removes dual-write triggers
|
||||
|
||||
### **Service Façade Implementation**
|
||||
|
||||
The PlatformServiceMixin extension provides:
|
||||
|
||||
- **Dual-Read Logic**: Prefers new table, falls back to legacy during
|
||||
transition
|
||||
- **Dual-Write Logic**: Updates both tables during Phase A/B
|
||||
- **Validation**: Ensures DID exists before setting as active
|
||||
- **Transaction Safety**: Wraps operations in database transactions
|
||||
- **Error Handling**: Comprehensive logging and error propagation
|
||||
|
||||
### **Feature Flag System**
|
||||
|
||||
Controls migration phases through:
|
||||
|
||||
- **`USE_ACTIVE_IDENTITY_ONLY`**: Disables legacy fallback reads
|
||||
- **`DROP_SETTINGS_ACTIVEDID`**: Enables Phase C column removal
|
||||
- **`LOG_ACTIVE_ID_FALLBACK`**: Logs when legacy fallback is used
|
||||
- **`ENABLE_ACTIVE_IDENTITY_MIGRATION`**: Master switch for migration
|
||||
system
|
||||
|
||||
## Security Considerations
|
||||
|
||||
### **Data Validation**
|
||||
|
||||
- DID format validation (basic "did:" prefix check)
|
||||
- Foreign key constraints ensure referential integrity
|
||||
- Transaction wrapping prevents partial updates
|
||||
|
||||
### **Access Control**
|
||||
|
||||
- No row-level security implemented
|
||||
- Scope isolation framework in place for future use
|
||||
- Validation prevents setting non-existent DIDs as active
|
||||
|
||||
### **Audit Trail**
|
||||
|
||||
- Timestamps on all active identity changes
|
||||
- Logging of fallback usage and errors
|
||||
- Migration tracking through built-in system
|
||||
|
||||
## Performance Characteristics
|
||||
|
||||
### **Read Operations**
|
||||
|
||||
- **Primary Path**: Single query to `active_identity` table
|
||||
- **Fallback Path**: Additional query to `settings` table (Phase A only)
|
||||
- **Indexed Fields**: Both scope and active_did are indexed
|
||||
|
||||
### **Write Operations**
|
||||
|
||||
- **Dual-Write**: Updates both tables during transition (Phase A/B)
|
||||
- **Transaction Overhead**: All operations wrapped in transactions
|
||||
- **Trigger Execution**: Additional database operations per update
|
||||
|
||||
### **Migration Impact**
|
||||
|
||||
- **Table Creation**: Minimal impact (runs once)
|
||||
- **Column Removal**: Moderate impact (table rebuild required)
|
||||
- **Data Seeding**: Depends on existing data volume
|
||||
|
||||
## Testing Strategy
|
||||
|
||||
### **Unit Testing**
|
||||
|
||||
- Service façade method validation
|
||||
- Error handling and edge cases
|
||||
- Transaction rollback scenarios
|
||||
|
||||
### **Integration Testing**
|
||||
|
||||
- Migration execution and rollback
|
||||
- Cross-platform compatibility
|
||||
- Performance under load
|
||||
|
||||
### **End-to-End Testing**
|
||||
|
||||
- Component integration
|
||||
- User workflow validation
|
||||
- Migration scenarios
|
||||
|
||||
## Deployment Considerations
|
||||
|
||||
### **Rollout Strategy**
|
||||
|
||||
- **Phase A**: Deploy with dual-write enabled
|
||||
- **Phase B**: Update components to use new methods
|
||||
- **Phase C**: Enable column removal (irreversible)
|
||||
|
||||
### **Rollback Plan**
|
||||
|
||||
- **Phase A/B**: Disable feature flags, revert to legacy methods
|
||||
- **Phase C**: Requires database restore (no automatic rollback)
|
||||
|
||||
### **Monitoring**
|
||||
|
||||
- Track fallback usage through logging
|
||||
- Monitor migration success rates
|
||||
- Alert on validation failures
|
||||
|
||||
---
|
||||
|
||||
**Status**: Implementation complete, ready for testing and component migration
|
||||
**Next Review**: After initial testing and component updates
|
||||
**Maintainer**: Development team
|
||||
185
doc/active-identity-phase-b-progress.md
Normal file
185
doc/active-identity-phase-b-progress.md
Normal file
@@ -0,0 +1,185 @@
|
||||
# Active Identity Migration - Phase B Progress
|
||||
|
||||
**Author**: Matthew Raymer
|
||||
**Date**: 2025-08-22T07:05Z
|
||||
**Status**: 🚧 **IN PROGRESS** - Component Migration Active
|
||||
|
||||
## Objective
|
||||
|
||||
Complete **Phase B: Component Cutover** by updating all Vue components to use the new Active Identity façade methods instead of directly accessing `settings.activeDid`.
|
||||
|
||||
## Current Status
|
||||
|
||||
### ✅ **Completed**
|
||||
- **Migration Infrastructure**: Migrations 003 and 004 implemented
|
||||
- **Service Façade**: PlatformServiceMixin extended with all required methods
|
||||
- **TypeScript Types**: Added missing method declarations to Vue component interfaces
|
||||
- **Feature Flags**: Comprehensive flag system for controlling rollout phases
|
||||
|
||||
### 🔄 **In Progress**
|
||||
- **Component Migration**: Manually updating critical components
|
||||
- **Pattern Establishment**: Creating consistent migration approach
|
||||
|
||||
### ❌ **Pending**
|
||||
- **Bulk Component Updates**: 40+ components need migration
|
||||
- **Testing**: Validate migrated components work correctly
|
||||
- **Performance Validation**: Ensure no performance regressions
|
||||
|
||||
## Migration Progress
|
||||
|
||||
### **Components Migrated (3/40+)**
|
||||
|
||||
| Component | Status | Changes Made | Notes |
|
||||
|-----------|--------|--------------|-------|
|
||||
| `IdentitySwitcherView.vue` | ✅ Complete | - Updated `switchIdentity()` method<br>- Added FLAGS import<br>- Uses `$setActiveDid()` | Critical component for identity switching |
|
||||
| `ImportDerivedAccountView.vue` | ✅ Complete | - Updated `incrementDerivation()` method<br>- Added FLAGS import<br>- Uses `$setActiveDid()` | Handles new account creation |
|
||||
| `ClaimAddRawView.vue` | ✅ Complete | - Updated `initializeSettings()` method<br>- Uses `$getActiveDid()` | Reads active DID for claims |
|
||||
|
||||
### **Components Pending Migration (37+)**
|
||||
|
||||
| Component | Usage Pattern | Priority | Estimated Effort |
|
||||
|-----------|---------------|----------|------------------|
|
||||
| `HomeView.vue` | ✅ Updated | High | 5 min |
|
||||
| `ProjectsView.vue` | `settings.activeDid \|\| ""` | High | 3 min |
|
||||
| `ContactsView.vue` | `settings.activeDid \|\| ""` | High | 3 min |
|
||||
| `AccountViewView.vue` | `settings.activeDid \|\| ""` | High | 3 min |
|
||||
| `InviteOneView.vue` | `settings.activeDid \|\| ""` | Medium | 3 min |
|
||||
| `TestView.vue` | `settings.activeDid \|\| ""` | Medium | 3 min |
|
||||
| `SeedBackupView.vue` | `settings.activeDid \|\| ""` | Medium | 3 min |
|
||||
| `QuickActionBvcBeginView.vue` | `const activeDid = settings.activeDid \|\| ""` | Medium | 3 min |
|
||||
| `ConfirmGiftView.vue` | `this.activeDid = settings.activeDid \|\| ""` | Medium | 3 min |
|
||||
| `ClaimReportCertificateView.vue` | `this.activeDid = settings.activeDid \|\| ""` | Medium | 3 min |
|
||||
| `ImportAccountView.vue` | `settings.activeDid,` | Medium | 3 min |
|
||||
| `MembersList.vue` | `this.activeDid = settings.activeDid \|\| ""` | Medium | 3 min |
|
||||
| `ShareMyContactInfoView.vue` | `const activeDid = settings.activeDid \|\| ""` | Medium | 3 min |
|
||||
| `ClaimView.vue` | `this.activeDid = settings.activeDid \|\| ""` | Medium | 3 min |
|
||||
| `ImageMethodDialog.vue` | `this.activeDid = settings.activeDid \|\| ""` | Medium | 3 min |
|
||||
| `DiscoverView.vue` | `settings.activeDid as string` | Medium | 3 min |
|
||||
| `QuickActionBvcEndView.vue` | `this.activeDid = settings.activeDid \|\| ""` | Medium | 3 min |
|
||||
| `ContactQRScanFullView.vue` | `this.activeDid = settings.activeDid \|\| ""` | Medium | 3 min |
|
||||
| `ContactGiftingView.vue` | `this.activeDid = settings.activeDid \|\| ""` | Medium | 3 min |
|
||||
| `OfferDetailsView.vue` | `this.activeDid = settings.activeDid ?? ""` | Medium | 3 min |
|
||||
| `NewActivityView.vue` | `this.activeDid = settings.activeDid \|\| ""` | Medium | 3 min |
|
||||
| `OfferDialog.vue` | `this.activeDid = settings.activeDid \|\| ""` | Medium | 3 min |
|
||||
| `SharedPhotoView.vue` | `this.activeDid = settings.activeDid` | Medium | 3 min |
|
||||
| `ContactQRScanShowView.vue` | `this.activeDid = settings.activeDid \|\| ""` | Medium | 3 min |
|
||||
| `NewEditProjectView.vue` | `this.activeDid = settings.activeDid \|\| ""` | Medium | 3 min |
|
||||
| `GiftedDialog.vue` | `this.activeDid = settings.activeDid \|\| ""` | Medium | 3 min |
|
||||
| `HelpView.vue` | `if (settings.activeDid)` | Medium | 3 min |
|
||||
| `TopMessage.vue` | `settings.activeDid?.slice(11, 15)` | Medium | 3 min |
|
||||
| `ClaimCertificateView.vue` | `this.activeDid = settings.activeDid \|\| ""` | Medium | 3 min |
|
||||
| `UserProfileView.vue` | `this.activeDid = settings.activeDid \|\| ""` | Medium | 3 min |
|
||||
| `OnboardingDialog.vue` | `this.activeDid = settings.activeDid \|\| ""` | Medium | 3 min |
|
||||
| `RecentOffersToUserView.vue` | `this.activeDid = settings.activeDid \|\| ""` | Medium | 3 min |
|
||||
| `RecentOffersToUserProjectsView.vue` | `this.activeDid = settings.activeDid \|\| ""` | Medium | 3 min |
|
||||
| `ContactImportView.vue` | `this.activeDid = settings.activeDid \|\| ""` | Medium | 3 min |
|
||||
| `GiftedDetailsView.vue` | `this.activeDid = settings.activeDid \|\| ""` | Medium | 3 min |
|
||||
|
||||
## Migration Patterns
|
||||
|
||||
### **Pattern 1: Simple Read Replacement**
|
||||
```typescript
|
||||
// Before
|
||||
this.activeDid = settings.activeDid || "";
|
||||
|
||||
// After
|
||||
this.activeDid = await this.$getActiveDid() || "";
|
||||
```
|
||||
|
||||
### **Pattern 2: Write Replacement with Dual-Write**
|
||||
```typescript
|
||||
// Before
|
||||
await this.$saveSettings({ activeDid: newDid });
|
||||
|
||||
// After
|
||||
await this.$setActiveDid(newDid);
|
||||
|
||||
// Legacy fallback - remove after Phase C
|
||||
if (!FLAGS.USE_ACTIVE_IDENTITY_ONLY) {
|
||||
await this.$saveSettings({ activeDid: newDid });
|
||||
}
|
||||
```
|
||||
|
||||
### **Pattern 3: FLAGS Import Addition**
|
||||
```typescript
|
||||
// Add to imports section
|
||||
import { FLAGS } from "@/config/featureFlags";
|
||||
```
|
||||
|
||||
## Next Steps
|
||||
|
||||
### **Immediate Actions (Next 30 minutes)**
|
||||
1. **Complete High-Priority Components**: Update remaining critical components
|
||||
2. **Test Migration**: Verify migrated components work correctly
|
||||
3. **Run Linter**: Check for any remaining TypeScript issues
|
||||
|
||||
### **Short Term (Next 2 hours)**
|
||||
1. **Bulk Migration**: Use automated script for remaining components
|
||||
2. **Testing**: Validate all migrated components
|
||||
3. **Performance Check**: Ensure no performance regressions
|
||||
|
||||
### **Medium Term (Next 1 day)**
|
||||
1. **Phase C Preparation**: Enable `USE_ACTIVE_IDENTITY_ONLY` flag
|
||||
2. **Legacy Fallback Removal**: Remove dual-write patterns
|
||||
3. **Final Testing**: End-to-end validation
|
||||
|
||||
## Success Criteria
|
||||
|
||||
### **Phase B Complete When**
|
||||
- [ ] All 40+ components use new façade methods
|
||||
- [ ] No direct `settings.activeDid` access remains
|
||||
- [ ] All components pass linting
|
||||
- [ ] Basic functionality tested and working
|
||||
- [ ] Performance maintained or improved
|
||||
|
||||
### **Phase C Ready When**
|
||||
- [ ] All components migrated and tested
|
||||
- [ ] Feature flag `USE_ACTIVE_IDENTITY_ONLY` can be enabled
|
||||
- [ ] No legacy fallback usage in production
|
||||
- [ ] Performance benchmarks show improvement
|
||||
|
||||
## Risks & Mitigation
|
||||
|
||||
### **High Risk**
|
||||
- **Component Breakage**: Test each migrated component individually
|
||||
- **Performance Regression**: Monitor performance metrics during migration
|
||||
- **TypeScript Errors**: Ensure all method signatures are properly declared
|
||||
|
||||
### **Medium Risk**
|
||||
- **Migration Inconsistency**: Use consistent patterns across all components
|
||||
- **Testing Coverage**: Ensure comprehensive testing of identity switching flows
|
||||
|
||||
### **Low Risk**
|
||||
- **Backup Size**: Minimal backup strategy for critical files only
|
||||
- **Rollback Complexity**: Simple git revert if needed
|
||||
|
||||
## Tools & Scripts
|
||||
|
||||
### **Migration Scripts**
|
||||
- `scripts/migrate-active-identity-components.sh` - Full backup version
|
||||
- `scripts/migrate-active-identity-components-efficient.sh` - Minimal backup version
|
||||
|
||||
### **Testing Commands**
|
||||
```bash
|
||||
# Check for remaining settings.activeDid usage
|
||||
grep -r "settings\.activeDid" src/views/ src/components/
|
||||
|
||||
# Run linter
|
||||
npm run lint-fix
|
||||
|
||||
# Test specific component
|
||||
npm run test:web -- --grep "IdentitySwitcher"
|
||||
```
|
||||
|
||||
## References
|
||||
|
||||
- [Active Identity Implementation Overview](./active-identity-implementation-overview.md)
|
||||
- [PlatformServiceMixin Documentation](../component-communication-guide.md)
|
||||
- [Feature Flags Configuration](../feature-flags.md)
|
||||
- [Database Migration Guide](../database-migration-guide.md)
|
||||
|
||||
---
|
||||
|
||||
**Status**: Phase B in progress, 3/40+ components migrated
|
||||
**Next Review**: After completing high-priority components
|
||||
**Maintainer**: Development team
|
||||
298
doc/activeDid-table-separation-progress.md
Normal file
298
doc/activeDid-table-separation-progress.md
Normal file
@@ -0,0 +1,298 @@
|
||||
# ActiveDid Table Separation Progress Report
|
||||
|
||||
**Author**: Matthew Raymer
|
||||
**Date**: 2025-08-21T12:32Z
|
||||
**Status**: 🔍 **INVESTIGATION COMPLETE** - Ready for implementation planning
|
||||
|
||||
## Executive Summary
|
||||
|
||||
This document tracks the investigation and progress of separating the `activeDid` field
|
||||
from the `settings` table into a dedicated `active_identity` table. The project aims
|
||||
to improve data integrity, reduce cache drift, and simplify transaction logic for
|
||||
identity management in TimeSafari.
|
||||
|
||||
## Investigation Results
|
||||
|
||||
### Reference Audit Findings
|
||||
|
||||
**Total ActiveDid References**: 505 across the codebase
|
||||
|
||||
- **Write Operations**: 100 (20%)
|
||||
- **Read Operations**: 260 (51%)
|
||||
- **Other References**: 145 (29%) - includes type definitions, comments, etc.
|
||||
|
||||
**Component Impact**: 15+ Vue components directly access `settings.activeDid`
|
||||
|
||||
### Current Database Schema
|
||||
|
||||
The `settings` table currently contains **30 fields** mixing identity state with user
|
||||
preferences:
|
||||
|
||||
```sql
|
||||
CREATE TABLE IF NOT EXISTS settings (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
accountDid TEXT, -- Links to identity (null = master)
|
||||
activeDid TEXT, -- Current active identity (master only)
|
||||
apiServer TEXT, -- API endpoint
|
||||
filterFeedByNearby BOOLEAN,
|
||||
filterFeedByVisible BOOLEAN,
|
||||
finishedOnboarding BOOLEAN,
|
||||
firstName TEXT, -- User's name
|
||||
hideRegisterPromptOnNewContact BOOLEAN,
|
||||
isRegistered BOOLEAN,
|
||||
lastName TEXT, -- Deprecated
|
||||
lastAckedOfferToUserJwtId TEXT,
|
||||
lastAckedOfferToUserProjectsJwtId TEXT,
|
||||
lastNotifiedClaimId TEXT,
|
||||
lastViewedClaimId TEXT,
|
||||
notifyingNewActivityTime TEXT,
|
||||
notifyingReminderMessage TEXT,
|
||||
notifyingReminderTime TEXT,
|
||||
partnerApiServer TEXT,
|
||||
passkeyExpirationMinutes INTEGER,
|
||||
profileImageUrl TEXT,
|
||||
searchBoxes TEXT, -- JSON string
|
||||
showContactGivesInline BOOLEAN,
|
||||
showGeneralAdvanced BOOLEAN,
|
||||
showShortcutBvc BOOLEAN,
|
||||
vapid TEXT,
|
||||
warnIfProdServer BOOLEAN,
|
||||
warnIfTestServer BOOLEAN,
|
||||
webPushServer TEXT
|
||||
);
|
||||
```
|
||||
|
||||
### Component State Management
|
||||
|
||||
#### PlatformServiceMixin Cache System
|
||||
|
||||
- **`_currentActiveDid`**: Component-level cache for activeDid
|
||||
- **`$updateActiveDid()`**: Method to sync cache with database
|
||||
- **Change Detection**: Watcher triggers component updates on activeDid changes
|
||||
- **State Synchronization**: Cache updates when `$saveSettings()` changes activeDid
|
||||
|
||||
#### Common Usage Patterns
|
||||
|
||||
```typescript
|
||||
// Standard pattern across 15+ components
|
||||
this.activeDid = settings.activeDid || "";
|
||||
|
||||
// API header generation
|
||||
const headers = await getHeaders(this.activeDid);
|
||||
|
||||
// Identity validation
|
||||
if (claim.issuer === this.activeDid) { ... }
|
||||
```
|
||||
|
||||
### Migration Infrastructure Status
|
||||
|
||||
#### Existing Capabilities
|
||||
|
||||
- **`migrateSettings()`**: Fully implemented and functional
|
||||
- **Settings Migration**: Handles 30 fields with proper type conversion
|
||||
- **Data Integrity**: Includes validation and error handling
|
||||
- **Rollback Capability**: Migration service has rollback infrastructure
|
||||
|
||||
#### Migration Order
|
||||
|
||||
1. **Accounts** (foundational - contains DIDs)
|
||||
2. **Settings** (references accountDid, activeDid)
|
||||
3. **ActiveDid** (depends on accounts and settings)
|
||||
4. **Contacts** (independent, but migrated after accounts)
|
||||
|
||||
### Testing Infrastructure
|
||||
|
||||
#### Current Coverage
|
||||
|
||||
- **Playwright Tests**: `npm run test:web` and `npm run test:mobile`
|
||||
- **No Unit Tests**: Found for migration or settings management
|
||||
- **Integration Tests**: Available through Playwright test suite
|
||||
- **Platform Coverage**: Web, Mobile (Android/iOS), Desktop (Electron)
|
||||
|
||||
## Risk Assessment
|
||||
|
||||
### High Risk Areas
|
||||
|
||||
1. **Component State Synchronization**: 505 references across codebase
|
||||
2. **Cache Drift**: `_currentActiveDid` vs database `activeDid`
|
||||
3. **Cross-Platform Consistency**: Web + Mobile + Desktop
|
||||
|
||||
### Medium Risk Areas
|
||||
|
||||
1. **Foreign Key Constraints**: activeDid → accounts.did relationship
|
||||
2. **Migration Rollback**: Complex 30-field settings table
|
||||
3. **API Surface Changes**: Components expect `settings.activeDid`
|
||||
|
||||
### Low Risk Areas
|
||||
|
||||
1. **Migration Infrastructure**: Already exists and functional
|
||||
2. **Data Integrity**: Current migration handles complex scenarios
|
||||
3. **Testing Framework**: Playwright tests available for validation
|
||||
|
||||
## Implementation Phases
|
||||
|
||||
### Phase 1: Foundation Analysis ✅ **COMPLETE**
|
||||
|
||||
- [x] **ActiveDid Reference Audit**: 505 references identified and categorized
|
||||
- [x] **Database Schema Analysis**: 30-field settings table documented
|
||||
- [x] **Component Usage Mapping**: 15+ components usage patterns documented
|
||||
- [x] **Migration Infrastructure Assessment**: Existing service validated
|
||||
|
||||
### Phase 2: Design & Implementation (Medium Complexity)
|
||||
|
||||
- [ ] **New Table Schema Design**
|
||||
- Define `active_identity` table structure
|
||||
- Plan foreign key relationships to `accounts.did`
|
||||
- Design migration SQL statements
|
||||
- Validate against existing data patterns
|
||||
|
||||
- [ ] **Component Update Strategy**
|
||||
- Map all 505 references for update strategy
|
||||
- Plan computed property changes
|
||||
- Design state synchronization approach
|
||||
- Preserve existing API surface
|
||||
|
||||
- [ ] **Testing Infrastructure Planning**
|
||||
- Unit tests for new table operations
|
||||
- Integration tests for identity switching
|
||||
- Migration rollback validation
|
||||
- Cross-platform testing strategy
|
||||
|
||||
### Phase 3: Migration & Validation (Complex Complexity)
|
||||
|
||||
- [ ] **Migration Execution Testing**
|
||||
- Test on development database
|
||||
- Validate data integrity post-migration
|
||||
- Measure performance impact
|
||||
- Test rollback scenarios
|
||||
|
||||
- [ ] **Cross-Platform Validation**
|
||||
- Web platform functionality
|
||||
- Mobile platform functionality
|
||||
- Desktop platform functionality
|
||||
- Cross-platform consistency
|
||||
|
||||
- [ ] **User Acceptance Testing**
|
||||
- Identity switching workflows
|
||||
- Settings persistence
|
||||
- Error handling scenarios
|
||||
- Edge case validation
|
||||
|
||||
## Technical Requirements
|
||||
|
||||
### New Table Schema
|
||||
|
||||
```sql
|
||||
-- Proposed active_identity table
|
||||
CREATE TABLE IF NOT EXISTS active_identity (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
activeDid TEXT NOT NULL,
|
||||
lastUpdated TEXT NOT NULL,
|
||||
FOREIGN KEY (activeDid) REFERENCES accounts(did)
|
||||
);
|
||||
|
||||
-- Index for performance
|
||||
CREATE INDEX IF NOT EXISTS idx_active_identity_activeDid ON active_identity(activeDid);
|
||||
```
|
||||
|
||||
### Migration Strategy
|
||||
|
||||
1. **Extract activeDid**: Copy from settings table to new table
|
||||
2. **Update References**: Modify components to use new table
|
||||
3. **Remove Field**: Drop activeDid from settings table
|
||||
4. **Validate**: Ensure data integrity and functionality
|
||||
|
||||
### Component Updates Required
|
||||
|
||||
- **PlatformServiceMixin**: Update activeDid management
|
||||
- **15+ Vue Components**: Modify activeDid access patterns
|
||||
- **Migration Service**: Add activeDid table migration
|
||||
- **Database Utilities**: Update settings operations
|
||||
|
||||
## Success Criteria
|
||||
|
||||
### Phase 1 ✅ **ACHIEVED**
|
||||
|
||||
- Complete activeDid usage audit with counts
|
||||
- Database schema validation with data integrity check
|
||||
- Migration service health assessment
|
||||
- Clear dependency map for component updates
|
||||
|
||||
### Phase 2
|
||||
|
||||
- New table schema designed and validated
|
||||
- Component update strategy documented
|
||||
- Testing infrastructure planned
|
||||
- Migration scripts developed
|
||||
|
||||
### Phase 3
|
||||
|
||||
- Migration successfully executed
|
||||
- All platforms functional
|
||||
- Performance maintained or improved
|
||||
- Zero data loss
|
||||
|
||||
## Dependencies
|
||||
|
||||
### Technical Dependencies
|
||||
|
||||
- **Existing Migration Infrastructure**: Settings migration service
|
||||
- **Database Access Patterns**: PlatformServiceMixin methods
|
||||
- **Component Architecture**: Vue component patterns
|
||||
|
||||
### Platform Dependencies
|
||||
|
||||
- **Cross-Platform Consistency**: Web + Mobile + Desktop
|
||||
- **Testing Framework**: Playwright test suite
|
||||
- **Build System**: Vite configuration for all platforms
|
||||
|
||||
### Testing Dependencies
|
||||
|
||||
- **Migration Validation**: Rollback testing
|
||||
- **Integration Testing**: Cross-platform functionality
|
||||
- **User Acceptance**: Identity switching workflows
|
||||
|
||||
## Next Steps
|
||||
|
||||
### Immediate Actions (Next Session)
|
||||
|
||||
1. **Create New Table Schema**: Design `active_identity` table structure
|
||||
2. **Component Update Planning**: Map all 505 references for update strategy
|
||||
3. **Migration Script Development**: Create activeDid extraction migration
|
||||
|
||||
### Success Metrics
|
||||
|
||||
- **Data Integrity**: 100% activeDid data preserved
|
||||
- **Performance**: No degradation in identity switching
|
||||
- **Platform Coverage**: All platforms functional
|
||||
- **Testing Coverage**: Comprehensive migration validation
|
||||
|
||||
## References
|
||||
|
||||
- **Codebase Analysis**: `src/views/*.vue`, `src/utils/PlatformServiceMixin.ts`
|
||||
- **Database Schema**: `src/db-sql/migration.ts`
|
||||
- **Migration Service**: `src/services/indexedDBMigrationService.ts`
|
||||
- **Settings Types**: `src/db/tables/settings.ts`
|
||||
|
||||
## Competence Hooks
|
||||
|
||||
- **Why this works**: Separation of concerns improves data integrity, reduces
|
||||
cache drift, simplifies transaction logic
|
||||
- **Common pitfalls**: Missing component updates, foreign key constraint
|
||||
violations, migration rollback failures
|
||||
- **Next skill**: Database schema normalization and migration planning
|
||||
- **Teach-back**: "How would you ensure zero downtime during the activeDid
|
||||
table migration?"
|
||||
|
||||
## Collaboration Hooks
|
||||
|
||||
- **Reviewers**: Database team for schema design, Frontend team for component
|
||||
updates, QA team for testing strategy
|
||||
- **Sign-off checklist**: Migration tested, rollback verified, performance
|
||||
validated, component state consistent
|
||||
|
||||
---
|
||||
|
||||
**Status**: Investigation complete, ready for implementation planning
|
||||
**Next Review**: 2025-08-28
|
||||
**Estimated Complexity**: High (cross-platform refactoring with 505 references)
|
||||
69
doc/z-index-guide.md
Normal file
69
doc/z-index-guide.md
Normal file
@@ -0,0 +1,69 @@
|
||||
# Z-Index Guide — TimeSafari
|
||||
|
||||
**Author**: Development Team
|
||||
**Date**: 2025-08-25T19:38:09-08:00
|
||||
**Status**: 🎯 **ACTIVE** - Z-index layering standards
|
||||
|
||||
## Objective
|
||||
Establish consistent z-index values across the TimeSafari application to ensure proper layering of UI elements.
|
||||
|
||||
## Result
|
||||
This document defines the z-index hierarchy for all UI components.
|
||||
|
||||
## Use/Run
|
||||
Reference these values when implementing new components or modifying existing ones to maintain consistent layering.
|
||||
|
||||
## Z-Index Hierarchy
|
||||
|
||||
| Component | Z-Index | Usage |
|
||||
|-----------|---------|-------|
|
||||
| **Map** | `40` | Base map layer and map-related overlays |
|
||||
| **QuickNav** | `50` | Quick navigation bottom bar |
|
||||
| **Dialogs and Modals** | `100` | Modal dialogs, popups, and overlay content |
|
||||
| **Notifications and Toasts** | `120` | System notifications, alerts, and toast messages |
|
||||
|
||||
## Best Practices
|
||||
|
||||
1. **Never exceed 120** - Keep the highest z-index reserved for critical notifications
|
||||
2. **Use increments of 10** - Leave room for future additions between layers
|
||||
3. **Document exceptions** - If you need a z-index outside this range, document the reason
|
||||
4. **Test layering** - Verify z-index behavior across different screen sizes and devices
|
||||
|
||||
## Common Pitfalls
|
||||
|
||||
- **Avoid arbitrary values** - Don't use random z-index numbers
|
||||
- **Don't nest high z-index** - Keep child elements within their parent's z-index range
|
||||
- **Consider stacking context** - Remember that `position: relative` creates new stacking contexts
|
||||
|
||||
## Next Steps
|
||||
|
||||
| Owner | Task | Exit Criteria | Target Date |
|
||||
|-------|------|---------------|-------------|
|
||||
| Dev Team | Apply z-index classes to existing components | All components use defined z-index values | 2025-09-01 |
|
||||
|
||||
## Competence Hooks
|
||||
|
||||
- **Why this works**: Creates predictable layering hierarchy that prevents UI conflicts
|
||||
- **Common pitfalls**: Using arbitrary z-index values or exceeding the defined range
|
||||
- **Next skill unlock**: Learn about CSS stacking contexts and their impact on z-index
|
||||
- **Teach-back**: Explain the z-index hierarchy to a team member without referencing this guide
|
||||
|
||||
## Collaboration Hooks
|
||||
|
||||
- **Reviewers**: Frontend team, UI/UX designers
|
||||
- **Sign-off checklist**:
|
||||
- [ ] All new components follow z-index guidelines
|
||||
- [ ] Existing components updated to use defined values
|
||||
- [ ] Cross-browser testing completed
|
||||
- [ ] Mobile responsiveness verified
|
||||
|
||||
## Assumptions & Limits
|
||||
|
||||
- Assumes modern browser support for z-index
|
||||
- Limited to 4 defined layers (expandable if needed)
|
||||
- Requires team discipline to maintain consistency
|
||||
|
||||
## References
|
||||
|
||||
- [MDN Z-Index Documentation](https://developer.mozilla.org/en-US/docs/Web/CSS/z-index)
|
||||
- [CSS Stacking Context Guide](https://developer.mozilla.org/en-US/docs/Web/CSS/CSS_Positioning/Understanding_z_index/The_stacking_context)
|
||||
@@ -15,8 +15,10 @@ def capacitor_pods
|
||||
pod 'CapacitorMlkitBarcodeScanning', :path => '../../node_modules/@capacitor-mlkit/barcode-scanning'
|
||||
pod 'CapacitorApp', :path => '../../node_modules/@capacitor/app'
|
||||
pod 'CapacitorCamera', :path => '../../node_modules/@capacitor/camera'
|
||||
pod 'CapacitorClipboard', :path => '../../node_modules/@capacitor/clipboard'
|
||||
pod 'CapacitorFilesystem', :path => '../../node_modules/@capacitor/filesystem'
|
||||
pod 'CapacitorShare', :path => '../../node_modules/@capacitor/share'
|
||||
pod 'CapacitorStatusBar', :path => '../../node_modules/@capacitor/status-bar'
|
||||
pod 'CapawesomeCapacitorFilePicker', :path => '../../node_modules/@capawesome/capacitor-file-picker'
|
||||
end
|
||||
|
||||
|
||||
@@ -5,6 +5,8 @@ PODS:
|
||||
- Capacitor
|
||||
- CapacitorCamera (6.1.2):
|
||||
- Capacitor
|
||||
- CapacitorClipboard (6.0.2):
|
||||
- Capacitor
|
||||
- CapacitorCommunitySqlite (6.0.2):
|
||||
- Capacitor
|
||||
- SQLCipher
|
||||
@@ -17,6 +19,8 @@ PODS:
|
||||
- GoogleMLKit/BarcodeScanning (= 5.0.0)
|
||||
- CapacitorShare (6.0.3):
|
||||
- Capacitor
|
||||
- CapacitorStatusBar (6.0.2):
|
||||
- Capacitor
|
||||
- CapawesomeCapacitorFilePicker (6.2.0):
|
||||
- Capacitor
|
||||
- GoogleDataTransport (9.4.1):
|
||||
@@ -88,11 +92,13 @@ DEPENDENCIES:
|
||||
- "Capacitor (from `../../node_modules/@capacitor/ios`)"
|
||||
- "CapacitorApp (from `../../node_modules/@capacitor/app`)"
|
||||
- "CapacitorCamera (from `../../node_modules/@capacitor/camera`)"
|
||||
- "CapacitorClipboard (from `../../node_modules/@capacitor/clipboard`)"
|
||||
- "CapacitorCommunitySqlite (from `../../node_modules/@capacitor-community/sqlite`)"
|
||||
- "CapacitorCordova (from `../../node_modules/@capacitor/ios`)"
|
||||
- "CapacitorFilesystem (from `../../node_modules/@capacitor/filesystem`)"
|
||||
- "CapacitorMlkitBarcodeScanning (from `../../node_modules/@capacitor-mlkit/barcode-scanning`)"
|
||||
- "CapacitorShare (from `../../node_modules/@capacitor/share`)"
|
||||
- "CapacitorStatusBar (from `../../node_modules/@capacitor/status-bar`)"
|
||||
- "CapawesomeCapacitorFilePicker (from `../../node_modules/@capawesome/capacitor-file-picker`)"
|
||||
|
||||
SPEC REPOS:
|
||||
@@ -119,6 +125,8 @@ EXTERNAL SOURCES:
|
||||
:path: "../../node_modules/@capacitor/app"
|
||||
CapacitorCamera:
|
||||
:path: "../../node_modules/@capacitor/camera"
|
||||
CapacitorClipboard:
|
||||
:path: "../../node_modules/@capacitor/clipboard"
|
||||
CapacitorCommunitySqlite:
|
||||
:path: "../../node_modules/@capacitor-community/sqlite"
|
||||
CapacitorCordova:
|
||||
@@ -129,6 +137,8 @@ EXTERNAL SOURCES:
|
||||
:path: "../../node_modules/@capacitor-mlkit/barcode-scanning"
|
||||
CapacitorShare:
|
||||
:path: "../../node_modules/@capacitor/share"
|
||||
CapacitorStatusBar:
|
||||
:path: "../../node_modules/@capacitor/status-bar"
|
||||
CapawesomeCapacitorFilePicker:
|
||||
:path: "../../node_modules/@capawesome/capacitor-file-picker"
|
||||
|
||||
@@ -136,11 +146,13 @@ SPEC CHECKSUMS:
|
||||
Capacitor: c95400d761e376be9da6be5a05f226c0e865cebf
|
||||
CapacitorApp: e1e6b7d05e444d593ca16fd6d76f2b7c48b5aea7
|
||||
CapacitorCamera: 9bc7b005d0e6f1d5f525b8137045b60cffffce79
|
||||
CapacitorClipboard: 4443c3cdb7c77b1533dfe3ff0f9f7756aa8579df
|
||||
CapacitorCommunitySqlite: 0299d20f4b00c2e6aa485a1d8932656753937b9b
|
||||
CapacitorCordova: 8d93e14982f440181be7304aa9559ca631d77fff
|
||||
CapacitorFilesystem: 59270a63c60836248812671aa3b15df673fbaf74
|
||||
CapacitorMlkitBarcodeScanning: 7652be9c7922f39203a361de735d340ae37e134e
|
||||
CapacitorShare: d2a742baec21c8f3b92b361a2fbd2401cdd8288e
|
||||
CapacitorStatusBar: b16799a26320ffa52f6c8b01737d5a95bbb8f3eb
|
||||
CapawesomeCapacitorFilePicker: c40822f0a39f86855321943c7829d52bca7f01bd
|
||||
GoogleDataTransport: 6c09b596d841063d76d4288cc2d2f42cc36e1e2a
|
||||
GoogleMLKit: 90ba06e028795a50261f29500d238d6061538711
|
||||
@@ -157,6 +169,6 @@ SPEC CHECKSUMS:
|
||||
SQLCipher: 31878d8ebd27e5c96db0b7cb695c96e9f8ad77da
|
||||
ZIPFoundation: b8c29ea7ae353b309bc810586181fd073cb3312c
|
||||
|
||||
PODFILE CHECKSUM: f987510f7383b04a1b09ea8472bdadcd88b6c924
|
||||
PODFILE CHECKSUM: 5fa870b031c7c4e0733e2f96deaf81866c75ff7d
|
||||
|
||||
COCOAPODS: 1.16.2
|
||||
|
||||
614
package-lock.json
generated
614
package-lock.json
generated
@@ -15,10 +15,12 @@
|
||||
"@capacitor/app": "^6.0.0",
|
||||
"@capacitor/camera": "^6.0.0",
|
||||
"@capacitor/cli": "^6.2.0",
|
||||
"@capacitor/clipboard": "^6.0.2",
|
||||
"@capacitor/core": "^6.2.0",
|
||||
"@capacitor/filesystem": "^6.0.0",
|
||||
"@capacitor/ios": "^6.2.0",
|
||||
"@capacitor/share": "^6.0.3",
|
||||
"@capacitor/status-bar": "^6.0.2",
|
||||
"@capawesome/capacitor-file-picker": "^6.2.0",
|
||||
"@dicebear/collection": "^5.4.1",
|
||||
"@dicebear/core": "^5.4.1",
|
||||
@@ -136,6 +138,7 @@
|
||||
"postcss": "^8.4.38",
|
||||
"prettier": "^3.2.5",
|
||||
"rimraf": "^6.0.1",
|
||||
"serve": "^14.2.4",
|
||||
"tailwindcss": "^3.4.1",
|
||||
"ts-jest": "^29.4.0",
|
||||
"tsx": "^4.20.4",
|
||||
@@ -2301,6 +2304,14 @@
|
||||
"url": "https://github.com/sponsors/isaacs"
|
||||
}
|
||||
},
|
||||
"node_modules/@capacitor/clipboard": {
|
||||
"version": "6.0.2",
|
||||
"resolved": "https://registry.npmjs.org/@capacitor/clipboard/-/clipboard-6.0.2.tgz",
|
||||
"integrity": "sha512-jQ6UeFra5NP58THNZNb7HtzOZU7cHsjgrbQGVuMTgsK1uTILZpNeh+pfqHbKggba6KaNh5DAsJvEVQGpIR1VBA==",
|
||||
"peerDependencies": {
|
||||
"@capacitor/core": "^6.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@capacitor/core": {
|
||||
"version": "6.2.1",
|
||||
"resolved": "https://registry.npmjs.org/@capacitor/core/-/core-6.2.1.tgz",
|
||||
@@ -2337,6 +2348,14 @@
|
||||
"@capacitor/core": "^6.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@capacitor/status-bar": {
|
||||
"version": "6.0.2",
|
||||
"resolved": "https://registry.npmjs.org/@capacitor/status-bar/-/status-bar-6.0.2.tgz",
|
||||
"integrity": "sha512-AmRIX6QvFemItlY7/69ARkIAqitRQqJ2qwgZmD1KqgFb78pH+XFXm1guvS/a8CuOOm/IqZ4ddDbl20yxtBqzGA==",
|
||||
"peerDependencies": {
|
||||
"@capacitor/core": "^6.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@capawesome/capacitor-file-picker": {
|
||||
"version": "6.2.0",
|
||||
"resolved": "https://registry.npmjs.org/@capawesome/capacitor-file-picker/-/capacitor-file-picker-6.2.0.tgz",
|
||||
@@ -11742,6 +11761,13 @@
|
||||
"node": ">=10.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@zeit/schemas": {
|
||||
"version": "2.36.0",
|
||||
"resolved": "https://registry.npmjs.org/@zeit/schemas/-/schemas-2.36.0.tgz",
|
||||
"integrity": "sha512-7kjMwcChYEzMKjeex9ZFXkt1AyNov9R5HZtjBKVsmVpw7pa7ZtlCGvCBC2vnnXctaYN+aRI61HjIqeetZW5ROg==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@zxing/text-encoding": {
|
||||
"version": "0.9.0",
|
||||
"resolved": "https://registry.npmjs.org/@zxing/text-encoding/-/text-encoding-0.9.0.tgz",
|
||||
@@ -11804,9 +11830,8 @@
|
||||
"version": "1.3.8",
|
||||
"resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz",
|
||||
"integrity": "sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==",
|
||||
"devOptional": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"mime-types": "~2.1.34",
|
||||
"negotiator": "0.6.3"
|
||||
@@ -11931,6 +11956,16 @@
|
||||
"optional": true,
|
||||
"peer": true
|
||||
},
|
||||
"node_modules/ansi-align": {
|
||||
"version": "3.0.1",
|
||||
"resolved": "https://registry.npmjs.org/ansi-align/-/ansi-align-3.0.1.tgz",
|
||||
"integrity": "sha512-IOfwwBF5iczOjp/WeY4YxyjqAFMQoZufdQWDd19SEExbVLNXqvpzSJ/M7Za4/sCPmQ0+GRquoA7bGcINcxew6w==",
|
||||
"dev": true,
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"string-width": "^4.1.0"
|
||||
}
|
||||
},
|
||||
"node_modules/ansi-escapes": {
|
||||
"version": "4.3.2",
|
||||
"resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-4.3.2.tgz",
|
||||
@@ -12135,6 +12170,27 @@
|
||||
"license": "ISC",
|
||||
"optional": true
|
||||
},
|
||||
"node_modules/arch": {
|
||||
"version": "2.2.0",
|
||||
"resolved": "https://registry.npmjs.org/arch/-/arch-2.2.0.tgz",
|
||||
"integrity": "sha512-Of/R0wqp83cgHozfIYLbBMnej79U/SVGOOyuB3VVFv1NRM/PSFMK12x9KVtiYzJqmnU5WR2qp0Z5rHb7sWGnFQ==",
|
||||
"dev": true,
|
||||
"funding": [
|
||||
{
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/feross"
|
||||
},
|
||||
{
|
||||
"type": "patreon",
|
||||
"url": "https://www.patreon.com/feross"
|
||||
},
|
||||
{
|
||||
"type": "consulting",
|
||||
"url": "https://feross.org/support"
|
||||
}
|
||||
],
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/are-we-there-yet": {
|
||||
"version": "3.0.1",
|
||||
"resolved": "https://registry.npmjs.org/are-we-there-yet/-/are-we-there-yet-3.0.1.tgz",
|
||||
@@ -12931,6 +12987,153 @@
|
||||
"dev": true,
|
||||
"license": "ISC"
|
||||
},
|
||||
"node_modules/boxen": {
|
||||
"version": "7.0.0",
|
||||
"resolved": "https://registry.npmjs.org/boxen/-/boxen-7.0.0.tgz",
|
||||
"integrity": "sha512-j//dBVuyacJbvW+tvZ9HuH03fZ46QcaKvvhZickZqtB271DxJ7SNRSNxrV/dZX0085m7hISRZWbzWlJvx/rHSg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"ansi-align": "^3.0.1",
|
||||
"camelcase": "^7.0.0",
|
||||
"chalk": "^5.0.1",
|
||||
"cli-boxes": "^3.0.0",
|
||||
"string-width": "^5.1.2",
|
||||
"type-fest": "^2.13.0",
|
||||
"widest-line": "^4.0.1",
|
||||
"wrap-ansi": "^8.0.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=14.16"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/boxen/node_modules/ansi-regex": {
|
||||
"version": "6.2.0",
|
||||
"resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.2.0.tgz",
|
||||
"integrity": "sha512-TKY5pyBkHyADOPYlRT9Lx6F544mPl0vS5Ew7BJ45hA08Q+t3GjbueLliBWN3sMICk6+y7HdyxSzC4bWS8baBdg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/chalk/ansi-regex?sponsor=1"
|
||||
}
|
||||
},
|
||||
"node_modules/boxen/node_modules/ansi-styles": {
|
||||
"version": "6.2.1",
|
||||
"resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.1.tgz",
|
||||
"integrity": "sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/chalk/ansi-styles?sponsor=1"
|
||||
}
|
||||
},
|
||||
"node_modules/boxen/node_modules/camelcase": {
|
||||
"version": "7.0.1",
|
||||
"resolved": "https://registry.npmjs.org/camelcase/-/camelcase-7.0.1.tgz",
|
||||
"integrity": "sha512-xlx1yCK2Oc1APsPXDL2LdlNP6+uu8OCDdhOBSVT279M/S+y75O30C2VuD8T2ogdePBBl7PfPF4504tnLgX3zfw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=14.16"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/boxen/node_modules/chalk": {
|
||||
"version": "5.6.0",
|
||||
"resolved": "https://registry.npmjs.org/chalk/-/chalk-5.6.0.tgz",
|
||||
"integrity": "sha512-46QrSQFyVSEyYAgQ22hQ+zDa60YHA4fBstHmtSApj1Y5vKtG27fWowW03jCk5KcbXEWPZUIR894aARCA/G1kfQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": "^12.17.0 || ^14.13 || >=16.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/chalk/chalk?sponsor=1"
|
||||
}
|
||||
},
|
||||
"node_modules/boxen/node_modules/emoji-regex": {
|
||||
"version": "9.2.2",
|
||||
"resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz",
|
||||
"integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/boxen/node_modules/string-width": {
|
||||
"version": "5.1.2",
|
||||
"resolved": "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz",
|
||||
"integrity": "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"eastasianwidth": "^0.2.0",
|
||||
"emoji-regex": "^9.2.2",
|
||||
"strip-ansi": "^7.0.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/boxen/node_modules/strip-ansi": {
|
||||
"version": "7.1.0",
|
||||
"resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.0.tgz",
|
||||
"integrity": "sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"ansi-regex": "^6.0.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/chalk/strip-ansi?sponsor=1"
|
||||
}
|
||||
},
|
||||
"node_modules/boxen/node_modules/type-fest": {
|
||||
"version": "2.19.0",
|
||||
"resolved": "https://registry.npmjs.org/type-fest/-/type-fest-2.19.0.tgz",
|
||||
"integrity": "sha512-RAH822pAdBgcNMAfWnCBU3CFZcfZ/i1eZjwFU/dsLKumyuuP3niueg2UAukXYF0E2AAoc82ZSSf9J0WQBinzHA==",
|
||||
"dev": true,
|
||||
"license": "(MIT OR CC0-1.0)",
|
||||
"engines": {
|
||||
"node": ">=12.20"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/boxen/node_modules/wrap-ansi": {
|
||||
"version": "8.1.0",
|
||||
"resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-8.1.0.tgz",
|
||||
"integrity": "sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"ansi-styles": "^6.1.0",
|
||||
"string-width": "^5.0.1",
|
||||
"strip-ansi": "^7.0.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/chalk/wrap-ansi?sponsor=1"
|
||||
}
|
||||
},
|
||||
"node_modules/bplist-creator": {
|
||||
"version": "0.1.0",
|
||||
"resolved": "https://registry.npmjs.org/bplist-creator/-/bplist-creator-0.1.0.tgz",
|
||||
@@ -13717,6 +13920,22 @@
|
||||
"url": "https://github.com/chalk/chalk?sponsor=1"
|
||||
}
|
||||
},
|
||||
"node_modules/chalk-template": {
|
||||
"version": "0.4.0",
|
||||
"resolved": "https://registry.npmjs.org/chalk-template/-/chalk-template-0.4.0.tgz",
|
||||
"integrity": "sha512-/ghrgmhfY8RaSdeo43hNXxpoHAtxdbskUHjPpfqUWGttFgycUhYPGx3YZBCnUCvOa7Doivn1IZec3DEGFoMgLg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"chalk": "^4.1.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/chalk/chalk-template?sponsor=1"
|
||||
}
|
||||
},
|
||||
"node_modules/char-regex": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/char-regex/-/char-regex-1.0.2.tgz",
|
||||
@@ -13941,6 +14160,19 @@
|
||||
"node": ">=6"
|
||||
}
|
||||
},
|
||||
"node_modules/cli-boxes": {
|
||||
"version": "3.0.0",
|
||||
"resolved": "https://registry.npmjs.org/cli-boxes/-/cli-boxes-3.0.0.tgz",
|
||||
"integrity": "sha512-/lzGpEWL/8PfI0BmBOPRwp0c/wFNX1RdUML3jK/RcSBA9T8mZDdQpqYBKtCFTOfQbwPqWEOpjqW+Fnayc0969g==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=10"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/cli-cursor": {
|
||||
"version": "3.1.0",
|
||||
"resolved": "https://registry.npmjs.org/cli-cursor/-/cli-cursor-3.1.0.tgz",
|
||||
@@ -13997,6 +14229,24 @@
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/clipboardy": {
|
||||
"version": "3.0.0",
|
||||
"resolved": "https://registry.npmjs.org/clipboardy/-/clipboardy-3.0.0.tgz",
|
||||
"integrity": "sha512-Su+uU5sr1jkUy1sGRpLKjKrvEOVXgSgiSInwa/qeID6aJ07yh+5NWc3h2QfjHjBnfX4LhtFcuAWKUsJ3r+fjbg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"arch": "^2.2.0",
|
||||
"execa": "^5.1.1",
|
||||
"is-wsl": "^2.2.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^12.20.0 || ^14.13.1 || >=16.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/cliui": {
|
||||
"version": "8.0.1",
|
||||
"resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz",
|
||||
@@ -14163,9 +14413,8 @@
|
||||
"version": "2.0.18",
|
||||
"resolved": "https://registry.npmjs.org/compressible/-/compressible-2.0.18.tgz",
|
||||
"integrity": "sha512-AF3r7P5dWxL8MxyITRMlORQNaOA2IkAFaTr4k7BUumjPtRpGDTZpl0Pb1XCO6JeDCBdp126Cgs9sMxqSjgYyRg==",
|
||||
"devOptional": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"mime-db": ">= 1.43.0 < 2"
|
||||
},
|
||||
@@ -14439,6 +14688,16 @@
|
||||
"license": "ISC",
|
||||
"optional": true
|
||||
},
|
||||
"node_modules/content-disposition": {
|
||||
"version": "0.5.2",
|
||||
"resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.2.tgz",
|
||||
"integrity": "sha512-kRGRZw3bLlFISDBgwTSA1TMBFN6J6GWDeubmDE3AF+3+yXL8hTWv8r5rkLbqYXY4RjPk/EzHnClI3zQf1cFmHA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 0.6"
|
||||
}
|
||||
},
|
||||
"node_modules/conventional-changelog": {
|
||||
"version": "3.1.25",
|
||||
"resolved": "https://registry.npmjs.org/conventional-changelog/-/conventional-changelog-3.1.25.tgz",
|
||||
@@ -19231,6 +19490,19 @@
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/is-port-reachable": {
|
||||
"version": "4.0.0",
|
||||
"resolved": "https://registry.npmjs.org/is-port-reachable/-/is-port-reachable-4.0.0.tgz",
|
||||
"integrity": "sha512-9UoipoxYmSk6Xy7QFgRv2HDyaysmgSG75TFQs6S+3pDM7ZhKTF/bskZV+0UlABHzKjNVhPjYCLfeZUEg1wXxig==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": "^12.20.0 || ^14.13.1 || >=16.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/is-stream": {
|
||||
"version": "2.0.1",
|
||||
"resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.1.tgz",
|
||||
@@ -24422,9 +24694,8 @@
|
||||
"version": "1.54.0",
|
||||
"resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.54.0.tgz",
|
||||
"integrity": "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ==",
|
||||
"devOptional": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">= 0.6"
|
||||
}
|
||||
@@ -25888,6 +26159,13 @@
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/path-is-inside": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/path-is-inside/-/path-is-inside-1.0.2.tgz",
|
||||
"integrity": "sha512-DUWJr3+ULp4zXmol/SZkFf3JGsS9/SIv+Y3Rt93/UjPpDpklB5f1er4O3POIbUuUJ3FXgqte2Q7SrU6zAqwk8w==",
|
||||
"dev": true,
|
||||
"license": "(WTFPL OR MIT)"
|
||||
},
|
||||
"node_modules/path-key": {
|
||||
"version": "3.1.1",
|
||||
"resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz",
|
||||
@@ -25929,6 +26207,13 @@
|
||||
"node": ">=16 || 14 >=14.17"
|
||||
}
|
||||
},
|
||||
"node_modules/path-to-regexp": {
|
||||
"version": "3.3.0",
|
||||
"resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-3.3.0.tgz",
|
||||
"integrity": "sha512-qyCH421YQPS2WFDxDjftfc1ZR5WKQzVzqsp4n9M2kQhVOo/ByahFoUNJfl58kOcEGfQ//7weFTDhm+ss8Ecxgw==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/path-type": {
|
||||
"version": "4.0.0",
|
||||
"resolved": "https://registry.npmjs.org/path-type/-/path-type-4.0.0.tgz",
|
||||
@@ -28094,6 +28379,30 @@
|
||||
"integrity": "sha512-CiD3ZSanZqcMPRhtfct5K9f7i3OLCcBBWsJjLh1gW9RO/nS94sVzY59iS+fgYBOBqaBpf4EzfqUF3j9IG+xo8A==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/registry-auth-token": {
|
||||
"version": "3.3.2",
|
||||
"resolved": "https://registry.npmjs.org/registry-auth-token/-/registry-auth-token-3.3.2.tgz",
|
||||
"integrity": "sha512-JL39c60XlzCVgNrO+qq68FoNb56w/m7JYvGR2jT5iR1xBrUA3Mfx5Twk5rqTThPmQKMWydGmq8oFtDlxfrmxnQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"rc": "^1.1.6",
|
||||
"safe-buffer": "^5.0.1"
|
||||
}
|
||||
},
|
||||
"node_modules/registry-url": {
|
||||
"version": "3.1.0",
|
||||
"resolved": "https://registry.npmjs.org/registry-url/-/registry-url-3.1.0.tgz",
|
||||
"integrity": "sha512-ZbgR5aZEdf4UKZVBPYIgaglBmSF2Hi94s2PcIHhRGFjKYu+chjJdYfHn4rt3hB6eCKLJ8giVIIfgMa1ehDfZKA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"rc": "^1.0.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/regjsgen": {
|
||||
"version": "0.8.0",
|
||||
"resolved": "https://registry.npmjs.org/regjsgen/-/regjsgen-0.8.0.tgz",
|
||||
@@ -29202,6 +29511,115 @@
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/serve": {
|
||||
"version": "14.2.4",
|
||||
"resolved": "https://registry.npmjs.org/serve/-/serve-14.2.4.tgz",
|
||||
"integrity": "sha512-qy1S34PJ/fcY8gjVGszDB3EXiPSk5FKhUa7tQe0UPRddxRidc2V6cNHPNewbE1D7MAkgLuWEt3Vw56vYy73tzQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@zeit/schemas": "2.36.0",
|
||||
"ajv": "8.12.0",
|
||||
"arg": "5.0.2",
|
||||
"boxen": "7.0.0",
|
||||
"chalk": "5.0.1",
|
||||
"chalk-template": "0.4.0",
|
||||
"clipboardy": "3.0.0",
|
||||
"compression": "1.7.4",
|
||||
"is-port-reachable": "4.0.0",
|
||||
"serve-handler": "6.1.6",
|
||||
"update-check": "1.5.4"
|
||||
},
|
||||
"bin": {
|
||||
"serve": "build/main.js"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 14"
|
||||
}
|
||||
},
|
||||
"node_modules/serve-handler": {
|
||||
"version": "6.1.6",
|
||||
"resolved": "https://registry.npmjs.org/serve-handler/-/serve-handler-6.1.6.tgz",
|
||||
"integrity": "sha512-x5RL9Y2p5+Sh3D38Fh9i/iQ5ZK+e4xuXRd/pGbM4D13tgo/MGwbttUk8emytcr1YYzBYs+apnUngBDFYfpjPuQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"bytes": "3.0.0",
|
||||
"content-disposition": "0.5.2",
|
||||
"mime-types": "2.1.18",
|
||||
"minimatch": "3.1.2",
|
||||
"path-is-inside": "1.0.2",
|
||||
"path-to-regexp": "3.3.0",
|
||||
"range-parser": "1.2.0"
|
||||
}
|
||||
},
|
||||
"node_modules/serve-handler/node_modules/brace-expansion": {
|
||||
"version": "1.1.12",
|
||||
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz",
|
||||
"integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"balanced-match": "^1.0.0",
|
||||
"concat-map": "0.0.1"
|
||||
}
|
||||
},
|
||||
"node_modules/serve-handler/node_modules/bytes": {
|
||||
"version": "3.0.0",
|
||||
"resolved": "https://registry.npmjs.org/bytes/-/bytes-3.0.0.tgz",
|
||||
"integrity": "sha512-pMhOfFDPiv9t5jjIXkHosWmkSyQbvsgEVNkz0ERHbuLh2T/7j4Mqqpz523Fe8MVY89KC6Sh/QfS2sM+SjgFDcw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 0.8"
|
||||
}
|
||||
},
|
||||
"node_modules/serve-handler/node_modules/mime-db": {
|
||||
"version": "1.33.0",
|
||||
"resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.33.0.tgz",
|
||||
"integrity": "sha512-BHJ/EKruNIqJf/QahvxwQZXKygOQ256myeN/Ew+THcAa5q+PjyTTMMeNQC4DZw5AwfvelsUrA6B67NKMqXDbzQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 0.6"
|
||||
}
|
||||
},
|
||||
"node_modules/serve-handler/node_modules/mime-types": {
|
||||
"version": "2.1.18",
|
||||
"resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.18.tgz",
|
||||
"integrity": "sha512-lc/aahn+t4/SWV/qcmumYjymLsWfN3ELhpmVuUFjgsORruuZPVSwAQryq+HHGvO/SI2KVX26bx+En+zhM8g8hQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"mime-db": "~1.33.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.6"
|
||||
}
|
||||
},
|
||||
"node_modules/serve-handler/node_modules/minimatch": {
|
||||
"version": "3.1.2",
|
||||
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz",
|
||||
"integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==",
|
||||
"dev": true,
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"brace-expansion": "^1.1.7"
|
||||
},
|
||||
"engines": {
|
||||
"node": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/serve-handler/node_modules/range-parser": {
|
||||
"version": "1.2.0",
|
||||
"resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.0.tgz",
|
||||
"integrity": "sha512-kA5WQoNVo4t9lNx2kQNFCxKeBl5IbbSNBl1M/tLkw9WCn+hxNBAW5Qh8gdhs63CJnhjJ2zQWFoqPJP2sK1AV5A==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 0.6"
|
||||
}
|
||||
},
|
||||
"node_modules/serve-static": {
|
||||
"version": "1.16.2",
|
||||
"resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.16.2.tgz",
|
||||
@@ -29333,6 +29751,106 @@
|
||||
"node": ">= 0.8"
|
||||
}
|
||||
},
|
||||
"node_modules/serve/node_modules/ajv": {
|
||||
"version": "8.12.0",
|
||||
"resolved": "https://registry.npmjs.org/ajv/-/ajv-8.12.0.tgz",
|
||||
"integrity": "sha512-sRu1kpcO9yLtYxBKvqfTeh9KzZEwO3STyX1HT+4CaDzC6HpTGYhIhPIzj9XuKU7KYDwnaeh5hcOwjy1QuJzBPA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"fast-deep-equal": "^3.1.1",
|
||||
"json-schema-traverse": "^1.0.0",
|
||||
"require-from-string": "^2.0.2",
|
||||
"uri-js": "^4.2.2"
|
||||
},
|
||||
"funding": {
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/epoberezkin"
|
||||
}
|
||||
},
|
||||
"node_modules/serve/node_modules/bytes": {
|
||||
"version": "3.0.0",
|
||||
"resolved": "https://registry.npmjs.org/bytes/-/bytes-3.0.0.tgz",
|
||||
"integrity": "sha512-pMhOfFDPiv9t5jjIXkHosWmkSyQbvsgEVNkz0ERHbuLh2T/7j4Mqqpz523Fe8MVY89KC6Sh/QfS2sM+SjgFDcw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 0.8"
|
||||
}
|
||||
},
|
||||
"node_modules/serve/node_modules/chalk": {
|
||||
"version": "5.0.1",
|
||||
"resolved": "https://registry.npmjs.org/chalk/-/chalk-5.0.1.tgz",
|
||||
"integrity": "sha512-Fo07WOYGqMfCWHOzSXOt2CxDbC6skS/jO9ynEcmpANMoPrD+W1r1K6Vx7iNm+AQmETU1Xr2t+n8nzkV9t6xh3w==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": "^12.17.0 || ^14.13 || >=16.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/chalk/chalk?sponsor=1"
|
||||
}
|
||||
},
|
||||
"node_modules/serve/node_modules/compression": {
|
||||
"version": "1.7.4",
|
||||
"resolved": "https://registry.npmjs.org/compression/-/compression-1.7.4.tgz",
|
||||
"integrity": "sha512-jaSIDzP9pZVS4ZfQ+TzvtiWhdpFhE2RDHz8QJkpX9SIpLq88VueF5jJw6t+6CUQcAoA6t+x89MLrWAqpfDE8iQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"accepts": "~1.3.5",
|
||||
"bytes": "3.0.0",
|
||||
"compressible": "~2.0.16",
|
||||
"debug": "2.6.9",
|
||||
"on-headers": "~1.0.2",
|
||||
"safe-buffer": "5.1.2",
|
||||
"vary": "~1.1.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.8.0"
|
||||
}
|
||||
},
|
||||
"node_modules/serve/node_modules/debug": {
|
||||
"version": "2.6.9",
|
||||
"resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz",
|
||||
"integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"ms": "2.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/serve/node_modules/json-schema-traverse": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz",
|
||||
"integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/serve/node_modules/ms": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz",
|
||||
"integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/serve/node_modules/on-headers": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/on-headers/-/on-headers-1.0.2.tgz",
|
||||
"integrity": "sha512-pZAE+FJLoyITytdqK0U5s+FIpjN0JP3OzFi/u8Rx+EV5/W+JTWGXG8xFzevE7AjBfDqHv/8vL8qQsIhHnqRkrA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 0.8"
|
||||
}
|
||||
},
|
||||
"node_modules/serve/node_modules/safe-buffer": {
|
||||
"version": "5.1.2",
|
||||
"resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz",
|
||||
"integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/set-blocking": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/set-blocking/-/set-blocking-2.0.0.tgz",
|
||||
@@ -31662,6 +32180,17 @@
|
||||
"browserslist": ">= 4.21.0"
|
||||
}
|
||||
},
|
||||
"node_modules/update-check": {
|
||||
"version": "1.5.4",
|
||||
"resolved": "https://registry.npmjs.org/update-check/-/update-check-1.5.4.tgz",
|
||||
"integrity": "sha512-5YHsflzHP4t1G+8WGPlvKbJEbAJGCgw+Em+dGR1KmBUbr1J36SJBqlHLjR7oob7sco5hWHGQVcr9B2poIVDDTQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"registry-auth-token": "3.3.2",
|
||||
"registry-url": "3.1.0"
|
||||
}
|
||||
},
|
||||
"node_modules/uri-js": {
|
||||
"version": "4.4.1",
|
||||
"resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz",
|
||||
@@ -31770,9 +32299,8 @@
|
||||
"version": "1.1.2",
|
||||
"resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz",
|
||||
"integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==",
|
||||
"devOptional": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">= 0.8"
|
||||
}
|
||||
@@ -32624,6 +33152,76 @@
|
||||
"string-width": "^1.0.2 || 2 || 3 || 4"
|
||||
}
|
||||
},
|
||||
"node_modules/widest-line": {
|
||||
"version": "4.0.1",
|
||||
"resolved": "https://registry.npmjs.org/widest-line/-/widest-line-4.0.1.tgz",
|
||||
"integrity": "sha512-o0cyEG0e8GPzT4iGHphIOh0cJOV8fivsXxddQasHPHfoZf1ZexrfeA21w2NaEN1RHE+fXlfISmOE8R9N3u3Qig==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"string-width": "^5.0.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/widest-line/node_modules/ansi-regex": {
|
||||
"version": "6.2.0",
|
||||
"resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.2.0.tgz",
|
||||
"integrity": "sha512-TKY5pyBkHyADOPYlRT9Lx6F544mPl0vS5Ew7BJ45hA08Q+t3GjbueLliBWN3sMICk6+y7HdyxSzC4bWS8baBdg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/chalk/ansi-regex?sponsor=1"
|
||||
}
|
||||
},
|
||||
"node_modules/widest-line/node_modules/emoji-regex": {
|
||||
"version": "9.2.2",
|
||||
"resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz",
|
||||
"integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/widest-line/node_modules/string-width": {
|
||||
"version": "5.1.2",
|
||||
"resolved": "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz",
|
||||
"integrity": "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"eastasianwidth": "^0.2.0",
|
||||
"emoji-regex": "^9.2.2",
|
||||
"strip-ansi": "^7.0.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/widest-line/node_modules/strip-ansi": {
|
||||
"version": "7.1.0",
|
||||
"resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.0.tgz",
|
||||
"integrity": "sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"ansi-regex": "^6.0.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/chalk/strip-ansi?sponsor=1"
|
||||
}
|
||||
},
|
||||
"node_modules/wonka": {
|
||||
"version": "6.3.5",
|
||||
"resolved": "https://registry.npmjs.org/wonka/-/wonka-6.3.5.tgz",
|
||||
|
||||
@@ -136,11 +136,7 @@
|
||||
"*.{js,ts,vue,css,json,yml,yaml}": "eslint --fix || true",
|
||||
"*.{md,markdown,mdc}": "markdownlint-cli2 --fix"
|
||||
},
|
||||
"commitlint": {
|
||||
"extends": [
|
||||
"@commitlint/config-conventional"
|
||||
]
|
||||
},
|
||||
|
||||
"dependencies": {
|
||||
"@capacitor-community/electron": "^5.0.1",
|
||||
"@capacitor-community/sqlite": "6.0.2",
|
||||
@@ -149,10 +145,12 @@
|
||||
"@capacitor/app": "^6.0.0",
|
||||
"@capacitor/camera": "^6.0.0",
|
||||
"@capacitor/cli": "^6.2.0",
|
||||
"@capacitor/clipboard": "^6.0.2",
|
||||
"@capacitor/core": "^6.2.0",
|
||||
"@capacitor/filesystem": "^6.0.0",
|
||||
"@capacitor/ios": "^6.2.0",
|
||||
"@capacitor/share": "^6.0.3",
|
||||
"@capacitor/status-bar": "^6.0.2",
|
||||
"@capawesome/capacitor-file-picker": "^6.2.0",
|
||||
"@dicebear/collection": "^5.4.1",
|
||||
"@dicebear/core": "^5.4.1",
|
||||
@@ -270,6 +268,7 @@
|
||||
"postcss": "^8.4.38",
|
||||
"prettier": "^3.2.5",
|
||||
"rimraf": "^6.0.1",
|
||||
"serve": "^14.2.4",
|
||||
"tailwindcss": "^3.4.1",
|
||||
"ts-jest": "^29.4.0",
|
||||
"tsx": "^4.20.4",
|
||||
|
||||
@@ -21,7 +21,7 @@ export default defineConfig({
|
||||
/* Retry on CI only */
|
||||
retries: process.env.CI ? 2 : 0,
|
||||
/* Opt out of parallel tests on CI. */
|
||||
workers: 1,
|
||||
workers: 4,
|
||||
/* Reporter to use. See https://playwright.dev/docs/test-reporters */
|
||||
reporter: [
|
||||
['list'],
|
||||
|
||||
35
src/App.vue
35
src/App.vue
@@ -4,7 +4,7 @@
|
||||
<!-- Messages in the upper-right - https://github.com/emmanuelsw/notiwind -->
|
||||
<NotificationGroup group="alert">
|
||||
<div
|
||||
class="fixed z-[90] top-[max(1rem,env(safe-area-inset-top))] right-4 left-4 sm:left-auto sm:w-full sm:max-w-sm flex flex-col items-start justify-end"
|
||||
class="fixed z-[120] top-[max(1rem,env(safe-area-inset-top),var(--safe-area-inset-top,0px))] right-4 left-4 sm:left-auto sm:w-full sm:max-w-sm flex flex-col items-start justify-end"
|
||||
>
|
||||
<Notification
|
||||
v-slot="{ notifications, close }"
|
||||
@@ -175,7 +175,9 @@
|
||||
"-permission", "-mute", "-off"
|
||||
-->
|
||||
<NotificationGroup group="modal">
|
||||
<div class="fixed z-[100] top-[env(safe-area-inset-top)] inset-x-0 w-full">
|
||||
<div
|
||||
class="fixed z-[100] top-[max(env(safe-area-inset-top),var(--safe-area-inset-top,0px))] inset-x-0 w-full"
|
||||
>
|
||||
<Notification
|
||||
v-slot="{ notifications, close }"
|
||||
enter="transform ease-out duration-300 transition"
|
||||
@@ -506,13 +508,32 @@ export default class App extends Vue {
|
||||
|
||||
<style>
|
||||
#Content {
|
||||
padding-left: max(1.5rem, env(safe-area-inset-left));
|
||||
padding-right: max(1.5rem, env(safe-area-inset-right));
|
||||
padding-top: max(1.5rem, env(safe-area-inset-top));
|
||||
padding-bottom: max(1.5rem, env(safe-area-inset-bottom));
|
||||
padding-left: max(
|
||||
1.5rem,
|
||||
env(safe-area-inset-left),
|
||||
var(--safe-area-inset-left, 0px)
|
||||
);
|
||||
padding-right: max(
|
||||
1.5rem,
|
||||
env(safe-area-inset-right),
|
||||
var(--safe-area-inset-right, 0px)
|
||||
);
|
||||
padding-top: max(
|
||||
1.5rem,
|
||||
env(safe-area-inset-top),
|
||||
var(--safe-area-inset-top, 0px)
|
||||
);
|
||||
padding-bottom: max(
|
||||
1.5rem,
|
||||
env(safe-area-inset-bottom),
|
||||
var(--safe-area-inset-bottom, 0px)
|
||||
);
|
||||
}
|
||||
|
||||
#QuickNav ~ #Content {
|
||||
padding-bottom: calc(env(safe-area-inset-bottom) + 6.333rem);
|
||||
padding-bottom: calc(
|
||||
max(env(safe-area-inset-bottom), var(--safe-area-inset-bottom, 0px)) +
|
||||
6.333rem
|
||||
);
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -14,4 +14,12 @@
|
||||
transform: translateX(100%);
|
||||
background-color: #FFF !important;
|
||||
}
|
||||
|
||||
.dialog-overlay {
|
||||
@apply z-[100] fixed inset-0 bg-black/50 flex justify-center items-center p-6;
|
||||
}
|
||||
|
||||
.dialog {
|
||||
@apply bg-white p-4 rounded-lg w-full max-w-lg;
|
||||
}
|
||||
}
|
||||
@@ -1,7 +1,7 @@
|
||||
<!-- similar to UserNameDialog -->
|
||||
<template>
|
||||
<div v-if="visible" :class="overlayClasses">
|
||||
<div :class="dialogClasses">
|
||||
<div v-if="visible" class="dialog-overlay">
|
||||
<div class="dialog">
|
||||
<h1 :class="titleClasses">{{ title }}</h1>
|
||||
{{ message }}
|
||||
Note that their name is only stored on this device.
|
||||
@@ -61,20 +61,6 @@ export default class ContactNameDialog extends Vue {
|
||||
title = "Contact Name";
|
||||
visible = false;
|
||||
|
||||
/**
|
||||
* CSS classes for the modal overlay backdrop
|
||||
*/
|
||||
get overlayClasses(): string {
|
||||
return "z-index-50 fixed top-0 left-0 right-0 bottom-0 bg-black/50 flex justify-center items-center p-6";
|
||||
}
|
||||
|
||||
/**
|
||||
* CSS classes for the modal dialog container
|
||||
*/
|
||||
get dialogClasses(): string {
|
||||
return "bg-white p-4 rounded-lg w-full max-w-[500px]";
|
||||
}
|
||||
|
||||
/**
|
||||
* CSS classes for the dialog title
|
||||
*/
|
||||
|
||||
@@ -22,7 +22,7 @@ projects, and special entities with selection. * * @author Matthew Raymer */
|
||||
<!-- "Unnamed" entity -->
|
||||
<SpecialEntityCard
|
||||
entity-type="unnamed"
|
||||
label="Unnamed"
|
||||
:label="unnamedEntityName"
|
||||
icon="circle-question"
|
||||
:entity-data="unnamedEntityData"
|
||||
:notify="notify"
|
||||
@@ -83,6 +83,7 @@ import ShowAllCard from "./ShowAllCard.vue";
|
||||
import { Contact } from "../db/tables/contacts";
|
||||
import { PlanData } from "../interfaces/records";
|
||||
import { NotificationIface } from "../constants/app";
|
||||
import { UNNAMED_ENTITY_NAME } from "@/constants/entities";
|
||||
|
||||
/**
|
||||
* EntityGrid - Unified grid layout for displaying people or projects
|
||||
@@ -159,6 +160,10 @@ export default class EntityGrid extends Vue {
|
||||
@Prop({ default: "other party" })
|
||||
conflictContext!: string;
|
||||
|
||||
/** Whether to hide the "Show All" navigation */
|
||||
@Prop({ default: false })
|
||||
hideShowAll!: boolean;
|
||||
|
||||
/**
|
||||
* Function to determine which entities to display (allows parent control)
|
||||
*
|
||||
@@ -245,7 +250,9 @@ export default class EntityGrid extends Vue {
|
||||
* Whether to show the "Show All" navigation
|
||||
*/
|
||||
get shouldShowAll(): boolean {
|
||||
return this.entities.length > 0 && this.showAllRoute !== "";
|
||||
return (
|
||||
!this.hideShowAll && this.entities.length > 0 && this.showAllRoute !== ""
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -271,10 +278,17 @@ export default class EntityGrid extends Vue {
|
||||
get unnamedEntityData(): { did: string; name: string } {
|
||||
return {
|
||||
did: "",
|
||||
name: "Unnamed",
|
||||
name: UNNAMED_ENTITY_NAME,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the unnamed entity name constant
|
||||
*/
|
||||
get unnamedEntityName(): string {
|
||||
return UNNAMED_ENTITY_NAME;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if a person DID is conflicted
|
||||
*/
|
||||
@@ -304,16 +318,13 @@ export default class EntityGrid extends Vue {
|
||||
|
||||
/**
|
||||
* Handle special entity selection from SpecialEntityCard
|
||||
* Treat "You" and "Unnamed" as person entities
|
||||
*/
|
||||
handleEntitySelected(event: {
|
||||
type: string;
|
||||
entityType: string;
|
||||
data: { did?: string; name: string };
|
||||
}): void {
|
||||
handleEntitySelected(event: { data: { did?: string; name: string } }): void {
|
||||
// Convert special entities to person entities since they represent people
|
||||
this.emitEntitySelected({
|
||||
type: "special",
|
||||
entityType: event.entityType,
|
||||
data: event.data,
|
||||
type: "person",
|
||||
data: event.data as Contact,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -321,13 +332,11 @@ export default class EntityGrid extends Vue {
|
||||
|
||||
@Emit("entity-selected")
|
||||
emitEntitySelected(data: {
|
||||
type: "person" | "project" | "special";
|
||||
entityType?: string;
|
||||
data: Contact | PlanData | { did?: string; name: string };
|
||||
type: "person" | "project";
|
||||
data: Contact | PlanData;
|
||||
}): {
|
||||
type: "person" | "project" | "special";
|
||||
entityType?: string;
|
||||
data: Contact | PlanData | { did?: string; name: string };
|
||||
type: "person" | "project";
|
||||
data: Contact | PlanData;
|
||||
} {
|
||||
return data;
|
||||
}
|
||||
|
||||
@@ -27,6 +27,7 @@ Matthew Raymer */
|
||||
:show-all-query-params="showAllQueryParams"
|
||||
:notify="notify"
|
||||
:conflict-context="conflictContext"
|
||||
:hide-show-all="hideShowAll"
|
||||
@entity-selected="handleEntitySelected"
|
||||
/>
|
||||
|
||||
@@ -55,9 +56,8 @@ interface EntityData {
|
||||
* Entity selection event data structure
|
||||
*/
|
||||
interface EntitySelectionEvent {
|
||||
type: "person" | "project" | "special";
|
||||
entityType?: string;
|
||||
data: Contact | PlanData | EntityData;
|
||||
type: "person" | "project";
|
||||
data: Contact | PlanData;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -154,6 +154,10 @@ export default class EntitySelectionStep extends Vue {
|
||||
@Prop()
|
||||
notify?: (notification: NotificationIface, timeout?: number) => void;
|
||||
|
||||
/** Whether to hide the "Show All" navigation */
|
||||
@Prop({ default: false })
|
||||
hideShowAll!: boolean;
|
||||
|
||||
/**
|
||||
* CSS classes for the cancel button
|
||||
*/
|
||||
|
||||
@@ -42,8 +42,8 @@ computed CSS properties * * @author Matthew Raymer */
|
||||
<p class="text-xs text-slate-500 leading-1 -mb-1 uppercase">
|
||||
{{ label }}
|
||||
</p>
|
||||
<h3 class="font-semibold truncate">
|
||||
{{ entity?.name || "Unnamed" }}
|
||||
<h3 :class="nameClasses">
|
||||
{{ displayName }}
|
||||
</h3>
|
||||
</div>
|
||||
|
||||
@@ -62,6 +62,7 @@ import { Component, Prop, Vue } from "vue-facing-decorator";
|
||||
import EntityIcon from "./EntityIcon.vue";
|
||||
import ProjectIcon from "./ProjectIcon.vue";
|
||||
import { Contact } from "../db/tables/contacts";
|
||||
import { UNNAMED_ENTITY_NAME } from "@/constants/entities";
|
||||
|
||||
/**
|
||||
* Entity interface for both person and project entities
|
||||
@@ -138,6 +139,38 @@ export default class EntitySummaryButton extends Vue {
|
||||
return this.editable ? "text-blue-500" : "text-slate-400";
|
||||
}
|
||||
|
||||
/**
|
||||
* Computed CSS classes for the entity name
|
||||
*/
|
||||
get nameClasses(): string {
|
||||
const baseClasses = "font-semibold truncate";
|
||||
|
||||
// Add italic styling for special "Unnamed" or entities without set names
|
||||
if (!this.entity?.name || this.entity?.did === "") {
|
||||
return `${baseClasses} italic text-slate-500`;
|
||||
}
|
||||
|
||||
return baseClasses;
|
||||
}
|
||||
|
||||
/**
|
||||
* Computed display name for the entity
|
||||
*/
|
||||
get displayName(): string {
|
||||
// If the entity has a set name, use that name
|
||||
if (this.entity?.name) {
|
||||
return this.entity.name;
|
||||
}
|
||||
|
||||
// If the entity is the special "Unnamed", use "Unnamed"
|
||||
if (this.entity?.did === "") {
|
||||
return UNNAMED_ENTITY_NAME;
|
||||
}
|
||||
|
||||
// If the entity does not have a set name, but is not the special "Unnamed", use their DID
|
||||
return this.entity?.did;
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle click event - only call function prop if editable
|
||||
* Allows parent to control edit behavior and validation
|
||||
|
||||
@@ -212,30 +212,7 @@ export default class FeedFilters extends Vue {
|
||||
</script>
|
||||
|
||||
<style>
|
||||
.dialog-overlay {
|
||||
z-index: 50;
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
background-color: rgba(0, 0, 0, 0.5);
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
padding: 1.5rem;
|
||||
}
|
||||
|
||||
#dialogFeedFilters.dialog-overlay {
|
||||
z-index: 100;
|
||||
overflow: scroll;
|
||||
}
|
||||
|
||||
.dialog {
|
||||
background-color: white;
|
||||
padding: 1rem;
|
||||
border-radius: 0.5rem;
|
||||
width: 100%;
|
||||
max-width: 500px;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -29,6 +29,7 @@
|
||||
:unit-code="unitCode"
|
||||
:offer-id="offerId"
|
||||
:notify="$notify"
|
||||
:hide-show-all="hideShowAll"
|
||||
@entity-selected="handleEntitySelected"
|
||||
@cancel="cancel"
|
||||
/>
|
||||
@@ -87,6 +88,7 @@ import {
|
||||
NOTIFY_GIFTED_DETAILS_NO_IDENTIFIER,
|
||||
NOTIFY_GIFTED_DETAILS_RECORDING_GIVE,
|
||||
} from "@/constants/notifications";
|
||||
import { UNNAMED_ENTITY_NAME } from "@/constants/entities";
|
||||
|
||||
@Component({
|
||||
components: {
|
||||
@@ -114,6 +116,7 @@ export default class GiftedDialog extends Vue {
|
||||
@Prop() fromProjectId = "";
|
||||
@Prop() toProjectId = "";
|
||||
@Prop() isFromProjectView = false;
|
||||
@Prop() hideShowAll = false;
|
||||
@Prop({ default: "person" }) giverEntityType = "person" as
|
||||
| "person"
|
||||
| "project";
|
||||
@@ -218,21 +221,15 @@ export default class GiftedDialog extends Vue {
|
||||
try {
|
||||
const settings = await this.$settings();
|
||||
this.apiServer = settings.apiServer || "";
|
||||
this.activeDid = settings.activeDid || "";
|
||||
// Use new façade method with legacy fallback
|
||||
const retrievedActiveDid = await this.$getActiveDid();
|
||||
this.activeDid = retrievedActiveDid || "";
|
||||
logger.debug("[GiftedDialog] Set activeDid from new system:", this.activeDid);
|
||||
|
||||
this.allContacts = await this.$contacts();
|
||||
|
||||
this.allMyDids = await retrieveAccountDids();
|
||||
|
||||
if (this.giver && !this.giver.name) {
|
||||
this.giver.name = didInfo(
|
||||
this.giver.did,
|
||||
this.activeDid,
|
||||
this.allMyDids,
|
||||
this.allContacts,
|
||||
);
|
||||
}
|
||||
|
||||
if (
|
||||
this.giverEntityType === "project" ||
|
||||
this.recipientEntityType === "project"
|
||||
@@ -292,7 +289,9 @@ export default class GiftedDialog extends Vue {
|
||||
}
|
||||
|
||||
async confirm() {
|
||||
logger.debug("[GiftedDialog] confirm() called with activeDid:", this.activeDid);
|
||||
if (!this.activeDid) {
|
||||
logger.error("[GiftedDialog] Validation failed - activeDid is empty/null:", this.activeDid);
|
||||
this.safeNotify.error(
|
||||
NOTIFY_GIFTED_DETAILS_NO_IDENTIFIER.message,
|
||||
TIMEOUTS.SHORT,
|
||||
@@ -455,14 +454,14 @@ export default class GiftedDialog extends Vue {
|
||||
if (contact) {
|
||||
this.giver = {
|
||||
did: contact.did,
|
||||
name: contact.name || contact.did,
|
||||
name: contact.name,
|
||||
};
|
||||
} else {
|
||||
// Only set to "Unnamed" if no giver is currently set
|
||||
if (!this.giver || !this.giver.did) {
|
||||
this.giver = {
|
||||
did: "",
|
||||
name: "Unnamed",
|
||||
name: UNNAMED_ENTITY_NAME,
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -517,14 +516,14 @@ export default class GiftedDialog extends Vue {
|
||||
if (contact) {
|
||||
this.receiver = {
|
||||
did: contact.did,
|
||||
name: contact.name || contact.did,
|
||||
name: contact.name,
|
||||
};
|
||||
} else {
|
||||
// Only set to "Unnamed" if no receiver is currently set
|
||||
if (!this.receiver || !this.receiver.did) {
|
||||
this.receiver = {
|
||||
did: "",
|
||||
name: "Unnamed",
|
||||
name: UNNAMED_ENTITY_NAME,
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -566,20 +565,21 @@ export default class GiftedDialog extends Vue {
|
||||
|
||||
/**
|
||||
* Handle entity selection from EntitySelectionStep
|
||||
* @param entity - The selected entity (person, project, or special) with stepType
|
||||
* @param entity - The selected entity (person or project) with stepType
|
||||
*/
|
||||
handleEntitySelected(entity: {
|
||||
type: "person" | "project" | "special";
|
||||
entityType?: string;
|
||||
data: Contact | PlanData | { did?: string; name: string };
|
||||
type: "person" | "project";
|
||||
data: Contact | PlanData;
|
||||
stepType: string;
|
||||
}) {
|
||||
if (entity.type === "person") {
|
||||
const contact = entity.data as Contact;
|
||||
// Apply DID-based logic for person entities
|
||||
const processedContact = this.processPersonEntity(contact);
|
||||
if (entity.stepType === "giver") {
|
||||
this.selectGiver(contact);
|
||||
this.selectGiver(processedContact);
|
||||
} else {
|
||||
this.selectRecipient(contact);
|
||||
this.selectRecipient(processedContact);
|
||||
}
|
||||
} else if (entity.type === "project") {
|
||||
const project = entity.data as PlanData;
|
||||
@@ -588,33 +588,22 @@ export default class GiftedDialog extends Vue {
|
||||
} else {
|
||||
this.selectRecipientProject(project);
|
||||
}
|
||||
} else if (entity.type === "special") {
|
||||
// Handle special entities like "You" and "Unnamed"
|
||||
if (entity.entityType === "you") {
|
||||
// "You" entity selected
|
||||
const youEntity = {
|
||||
did: this.activeDid,
|
||||
name: "You",
|
||||
};
|
||||
if (entity.stepType === "giver") {
|
||||
this.giver = youEntity;
|
||||
} else {
|
||||
this.receiver = youEntity;
|
||||
}
|
||||
this.firstStep = false;
|
||||
} else if (entity.entityType === "unnamed") {
|
||||
// "Unnamed" entity selected
|
||||
const unnamedEntity = {
|
||||
did: "",
|
||||
name: "Unnamed",
|
||||
};
|
||||
if (entity.stepType === "giver") {
|
||||
this.giver = unnamedEntity;
|
||||
} else {
|
||||
this.receiver = unnamedEntity;
|
||||
}
|
||||
this.firstStep = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Processes person entities using DID-based logic for "You" and "Unnamed"
|
||||
*/
|
||||
private processPersonEntity(contact: Contact): Contact {
|
||||
if (contact.did === this.activeDid) {
|
||||
// If DID matches active DID, create "You" entity
|
||||
return { ...contact, name: "You" };
|
||||
} else if (!contact.did || contact.did === "") {
|
||||
// If DID is empty/null, create "Unnamed" entity
|
||||
return { ...contact, name: UNNAMED_ENTITY_NAME };
|
||||
} else {
|
||||
// Return the contact as-is
|
||||
return contact;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -665,27 +654,3 @@ export default class GiftedDialog extends Vue {
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style>
|
||||
.dialog-overlay {
|
||||
z-index: 50;
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
background-color: rgba(0, 0, 0, 0.5);
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
padding: 1.5rem;
|
||||
}
|
||||
|
||||
.dialog {
|
||||
background-color: white;
|
||||
padding: 1rem;
|
||||
border-radius: 0.5rem;
|
||||
width: 100%;
|
||||
max-width: 500px;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -291,27 +291,3 @@ export default class GivenPrompts extends Vue {
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style>
|
||||
.dialog-overlay {
|
||||
z-index: 50;
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
background-color: rgba(0, 0, 0, 0.5);
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
padding: 1.5rem;
|
||||
}
|
||||
|
||||
.dialog {
|
||||
background-color: white;
|
||||
padding: 1rem;
|
||||
border-radius: 0.5rem;
|
||||
width: 100%;
|
||||
max-width: 500px;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -1,9 +1,6 @@
|
||||
<template>
|
||||
<div
|
||||
v-if="isOpen"
|
||||
class="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50"
|
||||
>
|
||||
<div class="bg-white rounded-lg p-6 max-w-2xl w-full mx-4">
|
||||
<div v-if="isOpen" class="dialog-overlay">
|
||||
<div class="dialog">
|
||||
<!-- Header -->
|
||||
<div class="flex justify-between items-center mb-4">
|
||||
<h2 class="text-xl font-bold capitalize">{{ roleName }} Details</h2>
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<template>
|
||||
<div v-if="visible" class="dialog-overlay z-[60]">
|
||||
<div v-if="visible" class="dialog-overlay">
|
||||
<div class="dialog relative">
|
||||
<div class="text-lg text-center font-bold relative">
|
||||
<h1 id="ViewHeading" class="text-center font-bold">
|
||||
@@ -931,32 +931,6 @@ export default class ImageMethodDialog extends Vue {
|
||||
</script>
|
||||
|
||||
<style>
|
||||
.dialog-overlay {
|
||||
z-index: 50;
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
background-color: rgba(0, 0, 0, 0.5);
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
padding: 1.5rem;
|
||||
}
|
||||
|
||||
.dialog {
|
||||
background-color: white;
|
||||
padding: 1rem;
|
||||
border-radius: 0.5rem;
|
||||
width: 100%;
|
||||
max-width: 700px;
|
||||
max-height: 90vh;
|
||||
overflow: hidden;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
/* Add styles for diagnostic panel */
|
||||
.diagnostic-panel {
|
||||
font-family: monospace;
|
||||
|
||||
@@ -93,27 +93,3 @@ export default class InviteDialog extends Vue {
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style>
|
||||
.dialog-overlay {
|
||||
z-index: 50;
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
background-color: rgba(0, 0, 0, 0.5);
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
padding: 1.5rem;
|
||||
}
|
||||
|
||||
.dialog {
|
||||
background-color: white;
|
||||
padding: 1rem;
|
||||
border-radius: 0.5rem;
|
||||
width: 100%;
|
||||
max-width: 500px;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -81,7 +81,7 @@
|
||||
<div class="flex items-center justify-between">
|
||||
<div class="flex items-center">
|
||||
<h3 class="text-lg font-medium">
|
||||
{{ member.name || "Unnamed Member" }}
|
||||
{{ member.name || unnamedMember }}
|
||||
</h3>
|
||||
<div
|
||||
v-if="!getContactFor(member.did) && member.did !== activeDid"
|
||||
@@ -177,6 +177,7 @@ import {
|
||||
NOTIFY_ADD_CONTACT_FIRST,
|
||||
NOTIFY_CONTINUE_WITHOUT_ADDING,
|
||||
} from "@/constants/notifications";
|
||||
import { SOMEONE_UNNAMED } from "@/constants/entities";
|
||||
|
||||
interface Member {
|
||||
admitted: boolean;
|
||||
@@ -220,6 +221,13 @@ export default class MembersList extends Vue {
|
||||
apiServer = "";
|
||||
contacts: Array<Contact> = [];
|
||||
|
||||
/**
|
||||
* Get the unnamed member constant
|
||||
*/
|
||||
get unnamedMember(): string {
|
||||
return SOMEONE_UNNAMED;
|
||||
}
|
||||
|
||||
async created() {
|
||||
this.notify = createNotifyHelpers(this.$notify);
|
||||
|
||||
|
||||
@@ -175,7 +175,8 @@ export default class OfferDialog extends Vue {
|
||||
|
||||
const settings = await this.$accountSettings();
|
||||
this.apiServer = settings.apiServer || "";
|
||||
this.activeDid = settings.activeDid || "";
|
||||
// Use new façade method with legacy fallback
|
||||
this.activeDid = (await this.$getActiveDid()) || "";
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
} catch (err: any) {
|
||||
@@ -312,28 +313,3 @@ export default class OfferDialog extends Vue {
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.dialog-overlay {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background: rgba(0, 0, 0, 0.5);
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
z-index: 50;
|
||||
}
|
||||
|
||||
.dialog {
|
||||
background: white;
|
||||
padding: 1.5rem;
|
||||
border-radius: 0.5rem;
|
||||
max-width: 500px;
|
||||
width: 90%;
|
||||
max-height: 90vh;
|
||||
overflow-y: auto;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -270,7 +270,8 @@ export default class OnboardingDialog extends Vue {
|
||||
async open(page: OnboardPage) {
|
||||
this.page = page;
|
||||
const settings = await this.$accountSettings();
|
||||
this.activeDid = settings.activeDid || "";
|
||||
// Use new façade method with legacy fallback
|
||||
this.activeDid = (await this.$getActiveDid()) || "";
|
||||
this.isRegistered = !!settings.isRegistered;
|
||||
|
||||
const contacts = await this.$getAllContacts();
|
||||
@@ -307,27 +308,3 @@ export default class OnboardingDialog extends Vue {
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style>
|
||||
.dialog-overlay {
|
||||
z-index: 40;
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
background-color: rgba(0, 0, 0, 0.5);
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
padding: 1.5rem;
|
||||
}
|
||||
|
||||
.dialog {
|
||||
background-color: white;
|
||||
padding: 1rem;
|
||||
border-radius: 0.5rem;
|
||||
width: 100%;
|
||||
max-width: 500px;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -25,7 +25,7 @@ conflict detection. * * @author Matthew Raymer */
|
||||
</div>
|
||||
|
||||
<h3 :class="nameClasses">
|
||||
{{ person.name || person.did || "Unnamed" }}
|
||||
{{ displayName }}
|
||||
</h3>
|
||||
</li>
|
||||
</template>
|
||||
@@ -98,9 +98,27 @@ export default class PersonCard extends Vue {
|
||||
return `${baseClasses} text-slate-400`;
|
||||
}
|
||||
|
||||
// Add italic styling for entities without set names
|
||||
if (!this.person.name) {
|
||||
return `${baseClasses} italic text-slate-500`;
|
||||
}
|
||||
|
||||
return baseClasses;
|
||||
}
|
||||
|
||||
/**
|
||||
* Computed display name for the person
|
||||
*/
|
||||
get displayName(): string {
|
||||
// If the entity has a set name, use that name
|
||||
if (this.person.name) {
|
||||
return this.person.name;
|
||||
}
|
||||
|
||||
// If the entity does not have a set name
|
||||
return this.person.did;
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle card click - emit if selectable and not conflicted, show warning if conflicted
|
||||
*/
|
||||
@@ -114,7 +132,7 @@ export default class PersonCard extends Vue {
|
||||
group: "alert",
|
||||
type: "warning",
|
||||
title: "Cannot Select",
|
||||
text: `You cannot select "${this.person.name || this.person.did || "Unnamed"}" because they are already selected as the ${this.conflictContext}.`,
|
||||
text: `You cannot select "${this.displayName}" because they are already selected as the ${this.conflictContext}.`,
|
||||
},
|
||||
3000,
|
||||
);
|
||||
|
||||
@@ -10,7 +10,7 @@ Comprehensive error handling * * @author Matthew Raymer * @version 1.0.0 * @file
|
||||
PhotoDialog.vue */
|
||||
|
||||
<template>
|
||||
<div v-if="visible" class="dialog-overlay z-[60]">
|
||||
<div v-if="visible" class="dialog-overlay">
|
||||
<div class="dialog relative">
|
||||
<div class="text-lg text-center font-light relative z-50">
|
||||
<div id="ViewHeading" :class="headingClasses">
|
||||
@@ -628,34 +628,6 @@ export default class PhotoDialog extends Vue {
|
||||
</script>
|
||||
|
||||
<style>
|
||||
/* Dialog overlay styling */
|
||||
.dialog-overlay {
|
||||
z-index: 60;
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
background-color: rgba(0, 0, 0, 0.5);
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
padding: 1.5rem;
|
||||
}
|
||||
|
||||
/* Dialog container styling */
|
||||
.dialog {
|
||||
background-color: white;
|
||||
padding: 1rem;
|
||||
border-radius: 0.5rem;
|
||||
width: 100%;
|
||||
max-width: 700px;
|
||||
max-height: 90vh;
|
||||
overflow: hidden;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
/* Camera preview styling */
|
||||
.camera-preview {
|
||||
flex: 1;
|
||||
|
||||
@@ -15,7 +15,7 @@ issuer information. * * @author Matthew Raymer */
|
||||
<h3
|
||||
class="text-xs font-medium text-ellipsis whitespace-nowrap overflow-hidden"
|
||||
>
|
||||
{{ project.name || "Unnamed Project" }}
|
||||
{{ project.name || unnamedProject }}
|
||||
</h3>
|
||||
|
||||
<div class="text-xs text-slate-500 truncate">
|
||||
@@ -31,6 +31,7 @@ import ProjectIcon from "./ProjectIcon.vue";
|
||||
import { PlanData } from "../interfaces/records";
|
||||
import { Contact } from "../db/tables/contacts";
|
||||
import { didInfo } from "../libs/endorserServer";
|
||||
import { UNNAMED_PROJECT } from "@/constants/entities";
|
||||
|
||||
/**
|
||||
* ProjectCard - Displays a project entity with selection capability
|
||||
@@ -63,6 +64,13 @@ export default class ProjectCard extends Vue {
|
||||
@Prop({ required: true })
|
||||
allContacts!: Contact[];
|
||||
|
||||
/**
|
||||
* Get the unnamed project constant
|
||||
*/
|
||||
get unnamedProject(): string {
|
||||
return UNNAMED_PROJECT;
|
||||
}
|
||||
|
||||
/**
|
||||
* Computed display name for the project issuer
|
||||
*/
|
||||
|
||||
@@ -115,6 +115,7 @@ import { urlBase64ToUint8Array } from "../libs/crypto/vc/util";
|
||||
import * as libsUtil from "../libs/util";
|
||||
import { logger } from "../utils/logger";
|
||||
import { PlatformServiceMixin } from "../utils/PlatformServiceMixin";
|
||||
import { UNNAMED_ENTITY_NAME } from "@/constants/entities";
|
||||
|
||||
// Example interface for error
|
||||
interface ErrorResponse {
|
||||
@@ -602,7 +603,7 @@ export default class PushNotificationPermission extends Vue {
|
||||
* Returns the default message for direct push
|
||||
*/
|
||||
get notificationMessagePlaceholder(): string {
|
||||
return "Click to share some gratitude with the world -- even if they're unnamed.";
|
||||
return `Click to share some gratitude with the world -- even if they're ${UNNAMED_ENTITY_NAME.toLowerCase()}.`;
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
<!-- QUICK NAV -->
|
||||
<nav
|
||||
id="QuickNav"
|
||||
class="fixed bottom-0 left-0 right-0 bg-slate-200 z-50 pb-[env(safe-area-inset-bottom)]"
|
||||
class="fixed bottom-0 left-0 right-0 bg-slate-200 z-50 pb-[max(env(safe-area-inset-bottom),var(--safe-area-inset-bottom,0px))]"
|
||||
>
|
||||
<ul class="flex text-2xl px-6 py-2 gap-1 max-w-3xl mx-auto">
|
||||
<!-- Home Feed -->
|
||||
|
||||
@@ -124,8 +124,6 @@ export default class SpecialEntityCard extends Vue {
|
||||
handleClick(): void {
|
||||
if (this.selectable && !this.conflicted) {
|
||||
this.emitEntitySelected({
|
||||
type: "special",
|
||||
entityType: this.entityType,
|
||||
data: this.entityData,
|
||||
});
|
||||
} else if (this.conflicted && this.notify) {
|
||||
@@ -145,13 +143,7 @@ export default class SpecialEntityCard extends Vue {
|
||||
// Emit methods using @Emit decorator
|
||||
|
||||
@Emit("entity-selected")
|
||||
emitEntitySelected(data: {
|
||||
type: string;
|
||||
entityType: string;
|
||||
data: { did?: string; name: string };
|
||||
}): {
|
||||
type: string;
|
||||
entityType: string;
|
||||
emitEntitySelected(data: { data: { did?: string; name: string } }): {
|
||||
data: { did?: string; name: string };
|
||||
} {
|
||||
return data;
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
<template>
|
||||
<div class="absolute right-5 top-[max(0.75rem,env(safe-area-inset-top))]">
|
||||
<div
|
||||
class="absolute right-5 top-[max(0.75rem,env(safe-area-inset-top),var(--safe-area-inset-top,0px))]"
|
||||
>
|
||||
<span class="align-center text-red-500 mr-2">{{ message }}</span>
|
||||
<span class="ml-2">
|
||||
<router-link
|
||||
|
||||
@@ -134,27 +134,3 @@ export default class UserNameDialog extends Vue {
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style>
|
||||
.dialog-overlay {
|
||||
z-index: 50;
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
background-color: rgba(0, 0, 0, 0.5);
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
padding: 1.5rem;
|
||||
}
|
||||
|
||||
.dialog {
|
||||
background-color: white;
|
||||
padding: 1rem;
|
||||
border-radius: 0.5rem;
|
||||
width: 100%;
|
||||
max-width: 500px;
|
||||
}
|
||||
</style>
|
||||
|
||||
52
src/config/featureFlags.ts
Normal file
52
src/config/featureFlags.ts
Normal file
@@ -0,0 +1,52 @@
|
||||
/**
|
||||
* Feature Flags Configuration
|
||||
*
|
||||
* Controls the rollout of new features and migrations
|
||||
*
|
||||
* @author Matthew Raymer
|
||||
* @date 2025-08-21
|
||||
*/
|
||||
|
||||
export const FLAGS = {
|
||||
/**
|
||||
* When true, disallow legacy fallback reads from settings.activeDid
|
||||
* Set to true after all components are migrated to the new façade
|
||||
*/
|
||||
USE_ACTIVE_IDENTITY_ONLY: false,
|
||||
|
||||
/**
|
||||
* Controls Phase C column removal from settings table
|
||||
* Set to true when ready to drop the legacy activeDid column
|
||||
*
|
||||
* ✅ ENABLED: Migration 004 has dropped the activeDid column (2025-08-22T10:30Z)
|
||||
*/
|
||||
DROP_SETTINGS_ACTIVEDID: true,
|
||||
|
||||
/**
|
||||
* Log warnings when dual-read falls back to legacy settings.activeDid
|
||||
* Useful for monitoring migration progress
|
||||
*/
|
||||
LOG_ACTIVE_ID_FALLBACK: process.env.NODE_ENV === "development",
|
||||
|
||||
/**
|
||||
* Enable the new active_identity table and migration
|
||||
* Set to true to start the migration process
|
||||
*/
|
||||
ENABLE_ACTIVE_IDENTITY_MIGRATION: true,
|
||||
};
|
||||
|
||||
/**
|
||||
* Get feature flag value with type safety
|
||||
*/
|
||||
export function getFlag<K extends keyof typeof FLAGS>(
|
||||
key: K,
|
||||
): (typeof FLAGS)[K] {
|
||||
return FLAGS[key];
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if a feature flag is enabled
|
||||
*/
|
||||
export function isFlagEnabled<K extends keyof typeof FLAGS>(key: K): boolean {
|
||||
return Boolean(FLAGS[key]);
|
||||
}
|
||||
14
src/constants/entities.ts
Normal file
14
src/constants/entities.ts
Normal file
@@ -0,0 +1,14 @@
|
||||
/**
|
||||
* Constants for entity-related strings, particularly for unnamed/unknown person entities
|
||||
*/
|
||||
|
||||
// Core unnamed entity names
|
||||
export const UNNAMED_ENTITY_NAME = "Unnamed";
|
||||
|
||||
// Descriptive phrases for unnamed entities
|
||||
export const SOMEONE_UNNAMED = "Someone Unnamed";
|
||||
export const THAT_UNNAMED_PERSON = "That unnamed person";
|
||||
export const UNNAMED_PERSON = "unnamed person";
|
||||
|
||||
// Project-related unnamed entities
|
||||
export const UNNAMED_PROJECT = "Unnamed Project";
|
||||
@@ -1,4 +1,5 @@
|
||||
import axios from "axios";
|
||||
import { THAT_UNNAMED_PERSON } from "./entities";
|
||||
|
||||
// Notification message constants for user-facing notifications
|
||||
// Add new notification messages here as needed
|
||||
@@ -873,7 +874,7 @@ export const NOTIFY_CONTACT_LINK_COPIED = {
|
||||
// Template for registration success message
|
||||
// Used in: ContactsView.vue (register method - registration success with contact name)
|
||||
export const getRegisterPersonSuccessMessage = (name?: string): string =>
|
||||
`${name || "That unnamed person"} ${NOTIFY_REGISTER_PERSON_SUCCESS.message}`;
|
||||
`${name || THAT_UNNAMED_PERSON} ${NOTIFY_REGISTER_PERSON_SUCCESS.message}`;
|
||||
|
||||
// Template for visibility success message
|
||||
// Used in: ContactsView.vue (setVisibility method - visibility success with contact name)
|
||||
@@ -1378,7 +1379,7 @@ export function createQRContactAddedMessage(hasVisibility: boolean): string {
|
||||
export function createQRRegistrationSuccessMessage(
|
||||
contactName: string,
|
||||
): string {
|
||||
return `${contactName || "That unnamed person"}${NOTIFY_QR_REGISTRATION_SUCCESS.message}`;
|
||||
return `${contactName || THAT_UNNAMED_PERSON}${NOTIFY_QR_REGISTRATION_SUCCESS.message}`;
|
||||
}
|
||||
|
||||
// ContactQRScanShowView.vue timeout constants
|
||||
|
||||
@@ -124,6 +124,131 @@ const MIGRATIONS = [
|
||||
ALTER TABLE contacts ADD COLUMN iViewContent BOOLEAN DEFAULT TRUE;
|
||||
`,
|
||||
},
|
||||
{
|
||||
name: "003_active_identity_table_separation",
|
||||
sql: `
|
||||
-- Create active_identity table with proper constraints
|
||||
CREATE TABLE IF NOT EXISTS active_identity (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
active_did TEXT NOT NULL,
|
||||
updated_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%fZ','now')),
|
||||
CONSTRAINT fk_active_identity_account FOREIGN KEY (active_did)
|
||||
REFERENCES accounts(did) ON UPDATE CASCADE ON DELETE RESTRICT
|
||||
);
|
||||
|
||||
-- Create index for performance
|
||||
CREATE INDEX IF NOT EXISTS idx_active_identity_active_did ON active_identity(active_did);
|
||||
|
||||
-- Seed from existing settings.activeDid if valid
|
||||
INSERT INTO active_identity (active_did)
|
||||
SELECT s.activeDid
|
||||
FROM settings s
|
||||
WHERE s.activeDid IS NOT NULL
|
||||
AND EXISTS (SELECT 1 FROM accounts a WHERE a.did = s.activeDid)
|
||||
AND s.id = 1;
|
||||
|
||||
-- Fallback: choose first known account if still empty
|
||||
INSERT INTO active_identity (active_did)
|
||||
SELECT a.did
|
||||
FROM accounts a
|
||||
WHERE NOT EXISTS (SELECT 1 FROM active_identity ai)
|
||||
LIMIT 1;
|
||||
|
||||
-- Create one-way mirroring trigger (settings.activeDid → active_identity.active_did)
|
||||
DROP TRIGGER IF EXISTS trg_settings_activeDid_to_active_identity;
|
||||
CREATE TRIGGER trg_settings_activeDid_to_active_identity
|
||||
AFTER UPDATE OF activeDid ON settings
|
||||
FOR EACH ROW
|
||||
WHEN NEW.activeDid IS NOT OLD.activeDid AND NEW.activeDid IS NOT NULL
|
||||
BEGIN
|
||||
UPDATE active_identity
|
||||
SET active_did = NEW.activeDid,
|
||||
updated_at = strftime('%Y-%m-%dT%H:%M:%fZ','now')
|
||||
WHERE id = 1;
|
||||
|
||||
INSERT INTO active_identity (id, active_did, updated_at)
|
||||
SELECT 1, NEW.activeDid, strftime('%Y-%m-%dT%H:%M:%fZ','now')
|
||||
WHERE NOT EXISTS (
|
||||
SELECT 1 FROM active_identity ai WHERE ai.id = 1
|
||||
);
|
||||
END;
|
||||
`,
|
||||
},
|
||||
// Migration 004 re-enabled - Phase 1 complete, critical components migrated
|
||||
{
|
||||
name: "004_drop_settings_activeDid_column",
|
||||
sql: `
|
||||
-- Phase C: Remove activeDid column from settings table
|
||||
-- Note: SQLite requires table rebuild for column removal
|
||||
|
||||
-- Create new settings table without activeDid column
|
||||
CREATE TABLE settings_new (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
accountDid TEXT,
|
||||
-- activeDid intentionally omitted
|
||||
apiServer TEXT,
|
||||
filterFeedByNearby BOOLEAN,
|
||||
filterFeedByVisible BOOLEAN,
|
||||
finishedOnboarding BOOLEAN,
|
||||
firstName TEXT,
|
||||
hideRegisterPromptOnNewContact BOOLEAN,
|
||||
isRegistered BOOLEAN,
|
||||
lastName TEXT,
|
||||
lastAckedOfferToUserJwtId TEXT,
|
||||
lastAckedOfferToUserProjectsJwtId TEXT,
|
||||
lastNotifiedClaimId TEXT,
|
||||
lastViewedClaimId TEXT,
|
||||
notifyingNewActivityTime TEXT,
|
||||
notifyingReminderMessage TEXT,
|
||||
notifyingReminderTime TEXT,
|
||||
partnerApiServer TEXT,
|
||||
passkeyExpirationMinutes INTEGER,
|
||||
profileImageUrl TEXT,
|
||||
searchBoxes TEXT,
|
||||
showContactGivesInline BOOLEAN,
|
||||
showGeneralAdvanced BOOLEAN,
|
||||
showShortcutBvc BOOLEAN,
|
||||
vapid TEXT,
|
||||
warnIfProdServer BOOLEAN,
|
||||
warnIfTestServer BOOLEAN,
|
||||
webPushServer TEXT
|
||||
);
|
||||
|
||||
-- Copy data from old table (excluding activeDid)
|
||||
INSERT INTO settings_new (
|
||||
id, accountDid, apiServer, filterFeedByNearby, filterFeedByVisible,
|
||||
finishedOnboarding, firstName, hideRegisterPromptOnNewContact,
|
||||
isRegistered, lastName, lastAckedOfferToUserJwtId,
|
||||
lastAckedOfferToUserProjectsJwtId, lastNotifiedClaimId,
|
||||
lastViewedClaimId, notifyingNewActivityTime, notifyingReminderMessage,
|
||||
notifyingReminderTime, partnerApiServer, passkeyExpirationMinutes,
|
||||
profileImageUrl, searchBoxes, showContactGivesInline,
|
||||
showGeneralAdvanced, showShortcutBvc, vapid, warnIfProdServer,
|
||||
warnIfTestServer, webPushServer
|
||||
)
|
||||
SELECT
|
||||
id, accountDid, apiServer, filterFeedByNearby, filterFeedByVisible,
|
||||
finishedOnboarding, firstName, hideRegisterPromptOnNewContact,
|
||||
isRegistered, lastName, lastAckedOfferToUserJwtId,
|
||||
lastAckedOfferToUserProjectsJwtId, lastNotifiedClaimId,
|
||||
lastViewedClaimId, notifyingNewActivityTime, notifyingReminderMessage,
|
||||
notifyingReminderTime, partnerApiServer, passkeyExpirationMinutes,
|
||||
profileImageUrl, searchBoxes, showContactGivesInline,
|
||||
showGeneralAdvanced, showShortcutBvc, vapid, warnIfProdServer,
|
||||
warnIfTestServer, webPushServer
|
||||
FROM settings;
|
||||
|
||||
-- Drop old table and rename new one
|
||||
DROP TABLE settings;
|
||||
ALTER TABLE settings_new RENAME TO settings;
|
||||
|
||||
-- Recreate indexes
|
||||
CREATE INDEX IF NOT EXISTS idx_settings_accountDid ON settings(accountDid);
|
||||
|
||||
-- Drop the mirroring trigger (no longer needed)
|
||||
DROP TRIGGER IF EXISTS trg_settings_activeDid_to_active_identity;
|
||||
`,
|
||||
},
|
||||
];
|
||||
|
||||
/**
|
||||
|
||||
61
src/db/tables/activeIdentity.ts
Normal file
61
src/db/tables/activeIdentity.ts
Normal file
@@ -0,0 +1,61 @@
|
||||
/**
|
||||
* Active Identity Table Definition
|
||||
*
|
||||
* Manages the currently active identity/DID for the application.
|
||||
* Replaces the activeDid field from the settings table to improve
|
||||
* data normalization and reduce cache drift.
|
||||
*
|
||||
* @author Matthew Raymer
|
||||
* @date 2025-08-21
|
||||
*/
|
||||
|
||||
/**
|
||||
* Active Identity record structure
|
||||
*/
|
||||
export interface ActiveIdentity {
|
||||
/** Primary key */
|
||||
id?: number;
|
||||
|
||||
/** The currently active DID - foreign key to accounts.did */
|
||||
active_did: string;
|
||||
|
||||
/** Last update timestamp in ISO format */
|
||||
updated_at?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Database schema for the active_identity table
|
||||
*/
|
||||
export const ActiveIdentitySchema = {
|
||||
active_identity: "++id, active_did, updated_at",
|
||||
};
|
||||
|
||||
/**
|
||||
* Default values for ActiveIdentity records
|
||||
*/
|
||||
export const ActiveIdentityDefaults = {
|
||||
updated_at: new Date().toISOString(),
|
||||
};
|
||||
|
||||
/**
|
||||
* Validation function for DID format
|
||||
*/
|
||||
export function isValidDid(did: string): boolean {
|
||||
return typeof did === "string" && did.length > 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a new ActiveIdentity record
|
||||
*/
|
||||
export function createActiveIdentity(
|
||||
activeDid: string,
|
||||
): ActiveIdentity {
|
||||
if (!isValidDid(activeDid)) {
|
||||
throw new Error(`Invalid DID format: ${activeDid}`);
|
||||
}
|
||||
|
||||
return {
|
||||
active_did: activeDid,
|
||||
updated_at: new Date().toISOString(),
|
||||
};
|
||||
}
|
||||
@@ -60,6 +60,7 @@ import { PlanSummaryRecord } from "../interfaces/records";
|
||||
import { logger } from "../utils/logger";
|
||||
import { PlatformServiceFactory } from "@/services/PlatformServiceFactory";
|
||||
import { APP_SERVER } from "@/constants/app";
|
||||
import { SOMEONE_UNNAMED } from "@/constants/entities";
|
||||
|
||||
/**
|
||||
* Standard context for schema.org data
|
||||
@@ -309,7 +310,7 @@ export function didInfoForContact(
|
||||
showDidForVisible: boolean = false,
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
): { known: boolean; displayName: string; profileImageUrl?: string } {
|
||||
if (!did) return { displayName: "Someone Not Named", known: false };
|
||||
if (!did) return { displayName: SOMEONE_UNNAMED, known: false };
|
||||
if (did === activeDid) {
|
||||
return { displayName: "You", known: true };
|
||||
} else if (contact) {
|
||||
|
||||
@@ -33,6 +33,7 @@ import { logger } from "../utils/logger";
|
||||
import { PlatformServiceFactory } from "../services/PlatformServiceFactory";
|
||||
import { IIdentifier } from "@veramo/core";
|
||||
import { DEFAULT_ROOT_DERIVATION_PATH } from "./crypto";
|
||||
import { UNNAMED_PERSON } from "@/constants/entities";
|
||||
|
||||
// Consolidate this with src/utils/PlatformServiceMixin.mapQueryResultToValues
|
||||
function mapQueryResultToValues(
|
||||
@@ -192,7 +193,7 @@ export const nameForContact = (
|
||||
): string => {
|
||||
return (
|
||||
(contact?.name as string) ||
|
||||
(capitalize ? "This" : "this") + " unnamed user"
|
||||
(capitalize ? "This" : "this") + " " + UNNAMED_PERSON
|
||||
);
|
||||
};
|
||||
|
||||
@@ -655,7 +656,38 @@ export async function saveNewIdentity(
|
||||
];
|
||||
await platformService.dbExec(sql, params);
|
||||
|
||||
await platformService.updateDefaultSettings({ activeDid: identity.did });
|
||||
// Set the new identity as active using Active Identity façade
|
||||
// Check if we need to avoid legacy settings table (Phase C)
|
||||
const FLAGS = await import("@/config/featureFlags");
|
||||
|
||||
if (!FLAGS.FLAGS.DROP_SETTINGS_ACTIVEDID) {
|
||||
// Phase A/B: Update legacy settings table
|
||||
await platformService.updateDefaultSettings({ activeDid: identity.did });
|
||||
}
|
||||
|
||||
// Always update/insert into new active_identity table
|
||||
const DEFAULT_SCOPE = "default";
|
||||
const existingRecord = await platformService.dbQuery(
|
||||
"SELECT id FROM active_identity WHERE scope = ? LIMIT 1",
|
||||
[DEFAULT_SCOPE],
|
||||
);
|
||||
|
||||
if (existingRecord?.values?.length) {
|
||||
// Update existing record
|
||||
await platformService.dbExec(
|
||||
`UPDATE active_identity
|
||||
SET active_did = ?, updated_at = strftime('%Y-%m-%dT%H:%M:%fZ','now')
|
||||
WHERE scope = ?`,
|
||||
[identity.did, DEFAULT_SCOPE],
|
||||
);
|
||||
} else {
|
||||
// Insert new record
|
||||
await platformService.dbExec(
|
||||
`INSERT INTO active_identity (scope, active_did, updated_at)
|
||||
VALUES (?, ?, strftime('%Y-%m-%dT%H:%M:%fZ','now'))`,
|
||||
[DEFAULT_SCOPE, identity.did],
|
||||
);
|
||||
}
|
||||
|
||||
await platformService.insertNewDidIntoSettings(identity.did);
|
||||
} catch (error) {
|
||||
@@ -714,7 +746,38 @@ export const registerSaveAndActivatePasskey = async (
|
||||
): Promise<Account> => {
|
||||
const account = await registerAndSavePasskey(keyName);
|
||||
const platformService = await getPlatformService();
|
||||
await platformService.updateDefaultSettings({ activeDid: account.did });
|
||||
|
||||
// Set the new account as active using Active Identity façade
|
||||
// Check if we need to avoid legacy settings table (Phase C)
|
||||
const FLAGS = await import("@/config/featureFlags");
|
||||
|
||||
if (!FLAGS.FLAGS.DROP_SETTINGS_ACTIVEDID) {
|
||||
// Phase A/B: Update legacy settings table
|
||||
await platformService.updateDefaultSettings({ activeDid: account.did });
|
||||
}
|
||||
|
||||
// Always update/insert into new active_identity table
|
||||
const existingRecord = await platformService.dbQuery(
|
||||
"SELECT id FROM active_identity LIMIT 1",
|
||||
);
|
||||
|
||||
if (existingRecord?.values?.length) {
|
||||
// Update existing record
|
||||
await platformService.dbExec(
|
||||
`UPDATE active_identity
|
||||
SET active_did = ?, updated_at = strftime('%Y-%m-%dT%H:%M:%fZ','now')
|
||||
WHERE id = ?`,
|
||||
[account.did, existingRecord.values[0][0]],
|
||||
);
|
||||
} else {
|
||||
// Insert new record
|
||||
await platformService.dbExec(
|
||||
`INSERT INTO active_identity (active_did, updated_at)
|
||||
VALUES (?, strftime('%Y-%m-%dT%H:%M:%fZ','now'))`,
|
||||
[account.did],
|
||||
);
|
||||
}
|
||||
|
||||
await platformService.updateDidSpecificSettings(account.did, {
|
||||
isRegistered: false,
|
||||
});
|
||||
|
||||
@@ -35,6 +35,7 @@ import { handleApiError } from "./services/api";
|
||||
import { AxiosError } from "axios";
|
||||
import { DeepLinkHandler } from "./services/deepLinks";
|
||||
import { logger, safeStringify } from "./utils/logger";
|
||||
import "./utils/safeAreaInset";
|
||||
|
||||
logger.log("[Capacitor] 🚀 Starting initialization");
|
||||
logger.log("[Capacitor] Platform:", process.env.VITE_PLATFORM);
|
||||
|
||||
185
src/services/ClipboardService.ts
Normal file
185
src/services/ClipboardService.ts
Normal file
@@ -0,0 +1,185 @@
|
||||
import { Capacitor } from "@capacitor/core";
|
||||
import { Clipboard } from "@capacitor/clipboard";
|
||||
import { useClipboard } from "@vueuse/core";
|
||||
import { logger } from "@/utils/logger";
|
||||
|
||||
/**
|
||||
* Platform-agnostic clipboard service that handles both web and native platforms
|
||||
* Provides reliable clipboard functionality across all platforms including iOS
|
||||
*/
|
||||
export class ClipboardService {
|
||||
private static instance: ClipboardService | null = null;
|
||||
|
||||
/**
|
||||
* Get singleton instance of ClipboardService
|
||||
*/
|
||||
public static getInstance(): ClipboardService {
|
||||
if (!ClipboardService.instance) {
|
||||
ClipboardService.instance = new ClipboardService();
|
||||
}
|
||||
return ClipboardService.instance;
|
||||
}
|
||||
|
||||
/**
|
||||
* Copy text to clipboard with platform-specific handling
|
||||
*
|
||||
* @param text - The text to copy to clipboard
|
||||
* @returns Promise that resolves when copy is complete
|
||||
* @throws Error if copy operation fails
|
||||
*/
|
||||
public async copyToClipboard(text: string): Promise<void> {
|
||||
const platform = Capacitor.getPlatform();
|
||||
const isNative = Capacitor.isNativePlatform();
|
||||
|
||||
logger.debug("[ClipboardService] Copying to clipboard:", {
|
||||
text: text.substring(0, 50) + (text.length > 50 ? "..." : ""),
|
||||
platform,
|
||||
isNative,
|
||||
timestamp: new Date().toISOString(),
|
||||
});
|
||||
|
||||
try {
|
||||
if (isNative && (platform === "ios" || platform === "android")) {
|
||||
// Use native Capacitor clipboard for mobile platforms
|
||||
await this.copyNative(text);
|
||||
} else {
|
||||
// Use web clipboard API for web/desktop platforms
|
||||
await this.copyWeb(text);
|
||||
}
|
||||
|
||||
logger.debug("[ClipboardService] Copy successful", {
|
||||
platform,
|
||||
timestamp: new Date().toISOString(),
|
||||
});
|
||||
} catch (error) {
|
||||
logger.error("[ClipboardService] Copy failed:", {
|
||||
error: error instanceof Error ? error.message : String(error),
|
||||
platform,
|
||||
timestamp: new Date().toISOString(),
|
||||
});
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Copy text using native Capacitor clipboard API
|
||||
*
|
||||
* @param text - The text to copy
|
||||
* @returns Promise that resolves when copy is complete
|
||||
*/
|
||||
private async copyNative(text: string): Promise<void> {
|
||||
try {
|
||||
await Clipboard.write({
|
||||
string: text,
|
||||
});
|
||||
} catch (error) {
|
||||
logger.error("[ClipboardService] Native copy failed:", {
|
||||
error: error instanceof Error ? error.message : String(error),
|
||||
timestamp: new Date().toISOString(),
|
||||
});
|
||||
throw new Error(
|
||||
`Native clipboard copy failed: ${error instanceof Error ? error.message : String(error)}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Copy text using web clipboard API with fallback
|
||||
*
|
||||
* @param text - The text to copy
|
||||
* @returns Promise that resolves when copy is complete
|
||||
*/
|
||||
private async copyWeb(text: string): Promise<void> {
|
||||
try {
|
||||
// Try VueUse clipboard first (handles some edge cases)
|
||||
const { copy } = useClipboard();
|
||||
await copy(text);
|
||||
} catch (error) {
|
||||
logger.warn(
|
||||
"[ClipboardService] VueUse clipboard failed, trying native API:",
|
||||
{
|
||||
error: error instanceof Error ? error.message : String(error),
|
||||
timestamp: new Date().toISOString(),
|
||||
},
|
||||
);
|
||||
|
||||
// Fallback to native navigator.clipboard
|
||||
if (navigator.clipboard && navigator.clipboard.writeText) {
|
||||
await navigator.clipboard.writeText(text);
|
||||
} else {
|
||||
throw new Error("Clipboard API not supported in this browser");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Read text from clipboard (platform-specific)
|
||||
*
|
||||
* @returns Promise that resolves to the clipboard text
|
||||
* @throws Error if read operation fails
|
||||
*/
|
||||
public async readFromClipboard(): Promise<string> {
|
||||
const platform = Capacitor.getPlatform();
|
||||
const isNative = Capacitor.isNativePlatform();
|
||||
|
||||
try {
|
||||
if (isNative && (platform === "ios" || platform === "android")) {
|
||||
// Use native Capacitor clipboard for mobile platforms
|
||||
const result = await Clipboard.read();
|
||||
return result.value || "";
|
||||
} else {
|
||||
// Use web clipboard API for web/desktop platforms
|
||||
if (navigator.clipboard && navigator.clipboard.readText) {
|
||||
return await navigator.clipboard.readText();
|
||||
} else {
|
||||
throw new Error("Clipboard read API not supported in this browser");
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
logger.error("[ClipboardService] Read from clipboard failed:", {
|
||||
error: error instanceof Error ? error.message : String(error),
|
||||
platform,
|
||||
timestamp: new Date().toISOString(),
|
||||
});
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if clipboard is supported on current platform
|
||||
*
|
||||
* @returns boolean indicating if clipboard is supported
|
||||
*/
|
||||
public isSupported(): boolean {
|
||||
const platform = Capacitor.getPlatform();
|
||||
const isNative = Capacitor.isNativePlatform();
|
||||
|
||||
if (isNative && (platform === "ios" || platform === "android")) {
|
||||
return true; // Capacitor clipboard should work on native platforms
|
||||
}
|
||||
|
||||
// Check web clipboard support
|
||||
return !!(navigator.clipboard && navigator.clipboard.writeText);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Convenience function to copy text to clipboard
|
||||
* Uses the singleton ClipboardService instance
|
||||
*
|
||||
* @param text - The text to copy to clipboard
|
||||
* @returns Promise that resolves when copy is complete
|
||||
*/
|
||||
export async function copyToClipboard(text: string): Promise<void> {
|
||||
return ClipboardService.getInstance().copyToClipboard(text);
|
||||
}
|
||||
|
||||
/**
|
||||
* Convenience function to read text from clipboard
|
||||
* Uses the singleton ClipboardService instance
|
||||
*
|
||||
* @returns Promise that resolves to the clipboard text
|
||||
*/
|
||||
export async function readFromClipboard(): Promise<string> {
|
||||
return ClipboardService.getInstance().readFromClipboard();
|
||||
}
|
||||
@@ -74,13 +74,13 @@ export class DeepLinkHandler {
|
||||
* @throws {DeepLinkError} If validation fails or route is invalid
|
||||
*/
|
||||
async handleDeepLink(url: string): Promise<void> {
|
||||
logger.info(`[DeepLink] 🚀 Starting deeplink processing for URL: ${url}`);
|
||||
logger.debug(`[DeepLink] 🚀 Starting deeplink processing for URL: ${url}`);
|
||||
|
||||
try {
|
||||
logger.info(`[DeepLink] 📍 Parsing URL: ${url}`);
|
||||
logger.debug(`[DeepLink] 📍 Parsing URL: ${url}`);
|
||||
const { path, params, query } = this.parseDeepLink(url);
|
||||
|
||||
logger.info(`[DeepLink] ✅ URL parsed successfully:`, {
|
||||
logger.debug(`[DeepLink] ✅ URL parsed successfully:`, {
|
||||
path,
|
||||
params: Object.keys(params),
|
||||
query: Object.keys(query),
|
||||
@@ -93,10 +93,10 @@ export class DeepLinkHandler {
|
||||
Object.entries(params).map(([key, value]) => [key, value ?? ""]),
|
||||
);
|
||||
|
||||
logger.info(`[DeepLink] 🧹 Parameters sanitized:`, sanitizedParams);
|
||||
logger.debug(`[DeepLink] 🧹 Parameters sanitized:`, sanitizedParams);
|
||||
|
||||
await this.validateAndRoute(path, sanitizedParams, query);
|
||||
logger.info(`[DeepLink] 🎯 Deeplink processing completed successfully`);
|
||||
logger.debug(`[DeepLink] 🎯 Deeplink processing completed successfully`);
|
||||
} catch (error) {
|
||||
logger.error(`[DeepLink] ❌ Deeplink processing failed:`, {
|
||||
url,
|
||||
@@ -159,7 +159,7 @@ export class DeepLinkHandler {
|
||||
logger.debug(`[DeepLink] 🔗 Query parameters extracted:`, query);
|
||||
}
|
||||
|
||||
logger.info(`[DeepLink] ✅ Parse completed:`, {
|
||||
logger.debug(`[DeepLink] ✅ Parse completed:`, {
|
||||
routePath,
|
||||
pathParams: pathParams.length,
|
||||
queryParams: Object.keys(query).length,
|
||||
@@ -186,7 +186,7 @@ export class DeepLinkHandler {
|
||||
params: Record<string, string>,
|
||||
query: Record<string, string>,
|
||||
): Promise<void> {
|
||||
logger.info(
|
||||
logger.debug(
|
||||
`[DeepLink] 🎯 Starting validation and routing for path: ${path}`,
|
||||
);
|
||||
|
||||
@@ -197,11 +197,11 @@ export class DeepLinkHandler {
|
||||
logger.debug(`[DeepLink] 🔍 Validating route path: ${path}`);
|
||||
// Validate route exists
|
||||
const validRoute = routeSchema.parse(path) as DeepLinkRoute;
|
||||
logger.info(`[DeepLink] ✅ Route validation passed: ${validRoute}`);
|
||||
logger.debug(`[DeepLink] ✅ Route validation passed: ${validRoute}`);
|
||||
|
||||
// Get route configuration
|
||||
const routeConfig = ROUTE_MAP[validRoute];
|
||||
logger.info(`[DeepLink] 📋 Route config retrieved:`, routeConfig);
|
||||
logger.debug(`[DeepLink] 📋 Route config retrieved:`, routeConfig);
|
||||
|
||||
if (!routeConfig) {
|
||||
logger.error(`[DeepLink] ❌ No route config found for: ${validRoute}`);
|
||||
@@ -209,7 +209,7 @@ export class DeepLinkHandler {
|
||||
}
|
||||
|
||||
routeName = routeConfig.name;
|
||||
logger.info(`[DeepLink] 🎯 Route name resolved: ${routeName}`);
|
||||
logger.debug(`[DeepLink] 🎯 Route name resolved: ${routeName}`);
|
||||
} catch (error) {
|
||||
logger.error(`[DeepLink] ❌ Route validation failed:`, {
|
||||
path,
|
||||
@@ -228,14 +228,14 @@ export class DeepLinkHandler {
|
||||
},
|
||||
});
|
||||
|
||||
logger.info(
|
||||
logger.debug(
|
||||
`[DeepLink] 🔄 Redirected to error page for invalid route: ${path}`,
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
// Continue with parameter validation
|
||||
logger.info(
|
||||
logger.debug(
|
||||
`[DeepLink] 🔍 Starting parameter validation for route: ${routeName}`,
|
||||
);
|
||||
|
||||
@@ -258,7 +258,7 @@ export class DeepLinkHandler {
|
||||
if (pathSchema) {
|
||||
logger.debug(`[DeepLink] 🔍 Validating path parameters:`, params);
|
||||
validatedPathParams = await pathSchema.parseAsync(params);
|
||||
logger.info(
|
||||
logger.debug(
|
||||
`[DeepLink] ✅ Path parameters validated:`,
|
||||
validatedPathParams,
|
||||
);
|
||||
@@ -270,7 +270,7 @@ export class DeepLinkHandler {
|
||||
if (querySchema) {
|
||||
logger.debug(`[DeepLink] 🔍 Validating query parameters:`, query);
|
||||
validatedQueryParams = await querySchema.parseAsync(query);
|
||||
logger.info(
|
||||
logger.debug(
|
||||
`[DeepLink] ✅ Query parameters validated:`,
|
||||
validatedQueryParams,
|
||||
);
|
||||
@@ -299,7 +299,7 @@ export class DeepLinkHandler {
|
||||
},
|
||||
});
|
||||
|
||||
logger.info(
|
||||
logger.debug(
|
||||
`[DeepLink] 🔄 Redirected to error page for invalid parameters`,
|
||||
);
|
||||
return;
|
||||
@@ -307,7 +307,7 @@ export class DeepLinkHandler {
|
||||
|
||||
// Attempt navigation
|
||||
try {
|
||||
logger.info(`[DeepLink] 🚀 Attempting navigation:`, {
|
||||
logger.debug(`[DeepLink] 🚀 Attempting navigation:`, {
|
||||
routeName,
|
||||
pathParams: validatedPathParams,
|
||||
queryParams: validatedQueryParams,
|
||||
@@ -319,7 +319,7 @@ export class DeepLinkHandler {
|
||||
query: validatedQueryParams,
|
||||
});
|
||||
|
||||
logger.info(`[DeepLink] ✅ Navigation successful to: ${routeName}`);
|
||||
logger.debug(`[DeepLink] ✅ Navigation successful to: ${routeName}`);
|
||||
} catch (error) {
|
||||
logger.error(`[DeepLink] ❌ Navigation failed:`, {
|
||||
routeName,
|
||||
@@ -342,7 +342,7 @@ export class DeepLinkHandler {
|
||||
},
|
||||
});
|
||||
|
||||
logger.info(
|
||||
logger.debug(
|
||||
`[DeepLink] 🔄 Redirected to error page for navigation failure`,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -49,6 +49,8 @@ import {
|
||||
type Settings,
|
||||
type SettingsWithJsonStrings,
|
||||
} from "@/db/tables/settings";
|
||||
import { type ActiveIdentity } from "@/db/tables/activeIdentity";
|
||||
import { FLAGS } from "@/config/featureFlags";
|
||||
import { logger } from "@/utils/logger";
|
||||
import { Contact, ContactMaybeWithJsonStrings } from "@/db/tables/contacts";
|
||||
import { Account } from "@/db/tables/accounts";
|
||||
@@ -964,6 +966,156 @@ export const PlatformServiceMixin = {
|
||||
return await this.$saveUserSettings(currentDid, changes);
|
||||
},
|
||||
|
||||
// =================================================
|
||||
// ACTIVE IDENTITY METHODS (New table separation)
|
||||
// =================================================
|
||||
|
||||
/**
|
||||
* Get the current active DID from the active_identity table
|
||||
* Falls back to legacy settings.activeDid during Phase A transition
|
||||
*
|
||||
* @returns Promise<string | null> The active DID or null if not found
|
||||
*/
|
||||
async $getActiveDid(): Promise<string | null> {
|
||||
try {
|
||||
logger.debug("[ActiveDid] Getting activeDid");
|
||||
|
||||
// Try new active_identity table first
|
||||
const row = await this.$first<ActiveIdentity>(
|
||||
"SELECT active_did FROM active_identity LIMIT 1",
|
||||
);
|
||||
|
||||
logger.debug("[ActiveDid] New system result:", row?.active_did || "null");
|
||||
|
||||
if (row?.active_did) {
|
||||
logger.debug("[ActiveDid] Using new system value:", row.active_did);
|
||||
return row.active_did;
|
||||
}
|
||||
|
||||
// Fallback to legacy settings.activeDid during Phase A/B (unless Phase C is complete)
|
||||
if (!FLAGS.DROP_SETTINGS_ACTIVEDID) {
|
||||
if (FLAGS.LOG_ACTIVE_ID_FALLBACK) {
|
||||
logger.warn("[ActiveDid] Fallback to legacy settings.activeDid");
|
||||
}
|
||||
|
||||
const legacy = await this.$first<Settings>(
|
||||
"SELECT activeDid FROM settings WHERE id = ? LIMIT 1",
|
||||
[MASTER_SETTINGS_KEY],
|
||||
);
|
||||
|
||||
logger.debug("[ActiveDid] Legacy fallback result:", legacy?.activeDid || "null");
|
||||
return legacy?.activeDid || null;
|
||||
}
|
||||
|
||||
logger.debug("[ActiveDid] No fallback available, returning null");
|
||||
|
||||
// Log current database state for debugging
|
||||
try {
|
||||
const activeIdentityCount = await this.$first<{ count: number }>(
|
||||
"SELECT COUNT(*) as count FROM active_identity",
|
||||
);
|
||||
logger.debug("[ActiveDid] Active identity records:", activeIdentityCount?.count || 0);
|
||||
} catch (error) {
|
||||
logger.debug("[ActiveDid] Could not count active identity records:", error);
|
||||
}
|
||||
|
||||
return null;
|
||||
} catch (error) {
|
||||
logger.error("[ActiveDid] Error getting activeDid:", error);
|
||||
|
||||
// Fallback to legacy settings.activeDid during Phase A/B
|
||||
if (!FLAGS.DROP_SETTINGS_ACTIVEDID) {
|
||||
try {
|
||||
const legacy = await this.$first<Settings>(
|
||||
"SELECT activeDid FROM settings WHERE id = ? LIMIT 1",
|
||||
[MASTER_SETTINGS_KEY],
|
||||
);
|
||||
return legacy?.activeDid || null;
|
||||
} catch (fallbackError) {
|
||||
logger.error("[ActiveDid] Legacy fallback also failed:", fallbackError);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Set the active DID in the active_identity table
|
||||
* Also updates legacy settings.activeDid during Phase A/B transition
|
||||
*
|
||||
* @param did The DID to set as active
|
||||
* @returns Promise<void>
|
||||
*/
|
||||
async $setActiveDid(did: string | null): Promise<void> {
|
||||
try {
|
||||
if (!did) {
|
||||
logger.warn("[ActiveDid] Attempting to set null activeDid - this may cause issues");
|
||||
}
|
||||
|
||||
logger.debug("[ActiveDid] Setting activeDid to:", did);
|
||||
|
||||
// Update/insert into new active_identity table
|
||||
const existingRecord = await this.$first<ActiveIdentity>(
|
||||
"SELECT id FROM active_identity LIMIT 1",
|
||||
);
|
||||
|
||||
if (existingRecord?.id) {
|
||||
// Update existing record
|
||||
await this.$exec(
|
||||
`UPDATE active_identity
|
||||
SET active_did = ?, updated_at = strftime('%Y-%m-%dT%H:%M:%fZ','now')
|
||||
WHERE id = ?`,
|
||||
[did, existingRecord.id],
|
||||
);
|
||||
logger.debug("[ActiveDid] Updated existing record");
|
||||
} else {
|
||||
// Insert new record
|
||||
await this.$exec(
|
||||
`INSERT INTO active_identity (active_did, updated_at)
|
||||
VALUES (?, strftime('%Y-%m-%dT%H:%M:%fZ','now'))`,
|
||||
[did],
|
||||
);
|
||||
logger.debug("[ActiveDid] Inserted new record");
|
||||
}
|
||||
|
||||
// Legacy fallback - update settings.activeDid during Phase A/B
|
||||
if (!FLAGS.USE_ACTIVE_IDENTITY_ONLY) {
|
||||
await this.$exec(
|
||||
"UPDATE settings SET activeDid = ? WHERE id = ?",
|
||||
[did, MASTER_SETTINGS_KEY],
|
||||
);
|
||||
logger.debug("[ActiveDid] Updated legacy settings.activeDid");
|
||||
}
|
||||
|
||||
logger.debug("[ActiveDid] Successfully set activeDid to:", did);
|
||||
} catch (error) {
|
||||
logger.error("[ActiveDid] Error setting activeDid:", error);
|
||||
throw error;
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Switch to a different active identity
|
||||
* Convenience method that validates and sets the new active DID
|
||||
*
|
||||
* @param did The DID to switch to
|
||||
* @returns Promise<void>
|
||||
*/
|
||||
async $switchActiveIdentity(did: string): Promise<void> {
|
||||
await this.$setActiveDid(did);
|
||||
},
|
||||
|
||||
/**
|
||||
* Get all available identity scopes (simplified to single scope)
|
||||
* @returns Promise<string[]> Array containing only 'default' scope
|
||||
*/
|
||||
async $getActiveIdentityScopes(): Promise<string[]> {
|
||||
// Simplified to single scope since we removed multi-scope support
|
||||
return ["default"];
|
||||
},
|
||||
|
||||
// =================================================
|
||||
// CACHE MANAGEMENT METHODS
|
||||
// =================================================
|
||||
@@ -1708,6 +1860,12 @@ export interface IPlatformServiceMixin {
|
||||
// Debug methods
|
||||
$debugDidSettings(did: string): Promise<Settings | null>;
|
||||
$debugMergedSettings(did: string): Promise<void>;
|
||||
|
||||
// Active Identity façade methods
|
||||
$getActiveDid(): Promise<string | null>;
|
||||
$setActiveDid(did: string | null): Promise<void>;
|
||||
$switchActiveIdentity(did: string): Promise<void>;
|
||||
$getActiveIdentityScopes(): Promise<string[]>;
|
||||
}
|
||||
|
||||
// TypeScript declaration merging to eliminate (this as any) type assertions
|
||||
@@ -1724,6 +1882,12 @@ declare module "@vue/runtime-core" {
|
||||
currentActiveDid: string | null;
|
||||
$updateActiveDid(newDid: string | null): Promise<void>;
|
||||
|
||||
// Active Identity façade methods
|
||||
$getActiveDid(): Promise<string | null>;
|
||||
$setActiveDid(did: string | null): Promise<void>;
|
||||
$switchActiveIdentity(did: string): Promise<void>;
|
||||
$getActiveIdentityScopes(): Promise<string[]>;
|
||||
|
||||
// Ultra-concise database methods (shortest possible names)
|
||||
$db(sql: string, params?: unknown[]): Promise<QueryExecResult | undefined>;
|
||||
$exec(sql: string, params?: unknown[]): Promise<DatabaseExecResult>;
|
||||
|
||||
226
src/utils/safeAreaInset.js
Normal file
226
src/utils/safeAreaInset.js
Normal file
@@ -0,0 +1,226 @@
|
||||
/**
|
||||
* Safe Area Inset Injection for Android WebView
|
||||
*
|
||||
* This script injects safe area inset values into CSS environment variables
|
||||
* when running in Android WebView, since Android doesn't natively support
|
||||
* CSS env(safe-area-inset-*) variables like iOS does.
|
||||
*/
|
||||
|
||||
// Check if we're running in Android WebView with Capacitor
|
||||
const isAndroidWebView = () => {
|
||||
// Check if we're on iOS - if so, skip this script entirely
|
||||
const isIOS =
|
||||
/iPad|iPhone|iPod/.test(navigator.userAgent) ||
|
||||
(navigator.platform === "MacIntel" && navigator.maxTouchPoints > 1);
|
||||
|
||||
if (isIOS) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Check if we're on Android
|
||||
const isAndroid = /Android/.test(navigator.userAgent);
|
||||
|
||||
// Check if we have Capacitor (required for Android WebView)
|
||||
const hasCapacitor = window.Capacitor !== undefined;
|
||||
|
||||
// Only run on Android with Capacitor
|
||||
return isAndroid && hasCapacitor;
|
||||
};
|
||||
|
||||
// Wait for Capacitor to be available
|
||||
const waitForCapacitor = () => {
|
||||
return new Promise((resolve) => {
|
||||
if (window.Capacitor) {
|
||||
resolve(window.Capacitor);
|
||||
return;
|
||||
}
|
||||
|
||||
// Wait for Capacitor to be available
|
||||
const checkCapacitor = () => {
|
||||
if (window.Capacitor) {
|
||||
resolve(window.Capacitor);
|
||||
} else {
|
||||
setTimeout(checkCapacitor, 100);
|
||||
}
|
||||
};
|
||||
|
||||
checkCapacitor();
|
||||
});
|
||||
};
|
||||
|
||||
// Inject safe area inset values into CSS custom properties
|
||||
const injectSafeAreaInsets = async () => {
|
||||
try {
|
||||
// Wait for Capacitor to be available
|
||||
const Capacitor = await waitForCapacitor();
|
||||
|
||||
// Try to get safe area insets using StatusBar plugin (which is already available)
|
||||
|
||||
let top = 0,
|
||||
bottom = 0,
|
||||
left = 0,
|
||||
right = 0;
|
||||
|
||||
try {
|
||||
// Use StatusBar plugin to get status bar height
|
||||
if (Capacitor.Plugins.StatusBar) {
|
||||
const statusBarInfo = await Capacitor.Plugins.StatusBar.getInfo();
|
||||
// Status bar height is typically the top safe area inset
|
||||
top = statusBarInfo.overlays ? 0 : statusBarInfo.height || 0;
|
||||
}
|
||||
} catch (error) {
|
||||
// Status bar info not available, will use fallback
|
||||
}
|
||||
|
||||
// Detect navigation bar and gesture bar heights
|
||||
const detectNavigationBar = () => {
|
||||
const screenHeight = window.screen.height;
|
||||
const screenWidth = window.screen.width;
|
||||
const windowHeight = window.innerHeight;
|
||||
const devicePixelRatio = window.devicePixelRatio || 1;
|
||||
|
||||
// Calculate navigation bar height
|
||||
let navBarHeight = 0;
|
||||
|
||||
// Method 1: Direct comparison (most reliable)
|
||||
if (windowHeight < screenHeight) {
|
||||
navBarHeight = screenHeight - windowHeight;
|
||||
}
|
||||
|
||||
// Method 2: Check for gesture navigation indicators
|
||||
if (navBarHeight === 0) {
|
||||
// Look for common gesture navigation patterns
|
||||
const isTallDevice = screenHeight > 2000;
|
||||
const isModernDevice = screenHeight > 1800;
|
||||
const hasHighDensity = devicePixelRatio >= 2.5;
|
||||
|
||||
if (isTallDevice && hasHighDensity) {
|
||||
// Modern gesture-based device
|
||||
navBarHeight = 12; // Typical gesture bar height
|
||||
} else if (isModernDevice) {
|
||||
// Modern device with traditional navigation
|
||||
navBarHeight = 48; // Traditional navigation bar height
|
||||
}
|
||||
}
|
||||
|
||||
// Method 3: Check visual viewport (more accurate for WebView)
|
||||
if (navBarHeight === 0) {
|
||||
if (window.visualViewport) {
|
||||
const visualHeight = window.visualViewport.height;
|
||||
|
||||
if (visualHeight < windowHeight) {
|
||||
navBarHeight = windowHeight - visualHeight;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Method 4: Device-specific estimation based on screen dimensions
|
||||
if (navBarHeight === 0) {
|
||||
// Common Android navigation bar heights in pixels
|
||||
const commonNavBarHeights = {
|
||||
"1080x2400": 48, // Common 1080p devices
|
||||
"1440x3200": 64, // QHD devices
|
||||
"720x1600": 32, // HD devices
|
||||
};
|
||||
|
||||
const resolution = `${screenWidth}x${screenHeight}`;
|
||||
const estimatedHeight = commonNavBarHeights[resolution];
|
||||
|
||||
if (estimatedHeight) {
|
||||
navBarHeight = estimatedHeight;
|
||||
} else {
|
||||
// Fallback: estimate based on screen height
|
||||
navBarHeight = screenHeight > 2000 ? 48 : 32;
|
||||
}
|
||||
}
|
||||
|
||||
return navBarHeight;
|
||||
};
|
||||
|
||||
// Get navigation bar height
|
||||
bottom = detectNavigationBar();
|
||||
|
||||
// If we still don't have a top value, estimate it
|
||||
if (top === 0) {
|
||||
const screenHeight = window.screen.height;
|
||||
// Common status bar heights: 24dp (48px) for most devices, 32dp (64px) for some
|
||||
top = screenHeight > 1920 ? 64 : 48;
|
||||
}
|
||||
|
||||
// Left/right safe areas are rare on Android
|
||||
left = 0;
|
||||
right = 0;
|
||||
|
||||
// Create CSS custom properties
|
||||
const style = document.createElement("style");
|
||||
style.textContent = `
|
||||
:root {
|
||||
--safe-area-inset-top: ${top}px;
|
||||
--safe-area-inset-bottom: ${bottom}px;
|
||||
--safe-area-inset-left: ${left}px;
|
||||
--safe-area-inset-right: ${right}px;
|
||||
}
|
||||
`;
|
||||
|
||||
// Inject the style into the document head
|
||||
document.head.appendChild(style);
|
||||
|
||||
// Also set CSS environment variables if supported
|
||||
if (CSS.supports("env(safe-area-inset-top)")) {
|
||||
document.documentElement.style.setProperty(
|
||||
"--env-safe-area-inset-top",
|
||||
`${top}px`,
|
||||
);
|
||||
document.documentElement.style.setProperty(
|
||||
"--env-safe-area-inset-bottom",
|
||||
`${bottom}px`,
|
||||
);
|
||||
document.documentElement.style.setProperty(
|
||||
"--env-safe-area-inset-left",
|
||||
`${left}px`,
|
||||
);
|
||||
document.documentElement.style.setProperty(
|
||||
"--env-safe-area-inset-right",
|
||||
`${right}px`,
|
||||
);
|
||||
}
|
||||
} catch (error) {
|
||||
// Error injecting safe area insets, will use fallback values
|
||||
}
|
||||
};
|
||||
|
||||
// Initialize when DOM is ready
|
||||
const initializeSafeArea = () => {
|
||||
// Check if we should run this script at all
|
||||
if (!isAndroidWebView()) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Add a small delay to ensure WebView is fully initialized
|
||||
setTimeout(() => {
|
||||
injectSafeAreaInsets();
|
||||
}, 100);
|
||||
};
|
||||
|
||||
if (document.readyState === "loading") {
|
||||
document.addEventListener("DOMContentLoaded", initializeSafeArea);
|
||||
} else {
|
||||
initializeSafeArea();
|
||||
}
|
||||
|
||||
// Re-inject on orientation change (only on Android)
|
||||
window.addEventListener("orientationchange", () => {
|
||||
if (isAndroidWebView()) {
|
||||
setTimeout(() => injectSafeAreaInsets(), 100);
|
||||
}
|
||||
});
|
||||
|
||||
// Re-inject on resize (only on Android)
|
||||
window.addEventListener("resize", () => {
|
||||
if (isAndroidWebView()) {
|
||||
setTimeout(() => injectSafeAreaInsets(), 100);
|
||||
}
|
||||
});
|
||||
|
||||
// Export for use in other modules
|
||||
export { injectSafeAreaInsets, isAndroidWebView };
|
||||
@@ -58,7 +58,8 @@
|
||||
v-if="!isRegistered"
|
||||
:passkeys-enabled="PASSKEYS_ENABLED"
|
||||
:given-name="givenName"
|
||||
message="Before you can publicly announce a new project or time commitment, a friend needs to register you."
|
||||
message="Before you can publicly announce a new project or time commitment,
|
||||
a friend needs to register you."
|
||||
/>
|
||||
|
||||
<!-- Notifications -->
|
||||
@@ -1038,7 +1039,8 @@ export default class AccountViewView extends Vue {
|
||||
// Then get the account-specific settings
|
||||
const settings: AccountSettings = await this.$accountSettings();
|
||||
|
||||
this.activeDid = settings.activeDid || "";
|
||||
// Use new Active Identity façade instead of settings.activeDid
|
||||
this.activeDid = (await this.$getActiveDid()) || "";
|
||||
this.apiServer = settings.apiServer || "";
|
||||
this.apiServerInput = settings.apiServer || "";
|
||||
this.givenName =
|
||||
|
||||
@@ -112,7 +112,8 @@ export default class ClaimAddRawView extends Vue {
|
||||
*/
|
||||
private async initializeSettings() {
|
||||
const settings = await this.$accountSettings();
|
||||
this.activeDid = settings.activeDid || "";
|
||||
// Use new façade method with legacy fallback
|
||||
this.activeDid = (await this.$getActiveDid()) || "";
|
||||
this.apiServer = settings.apiServer || "";
|
||||
}
|
||||
|
||||
|
||||
@@ -40,7 +40,8 @@ export default class ClaimCertificateView extends Vue {
|
||||
async created() {
|
||||
this.notify = createNotifyHelpers(this.$notify);
|
||||
const settings = await this.$accountSettings();
|
||||
this.activeDid = settings.activeDid || "";
|
||||
// Use new Active Identity façade instead of settings.activeDid
|
||||
this.activeDid = (await this.$getActiveDid()) || "";
|
||||
this.apiServer = settings.apiServer || "";
|
||||
const pathParams = window.location.pathname.substring(
|
||||
"/claim-cert/".length,
|
||||
|
||||
@@ -54,7 +54,8 @@ export default class ClaimReportCertificateView extends Vue {
|
||||
this.notify = createNotifyHelpers(this.$notify);
|
||||
|
||||
const settings = await this.$settings();
|
||||
this.activeDid = settings.activeDid || "";
|
||||
// Use new Active Identity façade instead of settings.activeDid
|
||||
this.activeDid = (await this.$getActiveDid()) || "";
|
||||
this.apiServer = settings.apiServer || "";
|
||||
const pathParams = window.location.pathname.substring(
|
||||
"/claim-cert/".length,
|
||||
|
||||
@@ -728,7 +728,8 @@ export default class ClaimView extends Vue {
|
||||
|
||||
const settings = await this.$accountSettings();
|
||||
|
||||
this.activeDid = settings.activeDid || "";
|
||||
// Use new Active Identity façade instead of settings.activeDid
|
||||
this.activeDid = (await this.$getActiveDid()) || "";
|
||||
this.apiServer = settings.apiServer || "";
|
||||
this.allContacts = await this.$contacts();
|
||||
|
||||
|
||||
@@ -547,7 +547,8 @@ export default class ConfirmGiftView extends Vue {
|
||||
*/
|
||||
private async initializeSettings() {
|
||||
const settings = await this.$accountSettings();
|
||||
this.activeDid = settings.activeDid || "";
|
||||
// Use new Active Identity façade instead of settings.activeDid
|
||||
this.activeDid = (await this.$getActiveDid()) || "";
|
||||
this.apiServer = settings.apiServer || "";
|
||||
this.allContacts = await this.$getAllContacts();
|
||||
this.isRegistered = settings.isRegistered || false;
|
||||
|
||||
@@ -224,7 +224,8 @@ export default class ContactAmountssView extends Vue {
|
||||
this.contact = contact;
|
||||
|
||||
const settings = await this.$getSettings(MASTER_SETTINGS_KEY);
|
||||
this.activeDid = settings?.activeDid || "";
|
||||
// Use new Active Identity façade instead of settings.activeDid
|
||||
this.activeDid = (await this.$getActiveDid()) || "";
|
||||
this.apiServer = settings?.apiServer || "";
|
||||
|
||||
if (this.activeDid && this.contact) {
|
||||
|
||||
@@ -17,20 +17,40 @@
|
||||
|
||||
<!-- Results List -->
|
||||
<ul class="border-t border-slate-300">
|
||||
<li class="border-b border-slate-300 py-3">
|
||||
<!-- "You" entity -->
|
||||
<li v-if="shouldShowYouEntity" class="border-b border-slate-300 py-3">
|
||||
<h2 class="text-base flex gap-4 items-center">
|
||||
<span class="grow flex gap-2 items-center font-medium">
|
||||
<font-awesome
|
||||
icon="circle-question"
|
||||
class="text-slate-400 text-4xl"
|
||||
/>
|
||||
<span class="italic text-slate-400">(Not Named)</span>
|
||||
<font-awesome icon="hand" class="text-blue-500 text-4xl shrink-0" />
|
||||
<span class="text-ellipsis overflow-hidden text-blue-500">You</span>
|
||||
</span>
|
||||
<span class="text-right">
|
||||
<button
|
||||
type="button"
|
||||
class="block w-full text-center text-sm uppercase bg-gradient-to-b from-blue-400 to-blue-700 shadow-[inset_0_-1px_0_0_rgba(0,0,0,0.5)] text-white px-3 py-1.5 rounded-md"
|
||||
@click="openDialog('Unnamed')"
|
||||
@click="openDialog({ did: activeDid, name: 'You' })"
|
||||
>
|
||||
<font-awesome icon="gift" class="fa-fw"></font-awesome>
|
||||
</button>
|
||||
</span>
|
||||
</h2>
|
||||
</li>
|
||||
<li class="border-b border-slate-300 py-3">
|
||||
<h2 class="text-base flex gap-4 items-center">
|
||||
<span class="grow flex gap-2 items-center font-medium">
|
||||
<font-awesome
|
||||
icon="circle-question"
|
||||
class="text-slate-400 text-4xl shrink-0"
|
||||
/>
|
||||
<span class="text-ellipsis overflow-hidden italic text-slate-500">{{
|
||||
unnamedEntityName
|
||||
}}</span>
|
||||
</span>
|
||||
<span class="text-right">
|
||||
<button
|
||||
type="button"
|
||||
class="block w-full text-center text-sm uppercase bg-gradient-to-b from-blue-400 to-blue-700 shadow-[inset_0_-1px_0_0_rgba(0,0,0,0.5)] text-white px-3 py-1.5 rounded-md"
|
||||
@click="openDialog({ did: '', name: unnamedEntityName })"
|
||||
>
|
||||
<font-awesome icon="gift" class="fa-fw"></font-awesome>
|
||||
</button>
|
||||
@@ -43,14 +63,22 @@
|
||||
class="border-b border-slate-300 py-3"
|
||||
>
|
||||
<h2 class="text-base flex gap-4 items-center">
|
||||
<span class="grow flex gap-2 items-center font-medium">
|
||||
<span
|
||||
class="grow flex gap-2 items-center font-medium overflow-hidden"
|
||||
>
|
||||
<EntityIcon
|
||||
:contact="contact"
|
||||
:icon-size="34"
|
||||
class="inline-block align-middle border border-slate-300 rounded-full overflow-hidden"
|
||||
class="inline-block align-middle border border-slate-300 rounded-full overflow-hidden shrink-0"
|
||||
/>
|
||||
<span v-if="contact.name">{{ contact.name }}</span>
|
||||
<span v-else class="italic text-slate-400">(No name)</span>
|
||||
<span v-if="contact.name" class="text-ellipsis overflow-hidden">{{
|
||||
contact.name
|
||||
}}</span>
|
||||
<span
|
||||
v-else
|
||||
class="text-ellipsis overflow-hidden italic text-slate-500"
|
||||
>{{ contact.did }}</span
|
||||
>
|
||||
</span>
|
||||
<span class="text-right">
|
||||
<button
|
||||
@@ -72,6 +100,7 @@
|
||||
:from-project-id="fromProjectId"
|
||||
:to-project-id="toProjectId"
|
||||
:is-from-project-view="isFromProjectView"
|
||||
:hide-show-all="true"
|
||||
/>
|
||||
</section>
|
||||
</template>
|
||||
@@ -89,6 +118,7 @@ import { GiverReceiverInputInfo } from "../libs/util";
|
||||
import { logger } from "../utils/logger";
|
||||
import { PlatformServiceMixin } from "@/utils/PlatformServiceMixin";
|
||||
import { createNotifyHelpers, TIMEOUTS } from "@/utils/notify";
|
||||
import { UNNAMED_ENTITY_NAME } from "@/constants/entities";
|
||||
@Component({
|
||||
components: { GiftedDialog, QuickNav, EntityIcon },
|
||||
mixins: [PlatformServiceMixin],
|
||||
@@ -134,7 +164,8 @@ export default class ContactGiftingView extends Vue {
|
||||
try {
|
||||
const settings = await this.$accountSettings();
|
||||
this.apiServer = settings.apiServer || "";
|
||||
this.activeDid = settings.activeDid || "";
|
||||
// Use new Active Identity façade instead of settings.activeDid
|
||||
this.activeDid = (await this.$getActiveDid()) || "";
|
||||
|
||||
this.allContacts = await this.$getAllContacts();
|
||||
|
||||
@@ -188,147 +219,151 @@ export default class ContactGiftingView extends Vue {
|
||||
}
|
||||
}
|
||||
|
||||
openDialog(contact?: GiverReceiverInputInfo | "Unnamed") {
|
||||
if (contact === "Unnamed") {
|
||||
// Special case: Handle "Unnamed" contacts for both givers and recipients
|
||||
let recipient: GiverReceiverInputInfo;
|
||||
let giver: GiverReceiverInputInfo | undefined;
|
||||
openDialog(contact?: GiverReceiverInputInfo) {
|
||||
// Determine the selected entity based on contact type
|
||||
const selectedEntity = this.createEntityFromContact(contact);
|
||||
|
||||
if (this.stepType === "giver") {
|
||||
// We're selecting a giver, so preserve the existing recipient from context
|
||||
if (this.recipientEntityType === "project") {
|
||||
recipient = {
|
||||
did: this.recipientProjectHandleId,
|
||||
name: this.recipientProjectName,
|
||||
image: this.recipientProjectImage,
|
||||
handleId: this.recipientProjectHandleId,
|
||||
};
|
||||
} else {
|
||||
// Preserve the existing recipient from context
|
||||
if (this.recipientDid === this.activeDid) {
|
||||
// Recipient was "You"
|
||||
recipient = { did: this.activeDid, name: "You" };
|
||||
} else if (this.recipientDid) {
|
||||
// Recipient was a regular contact
|
||||
recipient = {
|
||||
did: this.recipientDid,
|
||||
name: this.recipientProjectName || "Someone",
|
||||
};
|
||||
} else {
|
||||
// Fallback to "You" if no recipient was previously selected
|
||||
recipient = { did: this.activeDid, name: "You" };
|
||||
}
|
||||
}
|
||||
giver = undefined; // Will be set to "Unnamed" in GiftedDialog
|
||||
} else {
|
||||
// We're selecting a recipient, so recipient is "Unnamed" and giver is preserved from context
|
||||
recipient = { did: "", name: "Unnamed" };
|
||||
// Create giver and recipient based on step type and selected entity
|
||||
const { giver, recipient } = this.createGiverAndRecipient(selectedEntity);
|
||||
|
||||
// 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 if (this.giverDid) {
|
||||
giver = {
|
||||
did: this.giverDid,
|
||||
name: this.giverProjectName || "Someone",
|
||||
};
|
||||
} else {
|
||||
giver = { did: this.activeDid, name: "You" };
|
||||
}
|
||||
}
|
||||
// Open the dialog
|
||||
(this.$refs.giftedDialog as GiftedDialog).open(
|
||||
giver,
|
||||
recipient,
|
||||
this.offerId,
|
||||
this.prompt,
|
||||
this.description,
|
||||
this.amountInput,
|
||||
this.unitCode,
|
||||
);
|
||||
|
||||
(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();
|
||||
}
|
||||
|
||||
// Move to Step 2 - entities are already set by the open() call
|
||||
(this.$refs.giftedDialog as GiftedDialog).moveToStep2();
|
||||
/**
|
||||
* Creates an entity object from the contact parameter
|
||||
* Uses DID-based logic to determine "You" and "Unnamed" entities
|
||||
*/
|
||||
private createEntityFromContact(
|
||||
contact?: GiverReceiverInputInfo,
|
||||
): GiverReceiverInputInfo | undefined {
|
||||
if (!contact) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
// Handle GiverReceiverInputInfo object
|
||||
if (contact.did === this.activeDid) {
|
||||
// If DID matches active DID, create "You" entity
|
||||
return { did: this.activeDid, name: "You" };
|
||||
} else if (!contact.did || contact.did === "") {
|
||||
// If DID is empty/null, create "Unnamed" entity
|
||||
return { did: "", name: UNNAMED_ENTITY_NAME };
|
||||
} else {
|
||||
// Regular case: contact is a GiverReceiverInputInfo
|
||||
let giver: GiverReceiverInputInfo;
|
||||
let recipient: GiverReceiverInputInfo;
|
||||
// Create a copy of the contact to avoid modifying the original
|
||||
return { ...contact };
|
||||
}
|
||||
}
|
||||
|
||||
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
|
||||
/**
|
||||
* Creates giver and recipient objects based on step type and selected entity
|
||||
*/
|
||||
private createGiverAndRecipient(selectedEntity?: GiverReceiverInputInfo): {
|
||||
giver: GiverReceiverInputInfo | undefined;
|
||||
recipient: GiverReceiverInputInfo;
|
||||
} {
|
||||
if (this.stepType === "giver") {
|
||||
// We're selecting a giver, so the selected entity becomes the giver
|
||||
const giver = selectedEntity;
|
||||
const recipient = this.createRecipientFromContext();
|
||||
return { giver, recipient };
|
||||
} else {
|
||||
// We're selecting a recipient, so the selected entity becomes the recipient
|
||||
const recipient = selectedEntity || {
|
||||
did: "",
|
||||
name: UNNAMED_ENTITY_NAME,
|
||||
};
|
||||
const giver = this.createGiverFromContext();
|
||||
return { giver, recipient };
|
||||
}
|
||||
}
|
||||
|
||||
// Preserve the existing recipient from the context
|
||||
if (this.recipientEntityType === "project") {
|
||||
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" };
|
||||
}
|
||||
}
|
||||
/**
|
||||
* Creates recipient object from context (preserves existing recipient)
|
||||
*/
|
||||
private createRecipientFromContext(): GiverReceiverInputInfo {
|
||||
if (this.recipientEntityType === "project") {
|
||||
return {
|
||||
name: this.recipientProjectName,
|
||||
image: this.recipientProjectImage,
|
||||
handleId: this.recipientProjectHandleId,
|
||||
};
|
||||
} else {
|
||||
if (this.recipientDid === this.activeDid) {
|
||||
return { did: this.activeDid, name: "You" };
|
||||
} else if (this.recipientDid) {
|
||||
return {
|
||||
did: this.recipientDid,
|
||||
name: this.recipientProjectName,
|
||||
};
|
||||
} 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" };
|
||||
}
|
||||
}
|
||||
return { did: "", name: UNNAMED_ENTITY_NAME };
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
(this.$refs.giftedDialog as GiftedDialog).open(
|
||||
giver,
|
||||
recipient,
|
||||
this.offerId,
|
||||
this.prompt,
|
||||
this.description,
|
||||
this.amountInput,
|
||||
this.unitCode,
|
||||
);
|
||||
/**
|
||||
* Creates giver object from context (preserves existing giver)
|
||||
*/
|
||||
private createGiverFromContext(): GiverReceiverInputInfo {
|
||||
if (this.giverEntityType === "project") {
|
||||
return {
|
||||
name: this.giverProjectName,
|
||||
image: this.giverProjectImage,
|
||||
handleId: this.giverProjectHandleId,
|
||||
};
|
||||
} else {
|
||||
if (this.giverDid === this.activeDid) {
|
||||
return { did: this.activeDid, name: "You" };
|
||||
} else if (this.giverDid) {
|
||||
return {
|
||||
did: this.giverDid,
|
||||
name: this.giverProjectName,
|
||||
};
|
||||
} else {
|
||||
return { did: "", name: UNNAMED_ENTITY_NAME };
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Move to Step 2 - entities are already set by the open() call
|
||||
(this.$refs.giftedDialog as GiftedDialog).moveToStep2();
|
||||
/**
|
||||
* Get the unnamed entity name constant
|
||||
*/
|
||||
get unnamedEntityName(): string {
|
||||
return UNNAMED_ENTITY_NAME;
|
||||
}
|
||||
|
||||
get shouldShowYouEntity(): boolean {
|
||||
if (this.stepType === "giver") {
|
||||
// When selecting a giver, show "You" if the current recipient is not "You"
|
||||
// This prevents selecting yourself as both giver and recipient
|
||||
if (this.recipientEntityType === "project") {
|
||||
// If recipient is a project, we can select "You" as giver
|
||||
return true;
|
||||
} else {
|
||||
// If recipient is a person, check if it's not "You"
|
||||
return this.recipientDid !== this.activeDid;
|
||||
}
|
||||
} else {
|
||||
// When selecting a recipient, show "You" if the current giver is not "You"
|
||||
// This prevents selecting yourself as both giver and recipient
|
||||
if (this.giverEntityType === "project") {
|
||||
// If giver is a project, we can select "You" as recipient
|
||||
return true;
|
||||
} else {
|
||||
// If giver is a person, check if it's not "You"
|
||||
return this.giverDid !== this.activeDid;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -340,7 +340,8 @@ export default class ContactImportView extends Vue {
|
||||
*/
|
||||
private async initializeSettings() {
|
||||
const settings = await this.$accountSettings();
|
||||
this.activeDid = settings.activeDid || "";
|
||||
// Use new Active Identity façade instead of settings.activeDid
|
||||
this.activeDid = (await this.$getActiveDid()) || "";
|
||||
this.apiServer = settings.apiServer || "";
|
||||
}
|
||||
|
||||
|
||||
@@ -220,21 +220,21 @@ export default class ContactQRScanFull extends Vue {
|
||||
* Computed property for QR code container CSS classes
|
||||
*/
|
||||
get qrContainerClasses(): string {
|
||||
return "block w-full max-w-[calc((100vh-env(safe-area-inset-top)-env(safe-area-inset-bottom))*0.4)] mx-auto mt-4";
|
||||
return "block w-full max-w-[calc((100vh-max(env(safe-area-inset-top),var(--safe-area-inset-top,0px))-max(env(safe-area-inset-bottom),var(--safe-area-inset-bottom,0px)))*0.4)] mx-auto mt-4";
|
||||
}
|
||||
|
||||
/**
|
||||
* Computed property for camera frame CSS classes
|
||||
*/
|
||||
get cameraFrameClasses(): string {
|
||||
return "relative w-full max-w-[calc((100vh-env(safe-area-inset-top)-env(safe-area-inset-bottom))*0.4)] mx-auto border border-dashed border-white mt-8 aspect-square";
|
||||
return "relative w-full max-w-[calc((100vh-max(env(safe-area-inset-top),var(--safe-area-inset-top,0px))-max(env(safe-area-inset-bottom),var(--safe-area-inset-bottom,0px)))*0.4)] mx-auto border border-dashed border-white mt-8 aspect-square";
|
||||
}
|
||||
|
||||
/**
|
||||
* Computed property for main content container CSS classes
|
||||
*/
|
||||
get mainContentClasses(): string {
|
||||
return "p-6 bg-white w-full max-w-[calc((100vh-env(safe-area-inset-top)-env(safe-area-inset-bottom))*0.4)] mx-auto";
|
||||
return "p-6 bg-white w-full max-w-[calc((100vh-max(env(safe-area-inset-top),var(--safe-area-inset-top,0px))-max(env(safe-area-inset-bottom),var(--safe-area-inset-bottom,0px)))*0.4)] mx-auto";
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -265,7 +265,8 @@ export default class ContactQRScanFull extends Vue {
|
||||
async created() {
|
||||
try {
|
||||
const settings = await this.$accountSettings();
|
||||
this.activeDid = settings.activeDid || "";
|
||||
// Use new Active Identity façade instead of settings.activeDid
|
||||
this.activeDid = (await this.$getActiveDid()) || "";
|
||||
this.apiServer = settings.apiServer || "";
|
||||
this.givenName = settings.firstName || "";
|
||||
this.isRegistered = !!settings.isRegistered;
|
||||
|
||||
@@ -140,7 +140,7 @@ import { AxiosError } from "axios";
|
||||
import { Buffer } from "buffer/";
|
||||
import QRCodeVue3 from "qr-code-generator-vue3";
|
||||
import { Component, Vue } from "vue-facing-decorator";
|
||||
import { useClipboard } from "@vueuse/core";
|
||||
|
||||
import { QrcodeStream } from "vue-qrcode-reader";
|
||||
|
||||
import QuickNav from "../components/QuickNav.vue";
|
||||
@@ -183,8 +183,6 @@ import {
|
||||
NOTIFY_QR_PROCESSING_ERROR,
|
||||
createQRContactAddedMessage,
|
||||
createQRRegistrationSuccessMessage,
|
||||
QR_TIMEOUT_SHORT,
|
||||
QR_TIMEOUT_MEDIUM,
|
||||
QR_TIMEOUT_STANDARD,
|
||||
QR_TIMEOUT_LONG,
|
||||
} from "@/constants/notifications";
|
||||
@@ -259,11 +257,11 @@ export default class ContactQRScanShow extends Vue {
|
||||
}
|
||||
|
||||
get qrCodeContainerClasses(): string {
|
||||
return "block w-[90vw] max-w-[calc((100vh-env(safe-area-inset-top)-env(safe-area-inset-bottom))*0.4)] mx-auto my-4";
|
||||
return "block w-[90vw] max-w-[calc((100vh-max(env(safe-area-inset-top),var(--safe-area-inset-top,0px))-max(env(safe-area-inset-bottom),var(--safe-area-inset-bottom,0px)))*0.4)] mx-auto my-4";
|
||||
}
|
||||
|
||||
get scannerContainerClasses(): string {
|
||||
return "relative aspect-square overflow-hidden bg-slate-800 w-[90vw] max-w-[calc((100vh-env(safe-area-inset-top)-env(safe-area-inset-bottom))*0.4)] mx-auto";
|
||||
return "relative aspect-square overflow-hidden bg-slate-800 w-[90vw] max-w-[calc((100vh-max(env(safe-area-inset-top),var(--safe-area-inset-top,0px))-max(env(safe-area-inset-bottom),var(--safe-area-inset-bottom,0px)))*0.4)] mx-auto";
|
||||
}
|
||||
|
||||
get statusMessageClasses(): string {
|
||||
@@ -288,7 +286,8 @@ export default class ContactQRScanShow extends Vue {
|
||||
|
||||
try {
|
||||
const settings = await this.$accountSettings();
|
||||
this.activeDid = settings.activeDid || "";
|
||||
// Use new Active Identity façade instead of settings.activeDid
|
||||
this.activeDid = (await this.$getActiveDid()) || "";
|
||||
this.apiServer = settings.apiServer || "";
|
||||
this.givenName = settings.firstName || "";
|
||||
this.hideRegisterPromptOnNewContact =
|
||||
@@ -544,11 +543,7 @@ export default class ContactQRScanShow extends Vue {
|
||||
did: contact.did,
|
||||
name: contact.name,
|
||||
});
|
||||
this.notify.toast(
|
||||
"Submitted",
|
||||
NOTIFY_QR_REGISTRATION_SUBMITTED.message,
|
||||
QR_TIMEOUT_SHORT,
|
||||
);
|
||||
this.notify.toast("Submitted", NOTIFY_QR_REGISTRATION_SUBMITTED.message);
|
||||
|
||||
try {
|
||||
const regResult = await register(
|
||||
@@ -624,18 +619,15 @@ export default class ContactQRScanShow extends Vue {
|
||||
);
|
||||
|
||||
// Copy the URL to clipboard
|
||||
useClipboard()
|
||||
.copy(jwtUrl)
|
||||
.then(() => {
|
||||
this.notify.toast(
|
||||
"Copied",
|
||||
NOTIFY_QR_URL_COPIED.message,
|
||||
QR_TIMEOUT_MEDIUM,
|
||||
);
|
||||
});
|
||||
const { copyToClipboard } = await import("../services/ClipboardService");
|
||||
await copyToClipboard(jwtUrl);
|
||||
this.notify.toast(
|
||||
NOTIFY_QR_URL_COPIED.title,
|
||||
NOTIFY_QR_URL_COPIED.message,
|
||||
);
|
||||
} catch (error) {
|
||||
logger.error("Failed to generate contact URL:", error);
|
||||
this.notify.error("Failed to generate contact URL. Please try again.");
|
||||
this.$logAndConsole(`Error copying URL to clipboard: ${error}`, true);
|
||||
this.notify.error("Failed to copy URL to clipboard.");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -643,13 +635,16 @@ export default class ContactQRScanShow extends Vue {
|
||||
this.notify.info(NOTIFY_QR_CODE_HELP.message, QR_TIMEOUT_LONG);
|
||||
}
|
||||
|
||||
onCopyDidToClipboard() {
|
||||
async onCopyDidToClipboard() {
|
||||
//this.onScanDetect([{ rawValue: this.qrValue }]); // good for testing
|
||||
useClipboard()
|
||||
.copy(this.activeDid)
|
||||
.then(() => {
|
||||
this.notify.info(NOTIFY_QR_DID_COPIED.message, QR_TIMEOUT_LONG);
|
||||
});
|
||||
try {
|
||||
const { copyToClipboard } = await import("../services/ClipboardService");
|
||||
await copyToClipboard(this.activeDid);
|
||||
this.notify.info(NOTIFY_QR_DID_COPIED.message, QR_TIMEOUT_LONG);
|
||||
} catch (error) {
|
||||
this.$logAndConsole(`Error copying DID to clipboard: ${error}`, true);
|
||||
this.notify.error("Failed to copy DID to clipboard.");
|
||||
}
|
||||
}
|
||||
|
||||
openUserNameDialog() {
|
||||
@@ -745,7 +740,6 @@ export default class ContactQRScanShow extends Vue {
|
||||
) {
|
||||
setTimeout(() => {
|
||||
this.notify.confirm(
|
||||
"Register",
|
||||
"Do you want to register them?",
|
||||
{
|
||||
onCancel: async (stopAsking?: boolean) => {
|
||||
|
||||
@@ -130,10 +130,9 @@ import { JWTPayload } from "did-jwt";
|
||||
import * as R from "ramda";
|
||||
import { Component, Vue } from "vue-facing-decorator";
|
||||
import { RouteLocationNormalizedLoaded, Router } from "vue-router";
|
||||
import { useClipboard } from "@vueuse/core";
|
||||
// Capacitor import removed - using PlatformService instead
|
||||
|
||||
import QuickNav from "../components/QuickNav.vue";
|
||||
import { copyToClipboard } from "../services/ClipboardService";
|
||||
import EntityIcon from "../components/EntityIcon.vue";
|
||||
import GiftedDialog from "../components/GiftedDialog.vue";
|
||||
import OfferDialog from "../components/OfferDialog.vue";
|
||||
@@ -295,7 +294,8 @@ export default class ContactsView extends Vue {
|
||||
this.notify = createNotifyHelpers(this.$notify);
|
||||
|
||||
const settings = await this.$accountSettings();
|
||||
this.activeDid = settings.activeDid || "";
|
||||
// Use new Active Identity façade instead of settings.activeDid
|
||||
this.activeDid = (await this.$getActiveDid()) || "";
|
||||
this.apiServer = settings.apiServer || "";
|
||||
this.isRegistered = !!settings.isRegistered;
|
||||
|
||||
@@ -1192,12 +1192,14 @@ export default class ContactsView extends Vue {
|
||||
});
|
||||
// Use production URL for sharing to avoid localhost issues in development
|
||||
const contactsJwtUrl = `${APP_SERVER}/deep-link/contact-import/${contactsJwt}`;
|
||||
useClipboard()
|
||||
.copy(contactsJwtUrl)
|
||||
.then(() => {
|
||||
// Use notification helper
|
||||
this.notify.copied(NOTIFY_CONTACT_LINK_COPIED.message);
|
||||
});
|
||||
try {
|
||||
await copyToClipboard(contactsJwtUrl);
|
||||
// Use notification helper
|
||||
this.notify.copied(NOTIFY_CONTACT_LINK_COPIED.message);
|
||||
} catch (error) {
|
||||
this.$logAndConsole(`Error copying to clipboard: ${error}`, true);
|
||||
this.notify.error("Failed to copy to clipboard. Please try again.");
|
||||
}
|
||||
}
|
||||
|
||||
private showCopySelectionsInfo() {
|
||||
|
||||
@@ -273,6 +273,7 @@ import {
|
||||
didInfoForContact,
|
||||
displayAmount,
|
||||
getHeaders,
|
||||
isDid,
|
||||
register,
|
||||
setVisibilityUtil,
|
||||
} from "../libs/endorserServer";
|
||||
@@ -289,7 +290,9 @@ import {
|
||||
NOTIFY_REGISTRATION_ERROR,
|
||||
NOTIFY_SERVER_ACCESS_ERROR,
|
||||
NOTIFY_NO_IDENTITY_ERROR,
|
||||
NOTIFY_CONTACT_INVALID_DID,
|
||||
} from "@/constants/notifications";
|
||||
import { THAT_UNNAMED_PERSON } from "@/constants/entities";
|
||||
|
||||
/**
|
||||
* DIDView Component
|
||||
@@ -373,28 +376,36 @@ export default class DIDView extends Vue {
|
||||
*/
|
||||
private async initializeSettings() {
|
||||
const settings = await this.$accountSettings();
|
||||
this.activeDid = settings.activeDid || "";
|
||||
// Use new Active Identity façade instead of settings.activeDid
|
||||
this.activeDid = (await this.$getActiveDid()) || "";
|
||||
this.apiServer = settings.apiServer || "";
|
||||
}
|
||||
|
||||
/**
|
||||
* Determines which DID to display based on URL parameters
|
||||
* Falls back to active DID if no parameter provided
|
||||
* Validates DID format and shows error for invalid DIDs
|
||||
*/
|
||||
private async determineDIDToDisplay() {
|
||||
const pathParam = window.location.pathname.substring("/did/".length);
|
||||
let showDid = pathParam;
|
||||
|
||||
if (!showDid) {
|
||||
// No DID provided in URL, use active DID
|
||||
showDid = this.activeDid;
|
||||
if (showDid) {
|
||||
this.notifyDefaultToActiveDID();
|
||||
this.notifyDefaultToActiveDID();
|
||||
} else {
|
||||
// DID provided in URL, validate it
|
||||
const decodedDid = decodeURIComponent(showDid);
|
||||
if (!isDid(decodedDid)) {
|
||||
// Invalid DID format - show error and redirect
|
||||
this.notify.error(NOTIFY_CONTACT_INVALID_DID.message, TIMEOUTS.LONG);
|
||||
this.$router.push({ name: "home" });
|
||||
return;
|
||||
}
|
||||
showDid = decodedDid;
|
||||
}
|
||||
|
||||
if (showDid) {
|
||||
this.viewingDid = decodeURIComponent(showDid);
|
||||
}
|
||||
this.viewingDid = showDid;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -551,7 +562,7 @@ export default class DIDView extends Vue {
|
||||
contact.registered = true;
|
||||
await this.$updateContact(contact.did, { registered: true });
|
||||
|
||||
const name = contact.name || "That unnamed person";
|
||||
const name = contact.name || THAT_UNNAMED_PERSON;
|
||||
this.notify.success(
|
||||
`${name} ${NOTIFY_REGISTRATION_SUCCESS.message}`,
|
||||
TIMEOUTS.LONG,
|
||||
@@ -831,26 +842,3 @@ export default class DIDView extends Vue {
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style>
|
||||
.dialog-overlay {
|
||||
z-index: 50;
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
background-color: rgba(0, 0, 0, 0.5);
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
padding: 1.5rem;
|
||||
}
|
||||
.dialog {
|
||||
background-color: white;
|
||||
padding: 1rem;
|
||||
border-radius: 0.5rem;
|
||||
width: 100%;
|
||||
max-width: 500px;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<template>
|
||||
<div class="deep-link-error">
|
||||
<div class="safe-area-spacer"></div>
|
||||
<!-- CONTENT -->
|
||||
<section id="Content" class="p-6 pb-24 max-w-3xl mx-auto">
|
||||
<h1>Invalid Deep Link</h1>
|
||||
<div class="error-details">
|
||||
<div class="error-message">
|
||||
@@ -39,7 +39,7 @@
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
@@ -114,18 +114,6 @@ onMounted(() => {
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.deep-link-error {
|
||||
padding-top: 60px;
|
||||
padding-left: 20px;
|
||||
padding-right: 20px;
|
||||
max-width: 600px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
.safe-area-spacer {
|
||||
height: env(safe-area-inset-top);
|
||||
}
|
||||
|
||||
h1 {
|
||||
color: #ff4444;
|
||||
margin-bottom: 24px;
|
||||
|
||||
@@ -1,95 +1,87 @@
|
||||
<template>
|
||||
<!-- CONTENT -->
|
||||
<section id="Content" class="relative w-[100vw] h-[100vh]">
|
||||
<div
|
||||
class="p-6 bg-white w-full max-w-[calc((100vh-env(safe-area-inset-top)-env(safe-area-inset-bottom))*0.4)] mx-auto"
|
||||
>
|
||||
<div class="mb-4">
|
||||
<h1 class="text-xl text-center font-semibold relative mb-4">
|
||||
Redirecting to Time Safari
|
||||
</h1>
|
||||
<section id="Content" class="p-6 pb-24 max-w-3xl mx-auto">
|
||||
<div class="mb-4">
|
||||
<h1 class="text-2xl text-center font-semibold relative px-7">
|
||||
Redirecting to Time Safari
|
||||
</h1>
|
||||
|
||||
<div v-if="destinationUrl" class="space-y-4">
|
||||
<!-- Platform-specific messaging -->
|
||||
<div class="text-center text-gray-600 mb-4">
|
||||
<p v-if="isMobile">
|
||||
{{
|
||||
isIOS
|
||||
? "Opening Time Safari app on your iPhone..."
|
||||
: "Opening Time Safari app on your Android device..."
|
||||
}}
|
||||
</p>
|
||||
<p v-else>Opening Time Safari app...</p>
|
||||
<p class="text-sm mt-2">
|
||||
<span v-if="isMobile"
|
||||
>If the app doesn't open automatically, use one of these
|
||||
options:</span
|
||||
>
|
||||
<span v-else>Choose how you'd like to open this link:</span>
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<!-- Deep Link Button -->
|
||||
<div class="text-center">
|
||||
<a
|
||||
:href="deepLinkUrl || '#'"
|
||||
class="inline-block bg-blue-600 text-white px-6 py-3 rounded-lg font-medium hover:bg-blue-700 transition-colors"
|
||||
@click="handleDeepLinkClick"
|
||||
<div v-if="destinationUrl" class="space-y-4">
|
||||
<!-- Platform-specific messaging -->
|
||||
<div class="text-center text-gray-600 mb-4">
|
||||
<p v-if="isMobile">
|
||||
{{
|
||||
isIOS
|
||||
? "Opening Time Safari app on your iPhone..."
|
||||
: "Opening Time Safari app on your Android device..."
|
||||
}}
|
||||
</p>
|
||||
<p v-else>Opening Time Safari app...</p>
|
||||
<p class="text-sm mt-2">
|
||||
<span v-if="isMobile"
|
||||
>If the app doesn't open automatically, use one of these
|
||||
options:</span
|
||||
>
|
||||
<span v-if="isMobile">Open in Time Safari App</span>
|
||||
<span v-else>Try Opening in Time Safari App</span>
|
||||
</a>
|
||||
</div>
|
||||
<span v-else>Choose how you'd like to open this link:</span>
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<!-- Web Fallback Link -->
|
||||
<div class="text-center">
|
||||
<a
|
||||
:href="webUrl || '#'"
|
||||
target="_blank"
|
||||
class="inline-block bg-gray-600 text-white px-6 py-3 rounded-lg font-medium hover:bg-gray-700 transition-colors"
|
||||
@click="handleWebFallbackClick"
|
||||
>
|
||||
<span v-if="isMobile">Open in Web Browser Instead</span>
|
||||
<span v-else>Open in Web Browser</span>
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<!-- Manual Instructions -->
|
||||
<div class="text-center text-sm text-gray-500 mt-4">
|
||||
<p v-if="isMobile">
|
||||
Or manually open:
|
||||
<code class="bg-gray-100 px-2 py-1 rounded">{{
|
||||
deepLinkUrl
|
||||
}}</code>
|
||||
</p>
|
||||
<p v-else>
|
||||
If you have the Time Safari app installed, you can also copy this
|
||||
link:
|
||||
<code class="bg-gray-100 px-2 py-1 rounded">{{
|
||||
deepLinkUrl
|
||||
}}</code>
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<!-- Platform info for debugging -->
|
||||
<div
|
||||
v-if="isDevelopment"
|
||||
class="text-center text-xs text-gray-400 mt-4"
|
||||
<!-- Deep Link Button -->
|
||||
<div class="text-center">
|
||||
<a
|
||||
:href="deepLinkUrl || '#'"
|
||||
class="inline-block bg-blue-600 text-white px-6 py-3 rounded-lg font-medium hover:bg-blue-700 transition-colors"
|
||||
@click="handleDeepLinkClick"
|
||||
>
|
||||
<p>
|
||||
Platform: {{ isMobile ? (isIOS ? "iOS" : "Android") : "Desktop" }}
|
||||
</p>
|
||||
<p>User Agent: {{ userAgent.substring(0, 50) }}...</p>
|
||||
</div>
|
||||
<span v-if="isMobile">Open in Time Safari App</span>
|
||||
<span v-else>Try Opening in Time Safari App</span>
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<div v-else-if="pageError" class="text-center text-red-500 mb-4">
|
||||
{{ pageError }}
|
||||
<!-- Web Fallback Link -->
|
||||
<div class="text-center">
|
||||
<a
|
||||
:href="webUrl || '#'"
|
||||
target="_blank"
|
||||
class="inline-block bg-gray-600 text-white px-6 py-3 rounded-lg font-medium hover:bg-gray-700 transition-colors"
|
||||
@click="handleWebFallbackClick"
|
||||
>
|
||||
<span v-if="isMobile">Open in Web Browser Instead</span>
|
||||
<span v-else>Open in Web Browser</span>
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<div v-else class="text-center text-gray-600">
|
||||
<p>Processing redirect...</p>
|
||||
<!-- Manual Instructions -->
|
||||
<div class="text-center text-sm text-gray-500 mt-4">
|
||||
<p v-if="isMobile">
|
||||
Or manually open:
|
||||
<code class="bg-gray-100 px-2 py-1 rounded">{{ deepLinkUrl }}</code>
|
||||
</p>
|
||||
<p v-else>
|
||||
If you have the Time Safari app installed, you can also copy this
|
||||
link:
|
||||
<code class="bg-gray-100 px-2 py-1 rounded">{{ deepLinkUrl }}</code>
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<!-- Platform info for debugging -->
|
||||
<div
|
||||
v-if="isDevelopment"
|
||||
class="text-center text-xs text-gray-400 mt-4"
|
||||
>
|
||||
<p>
|
||||
Platform: {{ isMobile ? (isIOS ? "iOS" : "Android") : "Desktop" }}
|
||||
</p>
|
||||
<p>User Agent: {{ userAgent.substring(0, 50) }}...</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-else-if="pageError" class="text-center text-red-500 mb-4">
|
||||
{{ pageError }}
|
||||
</div>
|
||||
|
||||
<div v-else class="text-center text-gray-600">
|
||||
<p>Processing redirect...</p>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
@@ -233,7 +233,7 @@
|
||||
|
||||
<div class="grow">
|
||||
<h2 class="text-base font-semibold">
|
||||
{{ project.name || "Unnamed Project" }}
|
||||
{{ project.name || unnamedProject }}
|
||||
</h2>
|
||||
<div class="text-sm">
|
||||
<font-awesome
|
||||
@@ -340,6 +340,7 @@ import {
|
||||
NOTIFY_DISCOVER_LOCAL_SEARCH_ERROR,
|
||||
NOTIFY_DISCOVER_MAP_SEARCH_ERROR,
|
||||
} from "@/constants/notifications";
|
||||
import { UNNAMED_PROJECT } from "@/constants/entities";
|
||||
interface Tile {
|
||||
indexLat: number;
|
||||
indexLon: number;
|
||||
@@ -370,6 +371,13 @@ export default class DiscoverView extends Vue {
|
||||
|
||||
notify!: ReturnType<typeof createNotifyHelpers>;
|
||||
|
||||
/**
|
||||
* Get the unnamed project constant
|
||||
*/
|
||||
get unnamedProject(): string {
|
||||
return UNNAMED_PROJECT;
|
||||
}
|
||||
|
||||
activeDid = "";
|
||||
allContacts: Array<Contact> = [];
|
||||
allMyDids: Array<string> = [];
|
||||
@@ -407,7 +415,8 @@ export default class DiscoverView extends Vue {
|
||||
const searchPeople = !!this.$route.query["searchPeople"];
|
||||
|
||||
const settings = await this.$accountSettings();
|
||||
this.activeDid = (settings.activeDid as string) || "";
|
||||
// Use new Active Identity façade instead of settings.activeDid
|
||||
this.activeDid = (await this.$getActiveDid()) || "";
|
||||
this.apiServer = (settings.apiServer as string) || "";
|
||||
this.partnerApiServer =
|
||||
(settings.partnerApiServer as string) || this.partnerApiServer;
|
||||
|
||||
@@ -441,7 +441,8 @@ export default class GiftedDetails extends Vue {
|
||||
|
||||
const settings = await this.$accountSettings();
|
||||
this.apiServer = settings.apiServer || "";
|
||||
this.activeDid = settings.activeDid || "";
|
||||
// Use new Active Identity façade instead of settings.activeDid
|
||||
this.activeDid = (await this.$getActiveDid()) || "";
|
||||
|
||||
if (
|
||||
(this.giverDid && !this.giverName) ||
|
||||
|
||||
@@ -200,7 +200,7 @@
|
||||
</p>
|
||||
<p>
|
||||
Then you can record your appreciation for... whatever: select any contact on the home page
|
||||
(or "Unnamed") and send it. The main goal is to record what people
|
||||
(or "{{ unnamedEntityName }}") and send it. The main goal is to record what people
|
||||
have given you, to grow giving economies. You can also record your own
|
||||
ideas for projects. Each claim is recorded on a
|
||||
custom ledger.
|
||||
@@ -600,6 +600,7 @@ import QuickNav from "../components/QuickNav.vue";
|
||||
import { APP_SERVER } from "../constants/app";
|
||||
import { PlatformServiceMixin } from "@/utils/PlatformServiceMixin";
|
||||
import { QRNavigationService } from "@/services/QRNavigationService";
|
||||
import { UNNAMED_ENTITY_NAME } from "@/constants/entities";
|
||||
|
||||
/**
|
||||
* HelpView.vue - Comprehensive Help System Component
|
||||
@@ -647,6 +648,13 @@ export default class HelpView extends Vue {
|
||||
APP_SERVER = APP_SERVER;
|
||||
// Capacitor reference removed - using QRNavigationService instead
|
||||
|
||||
/**
|
||||
* Get the unnamed entity name constant
|
||||
*/
|
||||
get unnamedEntityName(): string {
|
||||
return UNNAMED_ENTITY_NAME;
|
||||
}
|
||||
|
||||
// Ideally, we put no functionality in here, especially in the setup,
|
||||
// because we never want this page to have a chance of throwing an error.
|
||||
|
||||
@@ -680,7 +688,9 @@ export default class HelpView extends Vue {
|
||||
try {
|
||||
const settings = await this.$accountSettings();
|
||||
|
||||
if (settings.activeDid) {
|
||||
// Use new Active Identity façade instead of settings.activeDid
|
||||
const activeDid = await this.$getActiveDid();
|
||||
if (activeDid) {
|
||||
await this.$updateSettings({
|
||||
...settings,
|
||||
finishedOnboarding: false,
|
||||
@@ -688,7 +698,7 @@ export default class HelpView extends Vue {
|
||||
|
||||
this.$log(
|
||||
"[HelpView] Onboarding reset successfully for DID: " +
|
||||
settings.activeDid,
|
||||
activeDid,
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -282,6 +282,7 @@ import { PlatformServiceMixin } from "@/utils/PlatformServiceMixin";
|
||||
import { createNotifyHelpers, TIMEOUTS } from "@/utils/notify";
|
||||
import { NOTIFY_CONTACT_LOADING_ISSUE } from "@/constants/notifications";
|
||||
import * as Package from "../../package.json";
|
||||
import { UNNAMED_ENTITY_NAME } from "@/constants/entities";
|
||||
|
||||
// consolidate this with GiveActionClaim in src/interfaces/claims.ts
|
||||
interface Claim {
|
||||
@@ -430,6 +431,7 @@ export default class HomeView extends Vue {
|
||||
* Called automatically by Vue lifecycle system
|
||||
*/
|
||||
async mounted() {
|
||||
logger.debug("[HomeView] mounted() starting");
|
||||
try {
|
||||
await this.initializeIdentity();
|
||||
// Settings already loaded in initializeIdentity()
|
||||
@@ -470,6 +472,7 @@ export default class HomeView extends Vue {
|
||||
* @throws Logs error if DID retrieval fails
|
||||
*/
|
||||
private async initializeIdentity() {
|
||||
logger.debug("[HomeView] initializeIdentity() starting");
|
||||
try {
|
||||
// Retrieve DIDs with better error handling
|
||||
try {
|
||||
@@ -514,7 +517,11 @@ export default class HomeView extends Vue {
|
||||
// **CRITICAL**: Ensure correct API server for platform
|
||||
await this.ensureCorrectApiServer();
|
||||
|
||||
this.activeDid = settings.activeDid || "";
|
||||
// Use new façade method with legacy fallback
|
||||
const retrievedActiveDid = await this.$getActiveDid();
|
||||
logger.debug("[HomeView] Retrieved activeDid:", retrievedActiveDid);
|
||||
this.activeDid = retrievedActiveDid || "";
|
||||
logger.debug("[HomeView] Set activeDid to:", this.activeDid);
|
||||
|
||||
// Load contacts with graceful fallback
|
||||
try {
|
||||
@@ -1546,30 +1553,41 @@ export default class HomeView extends Vue {
|
||||
* @param giver Optional contact info for giver
|
||||
* @param description Optional gift description
|
||||
*/
|
||||
openDialog(giver?: GiverReceiverInputInfo | "Unnamed", prompt?: string) {
|
||||
if (giver === "Unnamed") {
|
||||
// Special case: Pass undefined to trigger Step 1, but with "Unnamed" pre-selected
|
||||
(this.$refs.giftedDialog as GiftedDialog).open(
|
||||
undefined,
|
||||
{
|
||||
did: this.activeDid,
|
||||
name: "You",
|
||||
} as GiverReceiverInputInfo,
|
||||
undefined,
|
||||
prompt,
|
||||
);
|
||||
// Immediately select "Unnamed" and move to Step 2
|
||||
(this.$refs.giftedDialog as GiftedDialog).selectGiver();
|
||||
openDialog(giver?: GiverReceiverInputInfo, prompt?: string) {
|
||||
// Determine the giver entity based on DID logic
|
||||
const giverEntity = this.createGiverEntity(giver);
|
||||
|
||||
(this.$refs.giftedDialog as GiftedDialog).open(
|
||||
giverEntity,
|
||||
{
|
||||
did: this.activeDid,
|
||||
name: "You", // In HomeView, we always use "You" as the giver
|
||||
} as GiverReceiverInputInfo,
|
||||
undefined,
|
||||
prompt,
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates giver entity using DID-based logic
|
||||
*/
|
||||
private createGiverEntity(
|
||||
giver?: GiverReceiverInputInfo,
|
||||
): GiverReceiverInputInfo | undefined {
|
||||
if (!giver) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
// Handle GiverReceiverInputInfo object
|
||||
if (giver.did === this.activeDid) {
|
||||
// If DID matches active DID, create "You" entity
|
||||
return { did: this.activeDid, name: "You" };
|
||||
} else if (!giver.did || giver.did === "") {
|
||||
// If DID is empty/null, create "Unnamed" entity
|
||||
return { did: "", name: UNNAMED_ENTITY_NAME };
|
||||
} else {
|
||||
(this.$refs.giftedDialog as GiftedDialog).open(
|
||||
giver,
|
||||
{
|
||||
did: this.activeDid,
|
||||
name: "You",
|
||||
} as GiverReceiverInputInfo,
|
||||
undefined,
|
||||
prompt,
|
||||
);
|
||||
// Return the giver as-is
|
||||
return giver;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1632,10 +1650,15 @@ export default class HomeView extends Vue {
|
||||
this.isImageViewerOpen = true;
|
||||
}
|
||||
|
||||
openPersonDialog(
|
||||
giver?: GiverReceiverInputInfo | "Unnamed",
|
||||
prompt?: string,
|
||||
) {
|
||||
private handleQRCodeClick() {
|
||||
if (Capacitor.isNativePlatform()) {
|
||||
this.$router.push({ name: "contact-qr-scan-full" });
|
||||
} else {
|
||||
this.$router.push({ name: "contact-qr" });
|
||||
}
|
||||
}
|
||||
|
||||
openPersonDialog(giver?: GiverReceiverInputInfo, prompt?: string) {
|
||||
this.showProjectsDialog = false;
|
||||
this.openDialog(giver, prompt);
|
||||
}
|
||||
|
||||
@@ -46,7 +46,7 @@
|
||||
<div class="flex items-center justify-between mb-2">
|
||||
<div
|
||||
:class="identityListItemClasses"
|
||||
@click="switchAccount(ident.did)"
|
||||
@click="switchIdentity(ident.did)"
|
||||
>
|
||||
<font-awesome
|
||||
v-if="ident.did === activeDid"
|
||||
@@ -94,7 +94,7 @@
|
||||
<a
|
||||
href="#"
|
||||
:class="secondaryButtonClasses"
|
||||
@click="switchAccount(undefined)"
|
||||
@click="switchIdentity(undefined)"
|
||||
>
|
||||
No Identity
|
||||
</a>
|
||||
@@ -116,6 +116,7 @@ import {
|
||||
NOTIFY_DELETE_IDENTITY_CONFIRM,
|
||||
} from "@/constants/notifications";
|
||||
import { Account } from "@/db/tables/accounts";
|
||||
import { FLAGS } from "@/config/featureFlags";
|
||||
|
||||
@Component({
|
||||
components: { QuickNav },
|
||||
@@ -200,7 +201,8 @@ export default class IdentitySwitcherView extends Vue {
|
||||
async created() {
|
||||
try {
|
||||
const settings = await this.$accountSettings();
|
||||
this.activeDid = settings.activeDid || "";
|
||||
// Use new façade method with legacy fallback
|
||||
this.activeDid = (await this.$getActiveDid()) || "";
|
||||
this.apiServer = settings.apiServer || "";
|
||||
this.apiServerInput = settings.apiServer || "";
|
||||
|
||||
@@ -221,46 +223,63 @@ export default class IdentitySwitcherView extends Vue {
|
||||
}
|
||||
}
|
||||
|
||||
async switchAccount(did?: string) {
|
||||
// Save the new active DID to master settings
|
||||
await this.$saveSettings({ activeDid: did });
|
||||
async switchIdentity(did?: string) {
|
||||
try {
|
||||
if (did) {
|
||||
// Use new façade method instead of legacy settings
|
||||
await this.$setActiveDid(did);
|
||||
|
||||
// Check if we need to load user-specific settings for the new DID
|
||||
if (did) {
|
||||
try {
|
||||
const newSettings = await this.$accountSettings(did);
|
||||
logger.info(
|
||||
"[IdentitySwitcher Settings Trace] ✅ New account settings loaded",
|
||||
{
|
||||
did,
|
||||
settingsKeys: Object.keys(newSettings).filter(
|
||||
(k) =>
|
||||
k in newSettings &&
|
||||
newSettings[k as keyof typeof newSettings] !== 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
|
||||
// Update local state
|
||||
this.activeDid = did;
|
||||
|
||||
// Legacy fallback - remove after Phase C
|
||||
if (!FLAGS.USE_ACTIVE_IDENTITY_ONLY) {
|
||||
await this.$saveSettings({ activeDid: did });
|
||||
}
|
||||
|
||||
// Check if we need to load user-specific settings for the new DID
|
||||
try {
|
||||
const newSettings = await this.$accountSettings(did);
|
||||
logger.info(
|
||||
"[IdentitySwitcher Settings Trace] ✅ New account settings loaded",
|
||||
{
|
||||
did,
|
||||
settingsKeys: Object.keys(newSettings).filter(
|
||||
(k) =>
|
||||
k in newSettings &&
|
||||
newSettings[k as keyof typeof newSettings] !== 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
|
||||
}
|
||||
} else {
|
||||
// Handle "No Identity" case
|
||||
this.activeDid = "";
|
||||
// Note: We don't clear active DID in database for safety
|
||||
}
|
||||
|
||||
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" });
|
||||
} catch (error) {
|
||||
logger.error("[IdentitySwitcher] Error switching identity", error);
|
||||
this.notify.error("Error switching identity", TIMEOUTS.SHORT);
|
||||
}
|
||||
|
||||
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" });
|
||||
}
|
||||
|
||||
async deleteAccount(id: string) {
|
||||
|
||||
@@ -207,11 +207,12 @@ export default class ImportAccountView extends Vue {
|
||||
// Check what was actually imported
|
||||
const settings = await this.$accountSettings();
|
||||
|
||||
// Check account-specific settings
|
||||
if (settings?.activeDid) {
|
||||
// Check account-specific settings using Active Identity façade
|
||||
const activeDid = await this.$getActiveDid();
|
||||
if (activeDid) {
|
||||
try {
|
||||
await this.$query("SELECT * FROM settings WHERE accountDid = ?", [
|
||||
settings.activeDid,
|
||||
activeDid,
|
||||
]);
|
||||
} catch (error) {
|
||||
// Log error but don't interrupt import flow
|
||||
|
||||
@@ -173,8 +173,13 @@ export default class ImportAccountView extends Vue {
|
||||
try {
|
||||
await saveNewIdentity(newId, mne, newDerivPath);
|
||||
|
||||
// record that as the active DID
|
||||
await this.$saveSettings({ activeDid: newId.did });
|
||||
// record that as the active DID using new façade
|
||||
await this.$setActiveDid(newId.did);
|
||||
|
||||
// Legacy fallback - remove after Phase C
|
||||
if (!FLAGS.USE_ACTIVE_IDENTITY_ONLY) {
|
||||
await this.$saveSettings({ activeDid: newId.did });
|
||||
}
|
||||
await this.$saveUserSettings(newId.did, {
|
||||
isRegistered: false,
|
||||
});
|
||||
|
||||
@@ -46,6 +46,7 @@ import { APP_SERVER } from "../constants/app";
|
||||
import { decodeEndorserJwt } from "../libs/crypto/vc";
|
||||
import { errorStringForLog } from "../libs/endorserServer";
|
||||
import { generateSaveAndActivateIdentity } from "../libs/util";
|
||||
import { logger } from "../utils/logger";
|
||||
import { PlatformServiceMixin } from "@/utils/PlatformServiceMixin";
|
||||
import { createNotifyHelpers } from "@/utils/notify";
|
||||
import {
|
||||
@@ -120,7 +121,8 @@ export default class InviteOneAcceptView extends Vue {
|
||||
|
||||
// Load or generate identity
|
||||
const settings = await this.$accountSettings();
|
||||
this.activeDid = settings.activeDid || "";
|
||||
// Use new Active Identity façade instead of settings.activeDid
|
||||
this.activeDid = (await this.$getActiveDid()) || "";
|
||||
this.apiServer = settings.apiServer || "";
|
||||
|
||||
// Identity creation should be handled by router guard, but keep as fallback for deep links
|
||||
|
||||
@@ -283,7 +283,8 @@ export default class InviteOneView extends Vue {
|
||||
try {
|
||||
// Use PlatformServiceMixin for account settings
|
||||
const settings = await this.$accountSettings();
|
||||
this.activeDid = settings.activeDid || "";
|
||||
// Use new Active Identity façade instead of settings.activeDid
|
||||
this.activeDid = (await this.$getActiveDid()) || "";
|
||||
this.apiServer = settings.apiServer || "";
|
||||
this.isRegistered = !!settings.isRegistered;
|
||||
|
||||
|
||||
@@ -202,7 +202,8 @@ export default class NewActivityView extends Vue {
|
||||
try {
|
||||
const settings = await this.$accountSettings();
|
||||
this.apiServer = settings.apiServer || "";
|
||||
this.activeDid = settings.activeDid || "";
|
||||
// Use new Active Identity façade instead of settings.activeDid
|
||||
this.activeDid = (await this.$getActiveDid()) || "";
|
||||
this.lastAckedOfferToUserJwtId = settings.lastAckedOfferToUserJwtId || "";
|
||||
this.lastAckedOfferToUserProjectsJwtId =
|
||||
settings.lastAckedOfferToUserProjectsJwtId || "";
|
||||
|
||||
@@ -378,7 +378,8 @@ export default class NewEditProjectView extends Vue {
|
||||
this.numAccounts = await retrieveAccountCount();
|
||||
|
||||
const settings = await this.$accountSettings();
|
||||
this.activeDid = settings.activeDid || "";
|
||||
// Use new Active Identity façade instead of settings.activeDid
|
||||
this.activeDid = (await this.$getActiveDid()) || "";
|
||||
this.apiServer = settings.apiServer || "";
|
||||
this.showGeneralAdvanced = !!settings.showGeneralAdvanced;
|
||||
|
||||
|
||||
@@ -433,7 +433,8 @@ export default class OfferDetailsView extends Vue {
|
||||
private async loadAccountSettings() {
|
||||
const settings = await this.$accountSettings();
|
||||
this.apiServer = settings.apiServer ?? "";
|
||||
this.activeDid = settings.activeDid ?? "";
|
||||
// Use new Active Identity façade instead of settings.activeDid
|
||||
this.activeDid = (await this.$getActiveDid()) ?? "";
|
||||
this.showGeneralAdvanced = settings.showGeneralAdvanced ?? false;
|
||||
}
|
||||
|
||||
|
||||
@@ -174,7 +174,8 @@ export default class OnboardMeetingListView extends Vue {
|
||||
// Load user account settings
|
||||
const settings = await this.$accountSettings();
|
||||
|
||||
this.activeDid = settings?.activeDid || "";
|
||||
// Use new Active Identity façade instead of settings.activeDid
|
||||
this.activeDid = (await this.$getActiveDid()) || "";
|
||||
this.apiServer = settings?.apiServer || "";
|
||||
this.firstName = settings?.firstName || "";
|
||||
this.isRegistered = !!settings?.isRegistered;
|
||||
|
||||
@@ -106,7 +106,8 @@ export default class OnboardMeetingMembersView extends Vue {
|
||||
return;
|
||||
}
|
||||
const settings = await this.$accountSettings();
|
||||
this.activeDid = settings?.activeDid || "";
|
||||
// Use new Active Identity façade instead of settings.activeDid
|
||||
this.activeDid = (await this.$getActiveDid()) || "";
|
||||
this.apiServer = settings?.apiServer || "";
|
||||
this.firstName = settings?.firstName || "";
|
||||
this.isRegistered = !!settings?.isRegistered;
|
||||
|
||||
@@ -349,7 +349,8 @@ export default class OnboardMeetingView extends Vue {
|
||||
this.$notify as Parameters<typeof createNotifyHelpers>[0],
|
||||
);
|
||||
const settings = await this.$accountSettings();
|
||||
this.activeDid = settings?.activeDid || "";
|
||||
// Use new Active Identity façade instead of settings.activeDid
|
||||
this.activeDid = (await this.$getActiveDid()) || "";
|
||||
this.apiServer = settings?.apiServer || "";
|
||||
this.fullName = settings?.firstName || "";
|
||||
this.isRegistered = !!settings?.isRegistered;
|
||||
|
||||
@@ -183,7 +183,7 @@
|
||||
class="text-blue-500"
|
||||
@click="onClickLoadProject(plan.handleId)"
|
||||
>
|
||||
{{ plan.name || "Unnamed Project" }}
|
||||
{{ plan.name || unnamedProject }}
|
||||
</button>
|
||||
</div>
|
||||
<div v-if="fulfillersToHitLimit" class="text-center">
|
||||
@@ -207,7 +207,7 @@
|
||||
class="text-blue-500"
|
||||
@click="onClickLoadProject(fulfilledByThis.handleId)"
|
||||
>
|
||||
{{ fulfilledByThis.name || "Unnamed Project" }}
|
||||
{{ fulfilledByThis.name || unnamedProject }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
@@ -611,6 +611,7 @@ import { PlatformServiceMixin } from "@/utils/PlatformServiceMixin";
|
||||
import { createNotifyHelpers, TIMEOUTS } from "@/utils/notify";
|
||||
import { NOTIFY_CONFIRM_CLAIM } from "@/constants/notifications";
|
||||
import { APP_SERVER } from "@/constants/app";
|
||||
import { UNNAMED_PROJECT } from "@/constants/entities";
|
||||
/**
|
||||
* Project View Component
|
||||
* @author Matthew Raymer
|
||||
@@ -664,6 +665,13 @@ export default class ProjectViewView extends Vue {
|
||||
/** Notification helpers instance */
|
||||
notify!: ReturnType<typeof createNotifyHelpers>;
|
||||
|
||||
/**
|
||||
* Get the unnamed project constant
|
||||
*/
|
||||
get unnamedProject(): string {
|
||||
return UNNAMED_PROJECT;
|
||||
}
|
||||
|
||||
// Account and Settings State
|
||||
/** Currently active DID */
|
||||
activeDid = "";
|
||||
@@ -762,7 +770,8 @@ export default class ProjectViewView extends Vue {
|
||||
this.notify = createNotifyHelpers(this.$notify);
|
||||
|
||||
const settings = await this.$accountSettings();
|
||||
this.activeDid = settings.activeDid || "";
|
||||
// Use new Active Identity façade instead of settings.activeDid
|
||||
this.activeDid = (await this.$getActiveDid()) || "";
|
||||
this.apiServer = settings.apiServer || "";
|
||||
this.allContacts = await this.$getAllContacts();
|
||||
this.isRegistered = !!settings.isRegistered;
|
||||
|
||||
@@ -244,7 +244,7 @@
|
||||
|
||||
<div class="grow overflow-hidden">
|
||||
<h2 class="text-base font-semibold">
|
||||
{{ project.name || "Unnamed Project" }}
|
||||
{{ project.name || unnamedProject }}
|
||||
</h2>
|
||||
<div class="text-sm truncate">
|
||||
{{ project.description }}
|
||||
@@ -286,6 +286,7 @@ import {
|
||||
NOTIFY_OFFERS_LOAD_ERROR,
|
||||
NOTIFY_OFFERS_FETCH_ERROR,
|
||||
} from "@/constants/notifications";
|
||||
import { UNNAMED_PROJECT } from "@/constants/entities";
|
||||
|
||||
/**
|
||||
* Projects View Component
|
||||
@@ -324,6 +325,13 @@ export default class ProjectsView extends Vue {
|
||||
|
||||
notify!: ReturnType<typeof createNotifyHelpers>;
|
||||
|
||||
/**
|
||||
* Get the unnamed project constant
|
||||
*/
|
||||
get unnamedProject(): string {
|
||||
return UNNAMED_PROJECT;
|
||||
}
|
||||
|
||||
// User account state
|
||||
activeDid = "";
|
||||
allContacts: Array<Contact> = [];
|
||||
@@ -383,7 +391,8 @@ export default class ProjectsView extends Vue {
|
||||
*/
|
||||
private async initializeUserSettings() {
|
||||
const settings = await this.$accountSettings();
|
||||
this.activeDid = settings.activeDid || "";
|
||||
// Use new Active Identity façade instead of settings.activeDid
|
||||
this.activeDid = (await this.$getActiveDid()) || "";
|
||||
this.apiServer = settings.apiServer || "";
|
||||
this.isRegistered = !!settings.isRegistered;
|
||||
this.givenName = settings.firstName || "";
|
||||
|
||||
@@ -150,7 +150,8 @@ export default class QuickActionBvcBeginView extends Vue {
|
||||
|
||||
// Get account settings using PlatformServiceMixin
|
||||
const settings = await this.$accountSettings();
|
||||
const activeDid = settings.activeDid || "";
|
||||
// Use new Active Identity façade instead of settings.activeDid
|
||||
const activeDid = (await this.$getActiveDid()) || "";
|
||||
const apiServer = settings.apiServer || "";
|
||||
|
||||
if (!activeDid || !apiServer) {
|
||||
|
||||
@@ -227,7 +227,8 @@ export default class QuickActionBvcEndView extends Vue {
|
||||
|
||||
const settings = await this.$settings();
|
||||
this.apiServer = settings.apiServer || "";
|
||||
this.activeDid = settings.activeDid || "";
|
||||
// Use new Active Identity façade instead of settings.activeDid
|
||||
this.activeDid = (await this.$getActiveDid()) || "";
|
||||
|
||||
this.allContacts = await this.$contacts();
|
||||
|
||||
@@ -239,7 +240,8 @@ export default class QuickActionBvcEndView extends Vue {
|
||||
}
|
||||
const eventStartDateObj = currentOrPreviousSat
|
||||
.set({ weekday: 6 })
|
||||
.set({ hour: 9 })
|
||||
.set({ hour: 8 })
|
||||
.set({ minute: 30 }) // to catch if people put their claims 30 minutes early
|
||||
.startOf("hour");
|
||||
|
||||
// Hack, but full ISO pushes the length to 340 which crashes verifyJWT!
|
||||
|
||||
@@ -124,7 +124,8 @@ export default class RecentOffersToUserView extends Vue {
|
||||
try {
|
||||
const settings = await this.$accountSettings();
|
||||
this.apiServer = settings.apiServer || "";
|
||||
this.activeDid = settings.activeDid || "";
|
||||
// Use new Active Identity façade instead of settings.activeDid
|
||||
this.activeDid = (await this.$getActiveDid()) || "";
|
||||
this.lastAckedOfferToUserProjectsJwtId =
|
||||
settings.lastAckedOfferToUserProjectsJwtId || "";
|
||||
|
||||
|
||||
@@ -116,7 +116,8 @@ export default class RecentOffersToUserView extends Vue {
|
||||
try {
|
||||
const settings = await this.$accountSettings();
|
||||
this.apiServer = settings.apiServer || "";
|
||||
this.activeDid = settings.activeDid || "";
|
||||
// Use new Active Identity façade instead of settings.activeDid
|
||||
this.activeDid = (await this.$getActiveDid()) || "";
|
||||
this.lastAckedOfferToUserJwtId = settings.lastAckedOfferToUserJwtId || "";
|
||||
|
||||
this.allContacts = await this.$getAllContacts();
|
||||
|
||||
@@ -207,7 +207,8 @@ export default class SeedBackupView extends Vue {
|
||||
try {
|
||||
let activeDid = "";
|
||||
const settings = await this.$accountSettings();
|
||||
activeDid = settings.activeDid || "";
|
||||
// Use new Active Identity façade instead of settings.activeDid
|
||||
activeDid = (await this.$getActiveDid()) || "";
|
||||
|
||||
this.numAccounts = await retrieveAccountCount();
|
||||
this.activeAccount = await retrieveFullyDecryptedAccount(activeDid);
|
||||
|
||||
@@ -76,7 +76,8 @@ export default class ShareMyContactInfoView extends Vue {
|
||||
|
||||
async mounted() {
|
||||
const settings = await this.$settings();
|
||||
const activeDid = settings?.activeDid;
|
||||
// Use new Active Identity façade instead of settings.activeDid
|
||||
const activeDid = await this.$getActiveDid();
|
||||
if (!activeDid) {
|
||||
this.$router.push({ name: "home" });
|
||||
}
|
||||
@@ -116,7 +117,8 @@ export default class ShareMyContactInfoView extends Vue {
|
||||
private async retrieveAccount(
|
||||
settings: Settings,
|
||||
): Promise<Account | undefined> {
|
||||
const activeDid = settings.activeDid || "";
|
||||
// Use new Active Identity façade instead of settings.activeDid
|
||||
const activeDid = (await this.$getActiveDid()) || "";
|
||||
if (!activeDid) {
|
||||
return undefined;
|
||||
}
|
||||
@@ -144,8 +146,8 @@ export default class ShareMyContactInfoView extends Vue {
|
||||
* Copy the contact message to clipboard
|
||||
*/
|
||||
private async copyToClipboard(message: string): Promise<void> {
|
||||
const { useClipboard } = await import("@vueuse/core");
|
||||
await useClipboard().copy(message);
|
||||
const { copyToClipboard } = await import("../services/ClipboardService");
|
||||
await copyToClipboard(message);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -176,7 +176,8 @@ export default class SharedPhotoView extends Vue {
|
||||
|
||||
try {
|
||||
const settings = await this.$accountSettings();
|
||||
this.activeDid = settings.activeDid;
|
||||
// Use new Active Identity façade instead of settings.activeDid
|
||||
this.activeDid = (await this.$getActiveDid()) || "";
|
||||
|
||||
const temp = await this.$getTemp(SHARED_PHOTO_BASE64_KEY);
|
||||
const imageB64 = temp?.blobB64 as string;
|
||||
|
||||
@@ -541,7 +541,8 @@ export default class Help extends Vue {
|
||||
*/
|
||||
async mounted() {
|
||||
const settings = await this.$accountSettings();
|
||||
this.activeDid = settings.activeDid || "";
|
||||
// Use new Active Identity façade instead of settings.activeDid
|
||||
this.activeDid = (await this.$getActiveDid()) || "";
|
||||
this.apiServer = settings.apiServer || "";
|
||||
this.userName = settings.firstName;
|
||||
|
||||
|
||||
@@ -183,7 +183,8 @@ export default class UserProfileView extends Vue {
|
||||
*/
|
||||
private async initializeSettings() {
|
||||
const settings = await this.$accountSettings();
|
||||
this.activeDid = settings.activeDid || "";
|
||||
// Use new Active Identity façade instead of settings.activeDid
|
||||
this.activeDid = (await this.$getActiveDid()) || "";
|
||||
this.partnerApiServer = settings.partnerApiServer || this.partnerApiServer;
|
||||
}
|
||||
|
||||
|
||||
@@ -69,7 +69,9 @@
|
||||
*/
|
||||
|
||||
import { test, expect } from '@playwright/test';
|
||||
import { UNNAMED_ENTITY_NAME } from '../src/constants/entities';
|
||||
import { deleteContact, generateAndRegisterEthrUser, importUser } from './testUtils';
|
||||
import { NOTIFY_CONTACT_INVALID_DID } from '../src/constants/notifications';
|
||||
|
||||
test('Check activity feed - check that server is running', async ({ page }) => {
|
||||
// Load app homepage
|
||||
@@ -169,6 +171,19 @@ test('Confirm test API setting (may fail if you are running your own Time Safari
|
||||
await expect(page.locator('#apiServerInput')).toHaveValue(endorserServer);
|
||||
});
|
||||
|
||||
test('Check invalid DID shows error and redirects', async ({ page }) => {
|
||||
await importUser(page, '00');
|
||||
|
||||
// Navigate to an invalid DID URL
|
||||
await page.goto('./did/0');
|
||||
|
||||
// Should show error message about invalid DID format
|
||||
await expect(page.getByText(NOTIFY_CONTACT_INVALID_DID.message)).toBeVisible();
|
||||
|
||||
// Should redirect to homepage
|
||||
await expect(page).toHaveURL(/.*\/$/);
|
||||
});
|
||||
|
||||
test('Check User 0 can register a random person', async ({ page }) => {
|
||||
await importUser(page, '00');
|
||||
const newDid = await generateAndRegisterEthrUser(page);
|
||||
@@ -177,7 +192,7 @@ test('Check User 0 can register a random person', async ({ page }) => {
|
||||
await page.goto('./');
|
||||
await page.getByTestId('closeOnboardingAndFinish').click();
|
||||
await page.getByRole('button', { name: 'Person' }).click();
|
||||
await page.getByRole('listitem').filter({ hasText: 'Unnamed' }).locator('svg').click();
|
||||
await page.getByRole('listitem').filter({ hasText: UNNAMED_ENTITY_NAME }).locator('svg').click();
|
||||
await page.getByPlaceholder('What was given').fill('Gave me access!');
|
||||
await page.getByRole('button', { name: 'Sign & Send' }).click();
|
||||
await expect(page.getByText('That gift was recorded.')).toBeVisible();
|
||||
|
||||
@@ -79,6 +79,7 @@
|
||||
* ```
|
||||
*/
|
||||
import { test, expect } from '@playwright/test';
|
||||
import { UNNAMED_ENTITY_NAME } from '../src/constants/entities';
|
||||
import { importUser } from './testUtils';
|
||||
|
||||
test('Record something given', async ({ page }) => {
|
||||
@@ -101,7 +102,7 @@ test('Record something given', async ({ page }) => {
|
||||
await page.goto('./');
|
||||
await page.getByTestId('closeOnboardingAndFinish').click();
|
||||
await page.getByRole('button', { name: 'Person' }).click();
|
||||
await page.getByRole('listitem').filter({ hasText: 'Unnamed' }).locator('svg').click();
|
||||
await page.getByRole('listitem').filter({ hasText: UNNAMED_ENTITY_NAME }).locator('svg').click();
|
||||
await page.getByPlaceholder('What was given').fill(finalTitle);
|
||||
await page.getByRole('spinbutton').fill(randomNonZeroNumber.toString());
|
||||
await page.getByRole('button', { name: 'Sign & Send' }).click();
|
||||
|
||||
@@ -85,6 +85,7 @@
|
||||
*/
|
||||
|
||||
import { test, expect } from '@playwright/test';
|
||||
import { UNNAMED_ENTITY_NAME } from '../src/constants/entities';
|
||||
import { importUser, createUniqueStringsArray, createRandomNumbersArray } from './testUtils';
|
||||
|
||||
test('Record 9 new gifts', async ({ page }) => {
|
||||
@@ -116,7 +117,7 @@ test('Record 9 new gifts', async ({ page }) => {
|
||||
await page.getByTestId('closeOnboardingAndFinish').click();
|
||||
}
|
||||
await page.getByRole('button', { name: 'Person' }).click();
|
||||
await page.getByRole('listitem').filter({ hasText: 'Unnamed' }).locator('svg').click();
|
||||
await page.getByRole('listitem').filter({ hasText: UNNAMED_ENTITY_NAME }).locator('svg').click();
|
||||
await page.getByPlaceholder('What was given').fill(finalTitles[i]);
|
||||
await page.getByRole('spinbutton').fill(finalNumbers[i].toString());
|
||||
await page.getByRole('button', { name: 'Sign & Send' }).click();
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user