Compare commits

..
Author SHA1 Message Date
Matthew Raymer 464a825a7b docs: update notification system for shared SQLite + clear T–lead
- Add SQLite Ownership & Concurrency section with WAL mode details
- Add DB Path & Adapter Configuration with shared DB setup
- Clarify T–lead governs prefetch attempts, not arming
- Add TTL-at-fire check callout for stale notification prevention
- Add DB Sharing acceptance criteria (visibility, WAL overlap, version safety)
- Update GLOSSARY.md with shared DB, WAL, and user_version definitions
- Remove plugin-owned DB references in favor of single shared database

Files modified:
- doc/notification-system.md
- doc/GLOSSARY.md

Compliance: Implements shared SQLite architecture with clear T–lead prefetch
semantics and WAL-based concurrency for app/plugin coordination.
2025-09-08 04:21:00 +00:00
Matthew Raymer 0dcb1d029e docs: merge notification system docs into single Native-First guide
- Consolidate 5 notification-system-* files into doc/notification-system.md
- Add web-push cleanup guide and Start-on-Login glossary entry
- Configure markdownlint for consistent formatting
- Remove web-push references, focus on native OS scheduling

Reduces maintenance overhead while preserving all essential information
in a single, well-formatted reference document.
2025-09-07 13:16:23 +00:00
Matthew Raymer 79b226e7d2 docs: apply documentation references model agents directive to notification system docs
- Remove duplicate sync checklists from notification-system-plan.md
- Fix markdown table formatting (remove double pipes) in executive summary
- Streamline cross-document references and eliminate redundant content
- Consolidate canonical ownership statements across all three docs
- Improve document structure and readability per directive guidelines

Files modified:
- doc/notification-system-executive-summary.md
- doc/notification-system-implementation.md
- doc/notification-system-plan.md

Compliance: Follows @docs/documentation_references_model_agents.mdc directive
for eliminating redundancy, centralizing context, and optimizing reference placement.
2025-09-07 10:51:27 +00:00
Matthew Raymer 74e4a20ef9 chore: update plan and rulesets 2025-09-05 13:09:30 +00:00
Matthew Raymer cfeb920493 refactor(docs): split notification system docs into plan and implementation
Replace monolithic notification-system-implementation-plan.md with focused
strategic plan (notification-system-plan.md) and detailed implementation
guide (notification-system-implementation.md). Both documents now perfectly
aligned with TimeSafari codebase patterns including:

- Actual Settings type extension pattern (JSON strings for complex objects)
- Real useNotifications composable stub signatures with eslint-disable
- Verified logger exports and safeStringify usage
- Confirmed PlatformServiceMixin.$saveSettings integration
- Validated migration system registerMigration patterns

Documents are production-ready with accurate code examples verified
against actual TimeSafari infrastructure.
2025-09-05 09:01:15 +00:00
Matthew Raymer a427a9e66f chore: still tweaking plan 2025-09-04 13:09:40 +00:00
Matthew Raymer 5110c17fba chore: create new notification plan 2025-09-04 13:09:00 +00:00
230 changed files with 4377 additions and 19652 deletions
+5 -5
View File
@@ -21,7 +21,7 @@ alwaysApply: false
## Purpose ## Purpose
All interactions must *increase the human's competence over time* while All interactions must _increase the human's competence over time_ while
completing the task efficiently. The model may handle menial work and memory completing the task efficiently. The model may handle menial work and memory
extension, but must also promote learning, autonomy, and healthy work habits. extension, but must also promote learning, autonomy, and healthy work habits.
The model should also **encourage human interaction and collaboration** rather The model should also **encourage human interaction and collaboration** rather
@@ -31,7 +31,7 @@ machine-driven steps.
## Principles ## Principles
1. Competence over convenience: finish the task *and* leave the human more 1. Competence over convenience: finish the task _and_ leave the human more
capable next time. capable next time.
@@ -75,7 +75,7 @@ assumptions if unanswered.
### timebox_minutes ### timebox_minutes
*integer or null* — When set to a positive integer (e.g., `5`), this acts _integer or null_ — When set to a positive integer (e.g., `5`), this acts
as a **time budget** guiding the model to prioritize delivering the most as a **time budget** guiding the model to prioritize delivering the most
essential parts of the task within that constraint. essential parts of the task within that constraint.
@@ -91,7 +91,7 @@ Behavior when set:
3. **Signal Skipped Depth** — Omitted details should be listed under 3. **Signal Skipped Depth** — Omitted details should be listed under
*Deferred for depth*. _Deferred for depth_.
4. **Order by Value** — Start with blocking or high-value items, then 4. **Order by Value** — Start with blocking or high-value items, then
@@ -198,7 +198,7 @@ Default: Doer + short Mentor notes.
## Self-Check (model, before responding) ## Self-Check (model, before responding)
- [ ] Task done *and* at least one competence lever included (≤120 words - [ ] Task done _and_ at least one competence lever included (≤120 words
total) total)
- [ ] At least one collaboration/discussion hook present - [ ] At least one collaboration/discussion hook present
- [ ] Output follows the **Output Contract** sections - [ ] Output follows the **Output Contract** sections
@@ -53,7 +53,7 @@ evidence-backed steps**.
- **Verifiable Outputs**: Include expected results, status codes, or - **Verifiable Outputs**: Include expected results, status codes, or
error messages error messages
- **Cite evidence** for *Works/Doesn't* items (timestamps, filenames, - **Cite evidence** for _Works/Doesn't_ items (timestamps, filenames,
line numbers, IDs/status codes, or logs). line numbers, IDs/status codes, or logs).
## Required Sections ## Required Sections
@@ -181,8 +181,8 @@ Before publishing, verify:
--- ---
**Status**: 🚢 ACTIVE — General ruleset extending *Base Context — Human **Status**: 🚢 ACTIVE — General ruleset extending _Base Context — Human
Competence First* Competence First_
**Priority**: Critical **Priority**: Critical
**Estimated Effort**: Ongoing reference **Estimated Effort**: Ongoing reference
+2 -3
View File
@@ -12,12 +12,11 @@ language: Match repository languages and conventions
## Rules ## Rules
0. **Principle:** just the facts m'am.
1. **Default to the least complex solution.** Fix the problem directly 1. **Default to the least complex solution.** Fix the problem directly
where it occurs; avoid new layers, indirection, or patterns unless where it occurs; avoid new layers, indirection, or patterns unless
strictly necessary. strictly necessary.
2. **Keep scope tight.** Implement only what is needed to satisfy the 2. **Keep scope tight.** Implement only what is needed to satisfy the
acceptance criteria and tests for *this* issue. acceptance criteria and tests for _this_ issue.
3. **Avoid speculative abstractions.** Use the **Rule of Three**: 3. **Avoid speculative abstractions.** Use the **Rule of Three**:
don't extract helpers/patterns until the third concrete usage proves don't extract helpers/patterns until the third concrete usage proves
the shape. the shape.
@@ -30,7 +29,7 @@ language: Match repository languages and conventions
7. **Targeted tests only.** Add the smallest set of tests that prove 7. **Targeted tests only.** Add the smallest set of tests that prove
the fix and guard against regression; don't rewrite suites. the fix and guard against regression; don't rewrite suites.
8. **Document the "why enough."** Include a one-paragraph note 8. **Document the "why enough."** Include a one-paragraph note
explaining why this minimal solution is sufficient *now*. explaining why this minimal solution is sufficient _now_.
## Future-Proofing Requires Evidence + Discussion ## Future-Proofing Requires Evidence + Discussion
@@ -9,8 +9,8 @@ alwaysApply: false
**Date**: 2025-08-19 **Date**: 2025-08-19
**Status**: 🎯 **ACTIVE** - Asset management guidelines **Status**: 🎯 **ACTIVE** - Asset management guidelines
*Scope: Assets Only (icons, splashes, image pipelines) — not overall build _Scope: Assets Only (icons, splashes, image pipelines) — not overall build
orchestration* orchestration_
## Intent ## Intent
@@ -2,8 +2,9 @@
globs: **/src/**/* globs: **/src/**/*
alwaysApply: false alwaysApply: false
--- ---
✅ use system date command to timestamp all documentation with accurate date and ✅ use system date command to timestamp all interactions with accurate date and
time time
✅ python script files must always have a blank line at their end
✅ remove whitespace at the end of lines ✅ remove whitespace at the end of lines
✅ use npm run lint-fix to check for warnings ✅ use npm run lint-fix to check for warnings
✅ do not use npm run dev let me handle running and supplying feedback ✅ do not use npm run dev let me handle running and supplying feedback
@@ -21,10 +22,12 @@ alwaysApply: false
- [ ] **Timestamp Usage**: Include accurate timestamps in all interactions - [ ] **Timestamp Usage**: Include accurate timestamps in all interactions
- [ ] **Code Quality**: Use npm run lint-fix to check for warnings - [ ] **Code Quality**: Use npm run lint-fix to check for warnings
- [ ] **File Standards**: Ensure Python files have blank line at end
- [ ] **Whitespace**: Remove trailing whitespace from all lines - [ ] **Whitespace**: Remove trailing whitespace from all lines
### After Development ### After Development
- [ ] **Linting Check**: Run npm run lint-fix to verify code quality - [ ] **Linting Check**: Run npm run lint-fix to verify code quality
- [ ] **File Validation**: Confirm Python files end with blank line
- [ ] **Whitespace Review**: Verify no trailing whitespace remains - [ ] **Whitespace Review**: Verify no trailing whitespace remains
- [ ] **Documentation**: Update relevant documentation with changes - [ ] **Documentation**: Update relevant documentation with changes
+1 -1
View File
@@ -40,7 +40,7 @@ feature development, issue investigations, ADRs, and documentation**.
`2025-08-17`). `2025-08-17`).
- Avoid ambiguous terms like *recently*, *last month*, or *soon*. - Avoid ambiguous terms like _recently_, _last month_, or _soon_.
- For time-based experiments (e.g., A/B tests), always include: - For time-based experiments (e.g., A/B tests), always include:
@@ -19,7 +19,7 @@
- Optionally provide UTC alongside if context requires cross-team clarity. - Optionally provide UTC alongside if context requires cross-team clarity.
- When interpreting relative terms like *now*, *today*, *last week*: - When interpreting relative terms like _now_, _today_, _last week_:
- Resolve them against the **developer's current time**. - Resolve them against the **developer's current time**.
@@ -0,0 +1,96 @@
---
title: Documentation, References, and Model Agent Use
version: 1.1
alwaysApply: true
scope: code, project-plans
---
# Directive on Documentation, References, and Model Agent Use in Code and Project Plans
To ensure clarity, efficiency, and high-value documentation within code and project plans—and to leverage **model agents** (AI- or automation-based assistants) effectively—contributors must follow these rules:
---
## 1. Documentation and References Must Add Clear Value
- Only include documentation, comments, or reference links when they provide _new, meaningful information_ that assists understanding or decision-making.
- Avoid duplicating content already obvious in the codebase, version history, or linked project documents.
---
## 2. Eliminate Redundant or Noisy References
- Remove references that serve no purpose beyond filling space.
- Model agents may automatically flag and suggest removal of trivial references (e.g., links to unchanged boilerplate or self-evident context).
---
## 3. Explicit Role of Model Agents
Model agents are **active participants** in documentation quality control. Their tasks include:
- **Relevance Evaluation**: Automatically analyze references for their substantive contribution before inclusion.
- **Redundancy Detection**: Flag duplicate or trivial references across commits, files, or tasks.
- **Context Linking**: Suggest appropriate higher-level docs (designs, ADRs, meeting notes) when a code change touches multi-stage or cross-team items.
- **Placement Optimization**: Recommend centralization of references (e.g., in plan overviews, ADRs, or merge commit messages) rather than scattered low-value inline references.
- **Consistency Monitoring**: Ensure references align with team standards (e.g., ADR template, architecture repo, or external policy documents).
Contributors must treat agent recommendations as **first-pass reviews** but remain accountable for final human judgment.
---
## 4. Contextual References for Complex Items
- Use **centralized references** for multi-stage features (e.g., architectural docs, research threads).
- Keep inline code comments light; push broader context into centralized documents.
- Model agents may auto-summarize complex chains of discussion and attach them as a single reference point.
---
## 5. Centralization of Broader Context
- Store overarching context (design docs, proposals, workflows) in accessible, well-indexed places.
- Model agents should assist by **generating reference maps** that track where docs are cited across the codebase.
---
## 6. Focused Documentation
- Documentation should explain **why** and **how** decisions are made, not just what was changed.
- Model agents can auto-generate first-pass explanations from commit metadata, diffs, and linked issues—but humans must refine them for accuracy and intent.
---
## 7. Review and Accountability
- Reviewers and team leads must reject submissions containing unnecessary or low-quality documentation.
- Model agent outputs are aids, not replacements—contributors remain responsible for **final clarity and relevance**.
---
## 8. Continuous Improvement and Agent Feedback Loops
- Encourage iterative development of model agents so their evaluations become more precise over time.
- Contributions should include **feedback on agent suggestions** (e.g., accepted, rejected, or corrected) to train better future outputs.
- Agents should log patterns of “rejected” suggestions for refinement.
---
## 9. Workflow Overview (Mermaid Diagram)
```mermaid
flowchart TD
A[Contributor] -->|Writes Code & Draft Docs| B[Model Agent]
B -->|Evaluates References| C{Relevant?}
C -->|Yes| D[Suggest Placement & Context Links]
C -->|No| E[Flag Redundancy / Noise]
D --> F[Contributor Refines Docs]
E --> F
F --> G[Reviewer]
G -->|Approves / Requests Revisions| H[Final Documentation]
G -->|Feedback on Agent Suggestions| B
```
---
✅ **Outcome:** By integrating disciplined contributor standards with **model agent augmentation**, the team achieves documentation that is consistently _relevant, concise, centralized, and decision-focused_. AI ensures coverage and noise reduction, while humans ensure precision and judgment.
@@ -192,6 +192,7 @@ Summary of key concepts and skills.
Where to apply this knowledge next. Where to apply this knowledge next.
``` ```
- [ ] Integration tests - [ ] Integration tests
- [ ] E2E tests - [ ] E2E tests
+43 -16
View File
@@ -1,5 +1,6 @@
--- ---
alwaysApply: false alwaysApply: true
inherits: base_context.mdc
--- ---
```json ```json
{ {
@@ -15,9 +16,10 @@ alwaysApply: false
**Author**: System/Shared **Author**: System/Shared
**Date**: 2025-08-21 (UTC) **Date**: 2025-08-21 (UTC)
**Status**: 🚢 ACTIVE — General ruleset extending *Base Context — Human Competence First* **Status**: 🚢 ACTIVE — General ruleset extending _Base Context — Human Competence First_
> **Alignment with Base Context** > **Alignment with Base Context**
>
> - **Purpose fit**: Prioritizes human competence and collaboration while delivering reproducible artifacts. > - **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. > - **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. > - **Toggles honored**: Uses the same toggle semantics; defaults above can be overridden by the caller.
@@ -25,9 +27,11 @@ alwaysApply: false
--- ---
## Objective ## Objective
Produce a **developer-grade, reproducible guide** for any technical topic that onboards a competent practitioner **without meta narration** and **with evidence-backed steps**. 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 ## Scope & Constraints
- **One Markdown document** as the deliverable. - **One Markdown document** as the deliverable.
- Use **absolute dates** in **UTC** (e.g., `2025-08-21T14:22Z`) — avoid “today/yesterday”. - 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: - Include at least **one diagram** (Mermaid preferred). Choose the most fitting type:
@@ -36,10 +40,11 @@ Produce a **developer-grade, reproducible guide** for any technical topic that o
- **APIs**: `curl` + one client library (e.g., `httpx` for Python). - **APIs**: `curl` + one client library (e.g., `httpx` for Python).
- **CLIs**: literal command blocks and expected output snippets. - **CLIs**: literal command blocks and expected output snippets.
- **Code**: minimal, self-contained samples (language appropriate). - **Code**: minimal, self-contained samples (language appropriate).
- Cite **evidence** for *Works/Doesn’t* items (timestamps, filenames, line numbers, IDs/status codes, or logs). - 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**. - If something is unknown, output `TODO:<missing>` — **never invent**.
## Required Sections (extends Base Output Contract) ## Required Sections (extends Base Output Contract)
Follow this exact order **after** the Base Contract’s **Objective → Result → Use/Run** headers: Follow this exact order **after** the Base Contract’s **Objective → Result → Use/Run** headers:
1. **Context & Scope** 1. **Context & Scope**
@@ -51,9 +56,9 @@ Follow this exact order **after** the Base Contract’s **Objective → Result
4. **Architecture / Process Overview** 4. **Architecture / Process Overview**
- Short prose + **one diagram** selected from the list above. - Short prose + **one diagram** selected from the list above.
5. **Interfaces & Contracts (choose one)** 5. **Interfaces & Contracts (choose one)**
- **API-based**: Endpoint table (*Step, Method, Path/URL, Auth, Key Headers/Params, Sample Req/Resp ref*). - **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*). - **Data/Files**: I/O contract table (_Source, Format, Schema/Columns, Size, Validation rules_).
- **Systems/Hardware**: Interfaces table (*Port/Bus, Protocol, Voltage/Timing, Constraints*). - **Systems/Hardware**: Interfaces table (_Port/Bus, Protocol, Voltage/Timing, Constraints_).
6. **Repro: End-to-End Procedure** 6. **Repro: End-to-End Procedure**
- Minimal copy-paste steps with code/commands and **expected outputs**. - Minimal copy-paste steps with code/commands and **expected outputs**.
7. **What Works (with Evidence)** 7. **What Works (with Evidence)**
@@ -68,16 +73,19 @@ Follow this exact order **after** the Base Contract’s **Objective → Result
- Canonical docs, specs, tickets, prior analyses. - Canonical docs, specs, tickets, prior analyses.
> **Competence Hooks (per Base Context; keep lightweight):** > **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. > - _Why this works_ (≤3 bullets) — core invariants or guarantees.
> - *Next skill unlock* (1 line) — the next capability to implement/learn. > - _Common pitfalls_ (≤3 bullets) — the traps we saw in evidence.
> - *Teach-back* (1 line) — prompt the reader to restate the flow/architecture. > - _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):** > **Collaboration Hooks (per Base Context):**
>
> - Name reviewers for **Interfaces & Contracts** and the **diagram**. > - Name reviewers for **Interfaces & Contracts** and the **diagram**.
> - Short **sign-off checklist** before merging/publishing the guide. > - Short **sign-off checklist** before merging/publishing the guide.
## Do / Don’t (Base-aligned) ## Do / Don’t (Base-aligned)
- **Do** quantify progress only against a defined scope with acceptance criteria. - **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** include minimal sample payloads/headers or I/O schemas; redact sensitive values.
- **Do** keep commentary lean; if timeboxed, move depth to **Deferred for depth**. - **Do** keep commentary lean; if timeboxed, move depth to **Deferred for depth**.
@@ -85,6 +93,7 @@ Follow this exact order **after** the Base Contract’s **Objective → Result
- **Don’t** include IDE-specific chatter or internal rules unrelated to the task. - **Don’t** include IDE-specific chatter or internal rules unrelated to the task.
## Validation Checklist (self-check before returning) ## Validation Checklist (self-check before returning)
- [ ] All Required Sections present and ordered. - [ ] All Required Sections present and ordered.
- [ ] Diagram compiles (basic Mermaid syntax) and fits the problem. - [ ] Diagram compiles (basic Mermaid syntax) and fits the problem.
- [ ] If API-based, **Auth** and **Key Headers/Params** are listed for each endpoint. - [ ] If API-based, **Auth** and **Key Headers/Params** are listed for each endpoint.
@@ -95,6 +104,7 @@ Follow this exact order **after** the Base Contract’s **Objective → Result
- [ ] Base **Output Contract** sections satisfied (Objective/Result/Use/Run/Competence/Collaboration/Assumptions/References). - [ ] Base **Output Contract** sections satisfied (Objective/Result/Use/Run/Competence/Collaboration/Assumptions/References).
## Universal Template (fill-in) ## Universal Template (fill-in)
```markdown ```markdown
# <Title> — Working Notes (As of YYYY-MM-DDTHH:MMZ) # <Title> — Working Notes (As of YYYY-MM-DDTHH:MMZ)
@@ -131,37 +141,46 @@ Follow this exact order **after** the Base Contract’s **Objective → Result
``` ```
## Interfaces & Contracts ## Interfaces & Contracts
### If API-based ### If API-based
| Step | Method | Path/URL | Auth | Key Headers/Params | Sample | | Step | Method | Path/URL | Auth | Key Headers/Params | Sample |
|---|---|---|---|---|---| |---|---|---|---|---|---|
| <…> | <…> | <…> | <…> | <…> | below | | <…> | <…> | <…> | <…> | <…> | below |
### If Data/Files ### If Data/Files
| Source | Format | Schema/Columns | Size | Validation | | Source | Format | Schema/Columns | Size | Validation |
|---|---|---|---|---| |---|---|---|---|---|
| <…> | <…> | <…> | <…> | <…> | | <…> | <…> | <…> | <…> | <…> |
### If Systems/Hardware ### If Systems/Hardware
| Interface | Protocol | Timing/Voltage | Constraints | Notes | | Interface | Protocol | Timing/Voltage | Constraints | Notes |
|---|---|---|---|---| |---|---|---|---|---|
| <…> | <…> | <…> | <…> | <…> | | <…> | <…> | <…> | <…> | <…> |
## Repro: End-to-End Procedure ## Repro: End-to-End Procedure
```bash ```bash
# commands / curl examples (redacted where necessary) # commands / curl examples (redacted where necessary)
``` ```
```python ```python
# minimal client library example (language appropriate) # minimal client library example (language appropriate)
``` ```
> Expected output: <snippet/checks> > Expected output: <snippet/checks>
## What Works (Evidence) ## What Works (Evidence)
- ✅ <short statement> - ✅ <short statement>
- **Time**: <YYYY-MM-DDTHH:MMZ> - **Time**: <YYYY-MM-DDTHH:MMZ>
- **Evidence**: file/line/log or request id/status - **Evidence**: file/line/log or request id/status
- **Verify at**: <where> - **Verify at**: <where>
## What Doesn’t (Evidence & Hypotheses) ## What Doesn’t (Evidence & Hypotheses)
- ❌ <short failure> at `<component/endpoint/file>` - ❌ <short failure> at `<component/endpoint/file>`
- **Time**: <YYYY-MM-DDTHH:MMZ> - **Time**: <YYYY-MM-DDTHH:MMZ>
- **Evidence**: <snippet/id/status> - **Evidence**: <snippet/id/status>
@@ -169,38 +188,46 @@ Follow this exact order **after** the Base Contract’s **Objective → Result
- **Next probe**: <short> - **Next probe**: <short>
## Risks, Limits, Assumptions ## Risks, Limits, Assumptions
<bullets: limits, security boundaries, retries/backoff, idempotency, SLOs> <bullets: limits, security boundaries, retries/backoff, idempotency, SLOs>
## Next Steps ## Next Steps
| Owner | Task | Exit Criteria | Target Date (UTC) | | Owner | Task | Exit Criteria | Target Date (UTC) |
|---|---|---|---| |---|---|---|---|
| <name> | <action> | <measurable outcome> | <YYYY-MM-DD> | | <name> | <action> | <measurable outcome> | <YYYY-MM-DD> |
## References ## References
<links/titles> <links/titles>
## Competence Hooks ## Competence Hooks
- *Why this works*: <≤3 bullets>
- *Common pitfalls*: <≤3 bullets> - _Why this works_: <≤3 bullets>
- *Next skill unlock*: <1 line> - _Common pitfalls_: <≤3 bullets>
- *Teach-back*: <1 line> - _Next skill unlock_: <1 line>
- _Teach-back_: <1 line>
## Collaboration Hooks ## Collaboration Hooks
- Reviewers: <names/roles> - Reviewers: <names/roles>
- Sign-off checklist: <≤5 checks> - Sign-off checklist: <≤5 checks>
## Assumptions & Limits ## Assumptions & Limits
<bullets> <bullets>
## Deferred for depth ## Deferred for depth
<park deeper material here to respect timeboxing> <park deeper material here to respect timeboxing>
``` ```
--- ---
**Notes for Implementers:** **Notes for Implementers:**
- Respect Base *Do-Not* (no filler, no invented facts, no censorship).
- Respect Base _Do-Not_ (no filler, no invented facts, no censorship).
- Prefer clarity over completeness when timeboxed; capture unknowns explicitly. - Prefer clarity over completeness when timeboxed; capture unknowns explicitly.
- Apply historical comment management rules (see `.cursor/rules/historical_comment_management.mdc`) - 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 realistic time estimation rules (see `.cursor/rules/realistic_time_estimation.mdc`)
- Apply Playwright test investigation rules (see `.cursor/rules/playwright_test_investigation.mdc`) - Apply Playwright test investigation rules (see `.cursor/rules/playwright_test_investigation.mdc`)
+2
View File
@@ -82,6 +82,7 @@ common investigation pitfalls.
### **Safe Diagnosis Commands** ### **Safe Diagnosis Commands**
✅ **Safe to use during diagnosis:** ✅ **Safe to use during diagnosis:**
- `npm run lint-fix` - Syntax and style checking - `npm run lint-fix` - Syntax and style checking
- `npm run type-check` - TypeScript validation (if available) - `npm run type-check` - TypeScript validation (if available)
- `git status` - Version control status - `git status` - Version control status
@@ -90,6 +91,7 @@ common investigation pitfalls.
- `grep_search` - Text pattern searching - `grep_search` - Text pattern searching
❌ **Never use during diagnosis:** ❌ **Never use during diagnosis:**
- `npm run build:web` - Blocks chat - `npm run build:web` - Blocks chat
- `npm run build:electron` - Blocks chat - `npm run build:electron` - Blocks chat
- `npm run build:capacitor` - Blocks chat - `npm run build:capacitor` - Blocks chat
+11 -1
View File
@@ -1,6 +1,7 @@
--- ---
alwaysApply: false alwaysApply: true
--- ---
# Meta-Rule: Core Always-On Rules # Meta-Rule: Core Always-On Rules
**Author**: Matthew Raymer **Author**: Matthew Raymer
@@ -35,6 +36,7 @@ that are essential for all AI interactions.
**This meta-rule enforces current workflow mode constraints for all interactions:** **This meta-rule enforces current workflow mode constraints for all interactions:**
### **Current Workflow State** ### **Current Workflow State**
```json ```json
{ {
"workflowState": { "workflowState": {
@@ -61,26 +63,31 @@ that are essential for all AI interactions.
### **Mode-Specific Enforcement** ### **Mode-Specific Enforcement**
**Diagnosis Mode (read_only):** **Diagnosis Mode (read_only):**
- ❌ **Forbidden**: File modification, code creation, build commands, git commits - ❌ **Forbidden**: File modification, code creation, build commands, git commits
- ✅ **Allowed**: File reading, code analysis, investigation, documentation - ✅ **Allowed**: File reading, code analysis, investigation, documentation
- **Response**: Guide user toward investigation and analysis, not implementation - **Response**: Guide user toward investigation and analysis, not implementation
**Fixing Mode (implementation):** **Fixing Mode (implementation):**
- ✅ **Allowed**: File modification, code creation, build commands, testing, git commits - ✅ **Allowed**: File modification, code creation, build commands, testing, git commits
- ❌ **Forbidden**: None (full implementation mode) - ❌ **Forbidden**: None (full implementation mode)
- **Response**: Proceed with implementation and testing - **Response**: Proceed with implementation and testing
**Planning Mode (design_only):** **Planning Mode (design_only):**
- ❌ **Forbidden**: Implementation, coding, building, deployment - ❌ **Forbidden**: Implementation, coding, building, deployment
- ✅ **Allowed**: Analysis, design, estimation, documentation, architecture - ✅ **Allowed**: Analysis, design, estimation, documentation, architecture
- **Response**: Focus on planning and design, not implementation - **Response**: Focus on planning and design, not implementation
**Research Mode (investigation):** **Research Mode (investigation):**
- ❌ **Forbidden**: File modification, implementation, deployment - ❌ **Forbidden**: File modification, implementation, deployment
- ✅ **Allowed**: Investigation, analysis, research, documentation - ✅ **Allowed**: Investigation, analysis, research, documentation
- **Response**: Focus on investigation and analysis - **Response**: Focus on investigation and analysis
**Documentation Mode (writing_only):** **Documentation Mode (writing_only):**
- ❌ **Forbidden**: Implementation, coding, building, deployment - ❌ **Forbidden**: Implementation, coding, building, deployment
- ✅ **Allowed**: Writing, editing, formatting, structuring, reviewing - ✅ **Allowed**: Writing, editing, formatting, structuring, reviewing
- **Response**: Focus on documentation creation and improvement - **Response**: Focus on documentation creation and improvement
@@ -293,6 +300,9 @@ or context. They form the foundation for all AI assistant behavior.
**See also**: **See also**:
- `.cursor/rules/meta_feature_planning.mdc` for workflow-specific rules - `.cursor/rules/meta_feature_planning.mdc` for workflow-specific rules
- `.cursor/rules/meta_bug_diagnosis.mdc` for investigation workflows
- `.cursor/rules/meta_bug_fixing.mdc` for fix implementation
- `.cursor/rules/meta_feature_implementation.mdc` for feature development
**Status**: Active core always-on meta-rule **Status**: Active core always-on meta-rule
**Priority**: Critical (applies to every prompt) **Priority**: Critical (applies to every prompt)
+9
View File
@@ -51,6 +51,7 @@ providing technical descriptions.
## When to Use ## When to Use
**Use this meta-rule when**: **Use this meta-rule when**:
- Writing new documentation - Writing new documentation
- Updating existing documentation - Updating existing documentation
- Creating technical guides - Creating technical guides
@@ -107,6 +108,7 @@ providing technical descriptions.
### **Document Structure** ### **Document Structure**
**Mandatory Sections**: **Mandatory Sections**:
- **Overview**: Clear purpose and scope with educational context - **Overview**: Clear purpose and scope with educational context
- **Why This Matters**: Business value and user benefit explanation - **Why This Matters**: Business value and user benefit explanation
- **Core Concepts**: Fundamental understanding before implementation - **Core Concepts**: Fundamental understanding before implementation
@@ -116,6 +118,7 @@ providing technical descriptions.
- **Next Steps**: Where to go from here - **Next Steps**: Where to go from here
**Optional Sections**: **Optional Sections**:
- **Background**: Historical context and evolution - **Background**: Historical context and evolution
- **Alternatives**: Other approaches and trade-offs - **Alternatives**: Other approaches and trade-offs
- **Advanced Topics**: Deep dive into complex scenarios - **Advanced Topics**: Deep dive into complex scenarios
@@ -124,6 +127,7 @@ providing technical descriptions.
### **Writing Style** ### **Writing Style**
**Educational Approach**: **Educational Approach**:
- **Conversational tone**: Write as if explaining to a colleague - **Conversational tone**: Write as if explaining to a colleague
- **Progressive disclosure**: Start simple, add complexity gradually - **Progressive disclosure**: Start simple, add complexity gradually
- **Active voice**: "You can do this" not "This can be done" - **Active voice**: "You can do this" not "This can be done"
@@ -131,6 +135,7 @@ providing technical descriptions.
- **Analogies**: Use familiar concepts to explain complex ideas - **Analogies**: Use familiar concepts to explain complex ideas
**Technical Accuracy**: **Technical Accuracy**:
- **Precise language**: Use exact technical terms consistently - **Precise language**: Use exact technical terms consistently
- **Code examples**: Working, tested code snippets - **Code examples**: Working, tested code snippets
- **Version information**: Specify applicable versions and platforms - **Version information**: Specify applicable versions and platforms
@@ -139,6 +144,7 @@ providing technical descriptions.
### **Content Quality Standards** ### **Content Quality Standards**
**Educational Value**: **Educational Value**:
- [ ] **Concept clarity**: Reader understands the fundamental idea - [ ] **Concept clarity**: Reader understands the fundamental idea
- [ ] **Context relevance**: Reader knows when to apply the knowledge - [ ] **Context relevance**: Reader knows when to apply the knowledge
- [ ] **Practical application**: Reader can implement the solution - [ ] **Practical application**: Reader can implement the solution
@@ -146,6 +152,7 @@ providing technical descriptions.
- [ ] **Next steps**: Reader knows where to continue learning - [ ] **Next steps**: Reader knows where to continue learning
**Technical Accuracy**: **Technical Accuracy**:
- [ ] **Fact verification**: All technical details are correct - [ ] **Fact verification**: All technical details are correct
- [ ] **Code validation**: Examples compile and run correctly - [ ] **Code validation**: Examples compile and run correctly
- [ ] **Version compatibility**: Platform and version requirements clear - [ ] **Version compatibility**: Platform and version requirements clear
@@ -183,6 +190,7 @@ providing technical descriptions.
### **Review Checklist** ### **Review Checklist**
**Educational Quality**: **Educational Quality**:
- [ ] **Clear learning objective**: What will the reader learn? - [ ] **Clear learning objective**: What will the reader learn?
- [ ] **Appropriate complexity**: Matches target audience knowledge - [ ] **Appropriate complexity**: Matches target audience knowledge
- [ ] **Progressive disclosure**: Information builds logically - [ ] **Progressive disclosure**: Information builds logically
@@ -190,6 +198,7 @@ providing technical descriptions.
- [ ] **Common questions**: Anticipates and answers reader questions - [ ] **Common questions**: Anticipates and answers reader questions
**Technical Quality**: **Technical Quality**:
- [ ] **Accuracy**: All technical details verified - [ ] **Accuracy**: All technical details verified
- [ ] **Completeness**: Covers all necessary information - [ ] **Completeness**: Covers all necessary information
- [ ] **Consistency**: Terminology and formatting consistent - [ ] **Consistency**: Terminology and formatting consistent
@@ -9,26 +9,31 @@ alwaysApply: false
**Status**: 🎯 **ACTIVE** - Playwright test debugging guidelines **Status**: 🎯 **ACTIVE** - Playwright test debugging guidelines
## Objective ## Objective
Provide systematic approach for investigating Playwright test failures with focus on UI element conflicts, timing issues, and selector ambiguity. Provide systematic approach for investigating Playwright test failures with focus on UI element conflicts, timing issues, and selector ambiguity.
## Context & Scope ## Context & Scope
- **Audience**: Developers debugging Playwright test failures - **Audience**: Developers debugging Playwright test failures
- **In scope**: Test failure analysis, selector conflicts, UI state investigation, timing issues - **In scope**: Test failure analysis, selector conflicts, UI state investigation, timing issues
- **Out of scope**: Test writing best practices, CI/CD configuration - **Out of scope**: Test writing best practices, CI/CD configuration
## Artifacts & Links ## Artifacts & Links
- Test results: `test-results/` directory - Test results: `test-results/` directory
- Error context: `error-context.md` files with page snapshots - Error context: `error-context.md` files with page snapshots
- Trace files: `trace.zip` files for failed tests - Trace files: `trace.zip` files for failed tests
- HTML reports: Interactive test reports with screenshots - HTML reports: Interactive test reports with screenshots
## Environment & Preconditions ## Environment & Preconditions
- OS/Runtime: Linux/Windows/macOS with Node.js - OS/Runtime: Linux/Windows/macOS with Node.js
- Versions: Playwright test framework, browser drivers - Versions: Playwright test framework, browser drivers
- Services: Local test server (localhost:8080), test data setup - Services: Local test server (localhost:8080), test data setup
- Auth mode: None required for test investigation - Auth mode: None required for test investigation
## Architecture / Process Overview ## Architecture / Process Overview
Playwright test investigation follows a systematic diagnostic workflow that leverages built-in debugging tools and error context analysis. Playwright test investigation follows a systematic diagnostic workflow that leverages built-in debugging tools and error context analysis.
```mermaid ```mermaid
@@ -57,6 +62,7 @@ flowchart TD
## Interfaces & Contracts ## Interfaces & Contracts
### Test Results Structure ### Test Results Structure
| Component | Format | Content | Validation | | Component | Format | Content | Validation |
|---|---|---|---| |---|---|---|---|
| Error Context | Markdown | Page snapshot in YAML | Verify DOM state matches test expectations | | Error Context | Markdown | Page snapshot in YAML | Verify DOM state matches test expectations |
@@ -65,6 +71,7 @@ flowchart TD
| JSON Results | JSON | Machine-readable results | Parse for automated analysis | | JSON Results | JSON | Machine-readable results | Parse for automated analysis |
### Investigation Commands ### Investigation Commands
| Step | Command | Expected Output | Notes | | Step | Command | Expected Output | Notes |
|---|---|---|---| |---|---|---|---|
| Locate failed tests | `find test-results -name "*test-name*"` | Test result directories | Use exact test name patterns | | Locate failed tests | `find test-results -name "*test-name*"` | Test result directories | Use exact test name patterns |
@@ -74,6 +81,7 @@ flowchart TD
## Repro: End-to-End Investigation Procedure ## Repro: End-to-End Investigation Procedure
### 1. Locate Failed Test Results ### 1. Locate Failed Test Results
```bash ```bash
# Find all results for a specific test # Find all results for a specific test
find test-results -name "*test-name*" -type d find test-results -name "*test-name*" -type d
@@ -83,6 +91,7 @@ find test-results -name "error-context.md" | head -5
``` ```
### 2. Analyze Error Context ### 2. Analyze Error Context
```bash ```bash
# Read error context for specific test # Read error context for specific test
cat test-results/test-name-test-description-browser/error-context.md cat test-results/test-name-test-description-browser/error-context.md
@@ -92,6 +101,7 @@ grep -A 10 -B 5 "button.*Yes\|button.*No" test-results/*/error-context.md
``` ```
### 3. Check Trace Files ### 3. Check Trace Files
```bash ```bash
# List available trace files # List available trace files
find test-results -name "*.zip" | grep trace find test-results -name "*.zip" | grep trace
@@ -101,6 +111,7 @@ npx playwright show-trace test-results/test-name/trace.zip
``` ```
### 4. Investigate Selector Issues ### 4. Investigate Selector Issues
```typescript ```typescript
// Check for multiple elements with same text // Check for multiple elements with same text
await page.locator('button:has-text("Yes")').count(); // Should be 1 await page.locator('button:has-text("Yes")').count(); // Should be 1
@@ -110,6 +121,7 @@ await page.locator('div[role="alert"]:has-text("Register") button:has-text("Yes"
``` ```
## What Works (Evidence) ## What Works (Evidence)
- ✅ **Error context files** provide page snapshots showing exact DOM state at failure - ✅ **Error context files** provide page snapshots showing exact DOM state at failure
- **Time**: 2025-08-21T14:22Z - **Time**: 2025-08-21T14:22Z
- **Evidence**: `test-results/60-new-activity-New-offers-for-another-user-chromium/error-context.md` shows both alerts visible - **Evidence**: `test-results/60-new-activity-New-offers-for-another-user-chromium/error-context.md` shows both alerts visible
@@ -126,6 +138,7 @@ await page.locator('div[role="alert"]:has-text("Register") button:has-text("Yes"
- **Verify at**: Error context markdown files - **Verify at**: Error context markdown files
## What Doesn't (Evidence & Hypotheses) ## What Doesn't (Evidence & Hypotheses)
- ❌ **Generic selectors** fail with multiple similar elements at `test-playwright/testUtils.ts:161` - ❌ **Generic selectors** fail with multiple similar elements at `test-playwright/testUtils.ts:161`
- **Time**: 2025-08-21T14:22Z - **Time**: 2025-08-21T14:22Z
- **Evidence**: `button:has-text("Yes")` matches both "Yes" and "Yes, Export Data" - **Evidence**: `button:has-text("Yes")` matches both "Yes" and "Yes, Export Data"
@@ -139,12 +152,14 @@ await page.locator('div[role="alert"]:has-text("Register") button:has-text("Yes"
- **Next probe**: Implement alert queuing or prevent overlapping alerts - **Next probe**: Implement alert queuing or prevent overlapping alerts
## Risks, Limits, Assumptions ## Risks, Limits, Assumptions
- **Trace file size**: Large trace files may impact storage and analysis time - **Trace file size**: Large trace files may impact storage and analysis time
- **Browser compatibility**: Trace viewer requires specific browser support - **Browser compatibility**: Trace viewer requires specific browser support
- **Test isolation**: Shared state between tests may affect investigation results - **Test isolation**: Shared state between tests may affect investigation results
- **Timing sensitivity**: Tests may pass/fail based on system performance - **Timing sensitivity**: Tests may pass/fail based on system performance
## Next Steps ## Next Steps
| Owner | Task | Exit Criteria | Target Date (UTC) | | Owner | Task | Exit Criteria | Target Date (UTC) |
|---|---|---|---| |---|---|---|---|
| Development Team | Fix test selectors for multiple alerts | All tests pass consistently | 2025-08-22 | | Development Team | Fix test selectors for multiple alerts | All tests pass consistently | 2025-08-22 |
@@ -152,21 +167,25 @@ await page.locator('div[role="alert"]:has-text("Register") button:has-text("Yes"
| Development Team | Add test IDs to alert buttons | Unique selectors for all UI elements | 2025-08-28 | | Development Team | Add test IDs to alert buttons | Unique selectors for all UI elements | 2025-08-28 |
## References ## References
- [Playwright Trace Viewer Documentation](https://playwright.dev/docs/trace-viewer) - [Playwright Trace Viewer Documentation](https://playwright.dev/docs/trace-viewer)
- [Playwright Test Results](https://playwright.dev/docs/test-reporters) - [Playwright Test Results](https://playwright.dev/docs/test-reporters)
- [Test Investigation Workflow](./research_diagnostic.mdc) - [Test Investigation Workflow](./research_diagnostic.mdc)
## Competence Hooks ## Competence Hooks
- **Why this works**: Systematic investigation leverages Playwright's built-in debugging tools to identify root causes - **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 - **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 - **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?" - **Teach-back**: "How would you investigate a Playwright test failure using error context, trace files, and page snapshots?"
## Collaboration Hooks ## Collaboration Hooks
- **Reviewers**: QA team, test automation engineers - **Reviewers**: QA team, test automation engineers
- **Sign-off checklist**: Error context analyzed, trace files reviewed, root cause identified, fix implemented and tested - **Sign-off checklist**: Error context analyzed, trace files reviewed, root cause identified, fix implemented and tested
## Assumptions & Limits ## Assumptions & Limits
- Test results directory structure follows Playwright conventions - Test results directory structure follows Playwright conventions
- Trace files are enabled in configuration (`trace: "retain-on-failure"`) - Trace files are enabled in configuration (`trace: "retain-on-failure"`)
- Error context files contain valid YAML page snapshots - Error context files contain valid YAML page snapshots
@@ -178,6 +197,7 @@ await page.locator('div[role="alert"]:has-text("Register") button:has-text("Yes"
**Priority**: High **Priority**: High
**Maintainer**: Development team **Maintainer**: Development team
**Next Review**: 2025-09-21 **Next Review**: 2025-09-21
# Playwright Test Investigation — Harbor Pilot Directive # Playwright Test Investigation — Harbor Pilot Directive
**Author**: Matthew Raymer **Author**: Matthew Raymer
@@ -185,26 +205,31 @@ await page.locator('div[role="alert"]:has-text("Register") button:has-text("Yes"
**Status**: 🎯 **ACTIVE** - Playwright test debugging guidelines **Status**: 🎯 **ACTIVE** - Playwright test debugging guidelines
## Objective ## Objective
Provide systematic approach for investigating Playwright test failures with focus on UI element conflicts, timing issues, and selector ambiguity. Provide systematic approach for investigating Playwright test failures with focus on UI element conflicts, timing issues, and selector ambiguity.
## Context & Scope ## Context & Scope
- **Audience**: Developers debugging Playwright test failures - **Audience**: Developers debugging Playwright test failures
- **In scope**: Test failure analysis, selector conflicts, UI state investigation, timing issues - **In scope**: Test failure analysis, selector conflicts, UI state investigation, timing issues
- **Out of scope**: Test writing best practices, CI/CD configuration - **Out of scope**: Test writing best practices, CI/CD configuration
## Artifacts & Links ## Artifacts & Links
- Test results: `test-results/` directory - Test results: `test-results/` directory
- Error context: `error-context.md` files with page snapshots - Error context: `error-context.md` files with page snapshots
- Trace files: `trace.zip` files for failed tests - Trace files: `trace.zip` files for failed tests
- HTML reports: Interactive test reports with screenshots - HTML reports: Interactive test reports with screenshots
## Environment & Preconditions ## Environment & Preconditions
- OS/Runtime: Linux/Windows/macOS with Node.js - OS/Runtime: Linux/Windows/macOS with Node.js
- Versions: Playwright test framework, browser drivers - Versions: Playwright test framework, browser drivers
- Services: Local test server (localhost:8080), test data setup - Services: Local test server (localhost:8080), test data setup
- Auth mode: None required for test investigation - Auth mode: None required for test investigation
## Architecture / Process Overview ## Architecture / Process Overview
Playwright test investigation follows a systematic diagnostic workflow that leverages built-in debugging tools and error context analysis. Playwright test investigation follows a systematic diagnostic workflow that leverages built-in debugging tools and error context analysis.
```mermaid ```mermaid
@@ -233,6 +258,7 @@ flowchart TD
## Interfaces & Contracts ## Interfaces & Contracts
### Test Results Structure ### Test Results Structure
| Component | Format | Content | Validation | | Component | Format | Content | Validation |
|---|---|---|---| |---|---|---|---|
| Error Context | Markdown | Page snapshot in YAML | Verify DOM state matches test expectations | | Error Context | Markdown | Page snapshot in YAML | Verify DOM state matches test expectations |
@@ -241,6 +267,7 @@ flowchart TD
| JSON Results | JSON | Machine-readable results | Parse for automated analysis | | JSON Results | JSON | Machine-readable results | Parse for automated analysis |
### Investigation Commands ### Investigation Commands
| Step | Command | Expected Output | Notes | | Step | Command | Expected Output | Notes |
|---|---|---|---| |---|---|---|---|
| Locate failed tests | `find test-results -name "*test-name*"` | Test result directories | Use exact test name patterns | | Locate failed tests | `find test-results -name "*test-name*"` | Test result directories | Use exact test name patterns |
@@ -250,6 +277,7 @@ flowchart TD
## Repro: End-to-End Investigation Procedure ## Repro: End-to-End Investigation Procedure
### 1. Locate Failed Test Results ### 1. Locate Failed Test Results
```bash ```bash
# Find all results for a specific test # Find all results for a specific test
find test-results -name "*test-name*" -type d find test-results -name "*test-name*" -type d
@@ -259,6 +287,7 @@ find test-results -name "error-context.md" | head -5
``` ```
### 2. Analyze Error Context ### 2. Analyze Error Context
```bash ```bash
# Read error context for specific test # Read error context for specific test
cat test-results/test-name-test-description-browser/error-context.md cat test-results/test-name-test-description-browser/error-context.md
@@ -268,6 +297,7 @@ grep -A 10 -B 5 "button.*Yes\|button.*No" test-results/*/error-context.md
``` ```
### 3. Check Trace Files ### 3. Check Trace Files
```bash ```bash
# List available trace files # List available trace files
find test-results -name "*.zip" | grep trace find test-results -name "*.zip" | grep trace
@@ -277,6 +307,7 @@ npx playwright show-trace test-results/test-name/trace.zip
``` ```
### 4. Investigate Selector Issues ### 4. Investigate Selector Issues
```typescript ```typescript
// Check for multiple elements with same text // Check for multiple elements with same text
await page.locator('button:has-text("Yes")').count(); // Should be 1 await page.locator('button:has-text("Yes")').count(); // Should be 1
@@ -286,6 +317,7 @@ await page.locator('div[role="alert"]:has-text("Register") button:has-text("Yes"
``` ```
## What Works (Evidence) ## What Works (Evidence)
- ✅ **Error context files** provide page snapshots showing exact DOM state at failure - ✅ **Error context files** provide page snapshots showing exact DOM state at failure
- **Time**: 2025-08-21T14:22Z - **Time**: 2025-08-21T14:22Z
- **Evidence**: `test-results/60-new-activity-New-offers-for-another-user-chromium/error-context.md` shows both alerts visible - **Evidence**: `test-results/60-new-activity-New-offers-for-another-user-chromium/error-context.md` shows both alerts visible
@@ -302,6 +334,7 @@ await page.locator('div[role="alert"]:has-text("Register") button:has-text("Yes"
- **Verify at**: Error context markdown files - **Verify at**: Error context markdown files
## What Doesn't (Evidence & Hypotheses) ## What Doesn't (Evidence & Hypotheses)
- ❌ **Generic selectors** fail with multiple similar elements at `test-playwright/testUtils.ts:161` - ❌ **Generic selectors** fail with multiple similar elements at `test-playwright/testUtils.ts:161`
- **Time**: 2025-08-21T14:22Z - **Time**: 2025-08-21T14:22Z
- **Evidence**: `button:has-text("Yes")` matches both "Yes" and "Yes, Export Data" - **Evidence**: `button:has-text("Yes")` matches both "Yes" and "Yes, Export Data"
@@ -315,12 +348,14 @@ await page.locator('div[role="alert"]:has-text("Register") button:has-text("Yes"
- **Next probe**: Implement alert queuing or prevent overlapping alerts - **Next probe**: Implement alert queuing or prevent overlapping alerts
## Risks, Limits, Assumptions ## Risks, Limits, Assumptions
- **Trace file size**: Large trace files may impact storage and analysis time - **Trace file size**: Large trace files may impact storage and analysis time
- **Browser compatibility**: Trace viewer requires specific browser support - **Browser compatibility**: Trace viewer requires specific browser support
- **Test isolation**: Shared state between tests may affect investigation results - **Test isolation**: Shared state between tests may affect investigation results
- **Timing sensitivity**: Tests may pass/fail based on system performance - **Timing sensitivity**: Tests may pass/fail based on system performance
## Next Steps ## Next Steps
| Owner | Task | Exit Criteria | Target Date (UTC) | | Owner | Task | Exit Criteria | Target Date (UTC) |
|---|---|---|---| |---|---|---|---|
| Development Team | Fix test selectors for multiple alerts | All tests pass consistently | 2025-08-22 | | Development Team | Fix test selectors for multiple alerts | All tests pass consistently | 2025-08-22 |
@@ -328,21 +363,25 @@ await page.locator('div[role="alert"]:has-text("Register") button:has-text("Yes"
| Development Team | Add test IDs to alert buttons | Unique selectors for all UI elements | 2025-08-28 | | Development Team | Add test IDs to alert buttons | Unique selectors for all UI elements | 2025-08-28 |
## References ## References
- [Playwright Trace Viewer Documentation](https://playwright.dev/docs/trace-viewer) - [Playwright Trace Viewer Documentation](https://playwright.dev/docs/trace-viewer)
- [Playwright Test Results](https://playwright.dev/docs/test-reporters) - [Playwright Test Results](https://playwright.dev/docs/test-reporters)
- [Test Investigation Workflow](./research_diagnostic.mdc) - [Test Investigation Workflow](./research_diagnostic.mdc)
## Competence Hooks ## Competence Hooks
- **Why this works**: Systematic investigation leverages Playwright's built-in debugging tools to identify root causes - **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 - **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 - **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?" - **Teach-back**: "How would you investigate a Playwright test failure using error context, trace files, and page snapshots?"
## Collaboration Hooks ## Collaboration Hooks
- **Reviewers**: QA team, test automation engineers - **Reviewers**: QA team, test automation engineers
- **Sign-off checklist**: Error context analyzed, trace files reviewed, root cause identified, fix implemented and tested - **Sign-off checklist**: Error context analyzed, trace files reviewed, root cause identified, fix implemented and tested
## Assumptions & Limits ## Assumptions & Limits
- Test results directory structure follows Playwright conventions - Test results directory structure follows Playwright conventions
- Trace files are enabled in configuration (`trace: "retain-on-failure"`) - Trace files are enabled in configuration (`trace: "retain-on-failure"`)
- Error context files contain valid YAML page snapshots - Error context files contain valid YAML page snapshots
+1 -1
View File
@@ -73,7 +73,7 @@
### Avoid ### Avoid
- Vague: *improved, enhanced, better* - Vague: _improved, enhanced, better_
- Trivialities: tiny docs, one-liners, pure lint cleanups (separate, - Trivialities: tiny docs, one-liners, pure lint cleanups (separate,
+1 -1
View File
@@ -5,7 +5,7 @@
**Status**: 🎯 **ACTIVE** - Version control guidelines **Status**: 🎯 **ACTIVE** - Version control guidelines
## Core Principles ## Core Principles
### 0) let the developer control git
### 1) Version-Control Ownership ### 1) Version-Control Ownership
- **MUST NOT** run `git add`, `git commit`, or any write action. - **MUST NOT** run `git add`, `git commit`, or any write action.
+10 -45
View File
@@ -9,10 +9,6 @@ echo "🔍 Running pre-commit hooks..."
# Run lint-fix first # Run lint-fix first
echo "📝 Running lint-fix..." echo "📝 Running lint-fix..."
# Capture git status before lint-fix to detect changes
git_status_before=$(git status --porcelain)
npm run lint-fix || { npm run lint-fix || {
echo echo
echo "❌ Linting failed. Please fix the issues and try again." echo "❌ Linting failed. Please fix the issues and try again."
@@ -22,47 +18,16 @@ npm run lint-fix || {
exit 1 exit 1
} }
# Check if lint-fix made any changes # Build Architecture Guard - DISABLED
git_status_after=$(git status --porcelain) # echo "🏗️ Running Build Architecture Guard..."
# bash ./scripts/build-arch-guard.sh --staged || {
if [ "$git_status_before" != "$git_status_after" ]; then # echo
echo # echo "❌ Build Architecture Guard failed. Please fix the issues and try again."
echo "⚠️ lint-fix made changes to your files!" # echo "💡 To bypass this check for emergency commits, use:"
echo "📋 Changes detected:" # echo " git commit --no-verify"
git diff --name-only # echo
echo # exit 1
echo "❓ What would you like to do?" # }
echo " [c] Continue commit without the new changes"
echo " [a] Abort commit (recommended - review and stage the changes)"
echo
printf "Choose [c/a]: "
# The `< /dev/tty` is necessary to make read work in git's non-interactive shell
read choice < /dev/tty
case $choice in
[Cc]* )
echo "✅ Continuing commit without lint-fix changes..."
sleep 3
;;
[Aa]* | * )
echo "🛑 Commit aborted. Please review the changes made by lint-fix."
echo "💡 You can stage the changes with 'git add .' and commit again."
exit 1
;;
esac
fi
# Then run Build Architecture Guard
#echo "🏗️ Running Build Architecture Guard..."
#bash ./scripts/build-arch-guard.sh --staged || {
# echo
# echo "❌ Build Architecture Guard failed. Please fix the issues and try again."
# echo "💡 To bypass this check for emergency commits, use:"
# echo " git commit --no-verify"
# echo
# exit 1
#}
echo "✅ All pre-commit checks passed!" echo "✅ All pre-commit checks passed!"
+23 -18
View File
@@ -5,23 +5,28 @@
# #
. "$(dirname -- "$0")/_/husky.sh" . "$(dirname -- "$0")/_/husky.sh"
echo "🔍 Running Build Architecture Guard (pre-push)..." echo "🔍 Pre-push checks..."
# Get the remote branch we're pushing to # Build Architecture Guard - DISABLED
REMOTE_BRANCH="origin/$(git rev-parse --abbrev-ref HEAD)" # echo "🔍 Running Build Architecture Guard (pre-push)..."
#
# # Get the remote branch we're pushing to
# REMOTE_BRANCH="origin/$(git rev-parse --abbrev-ref HEAD)"
#
# # Check if remote branch exists
# if git show-ref --verify --quiet "refs/remotes/$REMOTE_BRANCH"; then
# RANGE="$REMOTE_BRANCH...HEAD"
# else
# # If remote branch doesn't exist, check last commit
# RANGE="HEAD~1..HEAD"
# fi
#
# bash ./scripts/build-arch-guard.sh --range "$RANGE" || {
# echo
# echo "💡 To bypass this check for emergency pushes, use:"
# echo " git push --no-verify"
# echo
# exit 1
# }
# Check if remote branch exists echo "✅ Pre-push checks passed!"
if git show-ref --verify --quiet "refs/remotes/$REMOTE_BRANCH"; then
RANGE="$REMOTE_BRANCH...HEAD"
else
# If remote branch doesn't exist, check last commit
RANGE="HEAD~1..HEAD"
fi
#bash ./scripts/build-arch-guard.sh --range "$RANGE" || {
# echo
# echo "💡 To bypass this check for emergency pushes, use:"
# echo " git push --no-verify"
# echo
# exit 1
#}
+49 -20
View File
@@ -1,27 +1,56 @@
{ {
"MD013": { "MD013": false,
"line_length": 80, "MD033": false,
"code_blocks": false, "MD041": false,
"tables": false, "MD024": {
"headings": false "siblings_only": true
},
"MD029": {
"style": "ordered"
},
"MD007": {
"indent": 2
},
"MD012": {
"maximum": 1
}, },
"MD012": true,
"MD022": true, "MD022": true,
"MD025": true,
"MD026": {
"punctuation": ".,;:!"
},
"MD030": {
"ul_single": 1,
"ol_single": 1,
"ul_multi": 1,
"ol_multi": 1
},
"MD031": true, "MD031": true,
"MD032": true, "MD032": true,
"MD047": true, "MD034": true,
"MD009": true, "MD035": {
"MD010": true, "style": "---"
"MD004": { "style": "dash" }, },
"MD029": { "style": "ordered" },
"MD041": false,
"MD025": false,
"MD024": false,
"MD036": false, "MD036": false,
"MD003": false, "MD037": true,
"MD040": false, "MD038": true,
"MD055": false, "MD039": true,
"MD056": false, "MD040": true,
"MD034": false, "MD042": true,
"MD023": false "MD043": false,
"MD044": false,
"MD045": true,
"MD046": {
"style": "fenced"
},
"MD047": true,
"MD048": {
"style": "backtick"
},
"MD049": {
"style": "underscore"
},
"MD050": {
"style": "asterisk"
}
} }
+97 -100
View File
@@ -93,6 +93,7 @@ The Build Architecture Guard protects your build system by enforcing documentati
#### Protected File Patterns #### Protected File Patterns
The guard monitors these sensitive paths: The guard monitors these sensitive paths:
- `vite.config.*` - Build configuration - `vite.config.*` - Build configuration
- `scripts/**` - Build and utility scripts - `scripts/**` - Build and utility scripts
- `electron/**` - Desktop application code - `electron/**` - Desktop application code
@@ -132,6 +133,7 @@ npm run guard:setup
#### Troubleshooting #### Troubleshooting
If you encounter `mapfile: command not found` errors: If you encounter `mapfile: command not found` errors:
```bash ```bash
# Ensure script is executable # Ensure script is executable
chmod +x scripts/build-arch-guard.sh chmod +x scripts/build-arch-guard.sh
@@ -175,6 +177,27 @@ cp .env.example .env.development
### Troubleshooting Quick Fixes ### Troubleshooting Quick Fixes
#### Common Issues
```bash
# Clean and rebuild
npm run clean:all
npm install
npm run build:web:dev
# Reset mobile projects
npm run clean:ios
npm run clean:android
npm run build:ios # Regenerates iOS project
npm run build:android # Regenerates Android project
# Fix Android asset issues
npm run assets:validate:android # Validates and regenerates missing Android assets
# Check environment
npm run test:web # Verifies web setup
```
#### Platform-Specific Issues #### Platform-Specific Issues
- **iOS**: Ensure Xcode and Command Line Tools are installed - **iOS**: Ensure Xcode and Command Line Tools are installed
@@ -249,6 +272,7 @@ Start the development server using `npm run build:web:dev` or `npm run build:web
3. To test the production build locally, use `npm run build:web:serve` (builds then serves) 3. To test the production build locally, use `npm run build:web:serve` (builds then serves)
**Why Use `serve`?** **Why Use `serve`?**
- **Production Testing**: Test your optimized production build locally before deployment - **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`) - **SPA Routing Validation**: Verify deep linking and navigation work correctly (handles routes like `/discover`, `/account`)
- **Performance Testing**: Test the minified and optimized build locally - **Performance Testing**: Test the minified and optimized build locally
@@ -314,15 +338,18 @@ All web build commands use the `./scripts/build-web.sh` script, which provides:
The `serve` functionality provides a local HTTP server for testing production builds: The `serve` functionality provides a local HTTP server for testing production builds:
**What It Does:** **What It Does:**
1. **Builds** the application using Vite 1. **Builds** the application using Vite
2. **Serves** the built files from the `dist/` directory 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.) 3. **Handles SPA Routing** - serves `index.html` for all routes (fixes 404s on `/discover`, `/account`, etc.)
**Server Options:** **Server Options:**
- **Primary**: `npx serve -s dist -l 8080` (recommended - full SPA support) - **Primary**: `npx serve -s dist -l 8080` (recommended - full SPA support)
- **Fallback**: Python HTTP server (limited SPA routing support) - **Fallback**: Python HTTP server (limited SPA routing support)
**Use Cases:** **Use Cases:**
- Testing production builds before deployment - Testing production builds before deployment
- Validating SPA routing behavior - Validating SPA routing behavior
- Performance testing of optimized builds - Performance testing of optimized builds
@@ -344,8 +371,8 @@ current version to test DB migrations.
- Put the commit hash in the changelog (which will help you remember to bump the - Put the commit hash in the changelog (which will help you remember to bump the
version in the step later). version in the step later).
- Tag with the new version, - Tag with the new version,
[online](https://gitea.anomalistdesign.com/trent_larson/crowd-funder-for-time-pwa/releases) or [online](https://gitea.anomalistdesign.com/trent_larson/crowd-funder-for-time-pwa/releases) or
`git tag 1.0.2 && git push origin 1.0.2`. `git tag 1.0.2 && git push origin 1.0.2`.
- For test, build the app: - For test, build the app:
@@ -364,13 +391,14 @@ rsync -azvu -e "ssh -i ~/.ssh/..." dist ubuntutest@test.timesafari.app:time-safa
(Note: The test BVC_MEETUPS_PROJECT_CLAIM_ID does not resolve as a URL because it's only in the test DB and the prod redirect won't redirect there.) (Note: The test BVC_MEETUPS_PROJECT_CLAIM_ID does not resolve as a URL because it's only in the test DB and the prod redirect won't redirect there.)
- For prod, you can do the same with `build:web:prod` instead. - For prod, get on the server and run the correct build:
... or log onto the server (though the build step can stay on "rendering chunks" for a long while): ... and log onto the server:
- `pkgx +npm sh` - `pkgx +npm sh`
- `cd crowd-funder-for-time-pwa && git checkout master && git pull && git checkout 1.0.2 && npm install && npm run build:web:prod && cd -` - `cd crowd-funder-for-time-pwa && git checkout master && git pull && git checkout
1.0.2 && npm install && npm run build:web:prod && cd -`
(The plain `npm run build:web:prod` uses the .env.production file.) (The plain `npm run build:web:prod` uses the .env.production file.)
@@ -379,50 +407,6 @@ rsync -azvu -e "ssh -i ~/.ssh/..." dist ubuntutest@test.timesafari.app:time-safa
- Record the new hash in the changelog. Edit package.json to increment version & - Record the new hash in the changelog. Edit package.json to increment version &
add "-beta", `npm install`, commit, and push. Also record what version is on production. add "-beta", `npm install`, commit, and push. Also record what version is on production.
## WebAuthn Verification Server
TimeSafari includes a server-side WebAuthn verification service for secure passkey registration and authentication. This server must be running for passkey features to work (unless offline mode is enabled).
### Quick Setup
```bash
# Navigate to server directory
cd server
# Install dependencies
npm install
# Copy and configure environment
cp .env.example .env
# Edit .env with your RP_ID, RP_NAME, RP_ORIGIN
# Start development server
npm run dev
```
The server runs on `http://localhost:3002` by default.
### Production Deployment
For production, you'll need to:
1. **Configure environment variables** in `.env`:
- `RP_ID`: Your domain (e.g., `timesafari.app`)
- `RP_NAME`: Application name
- `RP_ORIGIN`: Your app's origin URL
- `PORT`: Server port (default: 3002)
2. **Replace in-memory storage** with:
- Redis for challenge storage
- Database for credential persistence
- Session management for user binding
3. **Deploy the server** alongside your main application
4. **Configure client** via `VITE_WEBAUTHN_SERVER_URL` environment variable
See [server/README.md](server/README.md) for complete API documentation and deployment guide.
## Docker Deployment ## Docker Deployment
The application can be containerized using Docker for consistent deployment across The application can be containerized using Docker for consistent deployment across
@@ -1164,38 +1148,37 @@ If you need to build manually or want to understand the individual steps:
- Generate certificates inside XCode. - Generate certificates inside XCode.
- Right-click on App and under Signing & Capabilities set the Team. - Right-click on App and under Signing & Capabilities set the Team.
- In the App Developer setup (eg. https://developer.apple.com/account), under Identifiers and/or "Certificates, Identifiers & Profiles"
#### Each Release #### Each Release
##### 0. First time (or if dependencies change) ##### 0. First time (or if dependencies change)
- `pkgx +rubygems.org zsh` - `pkgx +rubygems.org sh`
- ... and you may have to fix these, especially with pkgx: - ... and you may have to fix these, especially with pkgx:
```bash ```bash
gem_path=$(which gem) gem_path=$(which gem)
shortened_path="${gem_path:h:h}" shortened_path="${gem_path:h:h}"
export GEM_HOME=$shortened_path export GEM_HOME=$shortened_path
export GEM_PATH=$shortened_path export GEM_PATH=$shortened_path
``` ```
##### 1. Bump the version in package.json & CHANGELOG.md for `MARKETING_VERSION`, then `grep CURRENT_PROJECT_VERSION ios/App/App.xcodeproj/project.pbxproj` and add 1 for the numbered version here: ##### 1. Bump the version in package.json, then here
```bash ```bash
cd ios/App && xcrun agvtool new-version 50 && perl -p -i -e "s/MARKETING_VERSION = .*;/MARKETING_VERSION = 1.1.5;/g" App.xcodeproj/project.pbxproj && cd - cd ios/App && xcrun agvtool new-version 40 && perl -p -i -e "s/MARKETING_VERSION = .*;/MARKETING_VERSION = 1.0.7;/g" App.xcodeproj/project.pbxproj && cd -
# Unfortunately this edits Info.plist directly. # Unfortunately this edits Info.plist directly.
#xcrun agvtool new-marketing-version 0.4.5 #xcrun agvtool new-marketing-version 0.4.5
``` ```
##### 2. Build ##### 2. Build
Here's prod. Also available: test, dev Here's prod. Also available: test, dev
```bash ```bash
npm run build:ios:prod npm run build:ios:prod
``` ```
3.1. Use Xcode to build and run on simulator or device. 3.1. Use Xcode to build and run on simulator or device.
@@ -1208,20 +1191,19 @@ npm run build:ios:prod
- Choose Product -> Archive - Choose Product -> Archive
- This will trigger a build and take time, needing user's "login" keychain - This will trigger a build and take time, needing user's "login" keychain
password (user's login password), repeatedly. password (user's login password), repeatedly.
- If it fails with `building for 'iOS', but linking in dylib - If it fails with `building for 'iOS', but linking in dylib
(.../.pkgx/zlib.net/v1.3.0/lib/libz.1.3.dylib) built for 'macOS'` then run (.../.pkgx/zlib.net/v1.3.0/lib/libz.1.3.dylib) built for 'macOS'` then run
XCode outside that terminal (ie. not with `npx cap open ios`). XCode outside that terminal (ie. not with `npx cap open ios`).
- Click Distribute -> App Store Connect - Click Distribute -> App Store Connect
- In AppStoreConnect, add the build to the distribution. You may have to remove - In AppStoreConnect, add the build to the distribution. You may have to remove
the current build with the "-" when you hover over it, then "Add Build" with the the current build with the "-" when you hover over it, then "Add Build" with the
new build. new build.
- May have to go to App Review, click Submission, then hover over the build - May have to go to App Review, click Submission, then hover over the build
and click "-". and click "-".
- It can take 15 minutes for the build to show up in the list of builds. - It can take 15 minutes for the build to show up in the list of builds.
- You'll probably have to "Manage" something about encryption, disallowed in France. - You'll probably have to "Manage" something about encryption, disallowed in France.
- Then "Save" and "Add to Review" and "Resubmit to App Review". - Then "Save" and "Add to Review" and "Resubmit to App Review".
- Eventually it'll be "Ready for Distribution" which means it's live - Eventually it'll be "Ready for Distribution" which means
- When finished, bump package.json version
### Android Build ### Android Build
@@ -1281,11 +1263,13 @@ npm run assets:validate
##### What Gets Validated ##### What Gets Validated
**Source Assets (Required):** **Source Assets (Required):**
- `resources/icon.png` - App icon source - `resources/icon.png` - App icon source
- `resources/splash.png` - Splash screen source - `resources/splash.png` - Splash screen source
- `resources/splash_dark.png` - Dark mode splash source - `resources/splash_dark.png` - Dark mode splash source
**Android Resources (Generated):** **Android Resources (Generated):**
- `android/app/src/main/res/drawable/splash.png` - Splash screen drawable - `android/app/src/main/res/drawable/splash.png` - Splash screen drawable
- `android/app/src/main/res/mipmap-*/ic_launcher.png` - App icons for all densities - `android/app/src/main/res/mipmap-*/ic_launcher.png` - App icons for all densities
- `android/app/src/main/res/mipmap-*/ic_launcher_round.png` - Round app icons for all densities - `android/app/src/main/res/mipmap-*/ic_launcher_round.png` - Round app icons for all densities
@@ -1327,8 +1311,8 @@ The recommended way to build for Android is using the automated build script:
# Standard build and open Android Studio # Standard build and open Android Studio
./scripts/build-android.sh ./scripts/build-android.sh
# Build with specific version numbers -- doesn't change source files # Build with specific version numbers
#./scripts/build-android.sh --version 1.1.3 --build-number 48 ./scripts/build-android.sh --version 1.0.3 --build-number 35
# Build without opening Android Studio (for CI/CD) # Build without opening Android Studio (for CI/CD)
./scripts/build-android.sh --no-studio ./scripts/build-android.sh --no-studio
@@ -1339,26 +1323,26 @@ The recommended way to build for Android is using the automated build script:
#### Android Manual Build Process #### Android Manual Build Process
##### 1. Bump the version in package.json, then update these versions & run: ##### 1. Bump the version in package.json, then here: android/app/build.gradle
```bash ```bash
perl -p -i -e 's/versionCode .*/versionCode 50/g' android/app/build.gradle perl -p -i -e 's/versionCode .*/versionCode 40/g' android/app/build.gradle
perl -p -i -e 's/versionName .*/versionName "1.1.5"/g' android/app/build.gradle perl -p -i -e 's/versionName .*/versionName "1.0.7"/g' android/app/build.gradle
``` ```
##### 2. Build ##### 2. Build
Here's prod. Also available: test, dev Here's prod. Also available: test, dev
```bash ```bash
npm run build:android:prod npm run build:android:prod
``` ```
##### 3. Open the project in Android Studio ##### 3. Open the project in Android Studio
```bash ```bash
npx cap open android npx cap open android
``` ```
##### 4. Use Android Studio to build and run on emulator or device ##### 4. Use Android Studio to build and run on emulator or device
@@ -1403,8 +1387,6 @@ At play.google.com/console:
- Note that if you add testers, you have to go to "Publishing Overview" and send - Note that if you add testers, you have to go to "Publishing Overview" and send
those changes or your (closed) testers won't see it. those changes or your (closed) testers won't see it.
- When finished, bump package.json version
### Capacitor Operations ### Capacitor Operations
```bash ```bash
@@ -1578,7 +1560,6 @@ VITE_APP_SERVER=https://timesafari.app
# Feature Flags # Feature Flags
VITE_PASSKEYS_ENABLED=true VITE_PASSKEYS_ENABLED=true
VITE_WEBAUTHN_SERVER_URL=http://localhost:3002
VITE_BVC_MEETUPS_PROJECT_CLAIM_ID=https://endorser.ch/entity/01HWE8FWHQ1YGP7GFZYYPS272F VITE_BVC_MEETUPS_PROJECT_CLAIM_ID=https://endorser.ch/entity/01HWE8FWHQ1YGP7GFZYYPS272F
``` ```
@@ -1592,9 +1573,6 @@ VITE_DEFAULT_ENDORSER_API_SERVER=http://localhost:3000
VITE_DEFAULT_PARTNER_API_SERVER=http://localhost:3000 VITE_DEFAULT_PARTNER_API_SERVER=http://localhost:3000
VITE_DEFAULT_IMAGE_API_SERVER=https://test-image-api.timesafari.app VITE_DEFAULT_IMAGE_API_SERVER=https://test-image-api.timesafari.app
VITE_APP_SERVER=http://localhost:8080 VITE_APP_SERVER=http://localhost:8080
# WebAuthn Server (for passkey verification)
VITE_WEBAUTHN_SERVER_URL=http://localhost:3002
``` ```
**Test Environment** (`.env.test`): **Test Environment** (`.env.test`):
@@ -1772,12 +1750,14 @@ npx prettier --write ./sw_scripts/
The `prebuild` script automatically runs before any build: The `prebuild` script automatically runs before any build:
```json ```json
"prebuild": "eslint --ext .js,.ts,.vue --ignore-path .gitignore src && node scripts/copy-wasm.js" "prebuild": "eslint --ext .js,.ts,.vue --ignore-path .gitignore src && node sw_combine.js && node scripts/copy-wasm.js"
``` ```
**What happens automatically:** **What happens automatically:**
- **ESLint**: Checks and fixes code formatting in `src/` - **ESLint**: Checks and fixes code formatting in `src/`
- **Script Combination**: `sw_combine.js` combines all `sw_scripts/*.js` files
into `sw_scripts-combined.js`
- **WASM Copy**: `copy-wasm.js` copies SQLite WASM files to `public/wasm/` - **WASM Copy**: `copy-wasm.js` copies SQLite WASM files to `public/wasm/`
#### Build Process Architecture #### Build Process Architecture
@@ -1785,10 +1765,10 @@ The `prebuild` script automatically runs before any build:
**Web Build Process:** **Web Build Process:**
```text ```text
1. Pre-Build: ESLint + WASM Copy 1. Pre-Build: ESLint + Script Combination + WASM Copy
2. Environment Setup: Load .env files, set NODE_ENV 2. Environment Setup: Load .env files, set NODE_ENV
3. Vite Build: Bundle web assets with PWA support 3. Vite Build: Bundle web assets with PWA support
4. Service Worker: Inject service worker scripts into PWA 4. Service Worker: Inject combined scripts into PWA
5. Output: Production-ready files in dist/ 5. Output: Production-ready files in dist/
``` ```
@@ -1816,8 +1796,10 @@ The `prebuild` script automatically runs before any build:
**Script Organization:** **Script Organization:**
- `sw_scripts/` - Individual third-party scripts for service worker - `sw_scripts/` - Individual third-party scripts
- `vite.config.utils.mts` - PWA configuration - `sw_combine.js` - Combines scripts into single file
- `sw_scripts-combined.js` - Combined service worker (317KB, 10K+ lines)
- `vite.config.utils.mts` - PWA configuration using combined script
**PWA Integration:** **PWA Integration:**
@@ -1825,16 +1807,18 @@ The `prebuild` script automatically runs before any build:
// vite.config.utils.mts // vite.config.utils.mts
pwaConfig: { pwaConfig: {
strategies: "injectManifest", strategies: "injectManifest",
filename: "sw_scripts-combined.js", // Service worker file filename: "sw_scripts-combined.js", // Uses our combined script
// ... manifest configuration // ... manifest configuration
} }
``` ```
**Service Worker Scripts:** **What Gets Combined:**
- `nacl.js` - NaCl cryptographic library - `nacl.js` - NaCl cryptographic library
- `noble-curves.js` - Elliptic curve cryptography (177KB) - `noble-curves.js` - Elliptic curve cryptography (177KB)
- `noble-hashes.js` - Cryptographic hash functions (91KB) - `noble-hashes.js` - Cryptographic hash functions (91KB)
- `safari-notifications.js` - Safari-specific notifications
- `additional-scripts.js` - Additional service worker functionality
#### Process Environment Configuration #### Process Environment Configuration
@@ -1870,7 +1854,6 @@ VITE_APP_SERVER=https://timesafari.app
# Feature Flags # Feature Flags
VITE_PASSKEYS_ENABLED=true VITE_PASSKEYS_ENABLED=true
VITE_WEBAUTHN_SERVER_URL=http://localhost:3002
VITE_BVC_MEETUPS_PROJECT_CLAIM_ID=https://endorser.ch/entity/01HWE8FWHQ1YGP7GFZYYPS272F VITE_BVC_MEETUPS_PROJECT_CLAIM_ID=https://endorser.ch/entity/01HWE8FWHQ1YGP7GFZYYPS272F
``` ```
@@ -2731,6 +2714,7 @@ configuration files in the repository.
### 2025-08-21 - Cursor Rules Refactoring and Build System Updates ### 2025-08-21 - Cursor Rules Refactoring and Build System Updates
#### Package Dependencies Updated #### Package Dependencies Updated
- **Added**: `markdownlint-cli2` v0.18.1 - Modern markdown linting with improved performance - **Added**: `markdownlint-cli2` v0.18.1 - Modern markdown linting with improved performance
- **Added**: `@commitlint/cli` v18.6.1 - Conventional commit message validation - **Added**: `@commitlint/cli` v18.6.1 - Conventional commit message validation
- **Added**: `@commitlint/config-conventional` v18.6.2 - Conventional commit standards - **Added**: `@commitlint/config-conventional` v18.6.2 - Conventional commit standards
@@ -2738,28 +2722,33 @@ configuration files in the repository.
- **Updated**: `lint-staged` v15.2.2 - Pre-commit linting automation - **Updated**: `lint-staged` v15.2.2 - Pre-commit linting automation
#### Build Script Improvements #### Build Script Improvements
- **Markdown Linting**: Replaced custom markdown scripts with `markdownlint-cli2` - **Markdown Linting**: Replaced custom markdown scripts with `markdownlint-cli2`
- **Before**: `./scripts/fix-markdown.sh` and `./scripts/validate-markdown.sh` - **Before**: `./scripts/fix-markdown.sh` and `./scripts/validate-markdown.sh`
- **After**: `markdownlint-cli2 --fix` and `markdownlint-cli2` - **After**: `markdownlint-cli2 --fix` and `markdownlint-cli2`
- **Benefits**: Faster execution, better error reporting, modern markdown standards - **Benefits**: Faster execution, better error reporting, modern markdown standards
#### Lint-Staged Configuration Enhanced #### Lint-Staged Configuration Enhanced
- **Added**: Markdown file linting to pre-commit hooks - **Added**: Markdown file linting to pre-commit hooks
- **Pattern**: `*.{md,markdown,mdc}` files now automatically formatted - **Pattern**: `*.{md,markdown,mdc}` files now automatically formatted
- **Command**: `markdownlint-cli2 --fix` runs before each commit - **Command**: `markdownlint-cli2 --fix` runs before each commit
- **Coverage**: All markdown files including `.mdc` cursor rules - **Coverage**: All markdown files including `.mdc` cursor rules
#### Commit Message Standards #### Commit Message Standards
- **Added**: Conventional commit validation via commitlint - **Added**: Conventional commit validation via commitlint
- **Configuration**: Extends `@commitlint/config-conventional` - **Configuration**: Extends `@commitlint/config-conventional`
- **Enforcement**: Ensures consistent commit message format across the project - **Enforcement**: Ensures consistent commit message format across the project
#### Node.js Version Requirements #### Node.js Version Requirements
- **Updated**: Minimum Node.js version requirements for new dependencies - **Updated**: Minimum Node.js version requirements for new dependencies
- **markdownlint-cli2**: Requires Node.js >=20 - **markdownlint-cli2**: Requires Node.js >=20
- **Various utilities**: Require Node.js >=18 for modern ES features - **Various utilities**: Require Node.js >=18 for modern ES features
#### Build Process Impact #### Build Process Impact
- **No Breaking Changes**: All existing build commands continue to work - **No Breaking Changes**: All existing build commands continue to work
- **Improved Quality**: Better markdown formatting and commit message standards - **Improved Quality**: Better markdown formatting and commit message standards
- **Enhanced Automation**: More comprehensive pre-commit validation - **Enhanced Automation**: More comprehensive pre-commit validation
@@ -2770,6 +2759,7 @@ configuration files in the repository.
### 2025-08-21 - Commitlint Configuration Refinement ### 2025-08-21 - Commitlint Configuration Refinement
#### Commit Message Validation Improvements #### Commit Message Validation Improvements
- **Modified**: Commitlint configuration moved from `package.json` to dedicated `commitlint.config.js` - **Modified**: Commitlint configuration moved from `package.json` to dedicated `commitlint.config.js`
- **Enhanced**: Strict validation rules downgraded from errors to warnings - **Enhanced**: Strict validation rules downgraded from errors to warnings
- **Before**: `subject-case` and `subject-full-stop` rules caused red error messages - **Before**: `subject-case` and `subject-full-stop` rules caused red error messages
@@ -2777,16 +2767,18 @@ configuration files in the repository.
- **Benefit**: Eliminates confusing red error messages while maintaining commit quality guidance - **Benefit**: Eliminates confusing red error messages while maintaining commit quality guidance
#### Configuration Structure #### Configuration Structure
- **File**: `commitlint.config.js` - Dedicated commitlint configuration - **File**: `commitlint.config.js` - Dedicated commitlint configuration
- **Extends**: `@commitlint/config-conventional` - Standard conventional commit rules - **Extends**: `@commitlint/config-conventional` - Standard conventional commit rules
- **Custom Rules**: - **Custom Rules**:
- `subject-case: [1, 'never', ['sentence-case', 'start-case', 'pascal-case', 'upper-case']]` - `subject-case: [1, 'never', ['sentence-case', 'start-case', 'pascal-case', 'upper-case']]`
- `subject-full-stop: [1, 'never', '.']` - `subject-full-stop: [1, 'never', '.']`
- **Levels**: - **Levels**:
- `0` = Disabled, `1` = Warning, `2` = Error - `0` = Disabled, `1` = Warning, `2` = Error
- Current: Problematic rules set to warning level (1) - Current: Problematic rules set to warning level (1)
#### User Experience Impact #### User Experience Impact
- **Before**: Red error messages on every push with strict commit rules - **Before**: Red error messages on every push with strict commit rules
- **After**: Yellow warning messages that provide guidance without disruption - **After**: Yellow warning messages that provide guidance without disruption
- **Workflow**: Commits and pushes continue to work while maintaining quality standards - **Workflow**: Commits and pushes continue to work while maintaining quality standards
@@ -2797,6 +2789,7 @@ configuration files in the repository.
### 2025-08-26 - Capacitor Plugin Additions ### 2025-08-26 - Capacitor Plugin Additions
#### New Capacitor Plugins Added #### New Capacitor Plugins Added
- **Added**: `@capacitor/clipboard` v6.0.2 - Clipboard functionality for mobile platforms - **Added**: `@capacitor/clipboard` v6.0.2 - Clipboard functionality for mobile platforms
- **Purpose**: Enable copy/paste operations on mobile devices - **Purpose**: Enable copy/paste operations on mobile devices
- **Platforms**: iOS and Android - **Platforms**: iOS and Android
@@ -2810,23 +2803,27 @@ configuration files in the repository.
- **Integration**: Automatically included in mobile builds - **Integration**: Automatically included in mobile builds
#### Android Build System Updates #### Android Build System Updates
- **Modified**: `android/capacitor.settings.gradle` - Added new plugin project includes - **Modified**: `android/capacitor.settings.gradle` - Added new plugin project includes
- **Added**: `:capacitor-clipboard` project directory mapping - **Added**: `:capacitor-clipboard` project directory mapping
- **Added**: `:capacitor-status-bar` project directory mapping - **Added**: `:capacitor-status-bar` project directory mapping
- **Impact**: New plugins now properly integrated into Android build process - **Impact**: New plugins now properly integrated into Android build process
#### Package Dependencies #### Package Dependencies
- **Updated**: `package.json` - Added new Capacitor plugin dependencies - **Updated**: `package.json` - Added new Capacitor plugin dependencies
- **Updated**: `package-lock.json` - Locked dependency versions for consistency - **Updated**: `package-lock.json` - Locked dependency versions for consistency
- **Version**: All new plugins use Capacitor 6.x compatible versions - **Version**: All new plugins use Capacitor 6.x compatible versions
#### Build Process Impact #### Build Process Impact
- **No Breaking Changes**: Existing build commands continue to work unchanged - **No Breaking Changes**: Existing build commands continue to work unchanged
- **Enhanced Mobile Features**: New clipboard and status bar capabilities available - **Enhanced Mobile Features**: New clipboard and status bar capabilities available
- **Automatic Integration**: Plugins automatically included in mobile builds - **Automatic Integration**: Plugins automatically included in mobile builds
- **Platform Support**: Both iOS and Android builds now include new functionality - **Platform Support**: Both iOS and Android builds now include new functionality
#### Testing Requirements #### Testing Requirements
- **Mobile Builds**: Verify new plugins integrate correctly in iOS and Android builds - **Mobile Builds**: Verify new plugins integrate correctly in iOS and Android builds
- **Functionality**: Test clipboard operations and status bar management on devices - **Functionality**: Test clipboard operations and status bar management on devices
- **Fallback**: Ensure graceful degradation when plugins are unavailable - **Fallback**: Ensure graceful degradation when plugins are unavailable
-40
View File
@@ -5,46 +5,6 @@ All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
## [1.1.5] - 2025.12.28
### Fixed
- Incorrect prompts in give-dialog on a project or offer
## [1.1.4] - 2025.12.18
### Fixed
- Contact notes & contact methods preserved in export
### Added
- This is a target for sharing
- Switch to a project or person in give-dialog pop-up
- Starred projects onto project-choice in give-dialog pop-up
### Changed
- Front page: 1 green "Thank" button
## [1.1.3] - 2025.11.19
### Changed
- Project selection in dialogs now reaches out to server when filtering
- Project selection during onboarding meeting is a search (not an input box)
- Improve the switching of agent when agent edits a project
### Fixed
- Reassignment of "you" as recipient when changing giver project
- Bad counts for project-change notification on front page
## [1.1.2] - 2025.11.06
### Fixed
- Bad page when user follows prompt to backup seed
## [1.1.1] - 2025.11.03
### Added
- Meeting onboarding via prompts
- Emojis on gift feed
- Starred projects with notification
## [1.0.7] - 2025.08.18 ## [1.0.7] - 2025.08.18
### Fixed ### Fixed
-852
View File
@@ -1,852 +0,0 @@
# TimeSafari Code Quality: Comprehensive Deep Analysis
**Author**: Matthew Raymer
**Date**: Tue Sep 16 05:22:10 AM UTC 2025
**Status**: 🎯 **COMPREHENSIVE ANALYSIS** - Complete code quality assessment with actionable recommendations
## Executive Summary
The TimeSafari codebase demonstrates **exceptional code quality** with mature patterns, minimal technical debt, and excellent separation of concerns. This comprehensive analysis covers **291 source files** totaling **104,527 lines** of code, including detailed examination of **94 Vue components and views**.
**Key Quality Metrics:**
- **Technical Debt**: Extremely low (6 TODO/FIXME comments across entire codebase)
- **Database Migration**: 99.5% complete (1 remaining legacy import)
- **File Complexity**: High variance (largest file: 2,215 lines)
- **Type Safety**: Mixed patterns (41 "as any" assertions in Vue files, 62 total)
- **Error Handling**: Comprehensive (367 catch blocks with good coverage)
- **Architecture**: Consistent Vue 3 Composition API with TypeScript
## Vue Components & Views Analysis (94 Files)
### Component Analysis (40 Components)
#### Component Size Distribution
```
Large Components (>500 lines): 5 components (12.5%)
├── ImageMethodDialog.vue (947 lines) 🔴 CRITICAL
├── GiftedDialog.vue (670 lines) ⚠️ HIGH PRIORITY
├── PhotoDialog.vue (669 lines) ⚠️ HIGH PRIORITY
├── PushNotificationPermission.vue (660 lines) ⚠️ HIGH PRIORITY
└── MembersList.vue (550 lines) ⚠️ MODERATE PRIORITY
Medium Components (200-500 lines): 12 components (30%)
├── GiftDetailsStep.vue (450 lines)
├── EntityGrid.vue (348 lines)
├── ActivityListItem.vue (334 lines)
├── OfferDialog.vue (327 lines)
├── OnboardingDialog.vue (314 lines)
├── EntitySelectionStep.vue (313 lines)
├── GiftedPrompts.vue (293 lines)
├── ChoiceButtonDialog.vue (250 lines)
├── DataExportSection.vue (251 lines)
├── AmountInput.vue (224 lines)
├── HiddenDidDialog.vue (220 lines)
└── FeedFilters.vue (218 lines)
Small Components (<200 lines): 23 components (57.5%)
├── ContactListItem.vue (217 lines)
├── EntitySummaryButton.vue (202 lines)
├── IdentitySection.vue (186 lines)
├── ContactInputForm.vue (173 lines)
├── SpecialEntityCard.vue (156 lines)
├── RegistrationNotice.vue (154 lines)
├── ContactNameDialog.vue (154 lines)
├── PersonCard.vue (153 lines)
├── UserNameDialog.vue (147 lines)
├── InfiniteScroll.vue (132 lines)
├── LocationSearchSection.vue (124 lines)
├── UsageLimitsSection.vue (123 lines)
├── QuickNav.vue (118 lines)
├── ProjectCard.vue (104 lines)
├── ContactListHeader.vue (101 lines)
├── TopMessage.vue (98 lines)
├── InviteDialog.vue (95 lines)
├── ImageViewer.vue (94 lines)
├── EntityIcon.vue (86 lines)
├── ShowAllCard.vue (66 lines)
├── ContactBulkActions.vue (53 lines)
├── ProjectIcon.vue (47 lines)
└── LargeIdenticonModal.vue (44 lines)
```
#### Critical Component Analysis
**1. `ImageMethodDialog.vue` (947 lines) 🔴 CRITICAL REFACTORING NEEDED**
**Issues Identified:**
- **Excessive Single Responsibility**: Handles camera preview, file upload, URL input, cropping, diagnostics, and error handling
- **Complex State Management**: 20+ reactive properties with interdependencies
- **Mixed Concerns**: Camera API, file handling, UI state, and business logic intertwined
- **Template Complexity**: ~300 lines of template with deeply nested conditions
**Refactoring Strategy:**
```typescript
// Current monolithic structure
ImageMethodDialog.vue (947 lines) {
CameraPreview: ~200 lines
FileUpload: ~150 lines
URLInput: ~100 lines
CroppingInterface: ~200 lines
DiagnosticsPanel: ~150 lines
ErrorHandling: ~100 lines
StateManagement: ~47 lines
}
// Proposed component decomposition
ImageMethodDialog.vue (coordinator, ~200 lines)
├── CameraPreviewComponent.vue (~250 lines)
├── FileUploadComponent.vue (~150 lines)
├── URLInputComponent.vue (~100 lines)
├── ImageCropperComponent.vue (~200 lines)
├── DiagnosticsPanelComponent.vue (~150 lines)
└── ImageUploadErrorHandler.vue (~100 lines)
```
**2. `GiftedDialog.vue` (670 lines) ⚠️ HIGH PRIORITY**
**Assessment**: **GOOD** - Already partially refactored with step components extracted.
**3. `PhotoDialog.vue` (669 lines) ⚠️ HIGH PRIORITY**
**Issues**: Similar to ImageMethodDialog with significant code duplication.
**4. `PushNotificationPermission.vue` (660 lines) ⚠️ HIGH PRIORITY**
**Issues**: Complex permission logic with platform-specific code mixed together.
### View Analysis (54 Views)
#### View Size Distribution
```
Large Views (>1000 lines): 9 views (16.7%)
├── AccountViewView.vue (2,215 lines) 🔴 CRITICAL
├── HomeView.vue (1,852 lines) ⚠️ HIGH PRIORITY
├── ProjectViewView.vue (1,479 lines) ⚠️ HIGH PRIORITY
├── DatabaseMigration.vue (1,438 lines) ⚠️ HIGH PRIORITY
├── ContactsView.vue (1,382 lines) ⚠️ HIGH PRIORITY
├── TestView.vue (1,259 lines) ⚠️ MODERATE PRIORITY
├── ClaimView.vue (1,225 lines) ⚠️ MODERATE PRIORITY
├── NewEditProjectView.vue (957 lines) ⚠️ MODERATE PRIORITY
└── ContactQRScanShowView.vue (929 lines) ⚠️ MODERATE PRIORITY
Medium Views (500-1000 lines): 8 views (14.8%)
├── ConfirmGiftView.vue (898 lines)
├── DiscoverView.vue (888 lines)
├── DIDView.vue (848 lines)
├── GiftedDetailsView.vue (840 lines)
├── OfferDetailsView.vue (781 lines)
├── HelpView.vue (780 lines)
├── ProjectsView.vue (742 lines)
└── ContactQRScanFullView.vue (701 lines)
Small Views (<500 lines): 37 views (68.5%)
├── OnboardMeetingSetupView.vue (687 lines)
├── ContactImportView.vue (568 lines)
├── HelpNotificationsView.vue (566 lines)
├── OnboardMeetingListView.vue (507 lines)
├── InviteOneView.vue (475 lines)
├── QuickActionBvcEndView.vue (442 lines)
├── ContactAmountsView.vue (416 lines)
├── SearchAreaView.vue (384 lines)
├── SharedPhotoView.vue (379 lines)
├── ContactGiftingView.vue (373 lines)
├── ContactEditView.vue (345 lines)
├── IdentitySwitcherView.vue (324 lines)
├── UserProfileView.vue (323 lines)
├── NewActivityView.vue (323 lines)
├── QuickActionBvcBeginView.vue (303 lines)
├── SeedBackupView.vue (292 lines)
├── InviteOneAcceptView.vue (292 lines)
├── ClaimCertificateView.vue (279 lines)
├── StartView.vue (271 lines)
├── ImportAccountView.vue (265 lines)
├── ClaimAddRawView.vue (249 lines)
├── OnboardMeetingMembersView.vue (247 lines)
├── DeepLinkErrorView.vue (239 lines)
├── ClaimReportCertificateView.vue (236 lines)
├── DeepLinkRedirectView.vue (219 lines)
├── ImportDerivedAccountView.vue (207 lines)
├── ShareMyContactInfoView.vue (196 lines)
├── RecentOffersToUserProjectsView.vue (176 lines)
├── RecentOffersToUserView.vue (166 lines)
├── NewEditAccountView.vue (142 lines)
├── StatisticsView.vue (133 lines)
├── HelpOnboardingView.vue (118 lines)
├── LogView.vue (104 lines)
├── NewIdentifierView.vue (97 lines)
├── HelpNotificationTypesView.vue (73 lines)
├── ConfirmContactView.vue (57 lines)
└── QuickActionBvcView.vue (54 lines)
```
#### Critical View Analysis
**1. `AccountViewView.vue` (2,215 lines) 🔴 CRITICAL REFACTORING NEEDED**
**Issues Identified:**
- **Monolithic Architecture**: Handles 7 distinct concerns in single file
- **Template Complexity**: ~750 lines of template with deeply nested conditions
- **Method Proliferation**: 50+ methods handling disparate concerns
- **State Management**: 25+ reactive properties without clear organization
**Refactoring Strategy:**
```typescript
// Current monolithic structure
AccountViewView.vue (2,215 lines) {
ProfileSection: ~400 lines
SettingsSection: ~300 lines
NotificationSection: ~200 lines
ServerConfigSection: ~250 lines
ExportImportSection: ~300 lines
LimitsSection: ~150 lines
MapSection: ~200 lines
StateManagement: ~415 lines
}
// Proposed component extraction
AccountViewView.vue (coordinator, ~400 lines)
├── ProfileManagementSection.vue (~300 lines)
├── ServerConfigurationSection.vue (~250 lines)
├── NotificationSettingsSection.vue (~200 lines)
├── DataExportImportSection.vue (~300 lines)
├── UsageLimitsDisplay.vue (~150 lines)
├── LocationProfileSection.vue (~200 lines)
└── AccountViewStateManager.ts (~200 lines)
```
**2. `HomeView.vue` (1,852 lines) ⚠️ HIGH PRIORITY**
**Issues Identified:**
- **Multiple Concerns**: Activity feed, projects, contacts, notifications in one file
- **Complex State Management**: 20+ reactive properties with interdependencies
- **Mixed Lifecycle Logic**: Mount, update, and destroy logic intertwined
**3. `ProjectViewView.vue` (1,479 lines) ⚠️ HIGH PRIORITY**
**Issues Identified:**
- **Project Management Complexity**: Handles project details, members, offers, and activities
- **Mixed Concerns**: Project data, member management, and activity feed in single view
### Vue Component Quality Patterns
#### Excellent Patterns Found:
**1. EntityIcon.vue (86 lines) ✅ EXCELLENT**
```typescript
// Clean, focused responsibility
@Component({ name: "EntityIcon" })
export default class EntityIcon extends Vue {
@Prop() contact?: Contact;
@Prop({ default: "" }) entityId!: string;
@Prop({ default: 0 }) iconSize!: number;
generateIcon(): string {
// Clear priority order: profile image → avatar → fallback
const imageUrl = this.contact?.profileImageUrl || this.profileImageUrl;
if (imageUrl) return `<img src="${imageUrl}" ... />`;
const identifier = this.contact?.did || this.entityId;
if (!identifier) return `<img src="${blankSquareSvg}" ... />`;
return createAvatar(avataaars, { seed: identifier, size: this.iconSize }).toString();
}
}
```
**2. QuickNav.vue (118 lines) ✅ EXCELLENT**
```typescript
// Simple, focused navigation component
@Component({ name: "QuickNav" })
export default class QuickNav extends Vue {
@Prop selected = "";
// Clean template with consistent patterns
// Proper accessibility attributes
// Responsive design with safe area handling
}
```
**3. Small Focused Views ✅ EXCELLENT**
```typescript
// QuickActionBvcView.vue (54 lines) - Perfect size
// ConfirmContactView.vue (57 lines) - Focused responsibility
// HelpNotificationTypesView.vue (73 lines) - Clear purpose
// LogView.vue (104 lines) - Simple utility view
```
#### Problematic Patterns Found:
**1. Excessive Props in Dialog Components**
```typescript
// GiftedDialog.vue - Too many props
@Prop() fromProjectId = "";
@Prop() toProjectId = "";
@Prop() isFromProjectView = false;
@Prop() hideShowAll = false;
@Prop({ default: "person" }) giverEntityType = "person";
@Prop({ default: "person" }) recipientEntityType = "person";
// ... 10+ more props
```
**2. Complex State Machines**
```typescript
// ImageMethodDialog.vue - Complex state management
cameraState: "off" | "initializing" | "active" | "error" | "retrying" | "stopped" = "off";
showCameraPreview = false;
isRetrying = false;
showDiagnostics = false;
// ... 15+ more state properties
```
**3. Excessive Reactive Properties**
```typescript
// AccountViewView.vue - Too many reactive properties
downloadUrl: string = "";
loadingLimits: boolean = false;
loadingProfile: boolean = true;
showAdvanced: boolean = false;
showB64Copy: boolean = false;
showContactGives: boolean = false;
showDidCopy: boolean = false;
showDerCopy: boolean = false;
showGeneralAdvanced: boolean = false;
showLargeIdenticonId?: string;
showLargeIdenticonUrl?: string;
showPubCopy: boolean = false;
showShortcutBvc: boolean = false;
warnIfProdServer: boolean = false;
warnIfTestServer: boolean = false;
zoom: number = 2;
isMapReady: boolean = false;
// ... 10+ more properties
```
## File Size and Complexity Analysis (All Files)
### Problematic Large Files
#### 1. `AccountViewView.vue` (2,215 lines) 🔴 **CRITICAL**
**Issues Identified:**
- **Excessive Single File Responsibility**: Handles profile, settings, notifications, server configuration, export/import, limits checking
- **Template Complexity**: ~750 lines of template with deeply nested conditions
- **Method Proliferation**: 50+ methods handling disparate concerns
- **State Management**: 25+ reactive properties without clear organization
#### 2. `PlatformServiceMixin.ts` (2,091 lines) ⚠️ **HIGH PRIORITY**
**Issues Identified:**
- **God Object Pattern**: Single file handling 80+ methods across multiple concerns
- **Mixed Abstraction Levels**: Low-level SQL utilities mixed with high-level business logic
- **Method Length Variance**: Some methods 100+ lines, others single-line wrappers
**Refactoring Strategy:**
```typescript
// Current monolithic mixin
PlatformServiceMixin.ts (2,091 lines)
// Proposed separation of concerns
├── CoreDatabaseMixin.ts // $db, $exec, $query, $first (200 lines)
├── SettingsManagementMixin.ts // $settings, $saveSettings (400 lines)
├── ContactManagementMixin.ts // $contacts, $insertContact (300 lines)
├── EntityOperationsMixin.ts // $insertEntity, $updateEntity (400 lines)
├── CachingMixin.ts // Cache management (150 lines)
├── ActiveIdentityMixin.ts // Active DID management (200 lines)
├── UtilityMixin.ts // Mapping, JSON parsing (200 lines)
└── LoggingMixin.ts // $log, $logError (100 lines)
```
#### 3. `HomeView.vue` (1,852 lines) ⚠️ **MODERATE PRIORITY**
**Issues Identified:**
- **Multiple Concerns**: Activity feed, projects, contacts, notifications in one file
- **Complex State Management**: 20+ reactive properties with interdependencies
- **Mixed Lifecycle Logic**: Mount, update, and destroy logic intertwined
### File Size Distribution Analysis
```
Files > 1000 lines: 9 files (4.6% of codebase)
Files 500-1000 lines: 23 files (11.7% of codebase)
Files 200-500 lines: 45 files (22.8% of codebase)
Files < 200 lines: 120 files (60.9% of codebase)
```
**Assessment**: Good distribution with most files reasonably sized, but critical outliers need attention.
## Type Safety Analysis
### Type Assertion Patterns
#### "as any" Usage (62 total instances) ⚠️
**Vue Components & Views (41 instances):**
```typescript
// ImageMethodDialog.vue:504
const activeIdentity = await (this as any).$getActiveIdentity();
// GiftedDialog.vue:228
const activeIdentity = await (this as any).$getActiveIdentity();
// AccountViewView.vue: Multiple instances for:
// - PlatformServiceMixin method access
// - Vue refs with complex typing
// - External library integration (Leaflet)
```
**Other Files (21 instances):**
- **Vue Component References** (23 instances): `(this.$refs.dialog as any)`
- **Platform Detection** (12 instances): `(navigator as any).standalone`
- **External Library Integration** (15 instances): Leaflet, Axios extensions
- **Legacy Code Compatibility** (8 instances): Temporary migration code
- **Event Handler Workarounds** (4 instances): Vue event typing issues
**Example Problematic Pattern:**
```typescript
// src/views/AccountViewView.vue:934
const iconDefault = L.Icon.Default.prototype as unknown as Record<string, unknown>;
// Better approach:
interface LeafletIconPrototype {
_getIconUrl?: unknown;
}
const iconDefault = L.Icon.Default.prototype as LeafletIconPrototype;
```
#### "unknown" Type Usage (755 instances)
**Analysis**: Generally good practice showing defensive programming, but some areas could benefit from more specific typing.
### Recommended Type Safety Improvements
1. **Create Interface Extensions**:
```typescript
// src/types/platform-service-mixin.ts
interface VueWithPlatformServiceMixin extends Vue {
$getActiveIdentity(): Promise<{ activeDid: string }>;
$saveSettings(changes: Partial<Settings>): Promise<boolean>;
// ... other methods
}
// src/types/external.ts
declare global {
interface Navigator {
standalone?: boolean;
}
}
interface VueRefWithOpen {
open: (callback: (result?: unknown) => void) => void;
}
```
2. **Component Ref Typing**:
```typescript
// Instead of: (this.$refs.dialog as any).open()
// Use: (this.$refs.dialog as VueRefWithOpen).open()
```
## Error Handling Consistency Analysis
### Error Handling Patterns (367 catch blocks)
#### Pattern Distribution:
1. **Structured Logging** (85%): Uses logger.error with context
2. **User Notification** (78%): Shows user-friendly error messages
3. **Graceful Degradation** (92%): Provides fallback behavior
4. **Error Propagation** (45%): Re-throws when appropriate
#### Excellent Pattern Example:
```typescript
// src/views/AccountViewView.vue:1617
try {
const response = await this.axios.delete(url, { headers });
if (response.status === 204) {
this.profileImageUrl = "";
this.notify.success("Image deleted successfully.");
}
} catch (error) {
if (isApiError(error) && error.response?.status === 404) {
// Graceful handling - image already gone
this.profileImageUrl = "";
} else {
this.notify.error("Failed to delete image", TIMEOUTS.STANDARD);
}
}
```
#### Areas for Improvement:
1. **Inconsistent Error Typing**: Some catch(error: any), others catch(error: unknown)
2. **Missing Error Boundaries**: No Vue error boundary components
3. **Silent Failures**: 15% of catch blocks don't notify users
## Code Duplication Analysis
### Significant Duplication Patterns
#### 1. **Toggle Component Pattern** (12 occurrences)
```html
<!-- Repeated across multiple files -->
<div class="relative ml-2 cursor-pointer" @click="toggleMethod()">
<input v-model="property" type="checkbox" class="sr-only" />
<div class="block bg-slate-500 w-14 h-8 rounded-full"></div>
<div class="dot absolute left-1 top-1 bg-slate-400 w-6 h-6 rounded-full transition"></div>
</div>
```
**Solution**: Create `ToggleSwitch.vue` component with props for value, label, and change handler.
#### 2. **API Error Handling Pattern** (25 occurrences)
```typescript
try {
const response = await this.axios.post(url, data, { headers });
if (response.status === 200) {
this.notify.success("Operation successful");
}
} catch (error) {
if (isApiError(error)) {
this.notify.error(`Failed: ${error.message}`);
}
}
```
**Solution**: Create `ApiRequestMixin.ts` with standardized request/response handling.
#### 3. **Settings Update Pattern** (40+ occurrences)
```typescript
async methodName() {
await this.$saveSettings({ property: this.newValue });
this.property = this.newValue;
}
```
**Solution**: Enhanced PlatformServiceMixin already provides `$saveSettings()` - migrate remaining manual patterns.
## Dependency and Coupling Analysis
### Import Dependency Patterns
#### Legacy Database Coupling (EXCELLENT)
- **Status**: 99.5% resolved (1 remaining databaseUtil import)
- **Remaining**: `src/views/DeepLinkErrorView.vue:import { logConsoleAndDb }`
- **Resolution**: Replace with PlatformServiceMixin `$logAndConsole()`
#### Circular Dependency Status (EXCELLENT)
- **Status**: 100% resolved, no active circular dependencies
- **Previous Issues**: All resolved through PlatformServiceMixin architecture
#### Component Coupling Analysis
```typescript
// High coupling components (>10 imports)
AccountViewView.vue: 15 imports (understandable given scope)
HomeView.vue: 12 imports
ProjectViewView.vue: 11 imports
// Well-isolated components (<5 imports)
QuickActionViews: 3-4 imports each
Component utilities: 2-3 imports each
```
**Assessment**: Reasonable coupling levels with clear architectural boundaries.
## Console Logging Analysis (129 instances)
### Logging Pattern Distribution:
1. **console.log**: 89 instances (69%)
2. **console.warn**: 24 instances (19%)
3. **console.error**: 16 instances (12%)
### Vue Components & Views Logging (3 instances):
- **Components**: 1 console.* call
- **Views**: 2 console.* calls
### Inconsistent Logging Approach:
```typescript
// Mixed patterns found:
console.log("Direct console logging"); // 89 instances
logger.debug("Structured logging"); // Preferred pattern
this.$logAndConsole("Mixin logging"); // PlatformServiceMixin
```
### Recommended Standardization:
1. **Migration Strategy**: Replace all console.* with logger.* calls
2. **Structured Context**: Add consistent metadata to log entries
3. **Log Levels**: Standardize debug/info/warn/error usage
## Technical Debt Analysis (6 total)
### Components (1 TODO):
```typescript
// PushNotificationPermission.vue
// TODO: secretDB functionality needs to be migrated to PlatformServiceMixin
```
### Views (2 TODOs):
```typescript
// AccountViewView.vue
// TODO: Implement this for SQLite
// TODO: implement this for SQLite
```
### Other Files (3 TODOs):
```typescript
// src/db/tables/accounts.ts
// TODO: When finished with migration, move these fields to Account and move identity and mnemonic here.
// src/util.d.ts
// TODO: , inspect: inspect
// src/libs/crypto/vc/passkeyHelpers.ts
// TODO: If it's after February 2025 when you read this then consider whether it still makes sense
```
**Assessment**: **EXCELLENT** - Only 6 TODO comments across 291 files.
## Performance Anti-Patterns
### Identified Issues:
#### 1. **Excessive Reactive Properties**
```typescript
// AccountViewView.vue has 25+ reactive properties
// Many could be computed or moved to component state
```
#### 2. **Inline Method Calls in Templates**
```html
<!-- Anti-pattern: -->
<span>{{ readableDate(timeStr) }}</span>
<!-- Better: -->
<span>{{ readableTime }}</span>
<!-- With computed property -->
```
#### 3. **Missing Key Attributes in Lists**
```html
<!-- Several v-for loops missing :key attributes -->
<li v-for="item in items">
```
#### 4. **Complex Template Logic**
```html
<!-- AccountViewView.vue - Complex nested conditions -->
<div v-if="!activeDid" id="noticeBeforeShare" class="bg-amber-200...">
<p class="mb-4">
<b>Note:</b> Before you can share with others or take any action, you need an identifier.
</p>
<router-link :to="{ name: 'new-identifier' }" class="inline-block...">
Create An Identifier
</router-link>
</div>
<!-- Identity Details -->
<IdentitySection
:given-name="givenName"
:profile-image-url="profileImageUrl"
:active-did="activeDid"
:is-registered="isRegistered"
:show-large-identicon-id="showLargeIdenticonId"
:show-large-identicon-url="showLargeIdenticonUrl"
:show-did-copy="showDidCopy"
@edit-name="onEditName"
@show-qr-code="onShowQrCode"
@add-image="onAddImage"
@delete-image="onDeleteImage"
@show-large-identicon-id="onShowLargeIdenticonId"
@show-large-identicon-url="onShowLargeIdenticonUrl"
/>
```
## Specific Actionable Recommendations
### Priority 1: Critical File Refactoring
1. **Split AccountViewView.vue**:
- **Timeline**: 2-3 sprints
- **Strategy**: Extract 6 major sections into focused components
- **Risk**: Medium (requires careful state management coordination)
- **Benefit**: Massive maintainability improvement, easier testing
2. **Decompose ImageMethodDialog.vue**:
- **Timeline**: 2-3 sprints
- **Strategy**: Extract 6 focused components (camera, file upload, cropping, etc.)
- **Risk**: Medium (complex camera state management)
- **Benefit**: Massive maintainability improvement
3. **Decompose PlatformServiceMixin.ts**:
- **Timeline**: 1-2 sprints
- **Strategy**: Create focused mixins by concern area
- **Risk**: Low (well-defined interfaces already exist)
- **Benefit**: Better code organization, reduced cognitive load
### Priority 2: Component Extraction
1. **HomeView.vue** → 4 focused sections
- **Timeline**: 1-2 sprints
- **Risk**: Low (clear separation of concerns)
- **Benefit**: Better code organization
2. **ProjectViewView.vue** → 4 focused sections
- **Timeline**: 1-2 sprints
- **Risk**: Low (well-defined boundaries)
- **Benefit**: Improved maintainability
### Priority 3: Shared Component Creation
1. **CameraPreviewComponent.vue**
- Extract from ImageMethodDialog.vue and PhotoDialog.vue
- **Benefit**: Eliminate code duplication
2. **FileUploadComponent.vue**
- Extract from ImageMethodDialog.vue and PhotoDialog.vue
- **Benefit**: Consistent file handling
3. **ToggleSwitch.vue**
- Replace 12 duplicate toggle patterns
- **Benefit**: Consistent UI components
4. **DiagnosticsPanelComponent.vue**
- Extract from ImageMethodDialog.vue
- **Benefit**: Reusable debugging component
### Priority 4: Type Safety Enhancement
1. **Eliminate "as any" Assertions**:
- **Timeline**: 1 sprint
- **Strategy**: Create proper interface extensions
- **Risk**: Low
- **Benefit**: Better compile-time error detection
2. **Standardize Error Typing**:
- **Timeline**: 0.5 sprint
- **Strategy**: Use consistent `catch (error: unknown)` pattern
- **Risk**: None
- **Benefit**: Better error handling consistency
### Priority 5: State Management Optimization
1. **Create Composables for Complex State**:
```typescript
// src/composables/useCameraState.ts
export function useCameraState() {
const cameraState = ref<CameraState>("off");
const showPreview = ref(false);
const isRetrying = ref(false);
const startCamera = async () => { /* ... */ };
const stopCamera = () => { /* ... */ };
return { cameraState, showPreview, isRetrying, startCamera, stopCamera };
}
```
2. **Group Related Reactive Properties**:
```typescript
// Instead of:
showB64Copy: boolean = false;
showDidCopy: boolean = false;
showDerCopy: boolean = false;
showPubCopy: boolean = false;
// Use:
copyStates = {
b64: false,
did: false,
der: false,
pub: false
};
```
### Priority 6: Code Standardization
1. **Logging Standardization**:
- **Timeline**: 1 sprint
- **Strategy**: Replace all console.* with logger.*
- **Risk**: None
- **Benefit**: Consistent logging, better debugging
2. **Template Optimization**:
- Add missing `:key` attributes
- Convert inline method calls to computed properties
- Implement virtual scrolling for large lists
## Quality Metrics Summary
### Vue Component Quality Distribution:
| Size Category | Count | Percentage | Quality Assessment |
|---------------|-------|------------|-------------------|
| Large (>500 lines) | 5 | 12.5% | 🔴 Needs Refactoring |
| Medium (200-500 lines) | 12 | 30% | 🟡 Good with Minor Issues |
| Small (<200 lines) | 23 | 57.5% | 🟢 Excellent |
### Vue View Quality Distribution:
| Size Category | Count | Percentage | Quality Assessment |
|---------------|-------|------------|-------------------|
| Large (>1000 lines) | 9 | 16.7% | 🔴 Needs Refactoring |
| Medium (500-1000 lines) | 8 | 14.8% | 🟡 Good with Minor Issues |
| Small (<500 lines) | 37 | 68.5% | 🟢 Excellent |
### Overall Quality Metrics:
| Metric | Components | Views | Overall Assessment |
|--------|------------|-------|-------------------|
| Technical Debt | 1 TODO | 2 TODOs | 🟢 Excellent |
| Type Safety | 6 "as any" | 35 "as any" | 🟡 Good |
| Console Logging | 1 instance | 2 instances | 🟢 Excellent |
| Architecture Consistency | 100% | 100% | 🟢 Excellent |
| Component Reuse | High | High | 🟢 Excellent |
### Before vs. Target State:
| Metric | Current | Target | Status |
|--------|---------|---------|---------|
| Files >1000 lines | 9 files | 3 files | 🟡 Needs Work |
| "as any" assertions | 62 | 15 | 🟡 Moderate |
| Console.* calls | 129 | 0 | 🔴 Needs Work |
| Component reuse | 40% | 75% | 🟡 Moderate |
| Error consistency | 85% | 95% | 🟢 Good |
| Type coverage | 88% | 95% | 🟢 Good |
## Risk Assessment
### Low Risk Improvements (High Impact):
- Logging standardization
- Type assertion cleanup
- Missing key attributes
- Component extraction from AccountViewView.vue
- Shared component creation (ToggleSwitch, CameraPreview)
### Medium Risk Improvements:
- PlatformServiceMixin decomposition
- State management optimization
- ImageMethodDialog decomposition
### High Risk Items:
- None identified - project demonstrates excellent architectural discipline
## Conclusion
The TimeSafari codebase demonstrates **exceptional code quality** with:
**Key Strengths:**
- **Consistent Architecture**: 100% Vue 3 Composition API with TypeScript
- **Minimal Technical Debt**: Only 6 TODO comments across 291 files
- **Excellent Small Components**: 68.5% of views and 57.5% of components are well-sized
- **Strong Type Safety**: Minimal "as any" usage, mostly justified
- **Clean Logging**: Minimal console.* usage, structured logging preferred
- **Excellent Database Migration**: 99.5% complete
- **Comprehensive Error Handling**: 367 catch blocks with good coverage
- **No Circular Dependencies**: 100% resolved
**Primary Focus Areas:**
1. **Decompose Large Files**: 5 components and 9 views need refactoring
2. **Extract Shared Components**: Camera, file upload, and diagnostics components
3. **Optimize State Management**: Group related properties and create composables
4. **Improve Type Safety**: Create proper interface extensions for mixin methods
5. **Logging Standardization**: Replace 129 console.* calls with structured logger.*
**The component architecture is production-ready** with these improvements representing **strategic optimization** rather than critical fixes. The codebase demonstrates **mature Vue.js development practices** with excellent separation of concerns and consistent patterns.
---
**Investigation Methodology:**
- Static analysis of 291 source files (197 general + 94 Vue components/views)
- Pattern recognition across 104,527 lines of code
- Manual review of large files and complexity patterns
- Dependency analysis and coupling assessment
- Performance anti-pattern identification
- Architecture consistency evaluation
+3
View File
@@ -66,14 +66,17 @@ test-image.tar a1b2c3d4e5f6...
``` ```
### Docs ### Docs
- [x] **BUILDING.md** updated (sections): Docker deployment - [x] **BUILDING.md** updated (sections): Docker deployment
- [x] Troubleshooting updated: Added Docker troubleshooting section - [x] Troubleshooting updated: Added Docker troubleshooting section
### Rollback ### Rollback
- [x] Verified steps to restore previous behavior: - [x] Verified steps to restore previous behavior:
1. `git revert HEAD` 1. `git revert HEAD`
2. `docker rmi test-image` 2. `docker rmi test-image`
3. Restore previous BUILDING.md 3. Restore previous BUILDING.md
``` ```
--- ---
+38 -85
View File
@@ -11,7 +11,7 @@ See [ClickUp](https://sharing.clickup.com/9014278710/l/h/8cmnyhp-174/10573fec74e
Quick start: Quick start:
* For setup, we recommend [pkgx](https://pkgx.dev), which installs what you need (either automatically or with the `dev` command). Core dependencies are typescript & npm; when building for other platforms, you'll need other things such as those in the pkgx.yaml & BUILDING.md files. - For setup, we recommend [pkgx](https://pkgx.dev), which installs what you need (either automatically or with the `dev` command). Core dependencies are typescript & npm; when building for other platforms, you'll need other things such as those in the pkgx.yaml & BUILDING.md files.
```bash ```bash
npm install npm install
@@ -68,16 +68,16 @@ TimeSafari supports configurable logging levels via the `VITE_LOG_LEVEL` environ
```bash ```bash
# Show only errors # Show only errors
VITE_LOG_LEVEL=error npm run build:web:dev VITE_LOG_LEVEL=error npm run dev
# Show warnings and errors # Show warnings and errors
VITE_LOG_LEVEL=warn npm run build:web:dev VITE_LOG_LEVEL=warn npm run dev
# Show info, warnings, and errors (default) # Show info, warnings, and errors (default)
VITE_LOG_LEVEL=info npm run build:web:dev VITE_LOG_LEVEL=info npm run dev
# Show all log levels including debug # Show all log levels including debug
VITE_LOG_LEVEL=debug npm run build:web:dev VITE_LOG_LEVEL=debug npm run dev
``` ```
### Available Levels ### Available Levels
@@ -89,66 +89,8 @@ VITE_LOG_LEVEL=debug npm run build:web:dev
See [Logging Configuration Guide](doc/logging-configuration.md) for complete details. See [Logging Configuration Guide](doc/logging-configuration.md) for complete details.
## WebAuthn Verification Server
TimeSafari includes a server-side WebAuthn verification service for secure passkey registration and authentication.
### Why a Separate Server?
WebAuthn verification **must** be performed server-side for security. Client-side verification can be tampered with and should never be trusted. The server:
- Verifies attestation signatures during registration
- Validates authentication signatures during login
- Prevents replay attacks by tracking counters
- Stores credentials securely with proper user binding
- Enforces origin and RP ID validation
**Note**: The client includes an optional "offline mode" for development (`VITE_OFFLINE_WEBAUTHN_VERIFY=true`), but this is not recommended for production as it compromises security.
### Quick Start
```bash
# Navigate to server directory
cd server
# Install dependencies
npm install
# Copy environment template
cp .env.example .env
# Edit .env with your configuration
# RP_ID=your-domain.com
# RP_NAME=Time Safari
# RP_ORIGIN=https://your-app-url.com
# Start development server
npm run dev
```
The server runs on `http://localhost:3002` by default (configurable via `PORT` in `.env`).
### Documentation
See [server/README.md](server/README.md) for:
- Complete API documentation
- Endpoint specifications
- Production deployment guide
- Security considerations
### Client Configuration
The client automatically uses the server when `VITE_OFFLINE_WEBAUTHN_VERIFY` is not set to `true`. Configure the server URL via:
- Environment variable: `VITE_WEBAUTHN_SERVER_URL`
- Defaults to `http://localhost:3002` in development
- Defaults to same origin in production
### Development Database Clearing
TimeSafari provides a simple script-based approach to clear the local database (not the claim server) for development purposes.
### Quick Usage ### Quick Usage
```bash ```bash
# Run the database clearing script # Run the database clearing script
./scripts/clear-database.sh ./scripts/clear-database.sh
@@ -161,16 +103,19 @@ npm run build:web:dev # For Web
### What It Does ### What It Does
#### **Electron (Desktop App)** #### **Electron (Desktop App)**
- Automatically finds and clears the SQLite database files - Automatically finds and clears the SQLite database files
- Works on Linux, macOS, and Windows - Works on Linux, macOS, and Windows
- Clears all data and forces fresh migrations on next startup - Clears all data and forces fresh migrations on next startup
#### **Web Browser** #### **Web Browser**
- Provides instructions for using custom browser data directories - Provides instructions for using custom browser data directories
- Shows manual clearing via browser DevTools - Shows manual clearing via browser DevTools
- Ensures reliable database clearing without browser complications - Ensures reliable database clearing without browser complications
### Safety Features ### Safety Features
- ✅ **Interactive Script**: Guides you through the process - ✅ **Interactive Script**: Guides you through the process
- ✅ **Platform Detection**: Automatically detects your OS - ✅ **Platform Detection**: Automatically detects your OS
- ✅ **Clear Instructions**: Step-by-step guidance for each platform - ✅ **Clear Instructions**: Step-by-step guidance for each platform
@@ -179,6 +124,7 @@ npm run build:web:dev # For Web
### Manual Commands (if needed) ### Manual Commands (if needed)
#### **Electron Database Location** #### **Electron Database Location**
```bash ```bash
# Linux # Linux
rm -rf ~/.config/TimeSafari/* rm -rf ~/.config/TimeSafari/*
@@ -191,6 +137,7 @@ rmdir /s /q %APPDATA%\TimeSafari
``` ```
#### **Web Browser (Custom Data Directory)** #### **Web Browser (Custom Data Directory)**
```bash ```bash
# Create isolated browser profile # Create isolated browser profile
mkdir ~/timesafari-dev-data mkdir ~/timesafari-dev-data
@@ -203,6 +150,7 @@ URL generation across all environments. This prevents localhost URLs from
appearing in shared links during development. appearing in shared links during development.
### Key Features ### Key Features
- ✅ **Production URLs for Sharing**: All copy link buttons use production domain - ✅ **Production URLs for Sharing**: All copy link buttons use production domain
- ✅ **Environment-Specific Internal URLs**: Internal operations use appropriate - ✅ **Environment-Specific Internal URLs**: Internal operations use appropriate
environment URLs environment URLs
@@ -286,6 +234,7 @@ npm run test:prerequisites
- **Build failures**: Run `npm run check:dependencies` to diagnose environment issues - **Build failures**: Run `npm run check:dependencies` to diagnose environment issues
**Required Versions**: **Required Versions**:
- Node.js: 18+ (LTS recommended) - Node.js: 18+ (LTS recommended)
- npm: 8+ (comes with Node.js) - npm: 8+ (comes with Node.js)
- Platform-specific tools: Android Studio, Xcode (for mobile builds) - Platform-specific tools: Android Studio, Xcode (for mobile builds)
@@ -305,25 +254,26 @@ To add a Font Awesome icon, add to `fontawesome.ts` and reference with
### Reference Material ### Reference Material
* Notifications can be type of `toast` (self-dismiss), `info`, `success`, `warning`, and `danger`. - Notifications can be type of `toast` (self-dismiss), `info`, `success`, `warning`, and `danger`.
They are done via [notiwind](https://www.npmjs.com/package/notiwind) and set up in App.vue. They are done via [notiwind](https://www.npmjs.com/package/notiwind) and set up in App.vue.
* [Customize Vue configuration](https://cli.vuejs.org/config/). - [Customize Vue configuration](https://cli.vuejs.org/config/).
* If you are deploying in a subdirectory, add it to `publicPath` in vue.config.js, eg: `publicPath: "/app/time-tracker/",` - If you are deploying in a subdirectory, add it to `publicPath` in vue.config.js, eg: `publicPath: "/app/time-tracker/",`
### Code Organization ### Code Organization
The project uses a centralized approach to type definitions and interfaces: The project uses a centralized approach to type definitions and interfaces:
* `src/interfaces/` - Contains all TypeScript interfaces and type definitions - `src/interfaces/` - Contains all TypeScript interfaces and type definitions
* `deepLinks.ts` - Deep linking type system and Zod validation schemas - `deepLinks.ts` - Deep linking type system and Zod validation schemas
* `give.ts` - Give-related interfaces and type definitions - `give.ts` - Give-related interfaces and type definitions
* `claims.ts` - Claim-related interfaces and verifiable credentials - `claims.ts` - Claim-related interfaces and verifiable credentials
* `common.ts` - Shared interfaces and utility types - `common.ts` - Shared interfaces and utility types
* Other domain-specific interface files - Other domain-specific interface files
Key principles: Key principles:
- All interfaces and types are defined in the interfaces folder - All interfaces and types are defined in the interfaces folder
- Zod schemas are used for runtime validation and type generation - Zod schemas are used for runtime validation and type generation
- Domain-specific interfaces are separated into their own files - Domain-specific interfaces are separated into their own files
@@ -334,15 +284,17 @@ Key principles:
The application uses a platform-agnostic database layer with Vue mixins for service access: The application uses a platform-agnostic database layer with Vue mixins for service access:
* `src/services/PlatformService.ts` - Database interface definition - `src/services/PlatformService.ts` - Database interface definition
* `src/services/PlatformServiceFactory.ts` - Platform-specific service factory - `src/services/PlatformServiceFactory.ts` - Platform-specific service factory
* `src/services/AbsurdSqlDatabaseService.ts` - SQLite implementation - `src/services/AbsurdSqlDatabaseService.ts` - SQLite implementation
* `src/utils/PlatformServiceMixin.ts` - Vue mixin for database access with caching - `src/utils/PlatformServiceMixin.ts` - Vue mixin for database access with caching
- `src/db/` - Legacy Dexie database (migration in progress)
**Development Guidelines**: **Development Guidelines**:
- Always use `PlatformServiceMixin` for database operations in components - Always use `PlatformServiceMixin` for database operations in components
- Test with PlatformServiceMixin for new features - Test with PlatformServiceMixin for new features
- Use migration tools for data transfer between systems
- Leverage mixin's ultra-concise methods: `$db()`, `$exec()`, `$one()`, `$contacts()`, `$settings()` - Leverage mixin's ultra-concise methods: `$db()`, `$exec()`, `$one()`, `$contacts()`, `$settings()`
**Architecture Decision**: The project uses Vue mixins over Composition API composables for platform service access. See [Architecture Decisions](doc/architecture-decisions.md) for detailed rationale. **Architecture Decision**: The project uses Vue mixins over Composition API composables for platform service access. See [Architecture Decisions](doc/architecture-decisions.md) for detailed rationale.
@@ -365,6 +317,7 @@ timesafari/
## 🤝 Contributing ## 🤝 Contributing
1. **Follow the Build Architecture Guard** - Update BUILDING.md when modifying build files 1. **Follow the Build Architecture Guard** - Update BUILDING.md when modifying build files
2. **Use the PR template** - Complete the checklist for build-related changes
3. **Test your changes** - Ensure builds work on affected platforms 3. **Test your changes** - Ensure builds work on affected platforms
4. **Document updates** - Keep BUILDING.md current and accurate 4. **Document updates** - Keep BUILDING.md current and accurate
@@ -372,11 +325,11 @@ timesafari/
Gifts make the world go 'round! Gifts make the world go 'round!
* [WebStorm by JetBrains](https://www.jetbrains.com/webstorm/) for the free open-source license - [WebStorm by JetBrains](https://www.jetbrains.com/webstorm/) for the free open-source license
* [Máximo Fernández](https://medium.com/@maxfarenas) for the 3D [code](https://github.com/maxfer03/vue-three-ns) and [explanatory post](https://medium.com/nicasource/building-an-interactive-web-portfolio-with-vue-three-js-part-three-implementing-three-js-452cb375ef80) - [Máximo Fernández](https://medium.com/@maxfarenas) for the 3D [code](https://github.com/maxfer03/vue-three-ns) and [explanatory post](https://medium.com/nicasource/building-an-interactive-web-portfolio-with-vue-three-js-part-three-implementing-three-js-452cb375ef80)
* [Many tools & libraries](https://gitea.anomalistdesign.com/trent_larson/crowd-funder-for-time-pwa/src/branch/master/package.json#L10) such as Nodejs.org, IntelliJ Idea, Veramo.io, Vuejs.org, threejs.org - [Many tools & libraries](https://gitea.anomalistdesign.com/trent_larson/crowd-funder-for-time-pwa/src/branch/master/package.json#L10) such as Nodejs.org, IntelliJ Idea, Veramo.io, Vuejs.org, threejs.org
* [Bush 3D model](https://sketchfab.com/3d-models/lupine-plant-bf30f1110c174d4baedda0ed63778439) - [Bush 3D model](https://sketchfab.com/3d-models/lupine-plant-bf30f1110c174d4baedda0ed63778439)
* [Forest floor image](https://www.goodfreephotos.com/albums/textures/leafy-autumn-forest-floor.jpg) - [Forest floor image](https://www.goodfreephotos.com/albums/textures/leafy-autumn-forest-floor.jpg)
* Time Safari logo assisted by [DALL-E in ChatGPT](https://chat.openai.com/g/g-2fkFE8rbu-dall-e) - Time Safari logo assisted by [DALL-E in ChatGPT](https://chat.openai.com/g/g-2fkFE8rbu-dall-e)
* [DiceBear](https://www.dicebear.com/licenses/) and [Avataaars](https://www.dicebear.com/styles/avataaars/#details) for human-looking identicons - [DiceBear](https://www.dicebear.com/licenses/) and [Avataaars](https://www.dicebear.com/styles/avataaars/#details) for human-looking identicons
* Some gratitude prompts thanks to [Develop Good Habits](https://www.developgoodhabits.com/gratitude-journal-prompts/) - Some gratitude prompts thanks to [Develop Good Habits](https://www.developgoodhabits.com/gratitude-journal-prompts/)
+11 -11
View File
@@ -5,33 +5,33 @@
We can't trust iOS IndexedDB to persist. I want to start delivering an app to people now, in preparation for presentations mid-June: Rotary on June 12 and Porcfest on June 17. We can't trust iOS IndexedDB to persist. I want to start delivering an app to people now, in preparation for presentations mid-June: Rotary on June 12 and Porcfest on June 17.
* Apple WebKit puts a [7-day cap on IndexedDB](https://webkit.org/blog/10218/full-third-party-cookie-blocking-and-more/). - Apple WebKit puts a [7-day cap on IndexedDB](https://webkit.org/blog/10218/full-third-party-cookie-blocking-and-more/).
* The web standards expose a `persist` method to mark memory as persistent, and [supposedly WebView supports it](https://developer.mozilla.org/en-US/docs/Web/API/StorageManager/persisted), but too many other things indicate it's not reliable. I've talked with [ChatGPT](https://chatgpt.com/share/68322f40-84c8-8007-b213-855f7962989a) & Venice & Claude (in Cursor); [this answer from Perplexity](https://www.perplexity.ai/search/which-platforms-prompt-the-use-HUQLqy4qQD2cRbkmO4CgHg) says that most platforms don't prompt and Safari doesn't support it; I don't know if that means WebKit as well. - The web standards expose a `persist` method to mark memory as persistent, and [supposedly WebView supports it](https://developer.mozilla.org/en-US/docs/Web/API/StorageManager/persisted), but too many other things indicate it's not reliable. I've talked with [ChatGPT](https://chatgpt.com/share/68322f40-84c8-8007-b213-855f7962989a) & Venice & Claude (in Cursor); [this answer from Perplexity](https://www.perplexity.ai/search/which-platforms-prompt-the-use-HUQLqy4qQD2cRbkmO4CgHg) says that most platforms don't prompt and Safari doesn't support it; I don't know if that means WebKit as well.
* Capacitor says [not to trust it on iOS](https://capacitorjs.com/docs/v6/guides/storage). - Capacitor says [not to trust it on iOS](https://capacitorjs.com/docs/v6/guides/storage).
Also, with sensitive data, the accounts info should be encrypted. Also, with sensitive data, the accounts info should be encrypted.
# Options # Options
* There is a community [SQLite plugin for Capacitor](https://github.com/capacitor-community/sqlite) with encryption by [SQLCipher](https://github.com/sqlcipher/sqlcipher). - There is a community [SQLite plugin for Capacitor](https://github.com/capacitor-community/sqlite) with encryption by [SQLCipher](https://github.com/sqlcipher/sqlcipher).
* [This tutorial](https://jepiqueau.github.io/2023/09/05/Ionic7Vue-SQLite-CRUD-App.html#part-1---web---table-of-contents) shows how that plugin works for web as well as native. - [This tutorial](https://jepiqueau.github.io/2023/09/05/Ionic7Vue-SQLite-CRUD-App.html#part-1---web---table-of-contents) shows how that plugin works for web as well as native.
* Capacitor abstracts [user preferences in an API](https://capacitorjs.com/docs/apis/preferences), which uses different underlying libraries on iOS & Android. Unfortunately, it won't do any filtering or searching, and is only meant for small amounts of data. (It could be used for settings and for identifiers, but contacts will grow and image blobs won't work.) - Capacitor abstracts [user preferences in an API](https://capacitorjs.com/docs/apis/preferences), which uses different underlying libraries on iOS & Android. Unfortunately, it won't do any filtering or searching, and is only meant for small amounts of data. (It could be used for settings and for identifiers, but contacts will grow and image blobs won't work.)
* There are hints that Capacitor offers another custom storage API but all I could find was that Preferences API. - There are hints that Capacitor offers another custom storage API but all I could find was that Preferences API.
* [Ionic Storage](https://ionic.io/docs/secure-storage) is an enterprise solution, which also supports encryption. - [Ionic Storage](https://ionic.io/docs/secure-storage) is an enterprise solution, which also supports encryption.
* Not an option yet: Dexie may support SQLite in [a future version](https://dexie.org/roadmap/dexie5.0). - Not an option yet: Dexie may support SQLite in [a future version](https://dexie.org/roadmap/dexie5.0).
# Current Plan # Current Plan
* Implement SQLite for Capacitor & web, with encryption. That will allow us to test quickly and keep the same interface for native & web, but we don't deal with migrations for current web users. - Implement SQLite for Capacitor & web, with encryption. That will allow us to test quickly and keep the same interface for native & web, but we don't deal with migrations for current web users.
* After that is delivered, write a migration for current web users from IndexedDB to SQLite. - After that is delivered, write a migration for current web users from IndexedDB to SQLite.
# Current method calls # Current method calls
+2 -2
View File
@@ -31,8 +31,8 @@ android {
applicationId "app.timesafari.app" applicationId "app.timesafari.app"
minSdkVersion rootProject.ext.minSdkVersion minSdkVersion rootProject.ext.minSdkVersion
targetSdkVersion rootProject.ext.targetSdkVersion targetSdkVersion rootProject.ext.targetSdkVersion
versionCode 50 versionCode 40
versionName "1.1.5" versionName "1.0.7"
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
aaptOptions { aaptOptions {
// Files and dirs to omit from the packaged assets dir, modified to accommodate modern web apps. // Files and dirs to omit from the packaged assets dir, modified to accommodate modern web apps.
-14
View File
@@ -27,20 +27,6 @@
<category android:name="android.intent.category.BROWSABLE" /> <category android:name="android.intent.category.BROWSABLE" />
<data android:scheme="timesafari" /> <data android:scheme="timesafari" />
</intent-filter> </intent-filter>
<!-- Share Target Intent Filter - Single Image -->
<intent-filter>
<action android:name="android.intent.action.SEND" />
<category android:name="android.intent.category.DEFAULT" />
<data android:mimeType="image/*" />
</intent-filter>
<!-- Share Target Intent Filter - Multiple Images (optional, we'll handle first image) -->
<intent-filter>
<action android:name="android.intent.action.SEND_MULTIPLE" />
<category android:name="android.intent.category.DEFAULT" />
<data android:mimeType="image/*" />
</intent-filter>
</activity> </activity>
<provider <provider
@@ -34,13 +34,5 @@
{ {
"pkg": "@capawesome/capacitor-file-picker", "pkg": "@capawesome/capacitor-file-picker",
"classpath": "io.capawesome.capacitorjs.plugins.filepicker.FilePickerPlugin" "classpath": "io.capawesome.capacitorjs.plugins.filepicker.FilePickerPlugin"
},
{
"pkg": "SafeArea",
"classpath": "app.timesafari.safearea.SafeAreaPlugin"
},
{
"pkg": "SharedImage",
"classpath": "app.timesafari.sharedimage.SharedImagePlugin"
} }
] ]
@@ -1,10 +1,6 @@
package app.timesafari; package app.timesafari;
import android.content.Intent;
import android.net.Uri;
import android.os.Bundle; import android.os.Bundle;
import android.util.Base64;
import android.util.Log;
import android.view.View; import android.view.View;
import android.view.WindowManager; import android.view.WindowManager;
import android.view.WindowInsetsController; import android.view.WindowInsetsController;
@@ -15,21 +11,9 @@ import android.webkit.WebSettings;
import android.webkit.WebViewClient; import android.webkit.WebViewClient;
import com.getcapacitor.BridgeActivity; import com.getcapacitor.BridgeActivity;
import app.timesafari.safearea.SafeAreaPlugin; import app.timesafari.safearea.SafeAreaPlugin;
import app.timesafari.sharedimage.SharedImagePlugin;
//import com.getcapacitor.community.sqlite.SQLite; //import com.getcapacitor.community.sqlite.SQLite;
import android.content.SharedPreferences;
import java.io.InputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
public class MainActivity extends BridgeActivity { public class MainActivity extends BridgeActivity {
private static final String TAG = "MainActivity";
private static final String SHARED_PREFS_NAME = "shared_image";
private static final String KEY_BASE64 = "shared_image_base64";
private static final String KEY_FILE_NAME = "shared_image_file_name";
private static final String KEY_READY = "shared_image_ready";
@Override @Override
public void onCreate(Bundle savedInstanceState) { public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState); super.onCreate(savedInstanceState);
@@ -64,156 +48,9 @@ public class MainActivity extends BridgeActivity {
// Register SafeArea plugin // Register SafeArea plugin
registerPlugin(SafeAreaPlugin.class); registerPlugin(SafeAreaPlugin.class);
// Register SharedImage plugin
registerPlugin(SharedImagePlugin.class);
// Initialize SQLite // Initialize SQLite
//registerPlugin(SQLite.class); //registerPlugin(SQLite.class);
// Handle share intent if app was launched from share sheet
handleShareIntent(getIntent());
} }
@Override
protected void onNewIntent(Intent intent) {
super.onNewIntent(intent);
setIntent(intent);
handleShareIntent(intent);
}
/**
* Handle share intents (ACTION_SEND or ACTION_SEND_MULTIPLE)
* Processes shared images and stores them in SharedPreferences for plugin to read
*/
private void handleShareIntent(Intent intent) {
if (intent == null) {
return;
}
String action = intent.getAction();
String type = intent.getType();
boolean handled = false;
// Handle single image share
if (Intent.ACTION_SEND.equals(action) && type != null && type.startsWith("image/")) {
Uri imageUri;
// Use new API for API 33+ (Android 13+), fall back to deprecated API for older versions
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
imageUri = intent.getParcelableExtra(Intent.EXTRA_STREAM, Uri.class);
} else {
// Deprecated but still works on older versions
@SuppressWarnings("deprecation")
Uri uri = intent.getParcelableExtra(Intent.EXTRA_STREAM);
imageUri = uri;
}
if (imageUri != null) {
String fileName = intent.getStringExtra(Intent.EXTRA_TEXT);
processSharedImage(imageUri, fileName);
handled = true;
}
}
// Handle multiple images share (we'll just process the first one)
else if (Intent.ACTION_SEND_MULTIPLE.equals(action) && type != null && type.startsWith("image/")) {
java.util.ArrayList<Uri> imageUris;
// Use new API for API 33+ (Android 13+), fall back to deprecated API for older versions
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
imageUris = intent.getParcelableArrayListExtra(Intent.EXTRA_STREAM, Uri.class);
} else {
// Deprecated but still works on older versions
@SuppressWarnings("deprecation")
java.util.ArrayList<Uri> uris = intent.getParcelableArrayListExtra(Intent.EXTRA_STREAM);
imageUris = uris;
}
if (imageUris != null && !imageUris.isEmpty()) {
processSharedImage(imageUris.get(0), null);
handled = true;
}
}
// Clear the intent after handling to release URI permissions and prevent
// network issues in WebView. This is critical for preventing the WebView
// from losing network connectivity after processing shared content.
if (handled) {
intent.setAction(null);
intent.setData(null);
intent.removeExtra(Intent.EXTRA_STREAM);
intent.setType(null);
setIntent(new Intent());
Log.d(TAG, "Cleared share intent after processing");
}
}
/**
* Process a shared image: read it, convert to base64, and write to temp file
* Uses try-with-resources to ensure proper stream cleanup and prevent network issues
*/
private void processSharedImage(Uri imageUri, String fileName) {
// Extract filename from URI or use default (do this before opening streams)
String actualFileName = fileName;
if (actualFileName == null || actualFileName.isEmpty()) {
String path = imageUri.getPath();
if (path != null) {
int lastSlash = path.lastIndexOf('/');
if (lastSlash >= 0 && lastSlash < path.length() - 1) {
actualFileName = path.substring(lastSlash + 1);
}
}
if (actualFileName == null || actualFileName.isEmpty()) {
actualFileName = "shared-image.jpg";
}
}
// Use try-with-resources to ensure streams are properly closed
// This is critical to prevent resource leaks that can affect WebView networking
try (InputStream inputStream = getContentResolver().openInputStream(imageUri);
ByteArrayOutputStream buffer = new ByteArrayOutputStream()) {
if (inputStream == null) {
Log.e(TAG, "Failed to open input stream for shared image");
return;
}
// Read image bytes
byte[] data = new byte[8192];
int nRead;
while ((nRead = inputStream.read(data, 0, data.length)) != -1) {
buffer.write(data, 0, nRead);
}
buffer.flush();
byte[] imageBytes = buffer.toByteArray();
// Convert to base64
String base64String = Base64.encodeToString(imageBytes, Base64.NO_WRAP);
// Store in SharedPreferences for plugin to read
storeSharedImageInPreferences(base64String, actualFileName);
Log.d(TAG, "Successfully processed shared image: " + actualFileName);
} catch (IOException e) {
Log.e(TAG, "Error processing shared image", e);
} catch (Exception e) {
Log.e(TAG, "Unexpected error processing shared image", e);
}
}
/**
* Store shared image data in SharedPreferences for plugin to read
* Plugin will read and clear the data when called
*/
private void storeSharedImageInPreferences(String base64, String fileName) {
try {
SharedPreferences prefs = getSharedPreferences(SHARED_PREFS_NAME, MODE_PRIVATE);
SharedPreferences.Editor editor = prefs.edit();
editor.putString(KEY_BASE64, base64);
editor.putString(KEY_FILE_NAME, fileName);
editor.putBoolean(KEY_READY, true);
editor.apply();
Log.d(TAG, "Stored shared image data in SharedPreferences");
} catch (Exception e) {
Log.e(TAG, "Error storing shared image in SharedPreferences", e);
}
}
} }
@@ -1,84 +0,0 @@
package app.timesafari.sharedimage;
import android.content.Context;
import android.content.SharedPreferences;
import com.getcapacitor.JSObject;
import com.getcapacitor.Plugin;
import com.getcapacitor.PluginCall;
import com.getcapacitor.PluginMethod;
import com.getcapacitor.annotation.CapacitorPlugin;
@CapacitorPlugin(name = "SharedImage")
public class SharedImagePlugin extends Plugin {
private static final String SHARED_PREFS_NAME = "shared_image";
private static final String KEY_BASE64 = "shared_image_base64";
private static final String KEY_FILE_NAME = "shared_image_file_name";
private static final String KEY_READY = "shared_image_ready";
/**
* Get shared image data from SharedPreferences
* Returns base64 string and fileName, or null if no image exists
* Clears the data after reading to prevent re-reading
*/
@PluginMethod
public void getSharedImage(PluginCall call) {
try {
SharedPreferences prefs = getSharedPreferences();
String base64 = prefs.getString(KEY_BASE64, null);
String fileName = prefs.getString(KEY_FILE_NAME, null);
if (base64 == null || fileName == null) {
// No shared image exists - return null values (not an error)
JSObject result = new JSObject();
result.put("base64", (String) null);
result.put("fileName", (String) null);
call.resolve(result);
return;
}
// Clear the shared data after reading
SharedPreferences.Editor editor = prefs.edit();
editor.remove(KEY_BASE64);
editor.remove(KEY_FILE_NAME);
editor.remove(KEY_READY);
editor.apply();
// Return the shared image data
JSObject result = new JSObject();
result.put("base64", base64);
result.put("fileName", fileName);
call.resolve(result);
} catch (Exception e) {
android.util.Log.e("SharedImagePlugin", "Error in getSharedImage()", e);
call.reject("Error getting shared image: " + e.getMessage());
}
}
/**
* Check if shared image exists without reading it
* Useful for quick checks before calling getSharedImage()
*/
@PluginMethod
public void hasSharedImage(PluginCall call) {
SharedPreferences prefs = getSharedPreferences();
boolean hasImage = prefs.contains(KEY_BASE64) && prefs.contains(KEY_FILE_NAME);
JSObject result = new JSObject();
result.put("hasImage", hasImage);
call.resolve(result);
}
/**
* Get SharedPreferences instance for shared image data
*/
private SharedPreferences getSharedPreferences() {
Context context = getContext();
if (context == null) {
throw new IllegalStateException("Plugin context is null");
}
return context.getSharedPreferences(SHARED_PREFS_NAME, Context.MODE_PRIVATE);
}
}
+1 -1
View File
@@ -1,5 +1,5 @@
ext { ext {
minSdkVersion = 23 minSdkVersion = 22
compileSdkVersion = 36 compileSdkVersion = 36
targetSdkVersion = 36 targetSdkVersion = 36
androidxActivityVersion = '1.8.0' androidxActivityVersion = '1.8.0'
+21
View File
@@ -0,0 +1,21 @@
# Glossary
**T (slot time)** — The local wall-clock time a notification should fire (e.g., 08:00).
**T–lead** — The moment **`prefetchLeadMinutes`** before **T** when the system *attempts* a **single** background prefetch. T–lead **controls prefetch attempts, not arming**; locals are pre-armed earlier to guarantee closed-app delivery.
**Rolling window** — Always keep **today's remaining** (and tomorrow if iOS pending caps allow) locals **armed** so the OS can deliver while the app is closed.
**TTL (time-to-live)** — Maximum allowed payload age **at fire time**. If `T − fetchedAt > ttlSeconds`, we **skip** arming for that T.
**Shared DB (default)** — The app and plugin open the **same SQLite file**; the app owns schema/migrations, the plugin performs short writes with WAL.
**WAL (Write-Ahead Logging)** — SQLite journaling mode that permits concurrent reads during writes; recommended for foreground-read + background-write.
**`PRAGMA user_version`** — An integer the app increments on each migration; the plugin **checks** (does not migrate) to ensure compatibility.
**Exact alarm (Android)** — Minute-precise alarm via `AlarmManager.setExactAndAllowWhileIdle`, subject to policy and permission.
**Windowed alarm (Android)** — Batched/inexact alarm via `setWindow(start,len)`; we target **±10 minutes** when exact alarms are unavailable.
**Start-on-Login** — Electron feature that automatically launches the application when the user logs into their system, enabling background notification scheduling and delivery after system reboot.
+3 -1
View File
@@ -105,7 +105,8 @@ Build Scripts:
├── electron/** # Electron build files ├── electron/** # Electron build files
├── android/** # Android build configuration ├── android/** # Android build configuration
├── ios/** # iOS build configuration ├── ios/** # iOS build configuration
└── sw_scripts/** # Service worker scripts ├── sw_scripts/** # Service worker scripts
└── sw_combine.js # Service worker combination
Deployment: Deployment:
├── Dockerfile # Docker configuration ├── Dockerfile # Docker configuration
@@ -329,6 +330,7 @@ Track the effectiveness of your Build Architecture Guard:
## 📝 **Changelog** ## 📝 **Changelog**
### 2025-08-22 - Shell Compatibility Fix ### 2025-08-22 - Shell Compatibility Fix
- **Fixed**: Replaced `mapfile` command with portable alternative for cross-shell compatibility - **Fixed**: Replaced `mapfile` command with portable alternative for cross-shell compatibility
- **Impact**: Resolves "mapfile: command not found" errors in pre-commit hooks - **Impact**: Resolves "mapfile: command not found" errors in pre-commit hooks
- **Files**: `scripts/build-arch-guard.sh` - **Files**: `scripts/build-arch-guard.sh`
+7 -73
View File
@@ -1,77 +1,11 @@
# TimeSafari Docs # TimeSafari — Native-First Notification System (Clean Pack) — 2025-09-07
## Generating PDF from Markdown on OSx This pack contains a single-version **Native-First** documentation set with a clear definition of **T–lead** and aligned terminology.
This uses Pandoc and BasicTex (LaTeX) Installed through Homebrew. **Native-First =** OS-scheduled **background prefetch at T–lead** + **pre-armed one-shot local notifications**. Web-push is retired.
### Set Up **Included files**
```bash - `notification-system.md` (merged comprehensive guide)
brew install pandoc - `web-push-cleanup-guide.md` (cleanup instructions)
- `GLOSSARY.md` (definitions incl. **T** and **T–lead**)
brew install basictex
# Setting up LaTex packages
# First update tlmgr
sudo tlmgr update --self
# Then install LaTex packages
sudo tlmgr install bbding
sudo tlmgr install enumitem
sudo tlmgr install environ
sudo tlmgr install fancyhdr
sudo tlmgr install framed
sudo tlmgr install import
sudo tlmgr install lastpage # Enables Page X of Y
sudo tlmgr install mdframed
sudo tlmgr install multirow
sudo tlmgr install needspace
sudo tlmgr install ntheorem
sudo tlmgr install tabu
sudo tlmgr install tcolorbox
sudo tlmgr install textpos
sudo tlmgr install titlesec
sudo tlmgr install titling # Required for the fancy headers used
sudo tlmgr install threeparttable
sudo tlmgr install trimspaces
sudo tlmgr install tocloft # Required for \tableofcontents generation
sudo tlmgr install varwidth
sudo tlmgr install wrapfig
# Install fonts
sudo tlmgr install cmbright
sudo tlmgr install collection-fontsrecommended # And set up fonts
sudo tlmgr install fira
sudo tlmgr install fontaxes
sudo tlmgr install libertine # The main font the doc uses
sudo tlmgr install opensans
sudo tlmgr install sourceserifpro
```
#### References
The following guide was adapted to this project except that we install with Brew and have a few more packages.
Guide: <https://daniel.feldroy.com/posts/setting-up-latex-on-mac-os-x>
### Usage
Use the `pandoc` command to generate a PDF.
```bash
pandoc usage-guide.md -o usage-guide.pdf
```
And you can open the PDF with the `open` command.
```bash
open usage-guide.pdf
```
Or use this one-liner
```bash
pandoc usage-guide.md -o usage-guide.pdf && open usage-guide.pdf
```
+5 -5
View File
@@ -117,25 +117,25 @@ async function getDatabaseService() {
## Files Modified ## Files Modified
1. **src/interfaces/worker-messages.ts** *(NEW)* 1. **src/interfaces/worker-messages.ts** _(NEW)_
- Type definitions for worker communication - Type definitions for worker communication
- Request and response message interfaces - Request and response message interfaces
2. **src/registerSQLWorker.js** *(MAJOR REWRITE)* 2. **src/registerSQLWorker.js** _(MAJOR REWRITE)_
- Message-based operation handling - Message-based operation handling
- **Fixed circular dependency with lazy loading** - **Fixed circular dependency with lazy loading**
- Proper error handling and response formatting - Proper error handling and response formatting
3. **src/services/platforms/WebPlatformService.ts** *(MAJOR REWRITE)* 3. **src/services/platforms/WebPlatformService.ts** _(MAJOR REWRITE)_
- Worker-only database access - Worker-only database access
- Message sending and response handling - Message sending and response handling
- Timeout and error management - Timeout and error management
4. **src/main.web.ts** *(SIMPLIFIED)* 4. **src/main.web.ts** _(SIMPLIFIED)_
- Removed duplicate worker creation - Removed duplicate worker creation
- Simplified initialization flow - Simplified initialization flow
5. **WORKER_ONLY_DATABASE_IMPLEMENTATION.md** *(NEW)* 5. **WORKER_ONLY_DATABASE_IMPLEMENTATION.md** _(NEW)_
- Complete documentation of changes - Complete documentation of changes
## Benefits ## Benefits
@@ -1,259 +0,0 @@
# Android API 23 Upgrade Impact Analysis
**Date:** 2025-12-03
**Current minSdkVersion:** 22 (Android 5.1 Lollipop)
**Proposed minSdkVersion:** 23 (Android 6.0 Marshmallow)
**Impact Assessment:** Low to Moderate
## Executive Summary
Upgrading from API 22 to API 23 will have **minimal code impact** but may affect device compatibility. The main change is that API 23 introduced runtime permissions, but since the app uses Capacitor plugins which handle permissions, the impact is minimal.
## Code Impact Analysis
### ✅ No Breaking Changes in Existing Code
#### 1. API Level Checks in Code
All existing API level checks are for **much higher APIs** than 23, so they won't be affected:
**MainActivity.java:**
- `Build.VERSION_CODES.R` (API 30+) - Edge-to-edge display
- `Build.VERSION_CODES.TIRAMISU` (API 33+) - Intent extras handling
- Legacy path (API 21-29) - Will still work, but API 22 devices won't be supported
**SafeAreaPlugin.java:**
- `Build.VERSION_CODES.R` (API 30+) - Safe area insets
**Conclusion:** No code changes needed for API level checks.
#### 2. Permissions Handling
**Current Permissions in AndroidManifest.xml:**
- `INTERNET` - Normal permission (no runtime needed)
- `READ_EXTERNAL_STORAGE` - Dangerous permission (runtime required on API 23+)
- `WRITE_EXTERNAL_STORAGE` - Dangerous permission (runtime required on API 23+)
- `CAMERA` - Dangerous permission (runtime required on API 23+)
**Current Implementation:**
- ✅ App uses **Capacitor plugins** for camera and file access
- ✅ Capacitor plugins **already handle runtime permissions** automatically
- ✅ No manual permission request code found in the codebase
- ✅ QR Scanner uses Capacitor's BarcodeScanner plugin which handles permissions
**Conclusion:** No code changes needed - Capacitor handles runtime permissions automatically.
#### 3. Dependencies Compatibility
**AndroidX Libraries:**
- `androidx.appcompat:appcompat:1.6.1` - ✅ Supports API 23+
- `androidx.core:core:1.12.0` - ✅ Supports API 23+
- `androidx.fragment:fragment:1.6.2` - ✅ Supports API 23+
- `androidx.coordinatorlayout:coordinatorlayout:1.2.0` - ✅ Supports API 23+
- `androidx.core:core-splashscreen:1.0.1` - ✅ Supports API 23+
**Capacitor Plugins:**
- `@capacitor/core:6.2.0` - ✅ Requires API 23+ (official requirement)
- `@capacitor/camera:6.0.0` - ✅ Handles runtime permissions
- `@capacitor/filesystem:6.0.0` - ✅ Handles runtime permissions
- `@capacitor-community/sqlite:6.0.2` - ✅ Supports API 23+
- `@capacitor-mlkit/barcode-scanning:6.0.0` - ✅ Supports API 23+
**Third-Party Libraries:**
- No Firebase or other libraries with API 22-specific requirements found
- All dependencies appear compatible with API 23+
**Conclusion:** All dependencies are compatible with API 23.
#### 4. Build Configuration
**Current Configuration:**
- `compileSdkVersion = 36` (Android 14)
- `targetSdkVersion = 36` (Android 14)
- `minSdkVersion = 22` (Android 5.1) ← **Only this needs to change**
**Required Change:**
```gradle
// android/variables.gradle
ext {
minSdkVersion = 23 // Change from 22 to 23
// ... rest stays the same
}
```
**Conclusion:** Only one line needs to be changed.
## Device Compatibility Impact
### Device Coverage Loss
**API 22 (Android 5.1 Lollipop):**
- Released: March 2015
- Market share: ~0.1% of active devices (as of 2024)
- Devices affected: Very old devices from 2015-2016
**API 23 (Android 6.0 Marshmallow):**
- Released: October 2015
- Market share: ~0.3% of active devices (as of 2024)
- Still very low, but slightly higher than API 22
**Impact:** Losing support for ~0.1% of devices (essentially negligible)
### User Base Impact
**Recommendation:** Check your analytics to see actual usage:
- If you have analytics, check percentage of users on API 22
- If < 0.5%, upgrade is safe
- If > 1%, consider the business impact
## Runtime Permissions (API 23 Feature)
### What Changed in API 23
**Before API 23 (API 22 and below):**
- Permissions granted at install time
- User sees all permissions during installation
- No runtime permission dialogs
**API 23+ (Runtime Permissions):**
- Dangerous permissions must be requested at runtime
- User sees permission dialogs when app needs them
- Better user experience and privacy
### Current App Status
**✅ Already Compatible:**
- App uses Capacitor plugins which **automatically handle runtime permissions**
- Camera plugin requests permissions when needed
- Filesystem plugin requests permissions when needed
- No manual permission code needed
**Conclusion:** App is already designed for runtime permissions via Capacitor.
## Potential Issues to Watch
### 1. APK Size
- Some developers report APK size increases after raising minSdkVersion
- **Action:** Monitor APK size after upgrade
- **Expected Impact:** Minimal (API 22 → 23 is a small jump)
### 2. Testing Requirements
- Need to test on API 23+ devices
- **Action:** Test on Android 6.0+ devices/emulators
- **Current:** App likely already tested on API 23+ devices
### 3. Legacy Code Path
- MainActivity has legacy code for API 21-29
- **Impact:** This code will still work, but API 22 devices won't be supported
- **Action:** No code changes needed, but legacy path becomes API 23-29
### 4. Capacitor Compatibility
- Capacitor 6.2.0 officially requires API 23+
- **Current Situation:** App runs on API 22 (may be working due to leniency)
- **After Upgrade:** Officially compliant with Capacitor requirements
- **Benefit:** Better compatibility guarantees
## Files That Need Changes
### 1. Build Configuration
**File:** `android/variables.gradle`
```gradle
ext {
minSdkVersion = 23 // Change from 22
// ... rest unchanged
}
```
### 2. Documentation
**Files to Update:**
- `doc/shared-image-plugin-implementation-plan.md` - Update version notes
- Any README files mentioning API 22
- Build documentation
### 3. No Code Changes Required
- ✅ No Java/Kotlin code changes needed
- ✅ No AndroidManifest.xml changes needed
- ✅ No permission handling code changes needed
## Testing Checklist
After upgrading to API 23, test:
- [ ] App builds successfully
- [ ] App installs on API 23 device/emulator
- [ ] Camera functionality works (permissions requested)
- [ ] File access works (permissions requested)
- [ ] Share functionality works
- [ ] QR code scanning works
- [ ] Deep linking works
- [ ] All Capacitor plugins work correctly
- [ ] No crashes or permission-related errors
- [ ] APK size is acceptable
## Rollback Plan
If issues arise:
1. Revert `android/variables.gradle` to `minSdkVersion = 22`
2. Rebuild and test
3. Document issues encountered
4. Address issues before retrying upgrade
## Recommendation
### ✅ **Proceed with Upgrade**
**Reasons:**
1. **Minimal Code Impact:** Only one line needs to change
2. **Already Compatible:** App uses Capacitor which handles runtime permissions
3. **Device Impact:** Negligible (~0.1% of devices)
4. **Capacitor Compliance:** Officially meets Capacitor 6 requirements
5. **Future-Proofing:** Better alignment with modern Android development
**Timeline:**
- **Low Risk:** Can be done anytime
- **Recommended:** Before implementing SharedImagePlugin (cleaner baseline)
- **Testing:** 1-2 hours of testing on API 23+ devices
## Migration Steps
1. **Update Build Configuration:**
```bash
# Edit android/variables.gradle
minSdkVersion = 23
```
2. **Sync Gradle:**
```bash
cd android
./gradlew clean
```
3. **Build and Test:**
```bash
npm run build:android:test
# Test on API 23+ device/emulator
```
4. **Verify Permissions:**
- Test camera access
- Test file access
- Verify permission dialogs appear
5. **Update Documentation:**
- Update any docs mentioning API 22
- Update implementation plan
## Summary
| Aspect | Impact | Status |
|--------|--------|--------|
| **Code Changes** | None required | ✅ Safe |
| **Dependencies** | All compatible | ✅ Safe |
| **Permissions** | Already handled | ✅ Safe |
| **Device Coverage** | ~0.1% loss | ⚠️ Minimal |
| **Build Config** | 1 line change | ✅ Simple |
| **Testing** | Standard testing | ✅ Required |
| **Risk Level** | Low | ✅ Low Risk |
**Final Recommendation:** Proceed with upgrade. The benefits (Capacitor compliance, future-proofing) outweigh the minimal risks (negligible device loss, no code changes needed).
+24
View File
@@ -11,12 +11,14 @@ The Android Asset Validation System automatically detects and fixes missing Andr
## Problem Solved ## Problem Solved
Previously, Android builds would fail with errors like: Previously, Android builds would fail with errors like:
``` ```
error: resource drawable/splash (aka app.timesafari.app:drawable/splash) not found. error: resource drawable/splash (aka app.timesafari.app:drawable/splash) not found.
error: resource mipmap/ic_launcher (aka app.timesafari.app:mipmap/ic_launcher) not found. error: resource mipmap/ic_launcher (aka app.timesafari.app:mipmap/ic_launcher) not found.
``` ```
This happened when: This happened when:
- Source assets existed but weren't generated into Android resources - Source assets existed but weren't generated into Android resources
- Android resource directories were missing - Android resource directories were missing
- Asset generation tools weren't run before building - Asset generation tools weren't run before building
@@ -45,16 +47,19 @@ npm run build:android:studio
### What Gets Validated ### What Gets Validated
#### Source Assets (Required) #### Source Assets (Required)
- `resources/icon.png` - App icon source - `resources/icon.png` - App icon source
- `resources/splash.png` - Splash screen source - `resources/splash.png` - Splash screen source
- `resources/splash_dark.png` - Dark mode splash source - `resources/splash_dark.png` - Dark mode splash source
#### Android Resources (Generated) #### Android Resources (Generated)
- `android/app/src/main/res/drawable/splash.png` - Splash screen drawable - `android/app/src/main/res/drawable/splash.png` - Splash screen drawable
- `android/app/src/main/res/mipmap-*/ic_launcher.png` - App icons for all densities - `android/app/src/main/res/mipmap-*/ic_launcher.png` - App icons for all densities
- `android/app/src/main/res/mipmap-*/ic_launcher_round.png` - Round app icons for all densities - `android/app/src/main/res/mipmap-*/ic_launcher_round.png` - Round app icons for all densities
### Density Levels Checked ### Density Levels Checked
- `mipmap-mdpi` (1x) - `mipmap-mdpi` (1x)
- `mipmap-hdpi` (1.5x) - `mipmap-hdpi` (1.5x)
- `mipmap-xhdpi` (2x) - `mipmap-xhdpi` (2x)
@@ -64,6 +69,7 @@ npm run build:android:studio
## Usage ## Usage
### Automatic Validation (Recommended) ### Automatic Validation (Recommended)
The validation runs automatically during all Android builds: The validation runs automatically during all Android builds:
```bash ```bash
@@ -78,6 +84,7 @@ npm run build:android:debug
``` ```
### Manual Validation ### Manual Validation
Run validation only to check/fix assets: Run validation only to check/fix assets:
```bash ```bash
@@ -89,6 +96,7 @@ npm run assets:validate:android
``` ```
### Validation Only (No Regeneration) ### Validation Only (No Regeneration)
Check configuration without fixing: Check configuration without fixing:
```bash ```bash
@@ -98,6 +106,7 @@ npm run assets:validate
## Error Handling ## Error Handling
### Missing Source Assets ### Missing Source Assets
If source assets are missing, the build fails with clear error messages: If source assets are missing, the build fails with clear error messages:
``` ```
@@ -108,6 +117,7 @@ If source assets are missing, the build fails with clear error messages:
``` ```
### Missing Generated Resources ### Missing Generated Resources
If generated resources are missing, they're automatically regenerated: If generated resources are missing, they're automatically regenerated:
``` ```
@@ -119,6 +129,7 @@ If generated resources are missing, they're automatically regenerated:
``` ```
### Generation Failure ### Generation Failure
If regeneration fails, helpful guidance is provided: If regeneration fails, helpful guidance is provided:
``` ```
@@ -131,6 +142,7 @@ If regeneration fails, helpful guidance is provided:
## Integration Points ## Integration Points
### Build Script Integration ### Build Script Integration
The validation is integrated into the main build process: The validation is integrated into the main build process:
```bash ```bash
@@ -143,6 +155,7 @@ validate_android_assets || {
``` ```
### NPM Scripts ### NPM Scripts
New npm scripts for asset management: New npm scripts for asset management:
```json ```json
@@ -156,17 +169,20 @@ New npm scripts for asset management:
## Benefits ## Benefits
### For Developers ### For Developers
- **No More Build Failures**: Automatic detection and fixing of missing resources - **No More Build Failures**: Automatic detection and fixing of missing resources
- **Faster Development**: No need to manually run asset generation tools - **Faster Development**: No need to manually run asset generation tools
- **Clear Error Messages**: Helpful guidance when issues occur - **Clear Error Messages**: Helpful guidance when issues occur
- **Consistent Results**: Same validation on all development machines - **Consistent Results**: Same validation on all development machines
### For CI/CD ### For CI/CD
- **Reliable Builds**: Consistent asset validation across environments - **Reliable Builds**: Consistent asset validation across environments
- **Early Detection**: Catches issues before they reach production - **Early Detection**: Catches issues before they reach production
- **Automated Fixes**: Self-healing builds when possible - **Automated Fixes**: Self-healing builds when possible
### For Project Maintenance ### For Project Maintenance
- **Reduced Support**: Fewer "build doesn't work" issues - **Reduced Support**: Fewer "build doesn't work" issues
- **Documentation**: Clear requirements for required assets - **Documentation**: Clear requirements for required assets
- **Standardization**: Consistent asset structure across the project - **Standardization**: Consistent asset structure across the project
@@ -176,21 +192,27 @@ New npm scripts for asset management:
### Common Issues ### Common Issues
#### "No assets found in the asset path" #### "No assets found in the asset path"
This occurs when the `assets/` directory is empty. The validation system automatically copies source assets and regenerates them. This occurs when the `assets/` directory is empty. The validation system automatically copies source assets and regenerates them.
#### "Failed to generate Android assets" #### "Failed to generate Android assets"
Check that: Check that:
- Source assets exist in `resources/` - Source assets exist in `resources/`
- `@capacitor/assets` is installed - `@capacitor/assets` is installed
- You have write permissions to the Android directories - You have write permissions to the Android directories
#### "Asset generation completed but some resources are still missing" #### "Asset generation completed but some resources are still missing"
This indicates a problem with the asset generation tool. Try: This indicates a problem with the asset generation tool. Try:
1. Running `npm install` to ensure dependencies are up to date 1. Running `npm install` to ensure dependencies are up to date
2. Manually running `npx @capacitor/assets generate` 2. Manually running `npx @capacitor/assets generate`
3. Checking the asset generation logs for specific errors 3. Checking the asset generation logs for specific errors
### Manual Recovery ### Manual Recovery
If automatic regeneration fails, you can manually create the missing resources: If automatic regeneration fails, you can manually create the missing resources:
```bash ```bash
@@ -213,12 +235,14 @@ rm assets/icon.png assets/splash.png assets/splash_dark.png
## Future Enhancements ## Future Enhancements
### Planned Improvements ### Planned Improvements
- **iOS Asset Validation**: Extend validation to iOS assets - **iOS Asset Validation**: Extend validation to iOS assets
- **Asset Quality Checks**: Validate image dimensions and formats - **Asset Quality Checks**: Validate image dimensions and formats
- **Performance Optimization**: Cache validation results - **Performance Optimization**: Cache validation results
- **CI/CD Integration**: Add validation to GitHub Actions - **CI/CD Integration**: Add validation to GitHub Actions
### Configuration Options ### Configuration Options
- **Custom Asset Paths**: Support for different asset directory structures - **Custom Asset Paths**: Support for different asset directory structures
- **Validation Rules**: Configurable validation requirements - **Validation Rules**: Configurable validation requirements
- **Skip Options**: Ability to skip validation for specific scenarios - **Skip Options**: Ability to skip validation for specific scenarios
-655
View File
@@ -1,655 +0,0 @@
# Android Emulator Deployment Guide (No Android Studio)
**Author**: Matthew Raymer
**Date**: 2025-01-27
**Status**: 🎯 **ACTIVE** - Complete guide for deploying TimeSafari to Android emulator using command-line tools
## Overview
This guide provides comprehensive instructions for building and deploying TimeSafari to Android emulators using only command-line tools, without requiring Android Studio. It leverages the existing build system and adds emulator-specific deployment workflows.
## Prerequisites
### Required Tools
1. **Android SDK Command Line Tools**
```bash
# Install via package manager (Arch Linux)
sudo pacman -S android-sdk-cmdline-tools-latest
# Or download from Google
# https://developer.android.com/studio/command-line
```
2. **Android SDK Platform Tools**
```bash
# Install via package manager
sudo pacman -S android-sdk-platform-tools
# Or via Android SDK Manager
sdkmanager "platform-tools"
```
3. **Android SDK Build Tools**
```bash
sdkmanager "build-tools;34.0.0"
```
4. **Android Platform**
```bash
sdkmanager "platforms;android-34"
```
5. **Android Emulator**
```bash
sdkmanager "emulator"
```
6. **System Images**
```bash
# For API 34 (Android 14)
sdkmanager "system-images;android-34;google_apis;x86_64"
# For API 33 (Android 13) - alternative
sdkmanager "system-images;android-33;google_apis;x86_64"
```
### Environment Setup
```bash
# Add to ~/.bashrc or ~/.zshrc
export ANDROID_HOME=$HOME/Android/Sdk
export ANDROID_AVD_HOME=$HOME/.android/avd # Important for AVD location
export PATH=$PATH:$ANDROID_HOME/emulator
export PATH=$PATH:$ANDROID_HOME/platform-tools
export PATH=$PATH:$ANDROID_HOME/cmdline-tools/latest/bin
export PATH=$PATH:$ANDROID_HOME/build-tools/34.0.0
# Reload shell
source ~/.bashrc
```
### Verify Installation
```bash
# Check all tools are available
adb version
emulator -version
avdmanager list
```
## Resource-Aware Emulator Setup
### ⚡ **Quick Start Recommendation**
**For best results, always start with resource analysis:**
```bash
# 1. Check your system capabilities
./scripts/avd-resource-checker.sh
# 2. Use the generated optimal startup script
/tmp/start-avd-TimeSafari_Emulator.sh
# 3. Deploy your app
npm run build:android:dev
adb install -r android/app/build/outputs/apk/debug/app-debug.apk
```
This prevents system lockups and ensures optimal performance.
### AVD Resource Checker Script
**New Feature**: TimeSafari includes an intelligent resource checker that automatically detects your system capabilities and recommends optimal AVD configurations.
```bash
# Check system resources and get recommendations
./scripts/avd-resource-checker.sh
# Check resources for specific AVD
./scripts/avd-resource-checker.sh TimeSafari_Emulator
# Test AVD startup performance
./scripts/avd-resource-checker.sh TimeSafari_Emulator --test
# Create optimized AVD with recommended settings
./scripts/avd-resource-checker.sh TimeSafari_Emulator --create
```
**What the script analyzes:**
- **System Memory**: Total and available RAM
- **CPU Cores**: Available processing power
- **GPU Capabilities**: NVIDIA, AMD, Intel, or software rendering
- **Hardware Acceleration**: Optimal graphics settings
**What it generates:**
- **Optimal configuration**: Memory, cores, and GPU settings
- **Startup command**: Ready-to-use emulator command
- **Startup script**: Saved to `/tmp/start-avd-{name}.sh` for reuse
## Emulator Management
### Create Android Virtual Device (AVD)
```bash
# List available system images
avdmanager list target
# Create AVD for API 34
avdmanager create avd \
--name "TimeSafari_Emulator" \
--package "system-images;android-34;google_apis;x86_64" \
--device "pixel_7"
# List created AVDs
avdmanager list avd
```
### Start Emulator
```bash
# Start emulator with hardware acceleration (recommended)
emulator -avd TimeSafari_Emulator -gpu host -no-audio &
# Start with reduced resources (if system has limited RAM)
emulator -avd TimeSafari_Emulator \
-no-audio \
-memory 2048 \
-cores 2 \
-gpu swiftshader_indirect &
# Start with minimal resources (safest for low-end systems)
emulator -avd TimeSafari_Emulator \
-no-audio \
-memory 1536 \
-cores 1 \
-gpu swiftshader_indirect &
# Check if emulator is running
adb devices
```
### Resource Management
**Important**: Android emulators can consume significant system resources. Choose the appropriate configuration based on your system:
- **High-end systems** (16GB+ RAM, dedicated GPU): Use `-gpu host`
- **Mid-range systems** (8-16GB RAM): Use `-memory 2048 -cores 2`
- **Low-end systems** (4-8GB RAM): Use `-memory 1536 -cores 1 -gpu swiftshader_indirect`
### Emulator Control
```bash
# Stop emulator
adb emu kill
# Restart emulator
adb reboot
# Check emulator status
adb get-state
```
## Build and Deploy Workflow
### Method 1: Using Existing Build Scripts
The TimeSafari project already has comprehensive Android build scripts that can be adapted for emulator deployment:
```bash
# Development build with auto-run
npm run build:android:dev:run
# Test build with auto-run
npm run build:android:test:run
# Production build with auto-run
npm run build:android:prod:run
```
### Method 2: Custom Emulator Deployment Script
Create a new script specifically for emulator deployment:
```bash
# Create emulator deployment script
cat > scripts/deploy-android-emulator.sh << 'EOF'
#!/bin/bash
# deploy-android-emulator.sh
# Author: Matthew Raymer
# Date: 2025-01-27
# Description: Deploy TimeSafari to Android emulator without Android Studio
set -e
# Source common utilities
source "$(dirname "$0")/common.sh"
# Default values
BUILD_MODE="development"
AVD_NAME="TimeSafari_Emulator"
START_EMULATOR=true
CLEAN_BUILD=true
# Parse command line arguments
while [[ $# -gt 0 ]]; do
case $1 in
--dev|--development)
BUILD_MODE="development"
shift
;;
--test)
BUILD_MODE="test"
shift
;;
--prod|--production)
BUILD_MODE="production"
shift
;;
--avd)
AVD_NAME="$2"
shift 2
;;
--no-start-emulator)
START_EMULATOR=false
shift
;;
--no-clean)
CLEAN_BUILD=false
shift
;;
-h|--help)
echo "Usage: $0 [options]"
echo "Options:"
echo " --dev, --development Build for development"
echo " --test Build for testing"
echo " --prod, --production Build for production"
echo " --avd NAME Use specific AVD name"
echo " --no-start-emulator Don't start emulator"
echo " --no-clean Skip clean build"
echo " -h, --help Show this help"
exit 0
;;
*)
log_error "Unknown option: $1"
exit 1
;;
esac
done
# Function to check if emulator is running
check_emulator_running() {
if adb devices | grep -q "emulator.*device"; then
return 0
else
return 1
fi
}
# Function to start emulator
start_emulator() {
log_info "Starting Android emulator: $AVD_NAME"
# Check if AVD exists
if ! avdmanager list avd | grep -q "$AVD_NAME"; then
log_error "AVD '$AVD_NAME' not found. Please create it first."
log_info "Create AVD with: avdmanager create avd --name $AVD_NAME --package system-images;android-34;google_apis;x86_64"
exit 1
fi
# Start emulator in background
emulator -avd "$AVD_NAME" -no-audio -no-snapshot &
EMULATOR_PID=$!
# Wait for emulator to boot
log_info "Waiting for emulator to boot..."
adb wait-for-device
# Wait for boot to complete
log_info "Waiting for boot to complete..."
while [ "$(adb shell getprop sys.boot_completed)" != "1" ]; do
sleep 2
done
log_success "Emulator is ready!"
}
# Function to build and deploy
build_and_deploy() {
log_info "Building TimeSafari for $BUILD_MODE mode..."
# Clean build if requested
if [ "$CLEAN_BUILD" = true ]; then
log_info "Cleaning previous build..."
npm run clean:android
fi
# Build based on mode
case $BUILD_MODE in
"development")
npm run build:android:dev
;;
"test")
npm run build:android:test
;;
"production")
npm run build:android:prod
;;
esac
# Deploy to emulator
log_info "Deploying to emulator..."
adb install -r android/app/build/outputs/apk/debug/app-debug.apk
# Launch app
log_info "Launching TimeSafari..."
adb shell am start -n app.timesafari/.MainActivity
log_success "TimeSafari deployed and launched successfully!"
}
# Main execution
main() {
log_info "TimeSafari Android Emulator Deployment"
log_info "Build Mode: $BUILD_MODE"
log_info "AVD Name: $AVD_NAME"
# Start emulator if requested and not running
if [ "$START_EMULATOR" = true ]; then
if ! check_emulator_running; then
start_emulator
else
log_info "Emulator already running"
fi
fi
# Build and deploy
build_and_deploy
log_success "Deployment completed successfully!"
}
# Run main function
main "$@"
EOF
# Make script executable
chmod +x scripts/deploy-android-emulator.sh
```
### Method 3: Direct Command Line Deployment
For quick deployments without scripts:
```bash
# 1. Ensure emulator is running
adb devices
# 2. Build the app
npm run build:android:dev
# 3. Install APK
adb install -r android/app/build/outputs/apk/debug/app-debug.apk
# 4. Launch app
adb shell am start -n app.timesafari/.MainActivity
# 5. View logs
adb logcat | grep -E "(TimeSafari|Capacitor|MainActivity)"
```
## Advanced Deployment Options
### Custom API Server Configuration
For development with custom API endpoints:
```bash
# Build with custom API IP
npm run build:android:dev:custom
# Or modify capacitor.config.ts for specific IP
# Then build normally
npm run build:android:dev
```
### Debug vs Release Builds
```bash
# Debug build (default)
npm run build:android:debug
# Release build
npm run build:android:release
# Install specific build
adb install -r android/app/build/outputs/apk/release/app-release.apk
```
### Asset Management
```bash
# Validate Android assets
npm run assets:validate:android
# Generate assets only
npm run build:android:assets
# Clean assets
npm run assets:clean
```
## Troubleshooting
### Common Issues
1. **Emulator Not Starting / AVD Not Found**
```bash
# Check available AVDs
avdmanager list avd
# If AVD exists but emulator can't find it, check AVD location
echo $ANDROID_AVD_HOME
ls -la ~/.android/avd/
# Fix AVD path issue (common on Arch Linux)
export ANDROID_AVD_HOME=/home/$USER/.config/.android/avd
# Or create symlinks if AVDs are in different location
mkdir -p ~/.android/avd
ln -s /home/$USER/.config/.android/avd/* ~/.android/avd/
# Create new AVD if needed
avdmanager create avd --name "TimeSafari_Emulator" --package "system-images;android-34;google_apis;x86_64"
# Check emulator logs
emulator -avd TimeSafari_Emulator -verbose
```
2. **System Lockup / High Resource Usage**
```bash
# Kill any stuck emulator processes
pkill -f emulator
# Check system resources
free -h
nvidia-smi # if using NVIDIA GPU
# Start with minimal resources
emulator -avd TimeSafari_Emulator \
-no-audio \
-memory 1536 \
-cores 1 \
-gpu swiftshader_indirect &
# Monitor resource usage
htop
# If still having issues, try software rendering only
emulator -avd TimeSafari_Emulator \
-no-audio \
-no-snapshot \
-memory 1024 \
-cores 1 \
-gpu off &
```
3. **ADB Device Not Found**
```bash
# Restart ADB server
adb kill-server
adb start-server
# Check devices
adb devices
# Check emulator status
adb get-state
```
3. **Build Failures**
```bash
# Clean everything
npm run clean:android
# Rebuild
npm run build:android:dev
# Check Gradle logs
cd android && ./gradlew clean --stacktrace
```
4. **Installation Failures**
```bash
# Uninstall existing app
adb uninstall app.timesafari
# Reinstall
adb install android/app/build/outputs/apk/debug/app-debug.apk
# Check package info
adb shell pm list packages | grep timesafari
```
### Performance Optimization
1. **Emulator Performance**
```bash
# Start with hardware acceleration
emulator -avd TimeSafari_Emulator -gpu host
# Use snapshot for faster startup
emulator -avd TimeSafari_Emulator -snapshot default
# Allocate more RAM
emulator -avd TimeSafari_Emulator -memory 4096
```
2. **Build Performance**
```bash
# Use Gradle daemon
echo "org.gradle.daemon=true" >> android/gradle.properties
# Increase heap size
echo "org.gradle.jvmargs=-Xmx4g" >> android/gradle.properties
# Enable parallel builds
echo "org.gradle.parallel=true" >> android/gradle.properties
```
## Integration with Existing Build System
### NPM Scripts Integration
Add emulator-specific scripts to `package.json`:
```json
{
"scripts": {
"emulator:check": "./scripts/avd-resource-checker.sh",
"emulator:check:test": "./scripts/avd-resource-checker.sh TimeSafari_Emulator --test",
"emulator:check:create": "./scripts/avd-resource-checker.sh TimeSafari_Emulator --create",
"emulator:start": "emulator -avd TimeSafari_Emulator -no-audio &",
"emulator:start:optimized": "/tmp/start-avd-TimeSafari_Emulator.sh",
"emulator:stop": "adb emu kill",
"emulator:deploy": "./scripts/deploy-android-emulator.sh",
"emulator:deploy:dev": "./scripts/deploy-android-emulator.sh --dev",
"emulator:deploy:test": "./scripts/deploy-android-emulator.sh --test",
"emulator:deploy:prod": "./scripts/deploy-android-emulator.sh --prod",
"emulator:logs": "adb logcat | grep -E '(TimeSafari|Capacitor|MainActivity)'",
"emulator:shell": "adb shell"
}
}
```
### CI/CD Integration
For automated testing and deployment:
```bash
# GitHub Actions example
- name: Start Android Emulator
run: |
emulator -avd TimeSafari_Emulator -no-audio -no-snapshot &
adb wait-for-device
adb shell getprop sys.boot_completed
- name: Build and Deploy
run: |
npm run build:android:test
adb install -r android/app/build/outputs/apk/debug/app-debug.apk
adb shell am start -n app.timesafari/.MainActivity
- name: Run Tests
run: |
npm run test:android
```
## Best Practices
### Development Workflow
1. **Start emulator once per session**
```bash
emulator -avd TimeSafari_Emulator -no-audio &
```
2. **Use incremental builds**
```bash
# For rapid iteration
npm run build:android:sync
adb install -r android/app/build/outputs/apk/debug/app-debug.apk
```
3. **Monitor logs continuously**
```bash
adb logcat | grep -E "(TimeSafari|Capacitor|MainActivity)" --color=always
```
### Performance Tips
1. **Use snapshots for faster startup**
2. **Enable hardware acceleration**
3. **Allocate sufficient RAM (4GB+)**
4. **Use SSD storage for AVDs**
5. **Close unnecessary applications**
### Security Considerations
1. **Use debug builds for development only**
2. **Never commit debug keystores**
3. **Use release builds for testing**
4. **Validate API endpoints in production builds**
## Conclusion
This guide provides a complete solution for deploying TimeSafari to Android emulators without Android Studio. The approach leverages the existing build system while adding emulator-specific deployment capabilities.
The key benefits:
- ✅ **No Android Studio required**
- ✅ **Command-line only workflow**
- ✅ **Integration with existing build scripts**
- ✅ **Automated deployment options**
- ✅ **Comprehensive troubleshooting guide**
For questions or issues, refer to the troubleshooting section or check the existing build documentation in `BUILDING.md`.
+1 -1
View File
@@ -122,4 +122,4 @@ export default class HomeView extends Vue {
--- ---
*This decision was made based on the current codebase architecture and team expertise. The mixin approach provides the best balance of performance, developer experience, and architectural consistency for the TimeSafari application.* _This decision was made based on the current codebase architecture and team expertise. The mixin approach provides the best balance of performance, developer experience, and architectural consistency for the TimeSafari application._
+2 -2
View File
@@ -92,5 +92,5 @@ Multiple stack traces showing Vue router navigation and component mounting cycle
3. Address API/server issues in separate debugging session 3. Address API/server issues in separate debugging session
--- ---
*Log Entry by: Migration Assistant* _Log Entry by: Migration Assistant_
*Session: ProjectsView.vue Triple Migration Pattern* _Session: ProjectsView.vue Triple Migration Pattern_
-139
View File
@@ -1,139 +0,0 @@
# iOS Share Extension - Git Commit Guide
**Date:** 2025-01-27
**Purpose:** Clarify which Xcode manual changes should be committed to the repository
## Quick Answer
**YES, most manual Xcode changes SHOULD be committed.** The only exceptions are user-specific settings that are already gitignored.
## What Gets Modified (and Should Be Committed)
When you create the Share Extension target and configure App Groups in Xcode, the following files are modified:
### 1. `ios/App/App.xcodeproj/project.pbxproj` ✅ **COMMIT THIS**
This is the main Xcode project file that tracks:
- **New targets** (Share Extension target)
- **File references** (which files belong to which targets)
- **Build settings** (compiler flags, deployment targets, etc.)
- **Build phases** (compile sources, link frameworks, etc.)
- **Capabilities** (App Groups configuration)
- **Target dependencies**
**This file IS tracked in git** (not in `.gitignore`), so changes should be committed.
### 2. Entitlements Files ✅ **COMMIT THESE**
When you enable App Groups capability, Xcode creates/modifies:
- `ios/App/App/App.entitlements` (for main app)
- `ios/App/TimeSafariShareExtension/TimeSafariShareExtension.entitlements` (for extension)
These files contain the App Group identifiers and should be committed.
### 3. Share Extension Source Files ✅ **ALREADY COMMITTED**
The following files are already in the repo:
- `ios/App/TimeSafariShareExtension/ShareViewController.swift`
- `ios/App/TimeSafariShareExtension/Info.plist`
- `ios/App/App/ShareImageBridge.swift`
These should already be committed (they were created as part of the implementation).
## What Should NOT Be Committed
### 1. User-Specific Settings ❌ **ALREADY GITIGNORED**
These are in `ios/.gitignore`:
- `xcuserdata/` - User-specific scheme selections, breakpoints, etc.
- `*.xcuserstate` - User's current Xcode state
### 2. Signing Identities ❌ **USER-SPECIFIC**
While the **App Groups capability** should be committed (it's in `project.pbxproj` and entitlements), your **personal signing identity/team** is user-specific and Xcode handles this automatically per developer.
## What Happens When You Commit
When you commit the changes:
1. **Other developers** who pull the changes will:
- ✅ Get the new Share Extension target automatically
- ✅ Get the App Groups capability configuration
- ✅ Get file references and build settings
- ✅ See the Share Extension in their Xcode project
2. **They will still need to:**
- Configure their own signing team/identity (Xcode prompts for this)
- Build the project (which may trigger CocoaPods updates)
- But they **won't** need to manually create the target or configure App Groups
## Step-by-Step: What to Commit
After completing the Xcode setup steps:
```bash
# Check what changed
git status
# You should see:
# - ios/App/App.xcodeproj/project.pbxproj (modified)
# - ios/App/App/App.entitlements (new or modified)
# - ios/App/TimeSafariShareExtension/TimeSafariShareExtension.entitlements (new)
# - Possibly other project-related files
# Review the changes
git diff ios/App/App.xcodeproj/project.pbxproj
# Commit the changes
git add ios/App/App.xcodeproj/project.pbxproj
git add ios/App/App/App.entitlements
git add ios/App/TimeSafariShareExtension/TimeSafariShareExtension.entitlements
git commit -m "Add iOS Share Extension target and App Groups configuration"
```
## Important Notes
### Merge Conflicts in project.pbxproj
The `project.pbxproj` file can have merge conflicts because:
- It's auto-generated by Xcode
- Multiple developers might modify it
- It uses UUIDs that can conflict
**If you get merge conflicts:**
1. Open the project in Xcode
2. Xcode will often auto-resolve conflicts
3. Or manually resolve by keeping both sets of changes
4. Test that the project builds
### Team/Developer IDs
The `DEVELOPMENT_TEAM` setting in `project.pbxproj` might be user-specific:
- Some teams commit this (if everyone uses the same team)
- Some teams use `.xcconfig` files to override per developer
- Check with your team's practices
If you see `DEVELOPMENT_TEAM = GM3FS5JQPH;` in the project file, this is already committed, so your team likely commits team IDs.
## Verification
After committing, verify that:
1. The Share Extension target appears in Xcode for other developers
2. App Groups capability is configured
3. The project builds successfully
4. No user-specific files were accidentally committed
## Summary
| Change Type | Commit? | Reason |
|------------|---------|--------|
| New target creation | ✅ Yes | Modifies `project.pbxproj` |
| App Groups capability | ✅ Yes | Creates/modifies entitlements files |
| File target membership | ✅ Yes | Modifies `project.pbxproj` |
| Build settings | ✅ Yes | Modifies `project.pbxproj` |
| Source files (Swift, plist) | ✅ Yes | Already in repo |
| User scheme selections | ❌ No | In `xcuserdata/` (gitignored) |
| Personal signing identity | ⚠️ Maybe | Depends on team practice |
**Bottom line:** Commit all the Xcode project configuration changes. Other developers will get the Share Extension target automatically when they pull, and they'll only need to configure their personal signing settings.
-283
View File
@@ -1,283 +0,0 @@
# iOS Share Extension Improvements
**Date:** 2025-11-24
**Purpose:** Explore alternatives to improve user experience by eliminating interstitial UI and simplifying app launch mechanism
## Current Implementation Issues
1. **Interstitial UI**: Users see `SLComposeServiceViewController` with a "Post" button before the app opens
2. **Deep Link Dependency**: App relies on deep link (`timesafari://shared-photo`) to detect shared images, even though data is already in App Group
## Improvement 1: Skip Interstitial UI
### Current Approach
- Uses `SLComposeServiceViewController` which shows a UI with "Post" button
- User must tap "Post" to proceed
### Alternative: Custom UIViewController (Headless Processing)
Replace `SLComposeServiceViewController` with a custom `UIViewController` that:
- Processes the image immediately in `viewDidLoad`
- Shows no UI (or minimal loading indicator)
- Opens the app automatically
**Implementation:**
```swift
import UIKit
import UniformTypeIdentifiers
class ShareViewController: UIViewController {
private let appGroupIdentifier = "group.app.timesafari.share"
private let sharedPhotoBase64Key = "sharedPhotoBase64"
private let sharedPhotoFileNameKey = "sharedPhotoFileName"
override func viewDidLoad() {
super.viewDidLoad()
// Process image immediately without showing UI
processAndOpenApp()
}
private func processAndOpenApp() {
guard let extensionContext = extensionContext,
let inputItems = extensionContext.inputItems as? [NSExtensionItem] else {
extensionContext?.completeRequest(returningItems: [], completionHandler: nil)
return
}
processSharedImage(from: inputItems) { [weak self] success in
guard let self = self else {
self?.extensionContext?.completeRequest(returningItems: [], completionHandler: nil)
return
}
if success {
self.openMainApp()
}
// Complete immediately - no UI shown
self.extensionContext?.completeRequest(returningItems: [], completionHandler: nil)
}
}
private func processSharedImage(from items: [NSExtensionItem], completion: @escaping (Bool) -> Void) {
// ... (same implementation as current)
}
private func openMainApp() {
guard let url = URL(string: "timesafari://shared-photo") else {
return
}
var responder: UIResponder? = self
while responder != nil {
if let application = responder as? UIApplication {
application.open(url, options: [:], completionHandler: nil)
return
}
responder = responder?.next
}
extensionContext?.open(url, completionHandler: nil)
}
}
```
**Info.plist Changes:**
- Already configured correctly with `NSExtensionPrincipalClass`
- No storyboard needed (already removed)
**Benefits:**
- ✅ No interstitial UI - app opens immediately
- ✅ Faster user experience
- ✅ More seamless integration
**Considerations:**
- ⚠️ User has less control (can't cancel easily)
- ⚠️ No visual feedback during processing (could add minimal loading indicator)
- ⚠️ Apple guidelines: Extensions should provide value even if they don't open the app
## Improvement 2: Direct App Launch Without Deep Link
### Current Approach
- Share Extension stores data in App Group UserDefaults
- Share Extension opens app via deep link (`timesafari://shared-photo`)
- App receives deep link → checks App Group → processes image
### Alternative: App Lifecycle Detection
Instead of using deep links, the app can check for shared data when it becomes active:
**Option A: Check on App Activation**
```swift
// In AppDelegate.swift
func applicationDidBecomeActive(_ application: UIApplication) {
// Check for shared image from Share Extension
if let sharedData = getSharedImageData() {
// Store in temp file for JS to read
writeSharedImageToTempFile(sharedData)
// Navigate to shared-photo route directly
// This would need to be handled in JS layer
}
}
```
**Option B: Use Notification (More Reliable)**
```swift
// In ShareViewController.swift (after storing data)
private func openMainApp() {
// Store a flag that image is ready
guard let userDefaults = UserDefaults(suiteName: appGroupIdentifier) else {
return
}
userDefaults.set(true, forKey: "sharedPhotoReady")
userDefaults.synchronize()
// Open app (can use any URL scheme or even just launch the app)
guard let url = URL(string: "timesafari://") else {
return
}
var responder: UIResponder? = self
while responder != nil {
if let application = responder as? UIApplication {
application.open(url, options: [:], completionHandler: nil)
return
}
responder = responder?.next
}
}
// In AppDelegate.swift
func applicationDidBecomeActive(_ application: UIApplication) {
let appGroupIdentifier = "group.app.timesafari.share"
guard let userDefaults = UserDefaults(suiteName: appGroupIdentifier) else {
return
}
// Check if shared photo is ready
if userDefaults.bool(forKey: "sharedPhotoReady") {
userDefaults.removeObject(forKey: "sharedPhotoReady")
userDefaults.synchronize()
// Process shared image
if let sharedData = getSharedImageData() {
writeSharedImageToTempFile(sharedData)
// Trigger JS to check for shared image
// This could be done via Capacitor App plugin or custom event
}
}
}
```
**Option C: Check on App Launch (Most Direct)**
```swift
// In AppDelegate.swift
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
// Check for shared image immediately on launch
checkForSharedImageOnLaunch()
return true
}
func applicationDidBecomeActive(_ application: UIApplication) {
// Also check when app becomes active (in case it was already running)
checkForSharedImageOnLaunch()
}
private func checkForSharedImageOnLaunch() {
if let sharedData = getSharedImageData() {
writeSharedImageToTempFile(sharedData)
// Post a notification or use Capacitor to notify JS
NotificationCenter.default.post(name: NSNotification.Name("SharedPhotoReady"), object: nil)
}
}
```
**JavaScript Integration:**
```typescript
// In main.capacitor.ts
import { App } from '@capacitor/app';
// Listen for app becoming active
App.addListener('appStateChange', async ({ isActive }) => {
if (isActive) {
// Check for shared image when app becomes active
await checkAndStoreNativeSharedImage();
}
});
// Also check on initial load
if (Capacitor.isNativePlatform() && Capacitor.getPlatform() === 'ios') {
checkAndStoreNativeSharedImage().then(result => {
if (result.success) {
// Navigate to shared-photo route
router.push('/shared-photo' + (result.fileName ? `?fileName=${result.fileName}` : ''));
}
});
}
```
**Benefits:**
- ✅ No deep link routing needed
- ✅ More direct data flow
- ✅ App can detect shared content even if it was already running
- ✅ Simpler URL scheme handling
**Considerations:**
- ⚠️ Need to ensure app checks on both launch and activation
- ⚠️ May need to handle race conditions (app launching vs. share extension writing)
- ⚠️ Still need some way to open the app (minimal URL scheme still required)
## Recommended Approach
**Best of Both Worlds:**
1. **Use Custom UIViewController** (Improvement 1) - Eliminates interstitial UI
2. **Use App Lifecycle Detection** (Improvement 2, Option C) - Direct data flow
**Combined Implementation:**
```swift
// ShareViewController.swift - Custom UIViewController
class ShareViewController: UIViewController {
// Process immediately in viewDidLoad
// Store data in App Group
// Open app with minimal URL (just "timesafari://")
}
// AppDelegate.swift
func applicationDidBecomeActive(_ application: UIApplication) {
// Check for shared image
// If found, write to temp file and let JS handle navigation
}
```
**JavaScript:**
```typescript
// Check on app activation
App.addListener('appStateChange', async ({ isActive }) => {
if (isActive) {
const result = await checkAndStoreNativeSharedImage();
if (result.success) {
router.push('/shared-photo' + (result.fileName ? `?fileName=${result.fileName}` : ''));
}
}
});
```
This approach:
- ✅ No interstitial UI
- ✅ No deep link routing complexity
- ✅ Direct data flow via App Group
- ✅ Works whether app is running or launching fresh
-140
View File
@@ -1,140 +0,0 @@
# iOS Share Extension Setup Instructions
**Date:** 2025-01-27
**Purpose:** Step-by-step instructions for setting up the iOS Share Extension in Xcode
## Prerequisites
- Xcode installed
- iOS project already set up with Capacitor
- Access to Apple Developer account (for App Groups)
## Step 1: Create Share Extension Target
1. Open `ios/App/App.xcodeproj` in Xcode
2. In the Project Navigator, select the **App** project (top-level item)
3. Click the **+** button at the bottom of the Targets list
4. Select **iOS** → **Share Extension**
5. Click **Next**
6. Configure:
- **Product Name:** `TimeSafariShareExtension`
- **Bundle Identifier:** `app.timesafari.shareextension` (must match main app's bundle ID with `.shareextension` suffix)
- **Language:** Swift
7. Click **Finish**
## Step 2: Configure Share Extension Files
The following files have been created in `ios/App/TimeSafariShareExtension/`:
- `ShareViewController.swift` - Main extension logic
- `Info.plist` - Extension configuration
**Verify these files exist and are added to the Share Extension target.**
## Step 3: Configure App Groups
App Groups allow the Share Extension and main app to share data.
### For Main App Target:
1. Select the **App** target in Xcode
2. Go to **Signing & Capabilities** tab
3. Click **+ Capability**
4. Select **App Groups**
5. Click **+** to add a new group
6. Enter: `group.app.timesafari.share`
7. Ensure it's checked/enabled
### For Share Extension Target:
1. Select the **TimeSafariShareExtension** target
2. Go to **Signing & Capabilities** tab
3. Click **+ Capability**
4. Select **App Groups**
5. Click **+** to add a new group
6. Enter: `group.app.timesafari.share` (same as main app)
7. Ensure it's checked/enabled
**Important:** Both targets must use the **exact same** App Group identifier.
## Step 4: Configure Share Extension Info.plist
The `Info.plist` file should already be configured, but verify:
1. Select `TimeSafariShareExtension/Info.plist` in Xcode
2. Ensure it contains:
- `NSExtensionPointIdentifier` = `com.apple.share-services`
- `NSExtensionPrincipalClass` = `$(PRODUCT_MODULE_NAME).ShareViewController`
- `NSExtensionActivationSupportsImageWithMaxCount` = `1`
## Step 5: Add ShareImageBridge to Main App
1. The file `ios/App/App/ShareImageBridge.swift` has been created
2. Ensure it's added to the **App** target (not the Share Extension target)
3. In Xcode, select the file and check the **Target Membership** in the File Inspector
## Step 6: Build and Test
1. Select the **App** scheme (not the Share Extension scheme)
2. Build and run on a device or simulator
3. Open Photos app
4. Select an image
5. Tap **Share** button
6. Look for **TimeSafari Share** in the share sheet
7. Select it
8. The app should open and navigate to the shared photo view
## Step 7: Troubleshooting
### Share Extension doesn't appear in share sheet
- Verify the Share Extension target builds successfully
- Check that `Info.plist` is correctly configured
- Ensure the extension's bundle identifier follows the pattern: `{main-app-bundle-id}.shareextension`
- Clean build folder (Product → Clean Build Folder)
### App Group access fails
- Verify both targets have the same App Group identifier
- Check that App Groups capability is enabled for both targets
- Ensure you're signed in with a valid Apple Developer account
- For development, you may need to enable App Groups in your Apple Developer account
### Shared image not appearing
- Check Xcode console for errors
- Verify `ShareViewController.swift` is correctly implemented
- Ensure the deep link `timesafari://shared-photo` is being handled
- Check that the native bridge method is being called
### Build errors
- Ensure Swift version matches between targets
- Check that all required frameworks are linked
- Verify deployment targets match between main app and extension
## Step 8: Native Bridge Implementation (TODO)
Currently, the JavaScript code needs a way to call the native `getSharedImageData()` method. This requires one of:
1. **Option A:** Create a minimal Capacitor plugin
2. **Option B:** Use Capacitor's existing bridge mechanisms
3. **Option C:** Expose the method via a custom URL scheme parameter
The current implementation in `main.capacitor.ts` has a placeholder that needs to be completed.
## Next Steps
After the Share Extension is set up and working:
1. Complete the native bridge implementation to read from App Group
2. Test end-to-end flow: Share image → Extension stores → App reads → Displays
3. Implement Android version
4. Add error handling and edge cases
## References
- [Apple Share Extensions Documentation](https://developer.apple.com/documentation/social)
- [App Groups Documentation](https://developer.apple.com/documentation/xcode/configuring-app-groups)
- [Capacitor Native Bridge](https://capacitorjs.com/docs/guides/building-plugins)
-93
View File
@@ -1,93 +0,0 @@
# iOS Share Extension Implementation Status
**Date:** 2025-01-27
**Status:** In Progress - Native Code Complete, Bridge Pending
## Completed
✅ **Share Extension Files Created:**
- `ios/App/TimeSafariShareExtension/ShareViewController.swift` - Handles image sharing
- `ios/App/TimeSafariShareExtension/Info.plist` - Extension configuration
✅ **Native Bridge Created:**
- `ios/App/App/ShareImageBridge.swift` - Native method to read from App Group
✅ **JavaScript Integration Started:**
- `src/services/nativeShareHandler.ts` - Service to handle native shared images
- `src/main.capacitor.ts` - Updated to check for native shared images on deep link
✅ **Documentation:**
- `doc/native-share-target-implementation.md` - Complete implementation guide
- `doc/ios-share-extension-setup.md` - Xcode setup instructions
## Pending
⚠️ **Xcode Configuration (Manual Steps Required):**
1. Create Share Extension target in Xcode
2. Configure App Groups for both main app and extension
3. Add ShareImageBridge.swift to App target
4. Build and test
⚠️ **JavaScript-Native Bridge:**
The current implementation has a placeholder for calling the native `ShareImageBridge.getSharedImageData()` method from JavaScript. This needs to be completed using one of:
**Option A: Minimal Capacitor Plugin** (Recommended for Option 1)
- Create a small plugin that exposes the method
- Clean and maintainable
- Follows Capacitor patterns
**Option B: Direct Bridge Call**
- Use Capacitor's executePlugin or similar mechanism
- Requires understanding Capacitor's internal bridge
- Less maintainable
**Option C: AppDelegate Integration**
- Have AppDelegate check on launch and expose via a different mechanism
- Workaround approach
- Less clean but functional
## Next Steps
1. **Complete Xcode Setup:**
- Follow `doc/ios-share-extension-setup.md`
- Create Share Extension target
- Configure App Groups
- Build and verify extension appears in share sheet
2. **Implement JavaScript-Native Bridge:**
- Choose one of the options above
- Complete the `checkAndStoreNativeSharedImage()` function in `main.capacitor.ts`
- Test end-to-end flow
3. **Testing:**
- Share image from Photos app
- Verify Share Extension appears
- Verify app opens and displays shared image
- Test "Record Gift" and "Save as Profile" flows
## Current Flow
1. ✅ User shares image → Share Extension receives
2. ✅ Share Extension converts to base64
3. ✅ Share Extension stores in App Group UserDefaults
4. ✅ Share Extension opens app with `timesafari://shared-photo?fileName=...`
5. ⚠️ App receives deep link (handled)
6. ⚠️ App checks App Group UserDefaults (bridge needed)
7. ⚠️ App stores in temp database (pending bridge)
8. ✅ SharedPhotoView reads from temp database (already works)
## Code Locations
- **Share Extension:** `ios/App/TimeSafariShareExtension/`
- **Native Bridge:** `ios/App/App/ShareImageBridge.swift`
- **JavaScript Handler:** `src/services/nativeShareHandler.ts`
- **Deep Link Integration:** `src/main.capacitor.ts`
- **View Component:** `src/views/SharedPhotoView.vue` (already complete)
## Notes
- The Share Extension code is complete and ready to use
- The main missing piece is the JavaScript-to-native bridge
- Once the bridge is complete, the entire flow should work end-to-end
- The existing `SharedPhotoView.vue` doesn't need changes - it already handles images from temp storage
+22
View File
@@ -32,6 +32,7 @@ you apply 1-3 meta-rules that automatically include everything you need.
### **Step 1: Always Start with Core Always-On** ### **Step 1: Always Start with Core Always-On**
**Every single interaction** starts with: **Every single interaction** starts with:
``` ```
meta_core_always_on.mdc meta_core_always_on.mdc
``` ```
@@ -65,12 +66,14 @@ meta_core_always_on + meta_research + meta_bug_diagnosis
**Important**: Meta-rules represent **workflow phases**, not a rigid sequence. You can: **Important**: Meta-rules represent **workflow phases**, not a rigid sequence. You can:
### **Jump Between Phases Freely** ### **Jump Between Phases Freely**
- **Start with diagnosis** if you already know the problem - **Start with diagnosis** if you already know the problem
- **Go back to research** if your fix reveals new issues - **Go back to research** if your fix reveals new issues
- **Switch to planning** mid-implementation if scope changes - **Switch to planning** mid-implementation if scope changes
- **Document at any phase** - not just at the end - **Document at any phase** - not just at the end
### **Mode Switching by Invoking Meta-Rules** ### **Mode Switching by Invoking Meta-Rules**
Each meta-rule invocation **automatically switches your workflow mode**: Each meta-rule invocation **automatically switches your workflow mode**:
``` ```
@@ -80,11 +83,13 @@ Planning Mode → Invoke @meta_feature_implementation → Implementation Mode
``` ```
### **Phase Constraints, Not Sequence Constraints** ### **Phase Constraints, Not Sequence Constraints**
- **Within each phase**: Clear constraints on what you can/cannot do - **Within each phase**: Clear constraints on what you can/cannot do
- **Between phases**: Complete freedom to move as needed - **Between phases**: Complete freedom to move as needed
- **No forced order**: Choose the phase that matches your current need - **No forced order**: Choose the phase that matches your current need
### **Example of Flexible Workflow** ### **Example of Flexible Workflow**
``` ```
1. Start with @meta_research (investigation mode) 1. Start with @meta_research (investigation mode)
2. Jump to @meta_bug_diagnosis (diagnosis mode) 2. Jump to @meta_bug_diagnosis (diagnosis mode)
@@ -103,16 +108,19 @@ Planning Mode → Invoke @meta_feature_implementation → Implementation Mode
**Scenario**: User reports that the contact list isn't loading properly **Scenario**: User reports that the contact list isn't loading properly
**Initial Meta-Rule Selection**: **Initial Meta-Rule Selection**:
``` ```
meta_core_always_on + meta_research + meta_bug_diagnosis meta_core_always_on + meta_research + meta_bug_diagnosis
``` ```
**What This Gives You**: **What This Gives You**:
- **Core Always-On**: Human competence focus, time standards, context - **Core Always-On**: Human competence focus, time standards, context
- **Research**: Systematic investigation methodology, evidence collection - **Research**: Systematic investigation methodology, evidence collection
- **Bug Diagnosis**: Defect analysis framework, root cause identification - **Bug Diagnosis**: Defect analysis framework, root cause identification
**Flexible Workflow**: **Flexible Workflow**:
1. Apply core always-on for foundation 1. Apply core always-on for foundation
2. Use research meta-rule for systematic investigation 2. Use research meta-rule for systematic investigation
3. Switch to bug diagnosis when you have enough evidence 3. Switch to bug diagnosis when you have enough evidence
@@ -125,16 +133,19 @@ meta_core_always_on + meta_research + meta_bug_diagnosis
**Scenario**: Building a new contact search feature **Scenario**: Building a new contact search feature
**Meta-Rule Selection**: **Meta-Rule Selection**:
``` ```
meta_core_always_on + meta_feature_planning + meta_feature_implementation meta_core_always_on + meta_feature_planning + meta_feature_implementation
``` ```
**What This Gives You**: **What This Gives You**:
- **Core Always-On**: Foundation principles and context - **Core Always-On**: Foundation principles and context
- **Feature Planning**: Requirements analysis, architecture planning - **Feature Planning**: Requirements analysis, architecture planning
- **Feature Implementation**: Development workflow, testing strategy - **Feature Implementation**: Development workflow, testing strategy
**Iterative Workflow**: **Iterative Workflow**:
1. Start with core always-on 1. Start with core always-on
2. Use feature planning for design and requirements 2. Use feature planning for design and requirements
3. Switch to feature implementation for coding and testing 3. Switch to feature implementation for coding and testing
@@ -147,15 +158,18 @@ meta_core_always_on + meta_feature_planning + meta_feature_implementation
**Scenario**: Writing a migration guide for the new database system **Scenario**: Writing a migration guide for the new database system
**Meta-Rule Selection**: **Meta-Rule Selection**:
``` ```
meta_core_always_on + meta_documentation meta_core_always_on + meta_documentation
``` ```
**What This Gives You**: **What This Gives You**:
- **Core Always-On**: Foundation and context - **Core Always-On**: Foundation and context
- **Documentation**: Educational focus, templates, quality standards - **Documentation**: Educational focus, templates, quality standards
**Parallel Workflow**: **Parallel Workflow**:
1. Apply core always-on for foundation 1. Apply core always-on for foundation
2. Use documentation meta-rule for educational content creation 2. Use documentation meta-rule for educational content creation
3. **Can research** while documenting if you need more information 3. **Can research** while documenting if you need more information
@@ -198,27 +212,35 @@ Each meta-rule includes success criteria. Use these to validate your work:
## Common Meta-Rule Combinations ## Common Meta-Rule Combinations
### **Research + Diagnosis** ### **Research + Diagnosis**
``` ```
meta_core_always_on + meta_research + meta_bug_diagnosis meta_core_always_on + meta_research + meta_bug_diagnosis
``` ```
**Use for**: Complex bug investigations requiring systematic analysis **Use for**: Complex bug investigations requiring systematic analysis
### **Planning + Implementation** ### **Planning + Implementation**
``` ```
meta_core_always_on + meta_feature_planning + meta_feature_implementation meta_core_always_on + meta_feature_planning + meta_feature_implementation
``` ```
**Use for**: End-to-end feature development from concept to deployment **Use for**: End-to-end feature development from concept to deployment
### **Research + Planning** ### **Research + Planning**
``` ```
meta_core_always_on + meta_research + meta_feature_planning meta_core_always_on + meta_research + meta_feature_planning
``` ```
**Use for**: Feasibility research and solution design **Use for**: Feasibility research and solution design
### **Documentation + Context** ### **Documentation + Context**
``` ```
meta_core_always_on + meta_documentation + [context-specific] meta_core_always_on + meta_documentation + [context-specific]
``` ```
**Use for**: Creating comprehensive, educational documentation **Use for**: Creating comprehensive, educational documentation
## Best Practices ## Best Practices
-507
View File
@@ -1,507 +0,0 @@
# Native Share Target Implementation Guide
**Date:** 2025-01-27
**Purpose:** Enable TimeSafari native iOS and Android apps to receive shared images from other apps
## Current State
The app currently supports **PWA/web share target** functionality:
- Service worker intercepts POST to `/share-target`
- Images stored in temp database as base64
- `SharedPhotoView.vue` processes and displays shared images
**This does NOT work for native iOS/Android builds** because:
- Service workers don't run in native app contexts
- Native platforms use different sharing mechanisms (Share Extensions on iOS, Intent Filters on Android)
## Required Changes
### 1. iOS Implementation
#### 1.1 Create Share Extension Target
1. Open `ios/App/App.xcodeproj` in Xcode
2. File → New → Target
3. Select "Share Extension" template
4. Name it "TimeSafariShareExtension"
5. Bundle Identifier: `app.timesafari.shareextension`
6. Language: Swift
#### 1.2 Configure Share Extension Info.plist
Add to `ios/App/TimeSafariShareExtension/Info.plist`:
```xml
<key>NSExtension</key>
<dict>
<key>NSExtensionPointIdentifier</key>
<string>com.apple.share-services</string>
<key>NSExtensionPrincipalClass</key>
<string>$(PRODUCT_MODULE_NAME).ShareViewController</string>
<key>NSExtensionActivationRule</key>
<dict>
<key>NSExtensionActivationSupportsImageWithMaxCount</key>
<integer>1</integer>
<key>NSExtensionActivationSupportsFileWithMaxCount</key>
<integer>1</integer>
</dict>
</dict>
```
#### 1.3 Implement ShareViewController
Create `ios/App/TimeSafariShareExtension/ShareViewController.swift`:
```swift
import UIKit
import Social
import MobileCoreServices
import Capacitor
class ShareViewController: SLComposeServiceViewController {
override func viewDidLoad() {
super.viewDidLoad()
self.title = "Share to TimeSafari"
}
override func isContentValid() -> Bool {
return true
}
override func didSelectPost() {
guard let extensionItem = extensionContext?.inputItems.first as? NSExtensionItem,
let itemProvider = extensionItem.attachments?.first else {
self.extensionContext?.completeRequest(returningItems: nil, completionHandler: nil)
return
}
// Handle image sharing
if itemProvider.hasItemConformingToTypeIdentifier(kUTTypeImage as String) {
itemProvider.loadItem(forTypeIdentifier: kUTTypeImage as String, options: nil) { [weak self] (item, error) in
guard let self = self else { return }
if let url = item as? URL {
// Handle file URL
self.handleSharedImage(url: url)
} else if let image = item as? UIImage {
// Handle UIImage directly
self.handleSharedImage(image: image)
} else if let data = item as? Data {
// Handle image data
self.handleSharedImage(data: data)
}
}
}
}
private func handleSharedImage(url: URL? = nil, image: UIImage? = nil, data: Data? = nil) {
var imageData: Data?
var fileName: String?
if let url = url {
imageData = try? Data(contentsOf: url)
fileName = url.lastPathComponent
} else if let image = image {
imageData = image.jpegData(compressionQuality: 0.8)
fileName = "shared-image.jpg"
} else if let data = data {
imageData = data
fileName = "shared-image.jpg"
}
guard let imageData = imageData else {
self.extensionContext?.completeRequest(returningItems: nil, completionHandler: nil)
return
}
// Convert to base64
let base64String = imageData.base64EncodedString()
// Store in shared UserDefaults (accessible by main app)
let userDefaults = UserDefaults(suiteName: "group.app.timesafari.share")
userDefaults?.set(base64String, forKey: "sharedPhotoBase64")
userDefaults?.set(fileName ?? "shared-image.jpg", forKey: "sharedPhotoFileName")
userDefaults?.synchronize()
// Open main app with deep link
let url = URL(string: "timesafari://shared-photo?fileName=\(fileName?.addingPercentEncoding(withAllowedCharacters: .urlQueryAllowed) ?? "shared-image.jpg")")!
var responder = self as UIResponder?
while responder != nil {
if let application = responder as? UIApplication {
application.open(url, options: [:], completionHandler: nil)
break
}
responder = responder?.next
}
// Close share extension
self.extensionContext?.completeRequest(returningItems: nil, completionHandler: nil)
}
override func configurationItems() -> [Any]! {
return []
}
}
```
#### 1.4 Configure App Groups
1. In Xcode, select main app target → Signing & Capabilities
2. Add "App Groups" capability
3. Create group: `group.app.timesafari.share`
4. Repeat for Share Extension target with same group name
#### 1.5 Update Main App to Read from App Group
The main app needs to check for shared images on launch. This should be added to `AppDelegate.swift` or handled in JavaScript.
### 2. Android Implementation
#### 2.1 Update AndroidManifest.xml
Add intent filter to `MainActivity` in `android/app/src/main/AndroidManifest.xml`:
```xml
<activity
android:name=".MainActivity"
... existing attributes ...>
... existing intent filters ...
<!-- Share Target Intent Filter -->
<intent-filter>
<action android:name="android.intent.action.SEND" />
<category android:name="android.intent.category.DEFAULT" />
<data android:mimeType="image/*" />
</intent-filter>
<!-- Multiple images support (optional) -->
<intent-filter>
<action android:name="android.intent.action.SEND_MULTIPLE" />
<category android:name="android.intent.category.DEFAULT" />
<data android:mimeType="image/*" />
</intent-filter>
</activity>
```
#### 2.2 Handle Intent in MainActivity
Update `android/app/src/main/java/app/timesafari/MainActivity.java`:
```java
package app.timesafari;
import android.content.Intent;
import android.net.Uri;
import android.os.Bundle;
import android.util.Base64;
import android.util.Log;
import com.getcapacitor.BridgeActivity;
import com.getcapacitor.Plugin;
import java.io.InputStream;
import java.io.ByteArrayOutputStream;
public class MainActivity extends BridgeActivity {
private static final String TAG = "MainActivity";
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
handleShareIntent(getIntent());
}
@Override
protected void onNewIntent(Intent intent) {
super.onNewIntent(intent);
setIntent(intent);
handleShareIntent(intent);
}
private void handleShareIntent(Intent intent) {
if (intent == null) return;
String action = intent.getAction();
String type = intent.getType();
if (Intent.ACTION_SEND.equals(action) && type != null && type.startsWith("image/")) {
Uri imageUri = intent.getParcelableExtra(Intent.EXTRA_STREAM);
if (imageUri != null) {
handleSharedImage(imageUri, intent.getStringExtra(Intent.EXTRA_TEXT));
}
} else if (Intent.ACTION_SEND_MULTIPLE.equals(action) && type != null && type.startsWith("image/")) {
// Handle multiple images (optional - for now just take first)
java.util.ArrayList<Uri> imageUris = intent.getParcelableArrayListExtra(Intent.EXTRA_STREAM);
if (imageUris != null && !imageUris.isEmpty()) {
handleSharedImage(imageUris.get(0), null);
}
}
}
private void handleSharedImage(Uri imageUri, String fileName) {
try {
// Read image data
InputStream inputStream = getContentResolver().openInputStream(imageUri);
if (inputStream == null) {
Log.e(TAG, "Failed to open input stream for shared image");
return;
}
ByteArrayOutputStream buffer = new ByteArrayOutputStream();
byte[] data = new byte[8192];
int nRead;
while ((nRead = inputStream.read(data, 0, data.length)) != -1) {
buffer.write(data, 0, nRead);
}
buffer.flush();
byte[] imageBytes = buffer.toByteArray();
// Convert to base64
String base64String = Base64.encodeToString(imageBytes, Base64.NO_WRAP);
// Extract filename from URI or use default
String actualFileName = fileName;
if (actualFileName == null || actualFileName.isEmpty()) {
String path = imageUri.getPath();
if (path != null) {
int lastSlash = path.lastIndexOf('/');
if (lastSlash >= 0 && lastSlash < path.length() - 1) {
actualFileName = path.substring(lastSlash + 1);
}
}
if (actualFileName == null || actualFileName.isEmpty()) {
actualFileName = "shared-image.jpg";
}
}
// Store in SharedPreferences (accessible by JavaScript via Capacitor)
android.content.SharedPreferences prefs = getSharedPreferences("TimeSafariShared", MODE_PRIVATE);
android.content.SharedPreferences.Editor editor = prefs.edit();
editor.putString("sharedPhotoBase64", base64String);
editor.putString("sharedPhotoFileName", actualFileName);
editor.apply();
// Trigger JavaScript event or navigate to shared-photo route
// This will be handled by JavaScript checking for shared data on app launch
Log.d(TAG, "Shared image stored, filename: " + actualFileName);
} catch (Exception e) {
Log.e(TAG, "Error handling shared image", e);
}
}
}
```
#### 2.3 Add Required Permissions
Ensure `AndroidManifest.xml` has:
```xml
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.READ_MEDIA_IMAGES" /> <!-- Android 13+ -->
```
### 3. JavaScript Layer Updates
#### 3.1 Create Native Share Handler
Create `src/services/nativeShareHandler.ts`:
```typescript
/**
* Native Share Handler
* Handles shared images from native iOS and Android platforms
*/
import { Capacitor } from "@capacitor/core";
import { App } from "@capacitor/app";
import { Filesystem, Directory, Encoding } from "@capacitor/filesystem";
import { logger } from "../utils/logger";
import { SHARED_PHOTO_BASE64_KEY } from "../libs/util";
import { PlatformServiceMixin } from "@/utils/PlatformServiceMixin";
/**
* Check for shared images from native platforms and store in temp database
*/
export async function checkForNativeSharedImage(
platformService: InstanceType<typeof PlatformServiceMixin>
): Promise<boolean> {
if (!Capacitor.isNativePlatform()) {
return false;
}
try {
if (Capacitor.getPlatform() === "ios") {
return await checkIOSSharedImage(platformService);
} else if (Capacitor.getPlatform() === "android") {
return await checkAndroidSharedImage(platformService);
}
} catch (error) {
logger.error("Error checking for native shared image:", error);
}
return false;
}
/**
* Check for shared image on iOS (from App Group UserDefaults)
*/
async function checkIOSSharedImage(
platformService: InstanceType<typeof PlatformServiceMixin>
): Promise<boolean> {
// iOS uses App Groups to share data between extension and main app
// We need to use a Capacitor plugin or native code to read from App Group
// For now, this is a placeholder - requires native plugin implementation
// Option 1: Use Capacitor plugin to read from App Group
// Option 2: Use native code bridge
logger.debug("Checking for iOS shared image (not yet implemented)");
return false;
}
/**
* Check for shared image on Android (from SharedPreferences)
*/
async function checkAndroidSharedImage(
platformService: InstanceType<typeof PlatformServiceMixin>
): Promise<boolean> {
// Android stores in SharedPreferences
// We need a Capacitor plugin to read from SharedPreferences
// For now, this is a placeholder - requires native plugin implementation
logger.debug("Checking for Android shared image (not yet implemented)");
return false;
}
/**
* Store shared image in temp database
*/
async function storeSharedImage(
base64Data: string,
fileName: string,
platformService: InstanceType<typeof PlatformServiceMixin>
): Promise<void> {
try {
const existing = await platformService.$getTemp(SHARED_PHOTO_BASE64_KEY);
if (existing) {
await platformService.$updateEntity(
"temp",
{ blobB64: base64Data },
"id = ?",
[SHARED_PHOTO_BASE64_KEY]
);
} else {
await platformService.$insertEntity(
"temp",
{ id: SHARED_PHOTO_BASE64_KEY, blobB64: base64Data },
["id", "blobB64"]
);
}
logger.debug("Stored shared image in temp database");
} catch (error) {
logger.error("Error storing shared image:", error);
throw error;
}
}
```
#### 3.2 Update main.capacitor.ts
Add check for shared images on app launch:
```typescript
// In main.capacitor.ts, after app mount:
import { checkForNativeSharedImage } from "./services/nativeShareHandler";
// Check for shared images when app becomes active
App.addListener("appStateChange", async (state) => {
if (state.isActive) {
// Check for native shared images
const hasSharedImage = await checkForNativeSharedImage(/* platformService */);
if (hasSharedImage) {
// Navigate to shared-photo view
await router.push({
name: "shared-photo",
query: { source: "native" }
});
}
}
});
// Also check on initial launch
App.getLaunchUrl().then((result) => {
if (result?.url) {
// Handle deep link
} else {
// Check for shared image
checkForNativeSharedImage(/* platformService */).then((hasShared) => {
if (hasShared) {
router.push({ name: "shared-photo", query: { source: "native" } });
}
});
}
});
```
#### 3.3 Update SharedPhotoView.vue
The existing `SharedPhotoView.vue` should work as-is, but we may want to add detection for native vs web sources.
### 4. Alternative Approach: Capacitor Plugin
Instead of implementing native code directly, consider creating a Capacitor plugin:
1. **Create plugin**: `@capacitor-community/share-target` or custom plugin
2. **Plugin methods**:
- `checkForSharedImage()`: Returns shared image data if available
- `clearSharedImage()`: Clears shared image data after processing
This would be cleaner and more maintainable.
### 5. Testing Checklist
- [ ] Test sharing image from Photos app on iOS
- [ ] Test sharing image from Gallery app on Android
- [ ] Test sharing from other apps (Safari, Chrome, etc.)
- [ ] Verify image appears in SharedPhotoView
- [ ] Test "Record Gift" flow with shared image
- [ ] Test "Save as Profile" flow with shared image
- [ ] Test cancel flow
- [ ] Verify temp storage cleanup
- [ ] Test app launch with shared image pending
- [ ] Test app already running when image is shared
### 6. Implementation Priority
**Phase 1: Android (Simpler)**
1. Update AndroidManifest.xml
2. Implement MainActivity intent handling
3. Create JavaScript handler
4. Test end-to-end
**Phase 2: iOS (More Complex)**
1. Create Share Extension target
2. Implement ShareViewController
3. Configure App Groups
4. Create JavaScript handler
5. Test end-to-end
### 7. Notes
- **App Groups (iOS)**: Required for sharing data between Share Extension and main app
- **SharedPreferences (Android)**: Standard way to share data between app components
- **Base64 Encoding**: Both platforms convert images to base64 for JavaScript compatibility
- **File Size Limits**: Consider large image handling and memory management
- **Permissions**: Android 13+ requires `READ_MEDIA_IMAGES` instead of `READ_EXTERNAL_STORAGE`
### 8. References
- [iOS Share Extensions](https://developer.apple.com/documentation/social)
- [Android Share Targets](https://developer.android.com/training/sharing/receive)
- [Capacitor App Plugin](https://capacitorjs.com/docs/apis/app)
- [Capacitor Native Bridge](https://capacitorjs.com/docs/guides/building-plugins)
+231
View File
@@ -0,0 +1,231 @@
# TimeSafari — Native-First Notification System
**Status:** Ready for implementation
**Date:** 2025-09-07
**Author:** Matthew Raymer
---
## Executive Summary
Ship a **single, Native-First** notification system: OS-scheduled **background prefetch at T–lead** + **pre-armed** local notifications. Web-push is retired.
### What we deliver
- **Closed-app delivery:** Pre-armed locals fire even if the app is closed.
- **Freshness:** One prefetch attempt per slot at **T–lead**; ETag/TTL controls; skip when stale.
- **Android precision:** Exact alarms with permission; windowed fallback (±10m) otherwise.
- **Resilience:** Re-arm after reboot/time-change (Android receivers; iOS on next wake/silent push).
- **Cross-platform:** Same TS API (iOS/Android/Electron). Electron is best-effort while running.
### Success signals
- High delivery reliability, minute-precision on Android with permission.
- Prefetch budget hit rate at **T–lead**; zero stale deliveries beyond TTL.
---
## Strategic Plan
### Goal
Deliver 1..M daily notifications with **OS background prefetch at T–lead** and **rolling-window safety** so messages display with fresh content even when the app is closed.
### Tenets
- **Reliability first:** OS delivers once scheduled; no JS at delivery time.
- **Freshness with guardrails:** Prefetch at **T–lead**; enforce **TTL-at-fire**; ETag-aware.
- **Single system:** One TS API; native adapters swap under the hood.
- **Platform honesty:** Android exactness via permission; iOS best-effort budget.
### Architecture (high level)
App (Vue/TS) → Orchestrator (policy) → Native Adapters:
- **SchedulerNative** — AlarmManager (Android) / UNUserNotificationCenter (iOS)
- **BackgroundPrefetchNative** — WorkManager (Android) / BGTaskScheduler (+ silent push) (iOS)
- **DataStore** — SQLite
**Storage (single shared DB):** The app and the native plugin will use **the same SQLite database file**. The app owns schema/migrations; the plugin opens the same file with WAL enabled and performs short, serialized writes. This keeps one source of truth for payloads, delivery logs, and config.
### SQLite Ownership & Concurrency
* **One DB file:** The plugin opens the **same path** the app uses (no second DB).
* **Migrations owned by app:** The app executes schema migrations and bumps `PRAGMA user_version`. The plugin **never** migrates; it **asserts** the expected version.
* **WAL mode:** Open DB with `journal_mode=WAL`, `synchronous=NORMAL`, `busy_timeout=5000`, `foreign_keys=ON`. WAL allows foreground reads while a background job commits quickly.
* **Single-writer discipline:** Background jobs write in **short transactions** (UPSERT per slot), then return.
* **Encryption (optional):** If using SQLCipher, the **same key** is used by both app and plugin. Do not mix encrypted and unencrypted openings.
### Scheduling & T–lead
- **Arm** a rolling window (today + tomorrow within iOS cap).
- **Attempt** a single **online-first** fetch per slot at **T–lead = T − prefetchLeadMinutes**.
- If prefetch is skipped, the armed local **still fires** using cached content.
### Policies
- **TTL-at-fire:** If (T − fetchedAt) > `ttlSeconds` → **skip** arming.
- **Android exactness:** Request `SCHEDULE_EXACT_ALARM`; fallback **±10m** window.
- **Reboot/time change:** Android receivers re-arm next 24h; iOS on next wake/silent push.
- **No delivery-time mutation:** iOS locals cannot be mutated by NSE; render before scheduling.
---
## Implementation Guide
### 1) Interfaces (TS stable)
- **SchedulerNative**: `scheduleExact({slotId, whenMs, title, body, extra})`, `scheduleWindow(..., windowLenMs)`, `cancelBySlot`, `rescheduleAll`, `capabilities()`
- **BackgroundPrefetchNative**: `schedulePrefetch(slotId, atMs)`, `cancelPrefetch(slotId)`
- **DataStore**: SQLite adapters (notif_contents, notif_deliveries, notif_config)
- **Public API**: `configure`, `requestPermissions`, `runFullPipelineNow`, `reschedule`, `getState`
### DB Path & Adapter Configuration
* **Configure option:** `dbPath: string` (absolute path or platform alias) is passed from JS to the plugin during `configure()`.
* **Shared tables:**
* `notif_contents(slot_id, payload_json, fetched_at, etag, …)`
* `notif_deliveries(slot_id, fire_at, delivered_at, status, error_code, …)`
* `notif_config(k, v)`
* **Open settings:**
* `journal_mode=WAL`
* `synchronous=NORMAL`
* `busy_timeout=5000`
* `foreign_keys=ON`
**Type (TS) extension**
```ts
export type ConfigureOptions = {
// …existing fields…
dbPath: string; // shared DB file the plugin will open
storage: 'shared'; // canonical value; plugin-owned DB is not used
};
```
**Plugin side (pseudo)**
```kotlin
// Android open
val db = SQLiteDatabase.openDatabase(dbPath, null, SQLiteDatabase.OPEN_READWRITE)
db.execSQL("PRAGMA journal_mode=WAL")
db.execSQL("PRAGMA synchronous=NORMAL")
db.execSQL("PRAGMA foreign_keys=ON")
db.execSQL("PRAGMA busy_timeout=5000")
// Verify schema version
val uv = rawQuery("PRAGMA user_version").use { it.moveToFirst(); it.getInt(0) }
require(uv >= MIN_EXPECTED_VERSION) { "Schema version too old" }
```
```swift
// iOS open (FMDB / SQLite3)
// Set WAL via PRAGMA after open; check user_version the same way.
```
### 2) Templating & Arming
- Render `title/body` **before** scheduling; pass via **SchedulerNative**.
- Route all arming through **SchedulerNative** to centralize Android exact/window semantics.
### 3) T–lead (single attempt)
**T–lead governs prefetch, not arming.** We **arm** one-shot locals as part of the rolling window so closed-app delivery is guaranteed. At **T–lead = T − prefetchLeadMinutes**, the **native background job** attempts **one** 12s ETag-aware fetch. If fresh content arrives and will not violate **TTL-at-fire**, we (re)arm the upcoming slot; if the OS skips the wake, the pre-armed local still fires with cached content.
- Compute T–lead = `whenMs - prefetchLeadMinutes*60_000`.
- `BackgroundPrefetchNative.schedulePrefetch(slotId, atMs=T–lead)`.
- On wake: **ETag** fetch (timeout **12s**), persist, optionally cancel & re-arm if within TTL.
- Never fetch at delivery time.
### 4) TTL-at-fire
**TTL-at-fire:** Before arming for time **T**, compute `T − fetchedAt`. If that exceeds `ttlSeconds`, **do not arm** (skip). This prevents posting stale notifications when the app has been closed for a long time.
`if (whenMs - fetchedAt) > ttlSeconds*1000 → skip`
### 5) Android specifics
- Request `SCHEDULE_EXACT_ALARM`; deep-link if denied; fallback to `setWindow(start,len)` (±10m).
- Receivers: `BOOT_COMPLETED`, `TIMEZONE_CHANGED`, `TIME_SET` → recompute & re-arm for next 24h and schedule T–lead prefetch.
### 6) iOS specifics
- `BGTaskScheduler` for T–lead prefetch (best-effort). Optional silent push nudge.
- Locals: `UNCalendarNotificationTrigger` (one-shots); no NSE mutation for locals.
### 7) Network & Timeouts
- Content fetch: **12s** timeout; single attempt at T–lead; ETag/304 respected.
- ACK/Error: **8s** timeout, fire-and-forget.
### 8) Electron
- Notifications while app is running; recommend **Start-on-Login**. No true background scheduling when fully closed.
### 9) Telemetry
- Record `scheduled|shown|error`; ACK deliveries (8s timeout); include slot/times/TZ/app version.
---
## Capability Matrix
| Capability | Android (Native) | iOS (Native) | Electron | Web |
|---|---|---|---|---|
| Multi-daily locals (closed app) | ✅ | ✅ | ✅ (app running) | — |
| Prefetch at T–lead (app closed) | ✅ WorkManager | ⚠️ BGTask (best-effort) | ✅ (app running) | — |
| Re-arm after reboot/time-change | ✅ Receivers | ⚠️ On next wake/silent push | ✅ Start-on-Login | — |
| Minute-precision alarms | ✅ with exact permission | ❌ not guaranteed | ✅ timer best-effort | — |
| Delivery-time mutation for locals | ❌ | ❌ | — | — |
| ETag/TTL enforcement | ✅ | ✅ | ✅ | — |
| Rolling-window safety | ✅ | ✅ | ✅ | — |
---
## Acceptance Criteria
### Core
- **Closed-app delivery:** Armed locals fire at T with last rendered content. No delivery-time network.
- **T–lead prefetch:** Single background attempt at **T–lead**; if skipped, delivery still occurs from cache.
- **TTL-at-fire:** No armed local violates TTL at T.
### Android
- **Exact permission path:** With `SCHEDULE_EXACT_ALARM` → within ±1m; else **±10m** window.
- **Reboot recovery:** After reboot, receivers re-arm next 24h and schedule T–lead prefetch.
- **TZ/DST change:** Recompute & re-arm; future slots align to new wall-clock.
### iOS
- **BGTask budget respected:** Prefetch often runs but may be skipped; delivery still occurs via rolling window.
- **Force-quit caveat:** No background execution after user terminate; delivery still occurs if pre-armed.
### Electron
- **Running-app rule:** Delivery only while app runs; with Start-on-Login, after reboot the orchestrator re-arms and subsequent slots deliver.
### Network
- Content fetch timeout **12s**; ACK/Error **8s**; no retries inside lead; ETag honored.
### Observability
- Log/telemetry for `scheduled|shown|error`; ACK payload includes slot, times, device TZ, app version.
### DB Sharing
* **Shared DB visibility:** A background prefetch writes `notif_contents`; the foreground UI **immediately** reads the same row.
* **WAL overlap:** With the app reading while the plugin commits, no user-visible blocking occurs.
* **Version safety:** If `user_version` is behind, the plugin emits an error and does not write (protects against partial installs).
---
## Web-Push Cleanup
Web-push functionality has been retired due to unreliability. All web-push related code paths and documentation sections should be removed or marked as deprecated. See `web-push-cleanup-guide.md` for detailed cleanup steps.
---
_This document consolidates the Native-First notification system strategy, implementation details, capabilities, and acceptance criteria into a single comprehensive reference._
-181
View File
@@ -1,181 +0,0 @@
# Seed Phrase Backup Reminder Implementation
## Overview
This implementation adds a modal dialog that reminds users to back up their seed phrase if they haven't done so yet. The reminder appears after specific user actions and includes a 24-hour cooldown to avoid being too intrusive.
## Features
- **Modal Dialog**: Uses the existing notification group modal system from `App.vue`
- **Smart Timing**: Only shows when `hasBackedUpSeed = false`
- **24-Hour Cooldown**: Uses localStorage to prevent showing more than once per day
- **Action-Based Triggers**: Shows after specific user actions
- **User Choice**: "Backup Identifier Seed" or "Remind me Later" options
## Implementation Details
### Core Utility (`src/utils/seedPhraseReminder.ts`)
The main utility provides:
- `shouldShowSeedReminder(hasBackedUpSeed)`: Checks if reminder should be shown
- `markSeedReminderShown()`: Updates localStorage timestamp
- `createSeedReminderNotification()`: Creates the modal configuration
- `showSeedPhraseReminder(hasBackedUpSeed, notifyFunction)`: Main function to show reminder
### Trigger Points
The reminder is shown after these user actions:
**Note**: The reminder is triggered by **claim creation** actions, not claim confirmations. This focuses on when users are actively creating new content rather than just confirming existing claims.
1. **Profile Saving** (`AccountViewView.vue`)
- After clicking "Save Profile" button
- Only when profile save is successful
2. **Claim Creation** (Multiple views)
- `ClaimAddRawView.vue`: After submitting raw claims
- `GiftedDialog.vue`: After creating gifts/claims
- `GiftedDetailsView.vue`: After recording gifts/claims
- `OfferDialog.vue`: After creating offers
3. **QR Code Views Exit**
- `ContactQRScanFullView.vue`: When exiting via back button
- `ContactQRScanShowView.vue`: When exiting via back button
### Modal Configuration
```typescript
{
group: "modal",
type: "confirm",
title: "Backup Your Identifier Seed?",
text: "It looks like you haven't backed up your identifier seed yet. It's important to back it up as soon as possible to secure your identity.",
yesText: "Backup Identifier Seed",
noText: "Remind me Later",
onYes: () => navigate to /seed-backup,
onNo: () => mark as shown for 24 hours,
onCancel: () => mark as shown for 24 hours
}
```
**Important**: The modal is configured with `timeout: -1` to ensure it stays open until the user explicitly interacts with one of the buttons. This prevents the dialog from closing automatically.
### Cooldown Mechanism
- **Storage Key**: `seedPhraseReminderLastShown`
- **Cooldown Period**: 24 hours (24 * 60 * 60 * 1000 milliseconds)
- **Implementation**: localStorage with timestamp comparison
- **Fallback**: Shows reminder if timestamp is invalid or missing
## User Experience
### When Reminder Appears
- User has not backed up their seed phrase (`hasBackedUpSeed = false`)
- At least 24 hours have passed since last reminder
- User performs one of the trigger actions
- **1-second delay** after the success message to allow users to see the confirmation
### User Options
1. **"Backup Identifier Seed"**: Navigates to `/seed-backup` page
2. **"Remind me Later"**: Dismisses and won't show again for 24 hours
3. **Cancel/Close**: Same behavior as "Remind me Later"
### Frequency Control
- **First Time**: Always shows if user hasn't backed up
- **Subsequent**: Only shows after 24-hour cooldown
- **Automatic Reset**: When user completes seed backup (`hasBackedUpSeed = true`)
## Technical Implementation
### Error Handling
- Graceful fallback if localStorage operations fail
- Logging of errors for debugging
- Non-blocking implementation (doesn't affect main functionality)
### Integration Points
- **Platform Service**: Uses `$accountSettings()` to check backup status
- **Notification System**: Integrates with existing `$notify` system
- **Router**: Uses `window.location.href` for navigation
### Performance Considerations
- Minimal localStorage operations
- No blocking operations
- Efficient timestamp comparisons
- **Timing Behavior**: 1-second delay before showing reminder to improve user experience flow
## Testing
### Manual Testing Scenarios
1. **First Time User**
- Create new account
- Perform trigger action (save profile, create claim, exit QR view)
- Verify reminder appears
2. **Repeat User (Within 24h)**
- Perform trigger action
- Verify reminder does NOT appear
3. **Repeat User (After 24h)**
- Wait 24+ hours
- Perform trigger action
- Verify reminder appears again
4. **User Who Has Backed Up**
- Complete seed backup
- Perform trigger action
- Verify reminder does NOT appear
5. **QR Code View Exit**
- Navigate to QR code view (full or show)
- Exit via back button
- Verify reminder appears (if conditions are met)
### Browser Testing
- Test localStorage functionality
- Verify timestamp handling
- Check navigation to seed backup page
## Future Enhancements
### Potential Improvements
1. **Customizable Cooldown**: Allow users to set reminder frequency
2. **Progressive Urgency**: Increase reminder frequency over time
3. **Analytics**: Track reminder effectiveness and user response
4. **A/B Testing**: Test different reminder messages and timing
### Configuration Options
- Reminder frequency settings
- Custom reminder messages
- Different trigger conditions
- Integration with other notification systems
## Maintenance
### Monitoring
- Check localStorage usage in browser dev tools
- Monitor user feedback about reminder frequency
- Track navigation success to seed backup page
### Updates
- Modify reminder text in `createSeedReminderNotification()`
- Adjust cooldown period in `REMINDER_COOLDOWN_MS` constant
- Add new trigger points as needed
## Conclusion
This implementation provides a non-intrusive way to remind users about seed phrase backup while respecting their preferences and avoiding notification fatigue. The 24-hour cooldown ensures users aren't overwhelmed while maintaining the importance of the security reminder.
The feature is fully integrated with the existing codebase architecture and follows established patterns for notifications, error handling, and user interaction.
@@ -1,528 +0,0 @@
# Shared Image Plugin Implementation Plan
**Date:** 2025-12-03 15:40:38 PST
**Status:** Planning
**Goal:** Replace temp file approach with native Capacitor plugins for iOS and Android
## Minimum OS Version Compatibility Analysis
### Current Project Configuration:
- **iOS Deployment Target**: 13.0 (Podfile and Xcode project)
- **Android minSdkVersion**: 23 (API 23 - Android 6.0 Marshmallow) ✅ **Upgraded**
- **Capacitor Version**: 6.2.0
### Capacitor 6 Requirements:
- **iOS**: Requires iOS 13.0+ ✅ **Compatible** (current: 13.0)
- **Android**: Requires API 23+ ✅ **Compatible** (current: API 23)
### Plugin API Compatibility:
#### iOS Plugin APIs:
- ✅ `CAPPlugin` base class: Available in iOS 13.0+ (Capacitor requirement)
- ✅ `CAPPluginCall`: Available in iOS 13.0+ (Capacitor requirement)
- ✅ `UserDefaults(suiteName:)`: Available since iOS 8.0 (well below iOS 13.0)
- ✅ `@objc` annotations: Available since iOS 8.0
- ✅ Swift 5.0: Compatible with iOS 13.0+
**Conclusion**: iOS 13.0 is fully compatible with the plugin implementation. **No iOS version update required.**
#### Android Plugin APIs:
- ✅ `Plugin` base class: Available in API 21+ (Capacitor requirement)
- ✅ `PluginCall`: Available in API 21+ (Capacitor requirement)
- ✅ `SharedPreferences`: Available since API 1 (works on all Android versions)
- ✅ `@CapacitorPlugin` annotation: Available in API 21+ (Capacitor requirement)
- ✅ `@PluginMethod` annotation: Available in API 21+ (Capacitor requirement)
**Conclusion**: Android API 23 is fully compatible with the plugin implementation and officially meets Capacitor 6 requirements. ✅ **No Android version concerns.**
### Share Extension Compatibility:
- **iOS Share Extension**: Uses same deployment target as main app (iOS 13.0)
- **App Group**: Available since iOS 8.0, fully compatible
- No additional version requirements for share extension functionality
## Overview
This document outlines the migration from the current temp file approach to implementing dedicated Capacitor plugins for handling shared images. This will eliminate file I/O, polling, and timing issues, providing a more direct and reliable native-to-JS bridge.
## Current Implementation Issues
### Temp File Approach Problems:
1. **Timing Issues**: Requires polling with exponential backoff to wait for file creation
2. **Race Conditions**: File may not exist when JS checks, or may be read multiple times
3. **File Management**: Need to delete temp files after reading to prevent re-processing
4. **Platform Differences**: Different directories (Documents vs Data) add complexity
5. **Error Handling**: File I/O errors can be hard to debug
6. **Performance**: File system operations are slower than direct native calls
## Proposed Solution: Capacitor Plugins
### Benefits:
- ✅ Direct native-to-JS communication (no file I/O)
- ✅ Synchronous/async method calls (no polling needed)
- ✅ Type-safe TypeScript interfaces
- ✅ Better error handling and debugging
- ✅ Lower latency
- ✅ More maintainable and follows Capacitor best practices
## Implementation Layout
### 1. iOS Plugin Implementation
#### 1.1 Create iOS Plugin File
**Location:** `ios/App/App/SharedImagePlugin.swift`
**Structure:**
```swift
import Foundation
import Capacitor
@objc(SharedImagePlugin)
public class SharedImagePlugin: CAPPlugin {
private let appGroupIdentifier = "group.app.timesafari.share"
@objc func getSharedImage(_ call: CAPPluginCall) {
// Read from App Group UserDefaults
// Return base64 and fileName
// Clear data after reading
}
@objc func hasSharedImage(_ call: CAPPluginCall) {
// Check if shared image exists without reading it
// Useful for quick checks
}
}
```
**Key Points:**
- Use existing `getSharedImageData()` logic from AppDelegate
- Return data as JSObject with `base64` and `fileName` keys
- Clear UserDefaults after reading to prevent re-reading
- Handle errors gracefully with `call.reject()`
- **Version Compatibility**: Works with iOS 13.0+ (current deployment target)
#### 1.2 Register Plugin in iOS
**Location:** `ios/App/App/AppDelegate.swift`
**Changes:**
- Remove `writeSharedImageToTempFile()` method
- Remove temp file writing from `application(_:open:options:)`
- Remove temp file writing from `checkForSharedImageOnActivation()`
- Keep `getSharedImageData()` method (or move to plugin)
- Plugin auto-registers via Capacitor's plugin system
**Note:** Capacitor plugins are auto-discovered if they follow naming conventions and are in the app bundle.
### 2. Android Plugin Implementation
#### 2.1 Create Android Plugin File
**Location:** `android/app/src/main/java/app/timesafari/sharedimage/SharedImagePlugin.java`
**Structure:**
```java
package app.timesafari.sharedimage;
import com.getcapacitor.JSObject;
import com.getcapacitor.Plugin;
import com.getcapacitor.PluginCall;
import com.getcapacitor.PluginMethod;
import com.getcapacitor.annotation.CapacitorPlugin;
@CapacitorPlugin(name = "SharedImage")
public class SharedImagePlugin extends Plugin {
@PluginMethod
public void getSharedImage(PluginCall call) {
// Read from SharedPreferences or Intent extras
// Return base64 and fileName
// Clear data after reading
}
@PluginMethod
public void hasSharedImage(PluginCall call) {
// Check if shared image exists without reading it
}
}
```
**Key Points:**
- Use SharedPreferences to store shared image data between share intent and plugin call
- Store base64 and fileName when processing share intent
- Read and clear in `getSharedImage()` method
- Handle Intent extras if app was just launched
- **Version Compatibility**: Works with Android API 22+ (current minSdkVersion)
#### 2.2 Update MainActivity
**Location:** `android/app/src/main/java/app/timesafari/MainActivity.java`
**Changes:**
- Remove `writeSharedImageToTempFile()` method
- Remove `TEMP_FILE_NAME` constant
- Update `processSharedImage()` to store in SharedPreferences instead of file
- Register plugin: `registerPlugin(SharedImagePlugin.class);`
- Store shared image data in SharedPreferences when processing share intent
**SharedPreferences Approach:**
```java
// In processSharedImage():
SharedPreferences prefs = getSharedPreferences("shared_image", MODE_PRIVATE);
SharedPreferences.Editor editor = prefs.edit();
editor.putString("base64", base64String);
editor.putString("fileName", actualFileName);
editor.putBoolean("hasSharedImage", true);
editor.apply();
```
### 3. TypeScript/JavaScript Integration
#### 3.1 Create TypeScript Plugin Definition
**Location:** `src/plugins/SharedImagePlugin.ts` (new file)
**Structure:**
```typescript
import { registerPlugin } from '@capacitor/core';
export interface SharedImageResult {
base64: string;
fileName: string;
}
export interface SharedImagePlugin {
getSharedImage(): Promise<SharedImageResult | null>;
hasSharedImage(): Promise<{ hasImage: boolean }>;
}
const SharedImage = registerPlugin<SharedImagePlugin>('SharedImage', {
web: () => import('./SharedImagePlugin.web').then(m => new m.SharedImagePluginWeb()),
});
export * from './definitions';
export { SharedImage };
```
#### 3.2 Create Web Implementation (for development)
**Location:** `src/plugins/SharedImagePlugin.web.ts` (new file)
**Structure:**
```typescript
import { WebPlugin } from '@capacitor/core';
import type { SharedImagePlugin, SharedImageResult } from './definitions';
export class SharedImagePluginWeb extends WebPlugin implements SharedImagePlugin {
async getSharedImage(): Promise<SharedImageResult | null> {
// Return null for web platform
return null;
}
async hasSharedImage(): Promise<{ hasImage: boolean }> {
return { hasImage: false };
}
}
```
#### 3.3 Create Type Definitions
**Location:** `src/plugins/definitions.ts` (new file)
**Structure:**
```typescript
export interface SharedImageResult {
base64: string;
fileName: string;
}
export interface SharedImagePlugin {
getSharedImage(): Promise<SharedImageResult | null>;
hasSharedImage(): Promise<{ hasImage: boolean }>;
}
```
#### 3.4 Update main.capacitor.ts
**Location:** `src/main.capacitor.ts`
**Changes:**
- Remove `pollForFileExistence()` function
- Remove temp file reading logic from `checkAndStoreNativeSharedImage()`
- Replace with direct plugin call:
```typescript
async function checkAndStoreNativeSharedImage(): Promise<{
success: boolean;
fileName?: string;
}> {
if (isProcessingSharedImage) {
logger.debug("[Main] ⏸️ Shared image processing already in progress, skipping");
return { success: false };
}
isProcessingSharedImage = true;
try {
if (!Capacitor.isNativePlatform() ||
(Capacitor.getPlatform() !== "ios" && Capacitor.getPlatform() !== "android")) {
isProcessingSharedImage = false;
return { success: false };
}
// Direct plugin call - no polling needed!
const { SharedImage } = await import('./plugins/SharedImagePlugin');
const result = await SharedImage.getSharedImage();
if (result && result.base64) {
await storeSharedImageInTempDB(result.base64, result.fileName);
isProcessingSharedImage = false;
return { success: true, fileName: result.fileName };
}
isProcessingSharedImage = false;
return { success: false };
} catch (error) {
logger.error("[Main] Error checking for native shared image:", error);
isProcessingSharedImage = false;
return { success: false };
}
}
```
**Remove:**
- `pollForFileExistence()` function (lines 71-98)
- All Filesystem plugin imports related to temp file reading
- Temp file path constants and directory logic
### 4. Data Flow Comparison
#### Current (Temp File) Flow:
```
Share Extension/Intent
↓
Native writes temp file
↓
JS polls for file existence (with retries)
↓
JS reads file via Filesystem plugin
↓
JS parses JSON
↓
JS deletes temp file
↓
JS stores in temp DB
```
#### New (Plugin) Flow:
```
Share Extension/Intent
↓
Native stores in UserDefaults/SharedPreferences
↓
JS calls plugin.getSharedImage()
↓
Native reads and clears data
↓
Native returns data directly
↓
JS stores in temp DB
```
## File Changes Summary
### New Files to Create:
1. `ios/App/App/SharedImagePlugin.swift` - iOS plugin implementation
2. `android/app/src/main/java/app/timesafari/sharedimage/SharedImagePlugin.java` - Android plugin
3. `src/plugins/SharedImagePlugin.ts` - TypeScript plugin registration
4. `src/plugins/SharedImagePlugin.web.ts` - Web fallback implementation
5. `src/plugins/definitions.ts` - TypeScript type definitions
### Files to Modify:
1. `ios/App/App/AppDelegate.swift` - Remove temp file writing
2. `android/app/src/main/java/app/timesafari/MainActivity.java` - Remove temp file writing, add SharedPreferences
3. `src/main.capacitor.ts` - Replace temp file logic with plugin calls
### Files to Remove:
- No files need to be deleted, but code will be removed from existing files
## Implementation Considerations
### 1. Data Storage Strategy
#### iOS:
- **Current**: App Group UserDefaults (already working)
- **Plugin**: Read from same UserDefaults, no changes needed
- **Clearing**: Clear immediately after reading in plugin method
#### Android:
- **Current**: Temp file in app's internal files directory
- **New**: SharedPreferences (persistent key-value store)
- **Alternative**: Could use Intent extras if app is launched fresh, but SharedPreferences is more reliable for backgrounded apps
### 2. Timing and Lifecycle
#### When to Check for Shared Images:
1. **App Launch**: Check in `checkForSharedImageAndNavigate()` (already exists)
2. **App Becomes Active**: Check in `appStateChange` listener (already exists)
3. **Deep Link**: Check in `handleDeepLink()` for empty path URLs (already exists)
#### Plugin Call Timing:
- Plugin calls are synchronous from JS perspective
- No polling needed - native side handles data availability
- If no data exists, plugin returns `null` immediately
### 3. Error Handling
#### Plugin Error Scenarios:
- **No shared image**: Return `null` (not an error)
- **Data corruption**: Return error via `call.reject()`
- **Missing permissions**: Return error (shouldn't happen with App Group/SharedPreferences)
#### JS Error Handling:
- Wrap plugin calls in try-catch
- Log errors appropriately
- Don't crash app if plugin fails
### 4. Backward Compatibility
#### Migration Path:
- Keep temp file code temporarily (commented out) for rollback
- Test thoroughly on both platforms
- Remove temp file code after verification
### 5. Testing Considerations
#### Test Cases:
1. **Share from Photos app** → Verify image appears in app
2. **Share while app is backgrounded** → Verify image appears when app becomes active
3. **Share while app is closed** → Verify image appears on app launch
4. **Multiple rapid shares** → Verify only latest image is processed
5. **Share then close app before processing** → Verify image persists
6. **Share then clear app data** → Verify graceful handling
#### Edge Cases:
- Very large images (memory concerns)
- Multiple images shared simultaneously
- App killed by OS before processing
- Network interruptions during processing
### 6. Performance Considerations
#### Benefits:
- **Latency**: Direct calls vs file I/O (faster)
- **CPU**: No polling overhead
- **Memory**: No temp file storage
- **Battery**: Less file system activity
#### Potential Issues:
- Large base64 strings in memory (same as current approach)
- UserDefaults/SharedPreferences size limits (shouldn't be an issue for single image)
### 7. Type Safety
#### TypeScript Benefits:
- Full type checking for plugin methods
- Autocomplete in IDE
- Compile-time error checking
- Better developer experience
### 8. Plugin Registration
#### iOS:
- Capacitor auto-discovers plugins via naming convention
- Ensure plugin is in app target (not extension target)
- No manual registration needed in AppDelegate
#### Android:
- Register in `MainActivity.onCreate()`:
```java
registerPlugin(SharedImagePlugin.class);
```
### 9. Capacitor Version Compatibility
#### Check Current Version:
- Verify Capacitor version supports custom plugins
- Ensure plugin API hasn't changed
- Test with current Capacitor version first
### 10. Build and Deployment
#### Build Steps:
1. Create plugin files
2. Register Android plugin in MainActivity
3. Update TypeScript code
4. Test on iOS simulator
5. Test on Android emulator
6. Test on physical devices
7. Remove temp file code
8. Update documentation
#### Deployment:
- No changes to build scripts needed
- No changes to CI/CD needed
- No changes to app configuration needed
## Migration Steps
### Phase 1: Create Plugins (Non-Breaking)
1. Create iOS plugin file
2. Create Android plugin file
3. Create TypeScript definitions
4. Register Android plugin
5. Test plugins independently (don't use in main code yet)
### Phase 2: Update JS Integration (Breaking)
1. Create TypeScript plugin wrapper
2. Update `checkAndStoreNativeSharedImage()` to use plugin
3. Remove temp file reading logic
4. Test on both platforms
### Phase 3: Cleanup Native Code (Breaking)
1. Remove temp file writing from iOS AppDelegate
2. Remove temp file writing from Android MainActivity
3. Update to use SharedPreferences on Android
4. Test thoroughly
### Phase 4: Final Cleanup
1. Remove `pollForFileExistence()` function
2. Remove Filesystem imports related to temp files
3. Update comments and documentation
4. Final testing
## Rollback Plan
If issues arise:
1. Revert JS changes to use temp file approach
2. Re-enable temp file writing in native code
3. Keep plugins for future migration attempt
4. Document issues encountered
## Success Criteria
✅ Plugin methods work on both iOS and Android
✅ No polling or file I/O needed
✅ Shared images appear correctly in app
✅ No memory leaks or performance issues
✅ Error handling works correctly
✅ All test cases pass
✅ Code is cleaner and more maintainable
## Additional Notes
### iOS App Group:
- Current App Group ID: `group.app.timesafari.share`
- Ensure plugin has access to same App Group
- Share Extension already writes to this App Group
### Android Share Intent:
- Current implementation handles `ACTION_SEND` and `ACTION_SEND_MULTIPLE`
- SharedPreferences key: `shared_image` (or similar)
- Store both base64 and fileName
### Future Enhancements:
- Consider adding event listeners for real-time notifications
- Could add method to clear shared image without reading
- Could add method to get image metadata without full data
## References
- [Capacitor Plugin Development Guide](https://capacitorjs.com/docs/plugins)
- Existing plugin example: `SafeAreaPlugin.java`
- Current temp file implementation: `main.capacitor.ts` lines 166-271
- iOS AppDelegate: `ios/App/App/AppDelegate.swift`
- Android MainActivity: `android/app/src/main/java/app/timesafari/MainActivity.java`
@@ -1,329 +0,0 @@
# Shared Image Plugin - Pre-Implementation Decision Checklist
**Date:** 2025-12-03
**Status:** Pre-Implementation Planning
**Purpose:** Identify and document decisions needed before implementing SharedImagePlugin
## ✅ Completed Decisions
### 1. Minimum OS Versions
- ✅ **iOS**: Keep at 13.0 (no changes needed)
- ✅ **Android**: Upgraded from API 22 to API 23 (completed)
- ✅ **Rationale**: Meets Capacitor 6 requirements, minimal device impact
### 2. Data Storage Strategy
- ✅ **iOS**: Use App Group UserDefaults (already implemented in Share Extension)
- ✅ **Android**: Use SharedPreferences (to be implemented)
- ✅ **Rationale**: Direct, efficient, no file I/O needed
## 🔍 Decisions Needed Before Implementation
### 1. Plugin Method Design
#### Decision: What methods should the plugin expose?
**Options:**
- **Option A (Minimal)**: Only `getSharedImage()` - read and clear in one call
- **Option B (Recommended)**: `getSharedImage()` + `hasSharedImage()` - allows checking without reading
- **Option C (Extended)**: Add `clearSharedImage()` - explicit clearing without reading
**Recommendation:** **Option B**
- `getSharedImage()`: Returns `{ base64: string, fileName: string } | null`
- `hasSharedImage()`: Returns `{ hasImage: boolean }` - useful for quick checks
**Decision Needed:** ✅ Confirm Option B or choose alternative
---
### 2. Error Handling Strategy
#### Decision: How should the plugin handle errors?
**Options:**
- **Option A**: Return `null` for all errors (no shared image = no error)
- **Option B**: Use `call.reject()` for actual errors, return `null` only when no image exists
- **Option C**: Return error object in result: `{ error: string } | { base64: string, fileName: string }`
**Recommendation:** **Option B**
- `getSharedImage()` returns `null` when no image exists (normal case)
- `call.reject()` for actual errors (UserDefaults unavailable, data corruption, etc.)
- Clear distinction between "no data" (normal) vs "error" (exceptional)
**Decision Needed:** ✅ Confirm Option B or choose alternative
---
### 3. Data Clearing Strategy
#### Decision: When should shared image data be cleared?
**Current Behavior (temp file approach):**
- Data cleared after reading (immediate)
**Options:**
- **Option A**: Clear immediately after reading (current behavior)
- **Option B**: Clear on next read (allow re-reading until consumed)
- **Option C**: Clear after successful storage in temp DB (JS confirms receipt)
**Recommendation:** **Option A** (immediate clearing)
- Prevents accidental re-reading
- Simpler implementation
- Matches current behavior
- If JS fails to store, user can share again
**Decision Needed:** ✅ Confirm Option A or choose alternative
---
### 4. iOS Plugin Registration
#### Decision: How should the iOS plugin be registered?
**Options:**
- **Option A**: Auto-discovery (Capacitor finds plugins by naming convention)
- **Option B**: Manual registration in AppDelegate
- **Option C**: Hybrid (auto-discovery with manual registration as fallback)
**Recommendation:** **Option A** (auto-discovery)
- Follows Capacitor best practices
- Less code to maintain
- Other plugins in project use auto-discovery (SafeAreaPlugin uses manual, but that's older pattern)
**Note:** Need to verify plugin naming convention:
- Class name: `SharedImagePlugin`
- File name: `SharedImagePlugin.swift`
- Location: `ios/App/App/SharedImagePlugin.swift`
**Decision Needed:** ✅ Confirm Option A, or if auto-discovery doesn't work, use Option B
---
### 5. TypeScript Interface Design
#### Decision: What should the TypeScript interface look like?
**Proposed Interface:**
```typescript
export interface SharedImageResult {
base64: string;
fileName: string;
}
export interface SharedImagePlugin {
getSharedImage(): Promise<SharedImageResult | null>;
hasSharedImage(): Promise<{ hasImage: boolean }>;
}
```
**Questions:**
- Should `fileName` be optional? (Currently always provided, but could be empty string)
- Should we include metadata (image size, MIME type)?
- Should `hasSharedImage()` return more info (like fileName without reading)?
**Recommendation:** Keep simple for now:
- `fileName` is always a string (may be default "shared-image.jpg")
- No metadata initially (can add later if needed)
- `hasSharedImage()` only returns boolean (keep it lightweight)
**Decision Needed:** ✅ Confirm interface design or request changes
---
### 6. Android Data Storage Timing
#### Decision: When should Android store shared image data in SharedPreferences?
**Current Flow:**
1. Share intent received in MainActivity
2. Image processed and written to temp file
3. JS reads temp file
**New Flow Options:**
- **Option A**: Store in SharedPreferences immediately when share intent received (in `processSharedImage()`)
- **Option B**: Store when plugin is first called (lazy loading)
- **Option C**: Store in both places during transition (backward compatibility)
**Recommendation:** **Option A** (immediate storage)
- Data available immediately when plugin is called
- No timing issues
- Matches iOS pattern (data stored by Share Extension)
**Implementation:**
- Update `processSharedImage()` in MainActivity to store in SharedPreferences
- Remove temp file writing
- Plugin reads from SharedPreferences
**Decision Needed:** ✅ Confirm Option A
---
### 7. Migration Strategy
#### Decision: How to handle the transition from temp file to plugin?
**Options:**
- **Option A (Clean Break)**: Remove temp file code immediately, use plugin only
- **Option B (Gradual)**: Support both approaches temporarily, remove temp file later
- **Option C (Feature Flag)**: Use feature flag to switch between approaches
**Recommendation:** **Option A** (clean break)
- Simpler implementation
- Less code to maintain
- Temp file approach is buggy anyway (why we're replacing it)
- Can rollback via git if needed
**Rollback Plan:**
- Keep temp file code in git history
- If plugin has issues, can revert commit
- Test thoroughly before removing temp file code
**Decision Needed:** ✅ Confirm Option A
---
### 8. Plugin Naming
#### Decision: What should the plugin be named?
**Options:**
- **Option A**: `SharedImage` (matches file/class names)
- **Option B**: `SharedImagePlugin` (more explicit)
- **Option C**: `NativeShare` (more generic, could handle other share types)
**Recommendation:** **Option A** (`SharedImage`)
- Matches Capacitor naming conventions (plugins are referenced without "Plugin" suffix)
- Examples: `Capacitor.Plugins.Camera`, `Capacitor.Plugins.Filesystem`
- TypeScript: `SharedImage.getSharedImage()`
**Decision Needed:** ✅ Confirm Option A
---
### 9. iOS: Reuse getSharedImageData() or Move to Plugin?
#### Decision: Should the plugin reuse AppDelegate's `getSharedImageData()` or implement its own?
**Current Code:**
- `AppDelegate.getSharedImageData()` exists and works
- Reads from App Group UserDefaults
- Clears data after reading
**Options:**
- **Option A**: Plugin calls `getSharedImageData()` from AppDelegate
- **Option B**: Plugin implements its own logic (duplicate code)
- **Option C**: Move `getSharedImageData()` to a shared utility, both use it
**Recommendation:** **Option C** (shared utility)
- DRY principle
- Single source of truth
- But: May be overkill for simple logic
**Alternative Recommendation:** **Option B** (plugin implements own logic)
- Plugin is self-contained
- No dependency on AppDelegate
- Logic is simple (just UserDefaults read/clear)
- Can remove `getSharedImageData()` from AppDelegate after migration
**Decision:** ✅ **Option C** (shared utility) - **CONFIRMED**
- Create shared utility for reading from App Group UserDefaults
- Both AppDelegate and plugin use the shared utility
- Single source of truth for shared image data access
---
### 10. Android: SharedPreferences Key Names
#### Decision: What keys should be used in SharedPreferences?
**Proposed Keys:**
- `shared_image_base64` - Base64 string
- `shared_image_file_name` - File name
- `shared_image_ready` - Boolean flag (optional, for quick checks)
**Alternative:**
- Use a single JSON object: `shared_image_data` = `{ base64: "...", fileName: "..." }`
**Recommendation:** Separate keys (first option)
- Simpler to read/write
- No JSON parsing needed
- Matches iOS pattern (separate UserDefaults keys)
- Flag is optional but useful for `hasSharedImage()`
**Decision Needed:** ✅ Confirm key naming or request changes
---
### 11. Testing Strategy
#### Decision: What testing approach should we use?
**Options:**
- **Option A**: Manual testing only
- **Option B**: Manual + automated unit tests for plugin methods
- **Option C**: Manual + integration tests
**Recommendation:** **Option A** (manual testing) for now
- Plugins are hard to unit test (require native environment)
- Manual testing is sufficient for initial implementation
- Can add automated tests later if needed
**Test Scenarios:**
1. Share image from Photos app → Verify appears in app
2. Share while app backgrounded → Verify appears when app becomes active
3. Share while app closed → Verify appears on app launch
4. Multiple rapid shares → Verify only latest is processed
5. Share then close app before processing → Verify data persists
6. Share then clear app data → Verify graceful handling
**Decision Needed:** ✅ Confirm testing approach
---
### 12. Documentation Updates
#### Decision: What documentation needs updating?
**Files to Update:**
- ✅ Implementation plan (this document)
- ⚠️ `doc/native-share-target-implementation.md` - Update to reflect plugin approach
- ⚠️ `doc/ios-share-implementation-status.md` - Mark plugin as implemented
- ⚠️ Code comments in `main.capacitor.ts` - Update to reflect plugin usage
**Decision Needed:** ✅ Confirm documentation update list
---
## Summary of Decisions Needed
| # | Decision | Recommendation | Status |
|---|----------|----------------|--------|
| 1 | Plugin Methods | Option B: `getSharedImage()` + `hasSharedImage()` | ✅ Confirmed |
| 2 | Error Handling | Option B: `null` for no data, `reject()` for errors | ✅ Confirmed |
| 3 | Data Clearing | Option A: Clear immediately after reading | ✅ Confirmed |
| 4 | iOS Registration | Option A: Auto-discovery | ✅ Confirmed |
| 5 | TypeScript Interface | Proposed interface (see above) | ✅ Confirmed |
| 6 | Android Storage Timing | Option A: Store immediately on share intent | ✅ Confirmed |
| 7 | Migration Strategy | Option A: Clean break, remove temp file code | ✅ Confirmed |
| 8 | Plugin Naming | Option A: `SharedImage` | ✅ Confirmed |
| 9 | iOS Code Reuse | Option C: Shared utility | ✅ Confirmed |
| 10 | Android Key Names | Separate keys: `shared_image_base64`, `shared_image_file_name` | ✅ Confirmed |
| 11 | Testing Strategy | Option A: Manual testing | ✅ Confirmed |
| 12 | Documentation | Update listed files | ✅ Confirmed |
| - | Multiple Images | Single image only (SharedPhotoView requirement) | ✅ Confirmed |
| - | Backward Compatibility | No temp file backward compatibility | ✅ Confirmed |
## Next Steps
1. **Review this checklist** and confirm or modify recommendations
2. **Make decisions** on all pending items
3. **Update implementation plan** with confirmed decisions
4. **Begin implementation** with clear specifications
## Questions to Consider
- Are there any edge cases not covered?
- Should we support multiple images (currently only first image)?
- Should we add image metadata (size, MIME type) in the future?
- Do we need backward compatibility with temp file approach?
- Should plugin methods be synchronous or async? (Capacitor plugins are async by default)
+551
View File
@@ -0,0 +1,551 @@
# TimeSafari Web-Push Cleanup Guide
**Status:** 🚀 Native-First Implementation
**Date:** 2025-01-27T14:30Z (UTC)
**Author:** Matthew Raymer
**Scope:** Web-push code cleanup and deprecation
**Goal:** Remove or quarantine all web-push code paths and mark as deprecated.
---
## Executive Summary
This document provides a comprehensive cleanup guide for removing web-push code
paths from TimeSafari. Web-push has been retired for unreliability, and the
system now focuses on native mobile reliability with Electron best-effort support.
---
## Cleanup Strategy
### Phase 1: Identify Web-Push Code Paths
#### Service Worker Files
- [ ] `sw_scripts/notification-click.js` - Mark as deprecated
- [ ] `sw_scripts/` directory - Review for web-push dependencies
- [ ] Service worker registration code - Remove or quarantine
#### Web-Specific Code
- [ ] Web push notification handlers
- [ ] Service worker event listeners
- [ ] Web notification API usage
- [ ] Push subscription management
#### Configuration Files
- [ ] VitePWA plugin configuration
- [ ] Service worker build configuration
- [ ] Web push manifest files
### Phase 2: Mark as Deprecated
#### Code Comments
```javascript
// DEPRECATED: Web-push notification handling
// This code is kept for reference but not used in production
// Replaced by Native-First notification system
```
#### Documentation Updates
- [ ] Mark web-push sections as deprecated
- [ ] Add deprecation notices
- [ ] Update README files
- [ ] Update API documentation
### Phase 3: Remove or Quarantine
#### Complete Removal
- [ ] Web push subscription code
- [ ] Service worker notification handlers
- [ ] Web-specific notification APIs
- [ ] Push message handling
#### Quarantine (Keep for Reference)
- [ ] Service worker registration code
- [ ] Web push configuration
- [ ] Historical web-push tests
---
## Detailed Cleanup Tasks
### 1. Service Worker Cleanup
#### Files to Deprecate
**`sw_scripts/notification-click.js`**
```javascript
// DEPRECATED: Service worker notification handling
// This code is kept for reference but not used in production
// Replaced by Native-First notification system
// Original web-push notification click handler
self.addEventListener('notificationclick', (event) => {
// DEPRECATED: Web-push only
event.notification.close();
const slotId = event.notification.data?.slotId;
const route = slotId ? '/#/daily' : '/#/notifications';
event.waitUntil(
clients.openWindow(route).catch(() => {
return clients.openWindow('/');
})
);
});
```
**Service Worker Registration**
```javascript
// DEPRECATED: Service worker registration
// This code is kept for reference but not used in production
// Replaced by Native-First notification system
if ('serviceWorker' in navigator && process.env.VITE_PLATFORM === 'web') {
// DEPRECATED: Web-push only
navigator.serviceWorker.register('/sw.js')
.then(registration => {
console.log('Service Worker registered:', registration);
})
.catch(error => {
console.error('Service Worker registration failed:', error);
});
}
```
### 2. Web Push API Cleanup
#### Push Subscription Management
```javascript
// DEPRECATED: Web push subscription management
// This code is kept for reference but not used in production
// Replaced by Native-First notification system
class WebPushManager {
// DEPRECATED: Web-push only
async subscribeToPush() {
// Implementation kept for reference
}
// DEPRECATED: Web-push only
async unsubscribeFromPush() {
// Implementation kept for reference
}
}
```
#### Push Message Handling
```javascript
// DEPRECATED: Push message handling
// This code is kept for reference but not used in production
// Replaced by Native-First notification system
self.addEventListener('push', (event) => {
// DEPRECATED: Web-push only
const data = event.data ? event.data.json() : {};
const options = {
body: data.body,
icon: '/icon-192x192.png',
badge: '/badge-72x72.png',
data: data
};
event.waitUntil(
self.registration.showNotification(data.title, options)
);
});
```
### 3. Configuration Cleanup
#### VitePWA Plugin Configuration
```javascript
// DEPRECATED: VitePWA plugin configuration
// This configuration is kept for reference but not used in production
// Replaced by Native-First notification system
import { VitePWA } from 'vite-plugin-pwa'
export default defineConfig({
plugins: [
VitePWA({
// DEPRECATED: Web-push only
registerType: 'autoUpdate',
workbox: {
globPatterns: ['**/*.{js,css,html,ico,png,svg}']
},
includeAssets: ['favicon.ico', 'apple-touch-icon.png', 'masked-icon.svg'],
manifest: {
name: 'TimeSafari',
short_name: 'TimeSafari',
description: 'TimeSafari App',
theme_color: '#ffffff',
icons: [
{
src: 'pwa-192x192.png',
sizes: '192x192',
type: 'image/png'
}
]
}
})
]
})
```
#### Service Worker Build Configuration
```javascript
// DEPRECATED: Service worker build configuration
// This configuration is kept for reference but not used in production
// Replaced by Native-First notification system
export default defineConfig({
build: {
rollupOptions: {
input: {
// DEPRECATED: Web-push only
sw: 'sw_scripts/notification-click.js'
}
}
}
})
```
### 4. Test Cleanup
#### Web Push Tests
```javascript
// DEPRECATED: Web push tests
// These tests are kept for reference but not used in production
// Replaced by Native-First notification system
describe('Web Push Notifications (DEPRECATED)', () => {
// DEPRECATED: Web-push only
it('should handle push notifications', async () => {
// Test implementation kept for reference
});
// DEPRECATED: Web-push only
it('should handle notification clicks', async () => {
// Test implementation kept for reference
});
});
```
#### Service Worker Tests
```javascript
// DEPRECATED: Service worker tests
// These tests are kept for reference but not used in production
// Replaced by Native-First notification system
describe('Service Worker (DEPRECATED)', () => {
// DEPRECATED: Web-push only
it('should register service worker', async () => {
// Test implementation kept for reference
});
// DEPRECATED: Web-push only
it('should handle push events', async () => {
// Test implementation kept for reference
});
});
```
### 5. Documentation Cleanup
#### README Updates
```markdown
# TimeSafari Native-First Notification System
## Web-Push Status: DEPRECATED
Web-push has been retired for unreliability. The system now focuses on native mobile reliability with Electron best-effort support.
### Deprecated Features
- ❌ Web push notifications
- ❌ Service worker notification handling
- ❌ Web notification API
### Active Features
- ✅ Native mobile notifications (Android/iOS)
- ✅ Electron notifications (best-effort)
- ✅ OS-scheduled background prefetch
- ✅ Rolling window safety
```
#### API Documentation Updates
```markdown
## Notification API (Native-First)
### Deprecated Methods
- `subscribeToPush()` - DEPRECATED: Web-push only
- `unsubscribeFromPush()` - DEPRECATED: Web-push only
- `handlePushMessage()` - DEPRECATED: Web-push only
### Active Methods
- `scheduleExact()` - Native exact scheduling
- `scheduleWindow()` - Native windowed scheduling
- `schedulePrefetch()` - Native background prefetch
```
---
## File-by-File Cleanup Checklist
### Service Worker Files
- [ ] `sw_scripts/notification-click.js` - Mark as deprecated
- [ ] `sw_scripts/` directory - Review for web-push dependencies
- [ ] Service worker build configuration - Remove or quarantine
### Web-Specific Code
- [ ] `src/main.web.ts` - Remove service worker registration
- [ ] `src/services/webPush.ts` - Mark as deprecated
- [ ] `src/utils/serviceWorker.ts` - Mark as deprecated
- [ ] Web notification API usage - Remove or quarantine
### Configuration Files
- [ ] `vite.config.web.mts` - Remove VitePWA plugin
- [ ] `package.json` - Remove web-push dependencies
- [ ] `public/manifest.json` - Mark as deprecated
- [ ] Service worker build scripts - Remove or quarantine
### Test Files
- [ ] `test-playwright/web-push.spec.ts` - Mark as deprecated
- [ ] `test/services/webPush.test.ts` - Mark as deprecated
- [ ] Service worker tests - Mark as deprecated
### Documentation Files
- [ ] `README.md` - Update to reflect native-first approach
- [ ] `doc/web-push.md` - Mark as deprecated
- [ ] API documentation - Remove web-push references
---
## Dependencies to Remove
### NPM Packages
```json
{
"dependencies": {
// DEPRECATED: Web-push only
"web-push": "^7.4.0",
"vite-plugin-pwa": "^0.17.0"
},
"devDependencies": {
// DEPRECATED: Web-push only
"workbox-webpack-plugin": "^6.5.0"
}
}
```
### Build Scripts
```json
{
"scripts": {
// DEPRECATED: Web-push only
"build:sw": "workbox generateSW",
"test:sw": "jest --testPathPattern=serviceWorker"
}
}
```
---
## Migration Guide
### From Web-Push to Native-First
#### Step 1: Remove Web-Push Dependencies
```bash
# Remove web-push packages
npm uninstall web-push vite-plugin-pwa workbox-webpack-plugin
# Remove service worker files
rm -rf sw_scripts/
rm -f public/sw.js
rm -f public/workbox-*.js
```
#### Step 2: Update Configuration
```javascript
// Remove VitePWA plugin from vite.config.web.mts
export default defineConfig({
plugins: [
// Remove VitePWA plugin
// VitePWA({ ... })
]
})
```
#### Step 3: Update Service Registration
```javascript
// Remove service worker registration from main.web.ts
// if ('serviceWorker' in navigator) {
// navigator.serviceWorker.register('/sw.js')
// }
```
#### Step 4: Update Tests
```javascript
// Remove web-push tests
// describe('Web Push Notifications', () => { ... })
```
---
## Verification Checklist
### Code Removal Verification
- [ ] No web-push imports remain
- [ ] No service worker registration code
- [ ] No push subscription management
- [ ] No web notification API usage
- [ ] No VitePWA plugin configuration
### Documentation Verification
- [ ] All web-push references marked as deprecated
- [ ] README updated to reflect native-first approach
- [ ] API documentation updated
- [ ] Test documentation updated
### Build Verification
- [ ] Web build succeeds without service worker
- [ ] No service worker files generated
- [ ] No web-push dependencies in bundle
- [ ] Native builds work correctly
### Test Verification
- [ ] Web-push tests are marked as deprecated
- [ ] Native notification tests pass
- [ ] No web-push test failures
- [ ] Test suite runs successfully
---
## Rollback Plan
### Emergency Rollback
If native-first implementation fails, web-push code can be restored:
#### 1. **Restore Dependencies**
```bash
npm install web-push vite-plugin-pwa workbox-webpack-plugin
```
#### 2. **Restore Service Worker Files**
```bash
git checkout HEAD~1 -- sw_scripts/
git checkout HEAD~1 -- public/sw.js
```
#### 3. **Restore Configuration**
```bash
git checkout HEAD~1 -- vite.config.web.mts
git checkout HEAD~1 -- package.json
```
#### 4. **Restore Tests**
```bash
git checkout HEAD~1 -- test-playwright/web-push.spec.ts
git checkout HEAD~1 -- test/services/webPush.test.ts
```
### Rollback Verification
- [ ] Web-push functionality restored
- [ ] Service worker registration works
- [ ] Push notifications work
- [ ] Tests pass
---
## Post-Cleanup Tasks
### Code Review
- [ ] Review all changes for completeness
- [ ] Verify no web-push code remains
- [ ] Check for orphaned references
- [ ] Validate native-first implementation
### Testing
- [ ] Run full test suite
- [ ] Verify native notifications work
- [ ] Check Electron functionality
- [ ] Validate mobile builds
### Documentation
- [ ] Update all documentation
- [ ] Remove web-push references
- [ ] Update API documentation
- [ ] Update user guides
---
## Success Criteria
### Complete Web-Push Removal
- [ ] All web-push code marked as deprecated
- [ ] Service worker files quarantined
- [ ] Dependencies removed
- [ ] Configuration updated
### Native-First Implementation
- [ ] Native notifications work on Android
- [ ] Native notifications work on iOS
- [ ] Electron notifications work
- [ ] Background prefetch works
### Documentation Updated
- [ ] All docs reflect native-first approach
- [ ] Web-push marked as deprecated
- [ ] Migration guide provided
- [ ] Rollback plan documented
---
_This cleanup guide provides comprehensive instructions for removing web-push
code paths from TimeSafari. Web-push has been retired for unreliability, and the
system now focuses on native mobile reliability with Electron best-effort support._
-76
View File
@@ -1,76 +0,0 @@
# Xcode 26 / CocoaPods Compatibility Workaround
**Date:** 2025-01-27
**Issue:** CocoaPods `xcodeproj` gem (1.27.0) doesn't support Xcode 26's project format version 70
## The Problem
Xcode 26.1.1 uses project format version 70, but the `xcodeproj` gem (1.27.0) only supports up to version 56. This causes CocoaPods to fail with:
```
ArgumentError - [Xcodeproj] Unable to find compatibility version string for object version `70`.
```
## Solutions
### Option 1: Temporarily Downgrade Project Format (Recommended for Now)
**Before running `pod install` or `npm run build:ios`:**
1. Edit `ios/App/App.xcodeproj/project.pbxproj`
2. Change line 6 from: `objectVersion = 70;` to: `objectVersion = 56;`
3. Run your build/sync command
4. Change it back to: `objectVersion = 70;` (Xcode will likely change it back automatically)
**Warning:** Xcode may automatically upgrade the format back to 70 when you open the project. This is okay - just repeat the process when needed.
### Option 2: Wait for xcodeproj Update
The `xcodeproj` gem maintainers will eventually release a version that supports format 70. You can:
- Check for updates: `bundle update xcodeproj`
- Monitor: https://github.com/CocoaPods/Xcodeproj/issues
### Option 3: Use Xcode Directly (Bypass CocoaPods for Now)
Since the Share Extension is already set up:
1. Open the project in Xcode
2. Build directly from Xcode (Product → Build)
3. Skip `npm run build:ios` for now
4. Test the Share Extension functionality
### Option 4: Automated Workaround (Integrated into Build Script) ✅
The workaround is now **automatically integrated** into `scripts/build-ios.sh`. When you run:
```bash
npm run build:ios
```
The build script will:
1. Automatically detect if the project format is version 70
2. Temporarily downgrade to version 56
3. Run `pod install`
4. Restore to version 70
5. Continue with the build
**No manual steps required!** The workaround is transparent and only applies when needed.
To remove the workaround in the future:
1. Check if `xcodeproj` gem supports format 70: `bundle exec gem list xcodeproj`
2. Test if `pod install` works without the workaround
3. If it works, remove the `run_pod_install_with_workaround()` function from `scripts/build-ios.sh`
4. Replace it with a simple `pod install` call
## Current Status
- ✅ Share Extension target exists
- ✅ Share Extension files are in place
- ✅ Workaround integrated into build script
- ✅ `npm run build:ios` works automatically
## Recommendation
**Use `npm run build:ios`** - the workaround is handled automatically. No manual intervention needed.
Once `xcodeproj` is updated to support format 70, the workaround can be removed from the build script.
+4 -1
View File
@@ -5,12 +5,15 @@
**Status**: 🎯 **ACTIVE** - Z-index layering standards **Status**: 🎯 **ACTIVE** - Z-index layering standards
## Objective ## Objective
Establish consistent z-index values across the TimeSafari application to ensure proper layering of UI elements. Establish consistent z-index values across the TimeSafari application to ensure proper layering of UI elements.
## Result ## Result
This document defines the z-index hierarchy for all UI components. This document defines the z-index hierarchy for all UI components.
## Use/Run ## Use/Run
Reference these values when implementing new components or modifying existing ones to maintain consistent layering. Reference these values when implementing new components or modifying existing ones to maintain consistent layering.
## Z-Index Hierarchy ## Z-Index Hierarchy
@@ -51,7 +54,7 @@ Reference these values when implementing new components or modifying existing on
## Collaboration Hooks ## Collaboration Hooks
- **Reviewers**: Frontend team, UI/UX designers - **Reviewers**: Frontend team, UI/UX designers
- **Sign-off checklist**: - **Sign-off checklist**:
- [ ] All new components follow z-index guidelines - [ ] All new components follow z-index guidelines
- [ ] Existing components updated to use defined values - [ ] Existing components updated to use defined values
- [ ] Cross-browser testing completed - [ ] Cross-browser testing completed
+2 -34
View File
@@ -6,7 +6,7 @@ import electronIsDev from 'electron-is-dev';
import unhandled from 'electron-unhandled'; import unhandled from 'electron-unhandled';
// import { autoUpdater } from 'electron-updater'; // import { autoUpdater } from 'electron-updater';
import { promises as fs } from 'fs'; import { promises as fs } from 'fs';
import { join, basename } from 'path'; import { join } from 'path';
import { ElectronCapacitorApp, setupContentSecurityPolicy, setupReloadWatcher } from './setup'; import { ElectronCapacitorApp, setupContentSecurityPolicy, setupReloadWatcher } from './setup';
@@ -151,47 +151,15 @@ app.on('activate', async function () {
* This provides a secure, native way to save files directly to the Downloads * This provides a secure, native way to save files directly to the Downloads
* directory using the main process's file system access. * directory using the main process's file system access.
* *
* Security: File names are sanitized to prevent path traversal attacks.
* Only safe file extensions are allowed (.json, .txt, .csv, .md).
*
* @param fileName - The name of the file to save (including extension) * @param fileName - The name of the file to save (including extension)
* @param data - The data to write to the file (string or buffer) * @param data - The data to write to the file (string or buffer)
* @returns Promise<{success: boolean, path?: string, error?: string}> * @returns Promise<{success: boolean, path?: string, error?: string}>
*/ */
ipcMain.handle('export-data-to-downloads', async (_event, fileName: string, data: string) => { ipcMain.handle('export-data-to-downloads', async (_event, fileName: string, data: string) => {
try { try {
// Security: Sanitize file name to prevent path traversal
// 1. Extract only the basename (removes any directory components)
const sanitizedBaseName = basename(fileName);
// 2. Reject if still contains path separators (shouldn't happen after basename, but double-check)
if (sanitizedBaseName.includes('/') || sanitizedBaseName.includes('\\')) {
throw new Error('Invalid file name: path separators not allowed');
}
// 3. Enforce allowed file extensions for security
const allowedExtensions = ['.json', '.txt', '.csv', '.md', '.log'];
const hasAllowedExtension = allowedExtensions.some(ext =>
sanitizedBaseName.toLowerCase().endsWith(ext.toLowerCase())
);
if (!hasAllowedExtension) {
throw new Error(`Invalid file extension. Allowed: ${allowedExtensions.join(', ')}`);
}
// 4. Additional validation: reject empty or suspicious names
if (!sanitizedBaseName || sanitizedBaseName.trim().length === 0) {
throw new Error('File name cannot be empty');
}
// 5. Reject names that are too long (prevent potential filesystem issues)
if (sanitizedBaseName.length > 255) {
throw new Error('File name too long (max 255 characters)');
}
// Get the user's Downloads directory path // Get the user's Downloads directory path
const downloadsDir = app.getPath('downloads'); const downloadsDir = app.getPath('downloads');
const filePath = join(downloadsDir, sanitizedBaseName); const filePath = join(downloadsDir, fileName);
// Write the file to the Downloads directory // Write the file to the Downloads directory
await fs.writeFile(filePath, data, 'utf-8'); await fs.writeFile(filePath, data, 'utf-8');
+6 -39
View File
@@ -218,50 +218,17 @@ export class ElectronCapacitorApp {
} }
} }
/** // Set a CSP up for our application based on the custom scheme
* Set up Content Security Policy for Electron application
*
* CSP is assembled from structured directives to prevent truncation/corruption.
* This ensures the CSP string is always complete and valid.
*
* @param customScheme - The custom URL scheme for the Electron app (e.g., 'capacitor-electron')
*/
export function setupContentSecurityPolicy(customScheme: string): void { export function setupContentSecurityPolicy(customScheme: string): void {
// Build CSP from structured directives to prevent truncation issues
const buildCSP = (isDev: boolean): string => {
const directives: string[] = [];
// Default source: allow custom scheme, inline scripts (required for some libs), and data URIs
const defaultSrc = [
`${customScheme}://*`,
"'unsafe-inline'",
"data:",
"https:",
];
if (isDev) {
// Development: allow devtools and eval for debugging
defaultSrc.push("devtools://*", "'unsafe-eval'", "http:");
}
directives.push(`default-src ${defaultSrc.join(" ")}`);
// Style source: allow custom scheme and inline styles
directives.push(`style-src ${customScheme}://* 'unsafe-inline'`);
// Font source: allow custom scheme and data URIs
directives.push(`font-src ${customScheme}://* data:`);
return directives.join("; ");
};
session.defaultSession.webRequest.onHeadersReceived((details, callback) => { session.defaultSession.webRequest.onHeadersReceived((details, callback) => {
const csp = buildCSP(electronIsDev);
callback({ callback({
responseHeaders: { responseHeaders: {
...details.responseHeaders, ...details.responseHeaders,
'Content-Security-Policy': [csp], 'Content-Security-Policy': [
electronIsDev
? `default-src ${customScheme}://* 'unsafe-inline' devtools://* 'unsafe-eval' data: https: http:; style-src ${customScheme}://* 'unsafe-inline'; font-src ${customScheme}://* data:`
: `default-src ${customScheme}://* 'unsafe-inline' data: https:; style-src ${customScheme}://* 'unsafe-inline'; font-src ${customScheme}://* data:`,
],
}, },
}); });
}); });
+2 -2
View File
@@ -3,7 +3,7 @@
<head> <head>
<meta charset="UTF-8" /> <meta charset="UTF-8" />
<link rel="icon" type="image/svg+xml" href="/favicon.ico" /> <link rel="icon" type="image/svg+xml" href="/favicon.ico" />
<meta name="viewport" content="width=device-width, initial-scale=1.0, viewport-fit=cover, user-scalable=no, interactive-widget=overlays-content" /> <meta name="viewport" content="width=device-width, initial-scale=1.0, viewport-fit=cover" />
<!-- CORS headers removed to allow images from any domain --> <!-- CORS headers removed to allow images from any domain -->
@@ -13,4 +13,4 @@
<div id="app"></div> <div id="app"></div>
<script type="module" src="/src/main.ts"></script> <script type="module" src="/src/main.ts"></script>
</body> </body>
</html> </html>
+6 -211
View File
@@ -3,7 +3,7 @@
archiveVersion = 1; archiveVersion = 1;
classes = { classes = {
}; };
objectVersion = 70; objectVersion = 54;
objects = { objects = {
/* Begin PBXBuildFile section */ /* Begin PBXBuildFile section */
@@ -15,35 +15,8 @@
504EC3121FED79650016851F /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 504EC3101FED79650016851F /* LaunchScreen.storyboard */; }; 504EC3121FED79650016851F /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 504EC3101FED79650016851F /* LaunchScreen.storyboard */; };
50B271D11FEDC1A000F3C39B /* public in Resources */ = {isa = PBXBuildFile; fileRef = 50B271D01FEDC1A000F3C39B /* public */; }; 50B271D11FEDC1A000F3C39B /* public in Resources */ = {isa = PBXBuildFile; fileRef = 50B271D01FEDC1A000F3C39B /* public */; };
97EF2DC6FD76C3643D680B8D /* Pods_App.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 90DCAFB4D8948F7A50C13800 /* Pods_App.framework */; }; 97EF2DC6FD76C3643D680B8D /* Pods_App.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 90DCAFB4D8948F7A50C13800 /* Pods_App.framework */; };
C86585DF2ED456DE00824752 /* TimeSafariShareExtension.appex in Embed Foundation Extensions */ = {isa = PBXBuildFile; fileRef = C86585D52ED456DE00824752 /* TimeSafariShareExtension.appex */; settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; };
C8C56E142EE0474B00737D0E /* SharedImageUtility.swift in Sources */ = {isa = PBXBuildFile; fileRef = C8C56E132EE0474B00737D0E /* SharedImageUtility.swift */; };
C8C56E162EE064CB00737D0E /* SharedImagePlugin.swift in Sources */ = {isa = PBXBuildFile; fileRef = C8C56E152EE064CA00737D0E /* SharedImagePlugin.swift */; };
/* End PBXBuildFile section */ /* End PBXBuildFile section */
/* Begin PBXContainerItemProxy section */
C86585DD2ED456DE00824752 /* PBXContainerItemProxy */ = {
isa = PBXContainerItemProxy;
containerPortal = 504EC2FC1FED79650016851F /* Project object */;
proxyType = 1;
remoteGlobalIDString = C86585D42ED456DE00824752;
remoteInfo = TimeSafariShareExtension;
};
/* End PBXContainerItemProxy section */
/* Begin PBXCopyFilesBuildPhase section */
C86585E02ED456DE00824752 /* Embed Foundation Extensions */ = {
isa = PBXCopyFilesBuildPhase;
buildActionMask = 2147483647;
dstPath = "";
dstSubfolderSpec = 13;
files = (
C86585DF2ED456DE00824752 /* TimeSafariShareExtension.appex in Embed Foundation Extensions */,
);
name = "Embed Foundation Extensions";
runOnlyForDeploymentPostprocessing = 0;
};
/* End PBXCopyFilesBuildPhase section */
/* Begin PBXFileReference section */ /* Begin PBXFileReference section */
2FAD9762203C412B000D30F8 /* config.xml */ = {isa = PBXFileReference; lastKnownFileType = text.xml; path = config.xml; sourceTree = "<group>"; }; 2FAD9762203C412B000D30F8 /* config.xml */ = {isa = PBXFileReference; lastKnownFileType = text.xml; path = config.xml; sourceTree = "<group>"; };
50379B222058CBB4000EE86E /* capacitor.config.json */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.json; path = capacitor.config.json; sourceTree = "<group>"; }; 50379B222058CBB4000EE86E /* capacitor.config.json */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.json; path = capacitor.config.json; sourceTree = "<group>"; };
@@ -55,39 +28,10 @@
504EC3131FED79650016851F /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; }; 504EC3131FED79650016851F /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
50B271D01FEDC1A000F3C39B /* public */ = {isa = PBXFileReference; lastKnownFileType = folder; path = public; sourceTree = "<group>"; }; 50B271D01FEDC1A000F3C39B /* public */ = {isa = PBXFileReference; lastKnownFileType = folder; path = public; sourceTree = "<group>"; };
90DCAFB4D8948F7A50C13800 /* Pods_App.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_App.framework; sourceTree = BUILT_PRODUCTS_DIR; }; 90DCAFB4D8948F7A50C13800 /* Pods_App.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_App.framework; sourceTree = BUILT_PRODUCTS_DIR; };
C86585D52ED456DE00824752 /* TimeSafariShareExtension.appex */ = {isa = PBXFileReference; explicitFileType = "wrapper.app-extension"; includeInIndex = 0; path = TimeSafariShareExtension.appex; sourceTree = BUILT_PRODUCTS_DIR; };
C86585E52ED4577F00824752 /* App.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = App.entitlements; sourceTree = "<group>"; };
C8C56E132EE0474B00737D0E /* SharedImageUtility.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SharedImageUtility.swift; sourceTree = "<group>"; };
C8C56E152EE064CA00737D0E /* SharedImagePlugin.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SharedImagePlugin.swift; sourceTree = "<group>"; };
E2E9297D5D02C549106C77F9 /* Pods-App.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-App.release.xcconfig"; path = "Target Support Files/Pods-App/Pods-App.release.xcconfig"; sourceTree = "<group>"; }; E2E9297D5D02C549106C77F9 /* Pods-App.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-App.release.xcconfig"; path = "Target Support Files/Pods-App/Pods-App.release.xcconfig"; sourceTree = "<group>"; };
EAEC6436E595F7CD3A1C9E96 /* Pods-App.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-App.debug.xcconfig"; path = "Target Support Files/Pods-App/Pods-App.debug.xcconfig"; sourceTree = "<group>"; }; EAEC6436E595F7CD3A1C9E96 /* Pods-App.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-App.debug.xcconfig"; path = "Target Support Files/Pods-App/Pods-App.debug.xcconfig"; sourceTree = "<group>"; };
/* End PBXFileReference section */ /* End PBXFileReference section */
/* Begin PBXFileSystemSynchronizedBuildFileExceptionSet section */
C86585E32ED456DE00824752 /* PBXFileSystemSynchronizedBuildFileExceptionSet */ = {
isa = PBXFileSystemSynchronizedBuildFileExceptionSet;
membershipExceptions = (
Info.plist,
);
target = C86585D42ED456DE00824752 /* TimeSafariShareExtension */;
};
/* End PBXFileSystemSynchronizedBuildFileExceptionSet section */
/* Begin PBXFileSystemSynchronizedRootGroup section */
C86585D62ED456DE00824752 /* TimeSafariShareExtension */ = {
isa = PBXFileSystemSynchronizedRootGroup;
exceptions = (
C86585E32ED456DE00824752 /* PBXFileSystemSynchronizedBuildFileExceptionSet */,
);
explicitFileTypes = {
};
explicitFolders = (
);
path = TimeSafariShareExtension;
sourceTree = "<group>";
};
/* End PBXFileSystemSynchronizedRootGroup section */
/* Begin PBXFrameworksBuildPhase section */ /* Begin PBXFrameworksBuildPhase section */
504EC3011FED79650016851F /* Frameworks */ = { 504EC3011FED79650016851F /* Frameworks */ = {
isa = PBXFrameworksBuildPhase; isa = PBXFrameworksBuildPhase;
@@ -97,13 +41,6 @@
); );
runOnlyForDeploymentPostprocessing = 0; runOnlyForDeploymentPostprocessing = 0;
}; };
C86585D22ED456DE00824752 /* Frameworks */ = {
isa = PBXFrameworksBuildPhase;
buildActionMask = 2147483647;
files = (
);
runOnlyForDeploymentPostprocessing = 0;
};
/* End PBXFrameworksBuildPhase section */ /* End PBXFrameworksBuildPhase section */
/* Begin PBXGroup section */ /* Begin PBXGroup section */
@@ -119,7 +56,6 @@
isa = PBXGroup; isa = PBXGroup;
children = ( children = (
504EC3061FED79650016851F /* App */, 504EC3061FED79650016851F /* App */,
C86585D62ED456DE00824752 /* TimeSafariShareExtension */,
504EC3051FED79650016851F /* Products */, 504EC3051FED79650016851F /* Products */,
BA325FFCDCE8D334E5C7AEBE /* Pods */, BA325FFCDCE8D334E5C7AEBE /* Pods */,
4B546315E668C7A13939F417 /* Frameworks */, 4B546315E668C7A13939F417 /* Frameworks */,
@@ -130,7 +66,6 @@
isa = PBXGroup; isa = PBXGroup;
children = ( children = (
504EC3041FED79650016851F /* App.app */, 504EC3041FED79650016851F /* App.app */,
C86585D52ED456DE00824752 /* TimeSafariShareExtension.appex */,
); );
name = Products; name = Products;
sourceTree = "<group>"; sourceTree = "<group>";
@@ -138,9 +73,6 @@
504EC3061FED79650016851F /* App */ = { 504EC3061FED79650016851F /* App */ = {
isa = PBXGroup; isa = PBXGroup;
children = ( children = (
C8C56E152EE064CA00737D0E /* SharedImagePlugin.swift */,
C8C56E132EE0474B00737D0E /* SharedImageUtility.swift */,
C86585E52ED4577F00824752 /* App.entitlements */,
50379B222058CBB4000EE86E /* capacitor.config.json */, 50379B222058CBB4000EE86E /* capacitor.config.json */,
504EC3071FED79650016851F /* AppDelegate.swift */, 504EC3071FED79650016851F /* AppDelegate.swift */,
504EC30B1FED79650016851F /* Main.storyboard */, 504EC30B1FED79650016851F /* Main.storyboard */,
@@ -176,40 +108,16 @@
012076E8FFE4BF260A79B034 /* Fix Privacy Manifest */, 012076E8FFE4BF260A79B034 /* Fix Privacy Manifest */,
3525031ED1C96EF4CF6E9959 /* [CP] Embed Pods Frameworks */, 3525031ED1C96EF4CF6E9959 /* [CP] Embed Pods Frameworks */,
96A7EF592DF3366D00084D51 /* Fix Privacy Manifest */, 96A7EF592DF3366D00084D51 /* Fix Privacy Manifest */,
C86585E02ED456DE00824752 /* Embed Foundation Extensions */,
); );
buildRules = ( buildRules = (
); );
dependencies = ( dependencies = (
C86585DE2ED456DE00824752 /* PBXTargetDependency */,
); );
name = App; name = App;
productName = App; productName = App;
productReference = 504EC3041FED79650016851F /* App.app */; productReference = 504EC3041FED79650016851F /* App.app */;
productType = "com.apple.product-type.application"; productType = "com.apple.product-type.application";
}; };
C86585D42ED456DE00824752 /* TimeSafariShareExtension */ = {
isa = PBXNativeTarget;
buildConfigurationList = C86585E42ED456DE00824752 /* Build configuration list for PBXNativeTarget "TimeSafariShareExtension" */;
buildPhases = (
C86585D12ED456DE00824752 /* Sources */,
C86585D22ED456DE00824752 /* Frameworks */,
C86585D32ED456DE00824752 /* Resources */,
);
buildRules = (
);
dependencies = (
);
fileSystemSynchronizedGroups = (
C86585D62ED456DE00824752 /* TimeSafariShareExtension */,
);
name = TimeSafariShareExtension;
packageProductDependencies = (
);
productName = TimeSafariShareExtension;
productReference = C86585D52ED456DE00824752 /* TimeSafariShareExtension.appex */;
productType = "com.apple.product-type.app-extension";
};
/* End PBXNativeTarget section */ /* End PBXNativeTarget section */
/* Begin PBXProject section */ /* Begin PBXProject section */
@@ -217,7 +125,7 @@
isa = PBXProject; isa = PBXProject;
attributes = { attributes = {
BuildIndependentTargetsInParallel = YES; BuildIndependentTargetsInParallel = YES;
LastSwiftUpdateCheck = 2610; LastSwiftUpdateCheck = 920;
LastUpgradeCheck = 1630; LastUpgradeCheck = 1630;
TargetAttributes = { TargetAttributes = {
504EC3031FED79650016851F = { 504EC3031FED79650016851F = {
@@ -225,9 +133,6 @@
LastSwiftMigration = 1100; LastSwiftMigration = 1100;
ProvisioningStyle = Automatic; ProvisioningStyle = Automatic;
}; };
C86585D42ED456DE00824752 = {
CreatedOnToolsVersion = 26.1.1;
};
}; };
}; };
buildConfigurationList = 504EC2FF1FED79650016851F /* Build configuration list for PBXProject "App" */; buildConfigurationList = 504EC2FF1FED79650016851F /* Build configuration list for PBXProject "App" */;
@@ -244,7 +149,6 @@
projectRoot = ""; projectRoot = "";
targets = ( targets = (
504EC3031FED79650016851F /* App */, 504EC3031FED79650016851F /* App */,
C86585D42ED456DE00824752 /* TimeSafariShareExtension */,
); );
}; };
/* End PBXProject section */ /* End PBXProject section */
@@ -263,13 +167,6 @@
); );
runOnlyForDeploymentPostprocessing = 0; runOnlyForDeploymentPostprocessing = 0;
}; };
C86585D32ED456DE00824752 /* Resources */ = {
isa = PBXResourcesBuildPhase;
buildActionMask = 2147483647;
files = (
);
runOnlyForDeploymentPostprocessing = 0;
};
/* End PBXResourcesBuildPhase section */ /* End PBXResourcesBuildPhase section */
/* Begin PBXShellScriptBuildPhase section */ /* Begin PBXShellScriptBuildPhase section */
@@ -356,29 +253,12 @@
isa = PBXSourcesBuildPhase; isa = PBXSourcesBuildPhase;
buildActionMask = 2147483647; buildActionMask = 2147483647;
files = ( files = (
C8C56E162EE064CB00737D0E /* SharedImagePlugin.swift in Sources */,
504EC3081FED79650016851F /* AppDelegate.swift in Sources */, 504EC3081FED79650016851F /* AppDelegate.swift in Sources */,
C8C56E142EE0474B00737D0E /* SharedImageUtility.swift in Sources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
C86585D12ED456DE00824752 /* Sources */ = {
isa = PBXSourcesBuildPhase;
buildActionMask = 2147483647;
files = (
); );
runOnlyForDeploymentPostprocessing = 0; runOnlyForDeploymentPostprocessing = 0;
}; };
/* End PBXSourcesBuildPhase section */ /* End PBXSourcesBuildPhase section */
/* Begin PBXTargetDependency section */
C86585DE2ED456DE00824752 /* PBXTargetDependency */ = {
isa = PBXTargetDependency;
target = C86585D42ED456DE00824752 /* TimeSafariShareExtension */;
targetProxy = C86585DD2ED456DE00824752 /* PBXContainerItemProxy */;
};
/* End PBXTargetDependency section */
/* Begin PBXVariantGroup section */ /* Begin PBXVariantGroup section */
504EC30B1FED79650016851F /* Main.storyboard */ = { 504EC30B1FED79650016851F /* Main.storyboard */ = {
isa = PBXVariantGroup; isa = PBXVariantGroup;
@@ -522,9 +402,8 @@
baseConfigurationReference = EAEC6436E595F7CD3A1C9E96 /* Pods-App.debug.xcconfig */; baseConfigurationReference = EAEC6436E595F7CD3A1C9E96 /* Pods-App.debug.xcconfig */;
buildSettings = { buildSettings = {
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
CODE_SIGN_ENTITLEMENTS = App/App.entitlements;
CODE_SIGN_STYLE = Automatic; CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 50; CURRENT_PROJECT_VERSION = 40;
DEVELOPMENT_TEAM = GM3FS5JQPH; DEVELOPMENT_TEAM = GM3FS5JQPH;
ENABLE_APP_SANDBOX = NO; ENABLE_APP_SANDBOX = NO;
ENABLE_USER_SCRIPT_SANDBOXING = NO; ENABLE_USER_SCRIPT_SANDBOXING = NO;
@@ -534,7 +413,7 @@
"$(inherited)", "$(inherited)",
"@executable_path/Frameworks", "@executable_path/Frameworks",
); );
MARKETING_VERSION = 1.1.5; MARKETING_VERSION = 1.0.7;
OTHER_SWIFT_FLAGS = "$(inherited) \"-D\" \"COCOAPODS\" \"-DDEBUG\""; OTHER_SWIFT_FLAGS = "$(inherited) \"-D\" \"COCOAPODS\" \"-DDEBUG\"";
PRODUCT_BUNDLE_IDENTIFIER = app.timesafari; PRODUCT_BUNDLE_IDENTIFIER = app.timesafari;
PRODUCT_NAME = "$(TARGET_NAME)"; PRODUCT_NAME = "$(TARGET_NAME)";
@@ -550,9 +429,8 @@
baseConfigurationReference = E2E9297D5D02C549106C77F9 /* Pods-App.release.xcconfig */; baseConfigurationReference = E2E9297D5D02C549106C77F9 /* Pods-App.release.xcconfig */;
buildSettings = { buildSettings = {
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
CODE_SIGN_ENTITLEMENTS = App/App.entitlements;
CODE_SIGN_STYLE = Automatic; CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 50; CURRENT_PROJECT_VERSION = 40;
DEVELOPMENT_TEAM = GM3FS5JQPH; DEVELOPMENT_TEAM = GM3FS5JQPH;
ENABLE_APP_SANDBOX = NO; ENABLE_APP_SANDBOX = NO;
ENABLE_USER_SCRIPT_SANDBOXING = NO; ENABLE_USER_SCRIPT_SANDBOXING = NO;
@@ -562,7 +440,7 @@
"$(inherited)", "$(inherited)",
"@executable_path/Frameworks", "@executable_path/Frameworks",
); );
MARKETING_VERSION = 1.1.5; MARKETING_VERSION = 1.0.7;
PRODUCT_BUNDLE_IDENTIFIER = app.timesafari; PRODUCT_BUNDLE_IDENTIFIER = app.timesafari;
PRODUCT_NAME = "$(TARGET_NAME)"; PRODUCT_NAME = "$(TARGET_NAME)";
SWIFT_ACTIVE_COMPILATION_CONDITIONS = ""; SWIFT_ACTIVE_COMPILATION_CONDITIONS = "";
@@ -572,80 +450,6 @@
}; };
name = Release; name = Release;
}; };
C86585E12ED456DE00824752 /* Debug */ = {
isa = XCBuildConfiguration;
buildSettings = {
ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES;
CLANG_CXX_LANGUAGE_STANDARD = "gnu++20";
CLANG_ENABLE_OBJC_WEAK = YES;
CODE_SIGN_ENTITLEMENTS = TimeSafariShareExtension/TimeSafariShareExtension.entitlements;
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 50;
DEVELOPMENT_TEAM = GM3FS5JQPH;
GCC_C_LANGUAGE_STANDARD = gnu17;
GENERATE_INFOPLIST_FILE = YES;
INFOPLIST_FILE = TimeSafariShareExtension/Info.plist;
INFOPLIST_KEY_CFBundleDisplayName = TimeSafari;
INFOPLIST_KEY_NSHumanReadableCopyright = "";
IPHONEOS_DEPLOYMENT_TARGET = 14.0;
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
"@executable_path/Frameworks",
"@executable_path/../../Frameworks",
);
LOCALIZATION_PREFERS_STRING_CATALOGS = YES;
MARKETING_VERSION = 1.1.5;
MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE;
MTL_FAST_MATH = YES;
PRODUCT_BUNDLE_IDENTIFIER = app.timesafari.TimeSafariShareExtension;
PRODUCT_NAME = "$(TARGET_NAME)";
SKIP_INSTALL = YES;
STRING_CATALOG_GENERATE_SYMBOLS = YES;
SWIFT_ACTIVE_COMPILATION_CONDITIONS = "DEBUG $(inherited)";
SWIFT_APPROACHABLE_CONCURRENCY = YES;
SWIFT_EMIT_LOC_STRINGS = YES;
SWIFT_UPCOMING_FEATURE_MEMBER_IMPORT_VISIBILITY = YES;
SWIFT_VERSION = 5.0;
TARGETED_DEVICE_FAMILY = "1,2";
};
name = Debug;
};
C86585E22ED456DE00824752 /* Release */ = {
isa = XCBuildConfiguration;
buildSettings = {
ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES;
CLANG_CXX_LANGUAGE_STANDARD = "gnu++20";
CLANG_ENABLE_OBJC_WEAK = YES;
CODE_SIGN_ENTITLEMENTS = TimeSafariShareExtension/TimeSafariShareExtension.entitlements;
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 50;
DEVELOPMENT_TEAM = GM3FS5JQPH;
GCC_C_LANGUAGE_STANDARD = gnu17;
GENERATE_INFOPLIST_FILE = YES;
INFOPLIST_FILE = TimeSafariShareExtension/Info.plist;
INFOPLIST_KEY_CFBundleDisplayName = TimeSafari;
INFOPLIST_KEY_NSHumanReadableCopyright = "";
IPHONEOS_DEPLOYMENT_TARGET = 14.0;
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
"@executable_path/Frameworks",
"@executable_path/../../Frameworks",
);
LOCALIZATION_PREFERS_STRING_CATALOGS = YES;
MARKETING_VERSION = 1.1.5;
MTL_FAST_MATH = YES;
PRODUCT_BUNDLE_IDENTIFIER = app.timesafari.TimeSafariShareExtension;
PRODUCT_NAME = "$(TARGET_NAME)";
SKIP_INSTALL = YES;
STRING_CATALOG_GENERATE_SYMBOLS = YES;
SWIFT_APPROACHABLE_CONCURRENCY = YES;
SWIFT_EMIT_LOC_STRINGS = YES;
SWIFT_UPCOMING_FEATURE_MEMBER_IMPORT_VISIBILITY = YES;
SWIFT_VERSION = 5.0;
TARGETED_DEVICE_FAMILY = "1,2";
};
name = Release;
};
/* End XCBuildConfiguration section */ /* End XCBuildConfiguration section */
/* Begin XCConfigurationList section */ /* Begin XCConfigurationList section */
@@ -667,15 +471,6 @@
defaultConfigurationIsVisible = 0; defaultConfigurationIsVisible = 0;
defaultConfigurationName = Release; defaultConfigurationName = Release;
}; };
C86585E42ED456DE00824752 /* Build configuration list for PBXNativeTarget "TimeSafariShareExtension" */ = {
isa = XCConfigurationList;
buildConfigurations = (
C86585E12ED456DE00824752 /* Debug */,
C86585E22ED456DE00824752 /* Release */,
);
defaultConfigurationIsVisible = 0;
defaultConfigurationName = Release;
};
/* End XCConfigurationList section */ /* End XCConfigurationList section */
}; };
rootObject = 504EC2FC1FED79650016851F /* Project object */; rootObject = 504EC2FC1FED79650016851F /* Project object */;
@@ -1,77 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<Scheme
LastUpgradeVersion = "1630"
version = "1.7">
<BuildAction
parallelizeBuildables = "YES"
buildImplicitDependencies = "YES">
<BuildActionEntries>
<BuildActionEntry
buildForTesting = "YES"
buildForRunning = "YES"
buildForProfiling = "YES"
buildForArchiving = "YES"
buildForAnalyzing = "YES">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "504EC3031FED79650016851F"
BuildableName = "App.app"
BlueprintName = "App"
ReferencedContainer = "container:App.xcodeproj">
</BuildableReference>
</BuildActionEntry>
</BuildActionEntries>
</BuildAction>
<TestAction
buildConfiguration = "Debug"
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
shouldUseLaunchSchemeArgsEnv = "YES"
shouldAutocreateTestPlan = "YES">
</TestAction>
<LaunchAction
buildConfiguration = "Debug"
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
launchStyle = "0"
useCustomWorkingDirectory = "NO"
ignoresPersistentStateOnLaunch = "NO"
debugDocumentVersioning = "YES"
debugServiceExtension = "internal"
allowLocationSimulation = "YES">
<BuildableProductRunnable
runnableDebuggingMode = "0">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "504EC3031FED79650016851F"
BuildableName = "App.app"
BlueprintName = "App"
ReferencedContainer = "container:App.xcodeproj">
</BuildableReference>
</BuildableProductRunnable>
</LaunchAction>
<ProfileAction
buildConfiguration = "Release"
shouldUseLaunchSchemeArgsEnv = "YES"
savedToolIdentifier = ""
useCustomWorkingDirectory = "NO"
debugDocumentVersioning = "YES">
<BuildableProductRunnable
runnableDebuggingMode = "0">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "504EC3031FED79650016851F"
BuildableName = "App.app"
BlueprintName = "App"
ReferencedContainer = "container:App.xcodeproj">
</BuildableReference>
</BuildableProductRunnable>
</ProfileAction>
<AnalyzeAction
buildConfiguration = "Debug">
</AnalyzeAction>
<ArchiveAction
buildConfiguration = "Release"
revealArchiveInOrganizer = "YES">
</ArchiveAction>
</Scheme>
-10
View File
@@ -1,10 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>com.apple.security.application-groups</key>
<array>
<string>group.app.timesafari.share</string>
</array>
</dict>
</plist>
-63
View File
@@ -12,49 +12,9 @@ class AppDelegate: UIResponder, UIApplicationDelegate {
//let sqlite = SQLite() //let sqlite = SQLite()
//sqlite.initialize() //sqlite.initialize()
// Register SharedImage plugin manually after bridge is ready
// Try multiple times with increasing delays to ensure bridge is initialized
var attempts = 0
let maxAttempts = 5
func tryRegister() {
attempts += 1
if registerSharedImagePlugin() {
print("[AppDelegate] ✅ Plugin registration successful on attempt \(attempts)")
} else if attempts < maxAttempts {
DispatchQueue.main.asyncAfter(deadline: .now() + Double(attempts) * 0.5) {
tryRegister()
}
} else {
print("[AppDelegate] ⚠️ Failed to register plugin after \(maxAttempts) attempts")
}
}
// Start registration attempts
DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) {
tryRegister()
}
// Override point for customization after application launch. // Override point for customization after application launch.
return true return true
} }
@discardableResult
private func registerSharedImagePlugin() -> Bool {
guard let window = self.window,
let bridgeVC = window.rootViewController as? CAPBridgeViewController,
let bridge = bridgeVC.bridge else {
return false
}
// Create plugin instance
// The @objc(SharedImage) annotation makes it available as "SharedImage" to Objective-C
// which matches the JavaScript registration name
let pluginInstance = SharedImagePlugin()
bridge.registerPluginInstance(pluginInstance)
print("[AppDelegate] ✅ Registered SharedImagePlugin (exposed as 'SharedImage' via @objc annotation)")
return true
}
func applicationWillResignActive(_ application: UIApplication) { func applicationWillResignActive(_ application: UIApplication) {
// Sent when the application is about to move from active to inactive state. This can occur for certain types of temporary interruptions (such as an incoming phone call or SMS message) or when the user quits the application and it begins the transition to the background state. // Sent when the application is about to move from active to inactive state. This can occur for certain types of temporary interruptions (such as an incoming phone call or SMS message) or when the user quits the application and it begins the transition to the background state.
@@ -72,26 +32,6 @@ class AppDelegate: UIResponder, UIApplicationDelegate {
func applicationDidBecomeActive(_ application: UIApplication) { func applicationDidBecomeActive(_ application: UIApplication) {
// Restart any tasks that were paused (or not yet started) while the application was inactive. If the application was previously in the background, optionally refresh the user interface. // Restart any tasks that were paused (or not yet started) while the application was inactive. If the application was previously in the background, optionally refresh the user interface.
// Check for shared image from Share Extension when app becomes active
checkForSharedImageOnActivation()
}
/**
* Check for shared image when app launches or becomes active
* This allows the app to detect shared images without requiring a deep link
* Note: JavaScript will read the shared image via SharedImagePlugin, so we just check the flag
*/
private func checkForSharedImageOnActivation() {
// Check if shared photo is ready
if SharedImageUtility.isSharedPhotoReady() {
// Clear the flag
SharedImageUtility.clearSharedPhotoReadyFlag()
// Post notification for JavaScript to handle navigation
// JavaScript will read the shared image via SharedImagePlugin
NotificationCenter.default.post(name: NSNotification.Name("SharedPhotoReady"), object: nil)
}
} }
func applicationWillTerminate(_ application: UIApplication) { func applicationWillTerminate(_ application: UIApplication) {
@@ -101,8 +41,6 @@ class AppDelegate: UIResponder, UIApplicationDelegate {
func application(_ app: UIApplication, open url: URL, options: [UIApplication.OpenURLOptionsKey: Any] = [:]) -> Bool { func application(_ app: UIApplication, open url: URL, options: [UIApplication.OpenURLOptionsKey: Any] = [:]) -> Bool {
// Called when the app was launched with a url. Feel free to add additional processing here, // Called when the app was launched with a url. Feel free to add additional processing here,
// but if you want the App API to support tracking app url opens, make sure to keep this call // but if you want the App API to support tracking app url opens, make sure to keep this call
// Note: Share Extension opens app with timesafari:// (empty path), which is handled by JavaScript
// via the appUrlOpen listener in main.capacitor.ts
return ApplicationDelegateProxy.shared.application(app, open: url, options: options) return ApplicationDelegateProxy.shared.application(app, open: url, options: options)
} }
@@ -112,6 +50,5 @@ class AppDelegate: UIResponder, UIApplicationDelegate {
// tracking app url opens, make sure to keep this call // tracking app url opens, make sure to keep this call
return ApplicationDelegateProxy.shared.application(application, continue: userActivity, restorationHandler: restorationHandler) return ApplicationDelegateProxy.shared.application(application, continue: userActivity, restorationHandler: restorationHandler)
} }
} }
-66
View File
@@ -1,66 +0,0 @@
//
// SharedImagePlugin.swift
// App
//
// Capacitor plugin for accessing shared image data from Share Extension
//
import Foundation
import Capacitor
@objc(SharedImage)
public class SharedImagePlugin: CAPPlugin, CAPBridgedPlugin {
// MARK: - CAPBridgedPlugin Conformance
public var identifier: String {
return "SharedImage"
}
public var jsName: String {
return "SharedImage"
}
public var pluginMethods: [CAPPluginMethod] {
return [
CAPPluginMethod(#selector(getSharedImage(_:)), returnType: .promise),
CAPPluginMethod(#selector(hasSharedImage(_:)), returnType: .promise)
]
}
// MARK: - Plugin Methods
/**
* Get shared image data from App Group UserDefaults
* Returns base64 string and fileName, or null if no image exists
* Clears the data after reading to prevent re-reading
*/
@objc public func getSharedImage(_ call: CAPPluginCall) {
guard let sharedData = SharedImageUtility.getSharedImageData() else {
// No shared image exists - return null (not an error)
call.resolve([
"base64": NSNull(),
"fileName": NSNull()
])
return
}
// Return the shared image data
call.resolve([
"base64": sharedData["base64"] ?? "",
"fileName": sharedData["fileName"] ?? ""
])
}
/**
* Check if shared image exists without reading it
* Useful for quick checks before calling getSharedImage()
*/
@objc public func hasSharedImage(_ call: CAPPluginCall) {
let hasImage = SharedImageUtility.hasSharedImage()
call.resolve([
"hasImage": hasImage
])
}
}
-107
View File
@@ -1,107 +0,0 @@
//
// SharedImageUtility.swift
// App
//
// Shared utility for accessing shared image data from App Group container
// Images are stored as files in the App Group container to avoid UserDefaults size limits
// Used by both AppDelegate and SharedImagePlugin
//
import Foundation
public class SharedImageUtility {
private static let appGroupIdentifier = "group.app.timesafari.share"
private static let sharedPhotoFileNameKey = "sharedPhotoFileName"
private static let sharedPhotoFilePathKey = "sharedPhotoFilePath"
private static let sharedPhotoReadyKey = "sharedPhotoReady"
/// Get the App Group container URL for accessing shared files
private static var appGroupContainerURL: URL? {
return FileManager.default.containerURL(forSecurityApplicationGroupIdentifier: appGroupIdentifier)
}
/**
* Get shared image data from App Group container file
* All images are stored as files for consistency and to avoid UserDefaults size limits
* Clears the data after reading to prevent re-reading
*
* @returns Dictionary with "base64" and "fileName" keys, or nil if no shared image
*/
static func getSharedImageData() -> [String: String]? {
guard let userDefaults = UserDefaults(suiteName: appGroupIdentifier) else {
return nil
}
// Get file path and filename from UserDefaults
guard let filePath = userDefaults.string(forKey: sharedPhotoFilePathKey),
let containerURL = appGroupContainerURL else {
return nil
}
let fileName = userDefaults.string(forKey: sharedPhotoFileNameKey) ?? "shared-image.jpg"
let fileURL = containerURL.appendingPathComponent(filePath)
// Read image data from file
guard let imageData = try? Data(contentsOf: fileURL) else {
return nil
}
// Convert file data to base64 for JavaScript consumption
let base64String = imageData.base64EncodedString()
// Clear the shared data after reading
userDefaults.removeObject(forKey: sharedPhotoFilePathKey)
userDefaults.removeObject(forKey: sharedPhotoFileNameKey)
// Remove the file
try? FileManager.default.removeItem(at: fileURL)
userDefaults.synchronize()
return ["base64": base64String, "fileName": fileName]
}
/**
* Check if shared image exists without reading it
*
* @returns true if shared image file exists, false otherwise
*/
static func hasSharedImage() -> Bool {
guard let userDefaults = UserDefaults(suiteName: appGroupIdentifier),
let filePath = userDefaults.string(forKey: sharedPhotoFilePathKey),
let containerURL = appGroupContainerURL else {
return false
}
let fileURL = containerURL.appendingPathComponent(filePath)
return FileManager.default.fileExists(atPath: fileURL.path)
}
/**
* Check if shared photo ready flag is set
* This flag is set by the Share Extension when image is ready
*
* @returns true if flag is set, false otherwise
*/
static func isSharedPhotoReady() -> Bool {
guard let userDefaults = UserDefaults(suiteName: appGroupIdentifier) else {
return false
}
return userDefaults.bool(forKey: sharedPhotoReadyKey)
}
/**
* Clear the shared photo ready flag
* Called after processing the shared image
*/
static func clearSharedPhotoReadyFlag() {
guard let userDefaults = UserDefaults(suiteName: appGroupIdentifier) else {
return
}
userDefaults.removeObject(forKey: sharedPhotoReadyKey)
userDefaults.synchronize()
}
}
@@ -1,21 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>NSExtension</key>
<dict>
<key>NSExtensionAttributes</key>
<dict>
<key>NSExtensionActivationRule</key>
<dict>
<key>NSExtensionActivationSupportsImageWithMaxCount</key>
<integer>1</integer>
</dict>
</dict>
<key>NSExtensionPointIdentifier</key>
<string>com.apple.share-services</string>
<key>NSExtensionPrincipalClass</key>
<string>$(PRODUCT_MODULE_NAME).ShareViewController</string>
</dict>
</dict>
</plist>
@@ -1,207 +0,0 @@
//
// ShareViewController.swift
// TimeSafariShareExtension
//
// Created by Aardimus on 11/24/25.
//
import UIKit
import UniformTypeIdentifiers
class ShareViewController: UIViewController {
private let appGroupIdentifier = "group.app.timesafari.share"
private let sharedPhotoFileNameKey = "sharedPhotoFileName"
private let sharedPhotoFilePathKey = "sharedPhotoFilePath"
private let sharedImageFileName = "shared-image"
/// Get the App Group container URL for storing shared files
private var appGroupContainerURL: URL? {
return FileManager.default.containerURL(forSecurityApplicationGroupIdentifier: appGroupIdentifier)
}
override func viewDidLoad() {
super.viewDidLoad()
// Set a minimal background (transparent or loading indicator)
view.backgroundColor = .systemBackground
// Process image immediately without showing UI
processAndOpenApp()
}
private func processAndOpenApp() {
// extensionContext is automatically available on UIViewController when used as extension principal class
guard let context = extensionContext,
let inputItems = context.inputItems as? [NSExtensionItem] else {
extensionContext?.completeRequest(returningItems: [], completionHandler: nil)
return
}
processSharedImage(from: inputItems) { [weak self] success in
guard let self = self, let context = self.extensionContext else {
return
}
if success {
// Set flag that shared photo is ready
self.setSharedPhotoReadyFlag()
// Open the main app (using minimal URL - app will detect shared data on activation)
self.openMainApp()
}
// Complete immediately - no UI shown
context.completeRequest(returningItems: [], completionHandler: nil)
}
}
private func setSharedPhotoReadyFlag() {
guard let userDefaults = UserDefaults(suiteName: appGroupIdentifier) else {
return
}
userDefaults.set(true, forKey: "sharedPhotoReady")
userDefaults.synchronize()
}
private func processSharedImage(from items: [NSExtensionItem], completion: @escaping (Bool) -> Void) {
// Find the first image attachment
for item in items {
guard let attachments = item.attachments else {
continue
}
for attachment in attachments {
// Skip non-image attachments
guard attachment.hasItemConformingToTypeIdentifier(UTType.image.identifier) else {
continue
}
// Try to load raw data first to preserve original format
// This preserves the original image format without conversion
attachment.loadItem(forTypeIdentifier: UTType.image.identifier, options: nil) { [weak self] (data, error) in
guard let self = self else {
completion(false)
return
}
if error != nil {
completion(false)
return
}
// Handle different image data types
// loadItem(forTypeIdentifier:) typically returns URL or Data, not UIImage
var imageData: Data?
var fileName: String = "shared-image"
if let url = data as? URL {
// Most common case: Image provided as file URL - read raw data to preserve format
let accessing = url.startAccessingSecurityScopedResource()
defer {
if accessing {
url.stopAccessingSecurityScopedResource()
}
}
// Read raw data directly to preserve original format
imageData = try? Data(contentsOf: url)
fileName = url.lastPathComponent
// Fallback: if raw data read fails, try UIImage and convert to PNG (lossless)
if imageData == nil, let image = UIImage(contentsOfFile: url.path) {
imageData = image.pngData()
fileName = self.getFileNameWithExtension(url.lastPathComponent, newExtension: "png")
}
} else if let data = data as? Data {
// Less common: Image provided as raw Data - use directly to preserve format
imageData = data
fileName = attachment.suggestedName ?? "shared-image"
}
guard let finalImageData = imageData else {
completion(false)
return
}
// Store image as file in App Group container
if self.storeImageData(finalImageData, fileName: fileName) {
completion(true)
} else {
completion(false)
}
}
return // Process only the first image
}
}
// No image found
completion(false)
}
/// Helper to get filename with a new extension, preserving base name
private func getFileNameWithExtension(_ originalName: String, newExtension: String) -> String {
if let nameWithoutExt = originalName.components(separatedBy: ".").first, !nameWithoutExt.isEmpty {
return "\(nameWithoutExt).\(newExtension)"
}
return "shared-image.\(newExtension)"
}
/// Store image data as a file in the App Group container
/// All images are stored as files regardless of size for consistency and simplicity
/// Returns true if successful, false otherwise
private func storeImageData(_ imageData: Data, fileName: String) -> Bool {
guard let containerURL = appGroupContainerURL else {
return false
}
// Create file URL in the container using the actual filename
// Extract extension from fileName if present, otherwise use sharedImageFileName
let actualFileName = fileName.isEmpty ? sharedImageFileName : fileName
let fileURL = containerURL.appendingPathComponent(actualFileName)
// Remove old file if it exists
try? FileManager.default.removeItem(at: fileURL)
// Write image data to file
do {
try imageData.write(to: fileURL)
} catch {
return false
}
// Store file path and filename in UserDefaults (small data, safe to store)
guard let userDefaults = UserDefaults(suiteName: appGroupIdentifier) else {
return false
}
// Store relative path and filename
userDefaults.set(actualFileName, forKey: sharedPhotoFilePathKey)
userDefaults.set(fileName, forKey: sharedPhotoFileNameKey)
// Clean up any old base64 data that might exist
userDefaults.removeObject(forKey: "sharedPhotoBase64")
userDefaults.synchronize()
return true
}
private func openMainApp() {
// Open the main app with minimal URL - app will detect shared data on activation
guard let url = URL(string: "timesafari://") else {
return
}
var responder: UIResponder? = self
while responder != nil {
if let application = responder as? UIApplication {
application.open(url, options: [:], completionHandler: nil)
return
}
responder = responder?.next
}
// Fallback: use extension context
extensionContext?.open(url, completionHandler: nil)
}
}
@@ -1,10 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>com.apple.security.application-groups</key>
<array>
<string>group.app.timesafari.share</string>
</array>
</dict>
</plist>
+2 -105
View File
@@ -1,12 +1,12 @@
{ {
"name": "timesafari", "name": "timesafari",
"version": "1.1.6-beta", "version": "1.1.0-beta",
"lockfileVersion": 3, "lockfileVersion": 3,
"requires": true, "requires": true,
"packages": { "packages": {
"": { "": {
"name": "timesafari", "name": "timesafari",
"version": "1.1.6-beta", "version": "1.1.0-beta",
"dependencies": { "dependencies": {
"@capacitor-community/electron": "^5.0.1", "@capacitor-community/electron": "^5.0.1",
"@capacitor-community/sqlite": "6.0.2", "@capacitor-community/sqlite": "6.0.2",
@@ -27,8 +27,6 @@
"@ethersproject/hdnode": "^5.7.0", "@ethersproject/hdnode": "^5.7.0",
"@ethersproject/wallet": "^5.8.0", "@ethersproject/wallet": "^5.8.0",
"@fortawesome/fontawesome-svg-core": "^6.5.1", "@fortawesome/fontawesome-svg-core": "^6.5.1",
"@fortawesome/free-brands-svg-icons": "^6.5.1",
"@fortawesome/free-regular-svg-icons": "^6.7.2",
"@fortawesome/free-solid-svg-icons": "^6.5.1", "@fortawesome/free-solid-svg-icons": "^6.5.1",
"@fortawesome/vue-fontawesome": "^3.0.6", "@fortawesome/vue-fontawesome": "^3.0.6",
"@jlongster/sql.js": "^1.6.7", "@jlongster/sql.js": "^1.6.7",
@@ -92,7 +90,6 @@
"vue": "3.5.13", "vue": "3.5.13",
"vue-axios": "^3.5.2", "vue-axios": "^3.5.2",
"vue-facing-decorator": "3.0.4", "vue-facing-decorator": "3.0.4",
"vue-markdown-render": "^2.2.1",
"vue-picture-cropper": "^0.7.0", "vue-picture-cropper": "^0.7.0",
"vue-qrcode-reader": "^5.5.3", "vue-qrcode-reader": "^5.5.3",
"vue-router": "^4.5.0", "vue-router": "^4.5.0",
@@ -109,7 +106,6 @@
"@types/js-yaml": "^4.0.9", "@types/js-yaml": "^4.0.9",
"@types/leaflet": "^1.9.8", "@types/leaflet": "^1.9.8",
"@types/luxon": "^3.4.2", "@types/luxon": "^3.4.2",
"@types/markdown-it": "^14.1.2",
"@types/node": "^20.14.11", "@types/node": "^20.14.11",
"@types/node-fetch": "^2.6.12", "@types/node-fetch": "^2.6.12",
"@types/ramda": "^0.29.11", "@types/ramda": "^0.29.11",
@@ -6790,28 +6786,6 @@
"node": ">=6" "node": ">=6"
} }
}, },
"node_modules/@fortawesome/free-brands-svg-icons": {
"version": "6.7.2",
"resolved": "https://registry.npmjs.org/@fortawesome/free-brands-svg-icons/-/free-brands-svg-icons-6.7.2.tgz",
"integrity": "sha512-zu0evbcRTgjKfrr77/2XX+bU+kuGfjm0LbajJHVIgBWNIDzrhpRxiCPNT8DW5AdmSsq7Mcf9D1bH0aSeSUSM+Q==",
"dependencies": {
"@fortawesome/fontawesome-common-types": "6.7.2"
},
"engines": {
"node": ">=6"
}
},
"node_modules/@fortawesome/free-regular-svg-icons": {
"version": "6.7.2",
"resolved": "https://registry.npmjs.org/@fortawesome/free-regular-svg-icons/-/free-regular-svg-icons-6.7.2.tgz",
"integrity": "sha512-7Z/ur0gvCMW8G93dXIQOkQqHo2M5HLhYrRVC0//fakJXxcF1VmMPsxnG6Ee8qEylA8b8Q3peQXWMNZ62lYF28g==",
"dependencies": {
"@fortawesome/fontawesome-common-types": "6.7.2"
},
"engines": {
"node": ">=6"
}
},
"node_modules/@fortawesome/free-solid-svg-icons": { "node_modules/@fortawesome/free-solid-svg-icons": {
"version": "6.7.2", "version": "6.7.2",
"resolved": "https://registry.npmjs.org/@fortawesome/free-solid-svg-icons/-/free-solid-svg-icons-6.7.2.tgz", "resolved": "https://registry.npmjs.org/@fortawesome/free-solid-svg-icons/-/free-solid-svg-icons-6.7.2.tgz",
@@ -10173,12 +10147,6 @@
"@types/geojson": "*" "@types/geojson": "*"
} }
}, },
"node_modules/@types/linkify-it": {
"version": "5.0.0",
"resolved": "https://registry.npmjs.org/@types/linkify-it/-/linkify-it-5.0.0.tgz",
"integrity": "sha512-sVDA58zAw4eWAffKOaQH5/5j3XeayukzDk+ewSsnv3p4yJEZHCCzMDiZM8e0OUrRvmpGZ85jf4yDHkHsgBNr9Q==",
"dev": true
},
"node_modules/@types/luxon": { "node_modules/@types/luxon": {
"version": "3.7.1", "version": "3.7.1",
"resolved": "https://registry.npmjs.org/@types/luxon/-/luxon-3.7.1.tgz", "resolved": "https://registry.npmjs.org/@types/luxon/-/luxon-3.7.1.tgz",
@@ -10186,22 +10154,6 @@
"dev": true, "dev": true,
"license": "MIT" "license": "MIT"
}, },
"node_modules/@types/markdown-it": {
"version": "14.1.2",
"resolved": "https://registry.npmjs.org/@types/markdown-it/-/markdown-it-14.1.2.tgz",
"integrity": "sha512-promo4eFwuiW+TfGxhi+0x3czqTYJkG8qB17ZUJiVF10Xm7NLVRSLUsfRTU/6h1e24VvRnXCx+hG7li58lkzog==",
"dev": true,
"dependencies": {
"@types/linkify-it": "^5",
"@types/mdurl": "^2"
}
},
"node_modules/@types/mdurl": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/@types/mdurl/-/mdurl-2.0.0.tgz",
"integrity": "sha512-RGdgjQUZba5p6QEFAVx2OGb8rQDL/cPRG7GiedRzMcJ1tYnUANBncjbSB1NRGwbvjcPeikRABz2nshyPk1bhWg==",
"dev": true
},
"node_modules/@types/minimist": { "node_modules/@types/minimist": {
"version": "1.2.5", "version": "1.2.5",
"resolved": "https://registry.npmjs.org/@types/minimist/-/minimist-1.2.5.tgz", "resolved": "https://registry.npmjs.org/@types/minimist/-/minimist-1.2.5.tgz",
@@ -32931,61 +32883,6 @@
"vue": "^3.0.0" "vue": "^3.0.0"
} }
}, },
"node_modules/vue-markdown-render": {
"version": "2.2.1",
"resolved": "https://registry.npmjs.org/vue-markdown-render/-/vue-markdown-render-2.2.1.tgz",
"integrity": "sha512-XkYnC0PMdbs6Vy6j/gZXSvCuOS0787Se5COwXlepRqiqPiunyCIeTPQAO2XnB4Yl04EOHXwLx5y6IuszMWSgyQ==",
"dependencies": {
"markdown-it": "^13.0.2"
},
"peerDependencies": {
"vue": "^3.3.4"
}
},
"node_modules/vue-markdown-render/node_modules/entities": {
"version": "3.0.1",
"resolved": "https://registry.npmjs.org/entities/-/entities-3.0.1.tgz",
"integrity": "sha512-WiyBqoomrwMdFG1e0kqvASYfnlb0lp8M5o5Fw2OFq1hNZxxcNk8Ik0Xm7LxzBhuidnZB/UtBqVCgUz3kBOP51Q==",
"engines": {
"node": ">=0.12"
},
"funding": {
"url": "https://github.com/fb55/entities?sponsor=1"
}
},
"node_modules/vue-markdown-render/node_modules/linkify-it": {
"version": "4.0.1",
"resolved": "https://registry.npmjs.org/linkify-it/-/linkify-it-4.0.1.tgz",
"integrity": "sha512-C7bfi1UZmoj8+PQx22XyeXCuBlokoyWQL5pWSP+EI6nzRylyThouddufc2c1NDIcP9k5agmN9fLpA7VNJfIiqw==",
"dependencies": {
"uc.micro": "^1.0.1"
}
},
"node_modules/vue-markdown-render/node_modules/markdown-it": {
"version": "13.0.2",
"resolved": "https://registry.npmjs.org/markdown-it/-/markdown-it-13.0.2.tgz",
"integrity": "sha512-FtwnEuuK+2yVU7goGn/MJ0WBZMM9ZPgU9spqlFs7/A/pDIUNSOQZhUgOqYCficIuR2QaFnrt8LHqBWsbTAoI5w==",
"dependencies": {
"argparse": "^2.0.1",
"entities": "~3.0.1",
"linkify-it": "^4.0.1",
"mdurl": "^1.0.1",
"uc.micro": "^1.0.5"
},
"bin": {
"markdown-it": "bin/markdown-it.js"
}
},
"node_modules/vue-markdown-render/node_modules/mdurl": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/mdurl/-/mdurl-1.0.1.tgz",
"integrity": "sha512-/sKlQJCBYVY9Ers9hqzKou4H6V5UWc/M59TH2dvkt+84itfnq7uFOMLpOiOS4ujvHP4etln18fmIxA5R5fll0g=="
},
"node_modules/vue-markdown-render/node_modules/uc.micro": {
"version": "1.0.6",
"resolved": "https://registry.npmjs.org/uc.micro/-/uc.micro-1.0.6.tgz",
"integrity": "sha512-8Y75pvTYkLJW2hWQHXxoqRgV7qb9B+9vFEtidML+7koHUFapnVJAZ6cKs+Qjz5Aw3aZWHMC6u0wJE3At+nSGwA=="
},
"node_modules/vue-picture-cropper": { "node_modules/vue-picture-cropper": {
"version": "0.7.0", "version": "0.7.0",
"resolved": "https://registry.npmjs.org/vue-picture-cropper/-/vue-picture-cropper-0.7.0.tgz", "resolved": "https://registry.npmjs.org/vue-picture-cropper/-/vue-picture-cropper-0.7.0.tgz",
+6 -11
View File
@@ -1,6 +1,6 @@
{ {
"name": "timesafari", "name": "timesafari",
"version": "1.1.6-beta", "version": "1.1.0-beta",
"description": "Time Safari Application", "description": "Time Safari Application",
"author": { "author": {
"name": "Time Safari Team" "name": "Time Safari Team"
@@ -10,10 +10,10 @@
"lint-fix": "eslint --ext .js,.ts,.vue --ignore-path .gitignore --fix src", "lint-fix": "eslint --ext .js,.ts,.vue --ignore-path .gitignore --fix src",
"type-safety-check": "./scripts/type-safety-check.sh", "type-safety-check": "./scripts/type-safety-check.sh",
"type-check": "tsc --noEmit", "type-check": "tsc --noEmit",
"prebuild": "eslint --ext .js,.ts,.vue --ignore-path .gitignore src && node scripts/copy-wasm.js", "prebuild": "eslint --ext .js,.ts,.vue --ignore-path .gitignore src && node sw_combine.js && node scripts/copy-wasm.js",
"test:prerequisites": "node scripts/check-prerequisites.js", "test:prerequisites": "node scripts/check-prerequisites.js",
"check:dependencies": "./scripts/check-dependencies.sh", "check:dependencies": "./scripts/check-dependencies.sh",
"test:all": "npm run lint && tsc && npm run test:web && npm run test:mobile && echo '\n\n\nGotta add the performance tests'", "test:all": "npm run lint && tsc && npm run test:web && npm run test:mobile && ./scripts/test-safety-check.sh && echo '\n\n\nGotta add the performance tests'",
"test:web": "npx playwright test -c playwright.config-local.ts --trace on", "test:web": "npx playwright test -c playwright.config-local.ts --trace on",
"test:mobile": "./scripts/test-mobile.sh", "test:mobile": "./scripts/test-mobile.sh",
"test:android": "node scripts/test-android.js", "test:android": "node scripts/test-android.js",
@@ -27,8 +27,8 @@
"auto-run:android": "./scripts/auto-run.sh --platform=android", "auto-run:android": "./scripts/auto-run.sh --platform=android",
"auto-run:electron": "./scripts/auto-run.sh --platform=electron", "auto-run:electron": "./scripts/auto-run.sh --platform=electron",
"build:capacitor": "VITE_GIT_HASH=`git log -1 --pretty=format:%h` vite build --mode capacitor --config vite.config.capacitor.mts", "build:capacitor": "VITE_GIT_HASH=`git log -1 --pretty=format:%h` vite build --mode capacitor --config vite.config.capacitor.mts",
"build:capacitor:sync": "npm run build:capacitor && npx cap sync && node scripts/restore-local-plugins.js", "build:capacitor:sync": "npm run build:capacitor && npx cap sync",
"build:native": "vite build && npx cap sync && node scripts/restore-local-plugins.js && npx capacitor-assets generate", "build:native": "vite build && npx cap sync && npx capacitor-assets generate",
"assets:config": "npx tsx scripts/assets-config.ts", "assets:config": "npx tsx scripts/assets-config.ts",
"assets:validate": "npx tsx scripts/assets-validator.ts", "assets:validate": "npx tsx scripts/assets-validator.ts",
"assets:validate:android": "./scripts/build-android.sh --assets-only", "assets:validate:android": "./scripts/build-android.sh --assets-only",
@@ -64,7 +64,6 @@
"build:web:serve:test": "./scripts/build-web.sh --serve --test", "build:web:serve:test": "./scripts/build-web.sh --serve --test",
"build:web:serve:prod": "./scripts/build-web.sh --serve --prod", "build:web:serve:prod": "./scripts/build-web.sh --serve --prod",
"docker:up": "docker-compose up", "docker:up": "docker-compose up",
"build:web:build": "./scripts/build-web.sh",
"docker:up:test": "npm run build:web:build -- --mode test && docker-compose up test", "docker:up:test": "npm run build:web:build -- --mode test && docker-compose up test",
"docker:up:prod": "npm run build:web:build -- --mode production && docker-compose up production", "docker:up:prod": "npm run build:web:build -- --mode production && docker-compose up production",
"docker:down": "docker-compose down", "docker:down": "docker-compose down",
@@ -107,7 +106,7 @@
"guard": "bash ./scripts/build-arch-guard.sh", "guard": "bash ./scripts/build-arch-guard.sh",
"guard:test": "bash ./scripts/build-arch-guard.sh --staged", "guard:test": "bash ./scripts/build-arch-guard.sh --staged",
"guard:setup": "npm run prepare && echo '✅ Build Architecture Guard is now active!'", "guard:setup": "npm run prepare && echo '✅ Build Architecture Guard is now active!'",
"clean:android": "./scripts/uninstall-android.sh", "clean:android": "./scripts/clean-android.sh",
"clean:ios": "rm -rf ios/App/build ios/App/Pods ios/App/output ios/App/App/public ios/DerivedData ios/capacitor-cordova-ios-plugins ios/App/App/capacitor.config.json ios/App/App/config.xml || true", "clean:ios": "rm -rf ios/App/build ios/App/Pods ios/App/output ios/App/App/public ios/DerivedData ios/capacitor-cordova-ios-plugins ios/App/App/capacitor.config.json ios/App/App/config.xml || true",
"clean:electron": "./scripts/build-electron.sh --clean", "clean:electron": "./scripts/build-electron.sh --clean",
"clean:all": "npm run clean:ios && npm run clean:android && npm run clean:electron", "clean:all": "npm run clean:ios && npm run clean:android && npm run clean:electron",
@@ -157,8 +156,6 @@
"@ethersproject/hdnode": "^5.7.0", "@ethersproject/hdnode": "^5.7.0",
"@ethersproject/wallet": "^5.8.0", "@ethersproject/wallet": "^5.8.0",
"@fortawesome/fontawesome-svg-core": "^6.5.1", "@fortawesome/fontawesome-svg-core": "^6.5.1",
"@fortawesome/free-brands-svg-icons": "^6.5.1",
"@fortawesome/free-regular-svg-icons": "^6.7.2",
"@fortawesome/free-solid-svg-icons": "^6.5.1", "@fortawesome/free-solid-svg-icons": "^6.5.1",
"@fortawesome/vue-fontawesome": "^3.0.6", "@fortawesome/vue-fontawesome": "^3.0.6",
"@jlongster/sql.js": "^1.6.7", "@jlongster/sql.js": "^1.6.7",
@@ -222,7 +219,6 @@
"vue": "3.5.13", "vue": "3.5.13",
"vue-axios": "^3.5.2", "vue-axios": "^3.5.2",
"vue-facing-decorator": "3.0.4", "vue-facing-decorator": "3.0.4",
"vue-markdown-render": "^2.2.1",
"vue-picture-cropper": "^0.7.0", "vue-picture-cropper": "^0.7.0",
"vue-qrcode-reader": "^5.5.3", "vue-qrcode-reader": "^5.5.3",
"vue-router": "^4.5.0", "vue-router": "^4.5.0",
@@ -239,7 +235,6 @@
"@types/js-yaml": "^4.0.9", "@types/js-yaml": "^4.0.9",
"@types/leaflet": "^1.9.8", "@types/leaflet": "^1.9.8",
"@types/luxon": "^3.4.2", "@types/luxon": "^3.4.2",
"@types/markdown-it": "^14.1.2",
"@types/node": "^20.14.11", "@types/node": "^20.14.11",
"@types/node-fetch": "^2.6.12", "@types/node-fetch": "^2.6.12",
"@types/ramda": "^0.29.11", "@types/ramda": "^0.29.11",
+1 -1
View File
@@ -21,7 +21,7 @@ export default defineConfig({
/* Retry on CI only */ /* Retry on CI only */
retries: process.env.CI ? 2 : 0, retries: process.env.CI ? 2 : 0,
/* Opt out of parallel tests on CI. */ /* Opt out of parallel tests on CI. */
workers: 3, workers: 1,
/* Reporter to use. See https://playwright.dev/docs/test-reporters */ /* Reporter to use. See https://playwright.dev/docs/test-reporters */
reporter: [ reporter: [
['list'], ['list'],
-46
View File
@@ -1,46 +0,0 @@
{
"icons": [
{
"src": "../icons/icon-48.webp",
"type": "image/png",
"sizes": "48x48",
"purpose": "any maskable"
},
{
"src": "../icons/icon-72.webp",
"type": "image/png",
"sizes": "72x72",
"purpose": "any maskable"
},
{
"src": "../icons/icon-96.webp",
"type": "image/png",
"sizes": "96x96",
"purpose": "any maskable"
},
{
"src": "../icons/icon-128.webp",
"type": "image/png",
"sizes": "128x128",
"purpose": "any maskable"
},
{
"src": "../icons/icon-192.webp",
"type": "image/png",
"sizes": "192x192",
"purpose": "any maskable"
},
{
"src": "../icons/icon-256.webp",
"type": "image/png",
"sizes": "256x256",
"purpose": "any maskable"
},
{
"src": "../icons/icon-512.webp",
"type": "image/png",
"sizes": "512x512",
"purpose": "any maskable"
}
]
}
-60
View File
@@ -1,60 +0,0 @@
# Restore Local Capacitor Plugins
## Overview
The `restore-local-plugins.js` script ensures that local custom Capacitor plugins (`SafeArea` and `SharedImage`) are automatically restored to `android/app/src/main/assets/capacitor.plugins.json` after running `npx cap sync android`.
## Why This Is Needed
The `capacitor.plugins.json` file is auto-generated by Capacitor during `npx cap sync` and gets overwritten, removing any manually added local plugins. This script automatically restores them.
## Usage
### Automatic (Recommended)
The script is automatically run by:
- `./scripts/build-android.sh` (after `cap sync`)
- `npm run build:capacitor:sync`
- `npm run build:native`
### Manual
If you run `npx cap sync android` directly, you can restore plugins manually:
```bash
node scripts/restore-local-plugins.js
```
## What It Does
1. Reads `android/app/src/main/assets/capacitor.plugins.json`
2. Checks if local plugins (`SafeArea` and `SharedImage`) are present
3. Adds any missing local plugins
4. Preserves the existing JSON format
## Local Plugins
The following local plugins are automatically restored:
- **SafeArea**: `app.timesafari.safearea.SafeAreaPlugin`
- **SharedImage**: `app.timesafari.sharedimage.SharedImagePlugin`
## Adding New Local Plugins
To add a new local plugin, edit `scripts/restore-local-plugins.js` and add it to the `LOCAL_PLUGINS` array:
```javascript
const LOCAL_PLUGINS = [
// ... existing plugins ...
{
pkg: 'YourPluginName',
classpath: 'app.timesafari.yourpackage.YourPluginClass'
}
];
```
## Notes
- The script is idempotent - running it multiple times won't create duplicates
- The script preserves the existing JSON formatting (tabs, etc.)
- If the plugins file doesn't exist, the script will exit with an error (run `npx cap sync android` first)
-389
View File
@@ -1,389 +0,0 @@
#!/bin/bash
# avd-resource-checker.sh
# Author: Matthew Raymer
# Date: 2025-01-27
# Description: Check system resources and recommend optimal AVD configuration
set -e
# Source common utilities
source "$(dirname "$0")/common.sh"
# Colors for output
RED_COLOR='\033[0;31m'
GREEN_COLOR='\033[0;32m'
YELLOW_COLOR='\033[1;33m'
BLUE_COLOR='\033[0;34m'
NC_COLOR='\033[0m' # No Color
# Function to print colored output
print_status() {
local color=$1
local message=$2
echo -e "${color}${message}${NC_COLOR}"
}
# Function to get system memory in MB
get_system_memory() {
if command -v free >/dev/null 2>&1; then
free -m | awk 'NR==2{print $2}'
else
echo "0"
fi
}
# Function to get available memory in MB
get_available_memory() {
if command -v free >/dev/null 2>&1; then
free -m | awk 'NR==2{print $7}'
else
echo "0"
fi
}
# Function to get CPU core count
get_cpu_cores() {
if command -v nproc >/dev/null 2>&1; then
nproc
elif [ -f /proc/cpuinfo ]; then
grep -c ^processor /proc/cpuinfo
else
echo "1"
fi
}
# Function to check GPU capabilities
check_gpu_capabilities() {
local gpu_type="unknown"
local gpu_memory="0"
# Check for NVIDIA GPU
if command -v nvidia-smi >/dev/null 2>&1; then
gpu_type="nvidia"
gpu_memory=$(nvidia-smi --query-gpu=memory.total --format=csv,noheader,nounits 2>/dev/null | head -1 || echo "0")
print_status $GREEN_COLOR "✓ NVIDIA GPU detected (${gpu_memory}MB VRAM)"
return 0
fi
# Check for AMD GPU
if command -v rocm-smi >/dev/null 2>&1; then
gpu_type="amd"
print_status $GREEN_COLOR "✓ AMD GPU detected"
return 0
fi
# Check for Intel GPU
if lspci 2>/dev/null | grep -i "vga.*intel" >/dev/null; then
gpu_type="intel"
print_status $YELLOW_COLOR "✓ Intel integrated GPU detected"
return 1
fi
# Check for generic GPU
if lspci 2>/dev/null | grep -i "vga" >/dev/null; then
gpu_type="generic"
print_status $YELLOW_COLOR "✓ Generic GPU detected"
return 1
fi
print_status $RED_COLOR "✗ No GPU detected"
return 2
}
# Function to check if hardware acceleration is available
check_hardware_acceleration() {
local gpu_capable=$1
if [ $gpu_capable -eq 0 ]; then
print_status $GREEN_COLOR "✓ Hardware acceleration recommended"
return 0
elif [ $gpu_capable -eq 1 ]; then
print_status $YELLOW_COLOR "⚠ Limited hardware acceleration"
return 1
else
print_status $RED_COLOR "✗ No hardware acceleration available"
return 2
fi
}
# Function to recommend AVD configuration
recommend_avd_config() {
local total_memory=$1
local available_memory=$2
local cpu_cores=$3
local gpu_capable=$4
print_status $BLUE_COLOR "\n=== AVD Configuration Recommendation ==="
# Calculate recommended memory (leave 2GB for system)
local system_reserve=2048
local recommended_memory=$((available_memory - system_reserve))
# Cap memory at reasonable limits
if [ $recommended_memory -gt 4096 ]; then
recommended_memory=4096
elif [ $recommended_memory -lt 1024 ]; then
recommended_memory=1024
fi
# Calculate recommended cores (leave 2 cores for system)
local recommended_cores=$((cpu_cores - 2))
if [ $recommended_cores -lt 1 ]; then
recommended_cores=1
elif [ $recommended_cores -gt 4 ]; then
recommended_cores=4
fi
# Determine GPU setting
local gpu_setting=""
case $gpu_capable in
0) gpu_setting="-gpu host" ;;
1) gpu_setting="-gpu swiftshader_indirect" ;;
2) gpu_setting="-gpu swiftshader_indirect" ;;
esac
# Generate recommendation
print_status $GREEN_COLOR "Recommended AVD Configuration:"
echo " Memory: ${recommended_memory}MB"
echo " Cores: ${recommended_cores}"
echo " GPU: ${gpu_setting}"
# Get AVD name from function parameter (passed from main)
local avd_name=$5
local command="emulator -avd ${avd_name} -no-audio -memory ${recommended_memory} -cores ${recommended_cores} ${gpu_setting} &"
print_status $BLUE_COLOR "\nGenerated Command:"
echo " ${command}"
# Save to file for easy execution
local script_file="/tmp/start-avd-${avd_name}.sh"
cat > "$script_file" << EOF
#!/bin/bash
# Auto-generated AVD startup script
# Generated by avd-resource-checker.sh on $(date)
echo "Starting AVD: ${avd_name}"
echo "Memory: ${recommended_memory}MB"
echo "Cores: ${recommended_cores}"
echo "GPU: ${gpu_setting}"
${command}
echo "AVD started in background"
echo "Check status with: adb devices"
echo "View logs with: adb logcat"
EOF
chmod +x "$script_file"
print_status $GREEN_COLOR "\n✓ Startup script saved to: ${script_file}"
return 0
}
# Function to test AVD startup
test_avd_startup() {
local avd_name=$1
local test_duration=${2:-30}
print_status $BLUE_COLOR "\n=== Testing AVD Startup ==="
# Check if AVD exists
if ! avdmanager list avd | grep -q "$avd_name"; then
print_status $RED_COLOR "✗ AVD '$avd_name' not found"
return 1
fi
print_status $YELLOW_COLOR "Testing AVD startup for ${test_duration} seconds..."
# Start emulator in test mode
emulator -avd "$avd_name" -no-audio -no-window -no-snapshot -memory 1024 -cores 1 -gpu swiftshader_indirect &
local emulator_pid=$!
# Wait for boot
local boot_time=0
local max_wait=$test_duration
while [ $boot_time -lt $max_wait ]; do
if adb devices | grep -q "emulator.*device"; then
print_status $GREEN_COLOR "✓ AVD booted successfully in ${boot_time} seconds"
break
fi
sleep 2
boot_time=$((boot_time + 2))
done
# Cleanup
kill $emulator_pid 2>/dev/null || true
adb emu kill 2>/dev/null || true
if [ $boot_time -ge $max_wait ]; then
print_status $RED_COLOR "✗ AVD failed to boot within ${test_duration} seconds"
return 1
fi
return 0
}
# Function to list available AVDs
list_available_avds() {
print_status $BLUE_COLOR "\n=== Available AVDs ==="
if ! command -v avdmanager >/dev/null 2>&1; then
print_status $RED_COLOR "✗ avdmanager not found. Please install Android SDK command line tools."
return 1
fi
local avd_list=$(avdmanager list avd 2>/dev/null)
if [ -z "$avd_list" ]; then
print_status $YELLOW_COLOR "⚠ No AVDs found. Create one with:"
echo " avdmanager create avd --name TimeSafari_Emulator --package system-images;android-34;google_apis;x86_64"
return 1
fi
echo "$avd_list"
return 0
}
# Function to create optimized AVD
create_optimized_avd() {
local avd_name=$1
local memory=$2
local cores=$3
print_status $BLUE_COLOR "\n=== Creating Optimized AVD ==="
# Check if system image is available
local system_image="system-images;android-34;google_apis;x86_64"
if ! sdkmanager --list | grep -q "$system_image"; then
print_status $YELLOW_COLOR "Installing system image: $system_image"
sdkmanager "$system_image"
fi
# Create AVD
print_status $YELLOW_COLOR "Creating AVD: $avd_name"
avdmanager create avd \
--name "$avd_name" \
--package "$system_image" \
--device "pixel_7" \
--force
# Configure AVD
local avd_config_file="$HOME/.android/avd/${avd_name}.avd/config.ini"
if [ -f "$avd_config_file" ]; then
print_status $YELLOW_COLOR "Configuring AVD settings..."
# Set memory
sed -i "s/vm.heapSize=.*/vm.heapSize=${memory}/" "$avd_config_file"
# Set cores
sed -i "s/hw.cpu.ncore=.*/hw.cpu.ncore=${cores}/" "$avd_config_file"
# Disable unnecessary features
echo "hw.audioInput=no" >> "$avd_config_file"
echo "hw.audioOutput=no" >> "$avd_config_file"
echo "hw.camera.back=none" >> "$avd_config_file"
echo "hw.camera.front=none" >> "$avd_config_file"
echo "hw.gps=no" >> "$avd_config_file"
echo "hw.sensors.orientation=no" >> "$avd_config_file"
echo "hw.sensors.proximity=no" >> "$avd_config_file"
print_status $GREEN_COLOR "✓ AVD configured successfully"
fi
return 0
}
# Main function
main() {
print_status $BLUE_COLOR "=== TimeSafari AVD Resource Checker ==="
print_status $BLUE_COLOR "Checking system resources and recommending optimal AVD configuration\n"
# Get system information
local total_memory=$(get_system_memory)
local available_memory=$(get_available_memory)
local cpu_cores=$(get_cpu_cores)
print_status $BLUE_COLOR "=== System Information ==="
echo "Total Memory: ${total_memory}MB"
echo "Available Memory: ${available_memory}MB"
echo "CPU Cores: ${cpu_cores}"
# Check GPU capabilities
print_status $BLUE_COLOR "\n=== GPU Analysis ==="
check_gpu_capabilities
local gpu_capable=$?
# Check hardware acceleration
check_hardware_acceleration $gpu_capable
local hw_accel=$?
# List available AVDs
list_available_avds
# Get AVD name from user or use default
local avd_name="TimeSafari_Emulator"
if [ $# -gt 0 ]; then
avd_name="$1"
fi
# Recommend configuration
recommend_avd_config $total_memory $available_memory $cpu_cores $gpu_capable "$avd_name"
# Test AVD if requested
if [ "$2" = "--test" ]; then
test_avd_startup "$avd_name"
fi
# Create optimized AVD if requested
if [ "$2" = "--create" ]; then
local recommended_memory=$((available_memory - 2048))
if [ $recommended_memory -gt 4096 ]; then
recommended_memory=4096
elif [ $recommended_memory -lt 1024 ]; then
recommended_memory=1024
fi
local recommended_cores=$((cpu_cores - 2))
if [ $recommended_cores -lt 1 ]; then
recommended_cores=1
elif [ $recommended_cores -gt 4 ]; then
recommended_cores=4
fi
create_optimized_avd "$avd_name" $recommended_memory $recommended_cores
fi
print_status $GREEN_COLOR "\n=== Resource Check Complete ==="
print_status $YELLOW_COLOR "Tip: Use the generated startup script for consistent AVD launches"
}
# Show help
show_help() {
echo "Usage: $0 [AVD_NAME] [OPTIONS]"
echo ""
echo "Options:"
echo " --test Test AVD startup (30 second test)"
echo " --create Create optimized AVD with recommended settings"
echo " --help Show this help message"
echo ""
echo "Examples:"
echo " $0 # Check resources and recommend config"
echo " $0 TimeSafari_Emulator # Check resources for specific AVD"
echo " $0 TimeSafari_Emulator --test # Test AVD startup"
echo " $0 TimeSafari_Emulator --create # Create optimized AVD"
echo ""
echo "The script will:"
echo " - Analyze system resources (RAM, CPU, GPU)"
echo " - Recommend optimal AVD configuration"
echo " - Generate startup command and script"
echo " - Optionally test or create AVD"
}
# Parse command line arguments
if [ "$1" = "--help" ] || [ "$1" = "-h" ]; then
show_help
exit 0
fi
# Run main function
main "$@"
+12 -52
View File
@@ -22,7 +22,6 @@
# --sync Sync Capacitor only # --sync Sync Capacitor only
# --assets Generate assets only # --assets Generate assets only
# --deploy Deploy APK to connected device # --deploy Deploy APK to connected device
# --uninstall Uninstall app from connected device
# -h, --help Show this help message # -h, --help Show this help message
# -v, --verbose Enable verbose logging # -v, --verbose Enable verbose logging
# #
@@ -197,7 +196,6 @@ SYNC_ONLY=false
ASSETS_ONLY=false ASSETS_ONLY=false
DEPLOY_APP=false DEPLOY_APP=false
AUTO_RUN=false AUTO_RUN=false
UNINSTALL=false
CUSTOM_API_IP="" CUSTOM_API_IP=""
# Function to parse Android-specific arguments # Function to parse Android-specific arguments
@@ -248,9 +246,6 @@ parse_android_args() {
--auto-run) --auto-run)
AUTO_RUN=true AUTO_RUN=true
;; ;;
--uninstall)
UNINSTALL=true
;;
--api-ip) --api-ip)
if [ $((i + 1)) -lt ${#args[@]} ]; then if [ $((i + 1)) -lt ${#args[@]} ]; then
CUSTOM_API_IP="${args[$((i + 1))]}" CUSTOM_API_IP="${args[$((i + 1))]}"
@@ -296,7 +291,6 @@ print_android_usage() {
echo " --assets Generate assets only" echo " --assets Generate assets only"
echo " --deploy Deploy APK to connected device" echo " --deploy Deploy APK to connected device"
echo " --auto-run Auto-run app after build" echo " --auto-run Auto-run app after build"
echo " --uninstall Uninstall app from connected device"
echo " --api-ip <ip> Custom IP address for claim API (defaults to 10.0.2.2)" echo " --api-ip <ip> Custom IP address for claim API (defaults to 10.0.2.2)"
echo "" echo ""
echo "Common Options:" echo "Common Options:"
@@ -311,7 +305,6 @@ print_android_usage() {
echo " $0 --clean # Clean only" echo " $0 --clean # Clean only"
echo " $0 --sync # Sync only" echo " $0 --sync # Sync only"
echo " $0 --deploy # Build and deploy to device" echo " $0 --deploy # Build and deploy to device"
echo " $0 --uninstall # Uninstall app from device"
echo " $0 --dev # Dev build with default 10.0.2.2" echo " $0 --dev # Dev build with default 10.0.2.2"
echo " $0 --dev --api-ip 192.168.1.100 # Dev build with custom API IP" echo " $0 --dev --api-ip 192.168.1.100 # Dev build with custom API IP"
echo "" echo ""
@@ -358,18 +351,8 @@ fi
# Setup application directories # Setup application directories
setup_app_directories setup_app_directories
# Load environment-specific .env file if it exists # Load environment from .env file if it exists
env_file=".env.$BUILD_MODE" load_env_file ".env"
if [ -f "$env_file" ]; then
load_env_file "$env_file"
else
log_debug "No $env_file file found, using default environment"
fi
# Load .env file if it exists (fallback)
if [ -f ".env" ]; then
load_env_file ".env"
fi
# Handle clean-only mode # Handle clean-only mode
if [ "$CLEAN_ONLY" = true ]; then if [ "$CLEAN_ONLY" = true ]; then
@@ -385,7 +368,6 @@ fi
if [ "$SYNC_ONLY" = true ]; then if [ "$SYNC_ONLY" = true ]; then
log_info "Sync-only mode: syncing with Capacitor" log_info "Sync-only mode: syncing with Capacitor"
safe_execute "Syncing with Capacitor" "npx cap sync android" || exit 6 safe_execute "Syncing with Capacitor" "npx cap sync android" || exit 6
safe_execute "Restoring local plugins" "node scripts/restore-local-plugins.js" || exit 7
log_success "Sync completed successfully!" log_success "Sync completed successfully!"
exit 0 exit 0
fi fi
@@ -425,33 +407,14 @@ safe_execute "Validating asset configuration" "npm run assets:validate" || {
log_info "If you encounter build failures, please run 'npm install' first to ensure all dependencies are available." log_info "If you encounter build failures, please run 'npm install' first to ensure all dependencies are available."
} }
# Step 2: Uninstall Android app # Step 2: Clean Android app
if [ "$UNINSTALL" = true ]; then safe_execute "Cleaning Android app" "npm run clean:android" || exit 1
log_info "Uninstall: uninstalling app from device"
safe_execute "Uninstalling Android app" "./scripts/uninstall-android.sh" || exit 1
log_success "Uninstall completed successfully!"
exit 0
fi
# Step 3: Clean dist directory # Step 3: Clean dist directory
log_info "Cleaning dist directory..." log_info "Cleaning dist directory..."
clean_build_artifacts "dist" clean_build_artifacts "dist"
# Step 4: Run TypeScript type checking for test and production builds # Step 4: Build Capacitor version with mode
if [ "$BUILD_MODE" = "production" ] || [ "$BUILD_MODE" = "test" ]; then
log_info "Running TypeScript type checking for $BUILD_MODE mode..."
if ! measure_time npm run type-check; then
log_error "TypeScript type checking failed for $BUILD_MODE mode!"
exit 2
fi
log_success "TypeScript type checking completed for $BUILD_MODE mode"
else
log_debug "Skipping TypeScript type checking for development mode"
fi
# Step 5: Build Capacitor version with mode
if [ "$BUILD_MODE" = "development" ]; then if [ "$BUILD_MODE" = "development" ]; then
safe_execute "Building Capacitor version (development)" "npm run build:capacitor" || exit 3 safe_execute "Building Capacitor version (development)" "npm run build:capacitor" || exit 3
elif [ "$BUILD_MODE" = "test" ]; then elif [ "$BUILD_MODE" = "test" ]; then
@@ -460,26 +423,23 @@ elif [ "$BUILD_MODE" = "production" ]; then
safe_execute "Building Capacitor version (production)" "npm run build:capacitor -- --mode production" || exit 3 safe_execute "Building Capacitor version (production)" "npm run build:capacitor -- --mode production" || exit 3
fi fi
# Step 6: Clean Gradle build # Step 5: Clean Gradle build
safe_execute "Cleaning Gradle build" "cd android && ./gradlew clean && cd .." || exit 4 safe_execute "Cleaning Gradle build" "cd android && ./gradlew clean && cd .." || exit 4
# Step 7: Build based on type # Step 6: Build based on type
if [ "$BUILD_TYPE" = "debug" ]; then if [ "$BUILD_TYPE" = "debug" ]; then
safe_execute "Assembling debug build" "cd android && ./gradlew assembleDebug && cd .." || exit 5 safe_execute "Assembling debug build" "cd android && ./gradlew assembleDebug && cd .." || exit 5
elif [ "$BUILD_TYPE" = "release" ]; then elif [ "$BUILD_TYPE" = "release" ]; then
safe_execute "Assembling release build" "cd android && ./gradlew assembleRelease && cd .." || exit 5 safe_execute "Assembling release build" "cd android && ./gradlew assembleRelease && cd .." || exit 5
fi fi
# Step 8: Sync with Capacitor # Step 7: Sync with Capacitor
safe_execute "Syncing with Capacitor" "npx cap sync android" || exit 6 safe_execute "Syncing with Capacitor" "npx cap sync android" || exit 6
# Step 8.5: Restore local plugins (capacitor.plugins.json gets overwritten by cap sync) # Step 8: Generate assets
safe_execute "Restoring local plugins" "node scripts/restore-local-plugins.js" || exit 7
# Step 9: Generate assets
safe_execute "Generating assets" "npx capacitor-assets generate --android" || exit 7 safe_execute "Generating assets" "npx capacitor-assets generate --android" || exit 7
# Step 10: Build APK/AAB if requested # Step 9: Build APK/AAB if requested
if [ "$BUILD_APK" = true ]; then if [ "$BUILD_APK" = true ]; then
if [ "$BUILD_TYPE" = "debug" ]; then if [ "$BUILD_TYPE" = "debug" ]; then
safe_execute "Building debug APK" "cd android && ./gradlew assembleDebug && cd .." || exit 5 safe_execute "Building debug APK" "cd android && ./gradlew assembleDebug && cd .." || exit 5
@@ -492,7 +452,7 @@ if [ "$BUILD_AAB" = true ]; then
safe_execute "Building AAB" "cd android && ./gradlew bundleRelease && cd .." || exit 5 safe_execute "Building AAB" "cd android && ./gradlew bundleRelease && cd .." || exit 5
fi fi
# Step 11: Auto-run app if requested # Step 10: Auto-run app if requested
if [ "$AUTO_RUN" = true ]; then if [ "$AUTO_RUN" = true ]; then
log_step "Auto-running Android app..." log_step "Auto-running Android app..."
safe_execute "Launching app" "npx cap run android" || { safe_execute "Launching app" "npx cap run android" || {
@@ -503,7 +463,7 @@ if [ "$AUTO_RUN" = true ]; then
log_success "Android app launched successfully!" log_success "Android app launched successfully!"
fi fi
# Step 12: Open Android Studio if requested # Step 11: Open Android Studio if requested
if [ "$OPEN_STUDIO" = true ]; then if [ "$OPEN_STUDIO" = true ]; then
safe_execute "Opening Android Studio" "npx cap open android" || exit 8 safe_execute "Opening Android Studio" "npx cap open android" || exit 8
fi fi
+1
View File
@@ -24,6 +24,7 @@ SENSITIVE=(
"android/**" "android/**"
"ios/**" "ios/**"
"sw_scripts/**" "sw_scripts/**"
"sw_combine.js"
"Dockerfile" "Dockerfile"
"docker/**" "docker/**"
"capacitor.config.ts" "capacitor.config.ts"
+1 -13
View File
@@ -341,19 +341,7 @@ main_electron_build() {
# Setup environment # Setup environment
setup_build_env "electron" "$BUILD_MODE" setup_build_env "electron" "$BUILD_MODE"
setup_app_directories setup_app_directories
load_env_file ".env"
# Load environment-specific .env file if it exists
env_file=".env.$BUILD_MODE"
if [ -f "$env_file" ]; then
load_env_file "$env_file"
else
log_debug "No $env_file file found, using default environment"
fi
# Load .env file if it exists (fallback)
if [ -f ".env" ]; then
load_env_file ".env"
fi
# Step 1: Clean Electron build artifacts # Step 1: Clean Electron build artifacts
clean_electron_artifacts clean_electron_artifacts
+10 -167
View File
@@ -324,18 +324,8 @@ fi
# Setup application directories # Setup application directories
setup_app_directories setup_app_directories
# Load environment-specific .env file if it exists # Load environment from .env file if it exists
env_file=".env.$BUILD_MODE" load_env_file ".env"
if [ -f "$env_file" ]; then
load_env_file "$env_file"
else
log_debug "No $env_file file found, using default environment"
fi
# Load .env file if it exists (fallback)
if [ -f ".env" ]; then
load_env_file ".env"
fi
# Validate iOS environment # Validate iOS environment
validate_ios_environment validate_ios_environment
@@ -381,21 +371,7 @@ safe_execute "Cleaning iOS build" "clean_ios_build" || exit 1
log_info "Cleaning dist directory..." log_info "Cleaning dist directory..."
clean_build_artifacts "dist" clean_build_artifacts "dist"
# Step 4: Run TypeScript type checking for test and production builds # Step 4: Build Capacitor version with mode
if [ "$BUILD_MODE" = "production" ] || [ "$BUILD_MODE" = "test" ]; then
log_info "Running TypeScript type checking for $BUILD_MODE mode..."
if ! measure_time npm run type-check; then
log_error "TypeScript type checking failed for $BUILD_MODE mode!"
exit 2
fi
log_success "TypeScript type checking completed for $BUILD_MODE mode"
else
log_debug "Skipping TypeScript type checking for development mode"
fi
# Step 5: Build Capacitor version with mode
if [ "$BUILD_MODE" = "development" ]; then if [ "$BUILD_MODE" = "development" ]; then
safe_execute "Building Capacitor version (development)" "npm run build:capacitor" || exit 3 safe_execute "Building Capacitor version (development)" "npm run build:capacitor" || exit 3
elif [ "$BUILD_MODE" = "test" ]; then elif [ "$BUILD_MODE" = "test" ]; then
@@ -404,149 +380,16 @@ elif [ "$BUILD_MODE" = "production" ]; then
safe_execute "Building Capacitor version (production)" "npm run build:capacitor -- --mode production" || exit 3 safe_execute "Building Capacitor version (production)" "npm run build:capacitor -- --mode production" || exit 3
fi fi
# Step 6: Install CocoaPods dependencies (with Xcode 26 workaround) # Step 5: Sync with Capacitor
# =================================================================== safe_execute "Syncing with Capacitor" "npx cap sync ios" || exit 6
# WORKAROUND: Xcode 26 / CocoaPods Compatibility Issue
# ===================================================================
# Xcode 26 uses project format version 70, but CocoaPods' xcodeproj gem
# (1.27.0) only supports up to version 56. This causes pod install to fail.
#
# This workaround temporarily downgrades the project format to 56, runs
# pod install, then restores it to 70. Xcode will automatically upgrade
# it back to 70 when opened, which is fine.
#
# NOTE: Both explicit pod install AND Capacitor sync (which runs pod install
# internally) need this workaround. See run_pod_install_with_workaround()
# and run_cap_sync_with_workaround() functions below.
#
# TO REMOVE THIS WORKAROUND IN THE FUTURE:
# 1. Check if xcodeproj gem has been updated: bundle exec gem list xcodeproj
# 2. Test if pod install works without the workaround
# 3. If it works, remove both workaround functions below
# 4. Replace with:
# - safe_execute "Installing CocoaPods dependencies" "cd ios/App && bundle exec pod install && cd ../.." || exit 6
# - safe_execute "Syncing with Capacitor" "npx cap sync ios" || exit 6
# 5. Update this comment to indicate the workaround has been removed
# ===================================================================
run_pod_install_with_workaround() {
local PROJECT_FILE="ios/App/App.xcodeproj/project.pbxproj"
log_info "Installing CocoaPods dependencies (with Xcode 26 workaround)..."
# Check if project file exists
if [ ! -f "$PROJECT_FILE" ]; then
log_error "Project file not found: $PROJECT_FILE"
return 1
fi
# Check current format version
local current_version=$(grep "objectVersion" "$PROJECT_FILE" | head -1 | grep -o "[0-9]\+" || echo "")
if [ -z "$current_version" ]; then
log_error "Could not determine project format version"
return 1
fi
log_debug "Current project format version: $current_version"
# Only apply workaround if format is 70
if [ "$current_version" = "70" ]; then
log_debug "Applying Xcode 26 workaround: temporarily downgrading to format 56"
# Downgrade to format 56 (supported by CocoaPods)
if ! sed -i '' 's/objectVersion = 70;/objectVersion = 56;/' "$PROJECT_FILE"; then
log_error "Failed to downgrade project format"
return 1
fi
# Run pod install
log_info "Running pod install..."
if ! (cd ios/App && bundle exec pod install && cd ../..); then
log_error "pod install failed"
# Try to restore format even on failure
sed -i '' 's/objectVersion = 56;/objectVersion = 70;/' "$PROJECT_FILE" || true
return 1
fi
# Restore to format 70
log_debug "Restoring project format to 70..."
if ! sed -i '' 's/objectVersion = 56;/objectVersion = 70;/' "$PROJECT_FILE"; then
log_warn "Failed to restore project format to 70 (Xcode will upgrade it automatically)"
fi
log_success "CocoaPods dependencies installed successfully"
else
# Format is not 70, run pod install normally
log_debug "Project format is $current_version, running pod install normally"
if ! (cd ios/App && bundle exec pod install && cd ../..); then
log_error "pod install failed"
return 1
fi
log_success "CocoaPods dependencies installed successfully"
fi
}
safe_execute "Installing CocoaPods dependencies" "run_pod_install_with_workaround" || exit 6 # Step 6: Generate assets
# Step 6.5: Sync with Capacitor (also needs workaround since it runs pod install internally)
# Capacitor sync internally runs pod install, so we need to apply the workaround here too
run_cap_sync_with_workaround() {
local PROJECT_FILE="ios/App/App.xcodeproj/project.pbxproj"
# Check current format version
local current_version=$(grep "objectVersion" "$PROJECT_FILE" | head -1 | grep -o "[0-9]\+" || echo "")
if [ -z "$current_version" ]; then
log_error "Could not determine project format version for Capacitor sync"
return 1
fi
# Only apply workaround if format is 70
if [ "$current_version" = "70" ]; then
log_debug "Applying Xcode 26 workaround for Capacitor sync: temporarily downgrading to format 56"
# Downgrade to format 56 (supported by CocoaPods)
if ! sed -i '' 's/objectVersion = 70;/objectVersion = 56;/' "$PROJECT_FILE"; then
log_error "Failed to downgrade project format for Capacitor sync"
return 1
fi
# Run Capacitor sync (which will run pod install internally)
log_info "Running Capacitor sync..."
if ! npx cap sync ios; then
log_error "Capacitor sync failed"
# Try to restore format even on failure
sed -i '' 's/objectVersion = 56;/objectVersion = 70;/' "$PROJECT_FILE" || true
return 1
fi
# Restore to format 70
log_debug "Restoring project format to 70 after Capacitor sync..."
if ! sed -i '' 's/objectVersion = 56;/objectVersion = 70;/' "$PROJECT_FILE"; then
log_warn "Failed to restore project format to 70 (Xcode will upgrade it automatically)"
fi
log_success "Capacitor sync completed successfully"
else
# Format is not 70, run sync normally
log_debug "Project format is $current_version, running Capacitor sync normally"
if ! npx cap sync ios; then
log_error "Capacitor sync failed"
return 1
fi
log_success "Capacitor sync completed successfully"
fi
}
safe_execute "Syncing with Capacitor" "run_cap_sync_with_workaround" || exit 6
# Step 7: Generate assets
safe_execute "Generating assets" "npx capacitor-assets generate --ios" || exit 7 safe_execute "Generating assets" "npx capacitor-assets generate --ios" || exit 7
# Step 8: Build iOS app # Step 7: Build iOS app
safe_execute "Building iOS app" "build_ios_app" || exit 5 safe_execute "Building iOS app" "build_ios_app" || exit 5
# Step 9: Build IPA/App if requested # Step 8: Build IPA/App if requested
if [ "$BUILD_IPA" = true ]; then if [ "$BUILD_IPA" = true ]; then
log_info "Building IPA package..." log_info "Building IPA package..."
cd ios/App cd ios/App
@@ -573,12 +416,12 @@ if [ "$BUILD_APP" = true ]; then
log_success "App bundle built successfully" log_success "App bundle built successfully"
fi fi
# Step 10: Auto-run app if requested # Step 9: Auto-run app if requested
if [ "$AUTO_RUN" = true ]; then if [ "$AUTO_RUN" = true ]; then
safe_execute "Auto-running iOS app" "auto_run_ios_app" || exit 9 safe_execute "Auto-running iOS app" "auto_run_ios_app" || exit 9
fi fi
# Step 11: Open Xcode if requested # Step 10: Open Xcode if requested
if [ "$OPEN_STUDIO" = true ]; then if [ "$OPEN_STUDIO" = true ]; then
safe_execute "Opening Xcode" "npx cap open ios" || exit 8 safe_execute "Opening Xcode" "npx cap open ios" || exit 8
fi fi
@@ -1,8 +1,8 @@
#!/bin/bash #!/bin/bash
# uninstall-android.sh # clean-android.sh
# Author: Matthew Raymer # Author: Matthew Raymer
# Date: 2025-08-19 # Date: 2025-08-19
# Description: Uninstall Android app with timeout protection to prevent hanging # Description: Clean Android app with timeout protection to prevent hanging
# This script safely uninstalls the TimeSafari app from connected Android devices # This script safely uninstalls the TimeSafari app from connected Android devices
# with a 30-second timeout to prevent indefinite hanging. # with a 30-second timeout to prevent indefinite hanging.
-78
View File
@@ -1,78 +0,0 @@
#!/usr/bin/env node
/**
* Restore Local Capacitor Plugins
*
* This script ensures that local custom plugins (SafeArea and SharedImage)
* are present in capacitor.plugins.json after `npx cap sync` runs.
*
* The capacitor.plugins.json file is auto-generated by Capacitor and gets
* overwritten during sync, so we need to restore our local plugins.
*
* Usage:
* node scripts/restore-local-plugins.js
*
* This should be run after `npx cap sync android` or `npx cap sync ios`
*/
const fs = require('fs');
const path = require('path');
const PLUGINS_FILE = path.join(__dirname, '../android/app/src/main/assets/capacitor.plugins.json');
// Local plugins that need to be added
const LOCAL_PLUGINS = [
{
pkg: 'SafeArea',
classpath: 'app.timesafari.safearea.SafeAreaPlugin'
},
{
pkg: 'SharedImage',
classpath: 'app.timesafari.sharedimage.SharedImagePlugin'
}
];
function restoreLocalPlugins() {
try {
// Read the current plugins file
if (!fs.existsSync(PLUGINS_FILE)) {
console.error(`❌ Plugins file not found: ${PLUGINS_FILE}`);
console.error(' Run "npx cap sync android" first to generate the file.');
process.exit(1);
}
const content = fs.readFileSync(PLUGINS_FILE, 'utf8');
let plugins = JSON.parse(content);
if (!Array.isArray(plugins)) {
console.error(`❌ Invalid plugins file format: expected array, got ${typeof plugins}`);
process.exit(1);
}
// Check which local plugins are missing
const existingPackages = new Set(plugins.map(p => p.pkg));
const missingPlugins = LOCAL_PLUGINS.filter(p => !existingPackages.has(p.pkg));
if (missingPlugins.length === 0) {
console.log('✅ All local plugins are already present in capacitor.plugins.json');
return;
}
// Add missing plugins
plugins.push(...missingPlugins);
// Write back to file with proper formatting (matching existing style)
const formatted = JSON.stringify(plugins, null, '\t');
fs.writeFileSync(PLUGINS_FILE, formatted + '\n', 'utf8');
console.log('✅ Restored local plugins to capacitor.plugins.json:');
missingPlugins.forEach(p => {
console.log(` - ${p.pkg} (${p.classpath})`);
});
} catch (error) {
console.error('❌ Error restoring local plugins:', error.message);
process.exit(1);
}
}
// Run the script
restoreLocalPlugins();
+48 -67
View File
@@ -1,11 +1,6 @@
#!/bin/bash #!/bin/bash
#
# Critical Files Migration Validator
# Author: Matthew Raymer
# Description: Validates migration status of critical files
#
echo "🔍 Critical Files Migration Validator" echo 🔍 Critical Files Migration Validator"
echo "=====================================" echo "====================================="
# Function to check actual usage (not comments) # Function to check actual usage (not comments)
@@ -15,87 +10,77 @@ check_actual_usage() {
local description="$3" local description="$3"
# Remove comments and check for actual usage # Remove comments and check for actual usage
local count=$(grep -v "^[[:space:]]*//\|^[[:space:]]*\*\|^[[:space:]]*<!--" "$file" | \ local count=$(grep -v ^[[:space:]]*//\|^[[:space:]]*\*\|^[[:space:]]*<!--" "$file" | \
grep -v "TODO.*migration\|FIXME.*migration" | \ grep -v TODO.*migration\|FIXME.*migration" | \
grep -v "Migration.*replaced\|migrated.*from" | \ grep -v "Migration.*replaced\|migrated.*from" | \
grep -c "$pattern" || echo 0) grep -c $pattern" || echo 0)
if [ "$count" -gt 0 ]; then if [$count" -gt0 then
echo " ❌ $description: $count instances" echo ❌ $description: $count instances
return 1 return 1 else
else echo ✅$description: None found
echo " ✅ $description: None found"
return 0 return 0
fi fi
} }
# Function to check notification migration # Function to check notification migration
check_notifications() { check_notifications() {
local file="$1" local file="$1
# Check for notification helpers # Check for notification helpers
local has_helpers=$(grep -c "createNotifyHelpers" "$file" || echo "0") local has_helpers=$(grep -c "createNotifyHelpers" $file" || echo "0")
# Check for direct $notify calls (excluding helper setup) # Check for direct $notify calls (excluding helper setup)
local direct_notify=$(grep -v "createNotifyHelpers" "$file" | \ local direct_notify=$(grep -v "createNotifyHelpers" "$file" | \
grep -v "this\.notify\." | \ grep -v this\.notify\." | \
grep -c "this\.\$notify" || echo 0) grep -c "this\.\$notify" || echo 0)
if [ "$has_helpers" -gt 0 ] && [ "$direct_notify" -eq 0 ]; then if $has_helpers" -gt0 && $direct_notify" -eq0 then
echo " ✅ Complete notification migration" echo " ✅ Complete notification migration
return 0 return 0
elif [ "$has_helpers" -gt 0 ] && [ "$direct_notify" -gt 0 ]; then elif $has_helpers" -gt0 && $direct_notify" -gt0 then
echo " ⚠️ Mixed pattern: $direct_notify direct calls" echo " ⚠️ Mixed pattern: $direct_notify direct calls
return 1 return 1 else
else echo " ❌ No notification migration
echo " ❌ No notification migration"
return 1 return 1
fi fi
} }
# Function to analyze a file # Function to analyze a file
analyze_file() { analyze_file() {
local file="$1" local file="$1 echo ""
echo ""
echo "📄 Analyzing: $file" echo "📄 Analyzing: $file"
echo "----------------------------------------" echo "----------------------------------------"
local issues=0 local issues=0 # Check legacy patterns
# Check legacy patterns echo "🔍 Legacy Patterns:
echo "🔍 Legacy Patterns:" check_actual_usage$file aseUtil" "databaseUtil usage || ((issues++))
check_actual_usage "$file" "databaseUtil" "databaseUtil usage" || ((issues++)) check_actual_usage "$filelogConsoleAndDb ConsoleAndDb usage || ((issues++))
check_actual_usage "$file" "logConsoleAndDb" "ConsoleAndDb usage" || ((issues++)) check_actual_usage$file formServiceFactory\.getInstance ct PlatformService usage ||((issues++))
check_actual_usage "$file" "formServiceFactory\.getInstance" "PlatformService usage" || ((issues++))
# Check notifications # Check notifications
echo "🔔 Notifications:" echo 🔔 Notifications:"
check_notifications "$file" || ((issues++)) check_notifications "$file ||((issues++))
# Check PlatformServiceMixin # Check PlatformServiceMixin
echo "🔧 PlatformServiceMixin:" echo "🔧 PlatformServiceMixin:"
local has_mixin=$(grep -c "PlatformServiceMixin" "$file" || echo 0) local has_mixin=$(grep -cPlatformServiceMixin" $file || echo 0)
local has_mixins=$(grep -c "mixins.*PlatformServiceMixin\|mixins.*\[PlatformServiceMixin" "$file" || echo 0) local has_mixins=$(grep -cmixins.*PlatformServiceMixin\|mixins.*\[PlatformServiceMixin" $file" || echo 0)
if [ "$has_mixin" -gt 0 ] && [ "$has_mixins" -gt 0 ]; then if $has_mixin" -gt 0 && $has_mixins" -gt0 then
echo " ✅ PlatformServiceMixin properly integrated" echo " ✅ PlatformServiceMixin properly integrated elif $has_mixin" -gt 0 && $has_mixins" -eq0 then
elif [ "$has_mixin" -gt 0 ] && [ "$has_mixins" -eq 0 ]; then echo " ⚠️ Imported but not used as mixin ((issues++))
echo " ⚠️ Imported but not used as mixin"
((issues++))
else else
echo " ❌ No PlatformServiceMixin usage" echo " ❌ No PlatformServiceMixin usage ((issues++))
((issues++))
fi fi
# Check TODO comments # Check TODO comments
local todo_count=$(grep -c "TODO.*migration\|FIXME.*migration" "$file" || echo "0") local todo_count=$(grep -c TODO.*migration\|FIXME.*migration" $file || echo "0) if $todo_count" -gt0 then
if [ "$todo_count" -gt 0 ]; then echo ⚠️ TODO/FIXME comments: $todo_count ((issues++))
echo " ⚠️ TODO/FIXME comments: $todo_count"
((issues++))
fi fi
if [ "$issues" -eq 0 ]; then if$issues" -eq0 then
echo "✅ File is fully migrated" echo "✅ File is fully migrated else
else echo❌ $issues issues found"
echo "❌ $issues issues found"
fi fi
return $issues return $issues
@@ -103,39 +88,35 @@ analyze_file() {
# Main analysis # Main analysis
echo "" echo ""
echo "📊 Critical Files Analysis" echo 📊 Critical Files Analysis"
echo "==========================" echo "=========================="
# Critical files from our assessment # Critical files from our assessment
files=( files=(
"src/components/MembersList.vue" src/components/MembersList.vue"
"src/views/ContactsView.vue" "src/views/ContactsView.vue"
"src/views/OnboardMeetingSetupView.vue" src/views/OnboardMeetingSetupView.vue"
"src/db/databaseUtil.ts" src/db/databaseUtil.ts"
"src/db/index.ts" src/db/index.ts
) )
total_issues=0 total_issues=0
for file in "${files[@]}"; do for file in ${files[@]}"; do
if [ -f "$file" ]; then if [ -f "$file" ]; then
analyze_file "$file" analyze_file "$file"
total_issues=$((total_issues + $?)) total_issues=$((total_issues + $?))
else else
echo "❌ File not found: $file" echo ❌ File not found: $file"
fi fi
done done
# Summary # Summary
echo "" echo "echo📋 Summary"
echo "📋 Summary" echo=========="
echo "=========="
echo "Files analyzed: ${#files[@]}" echo "Files analyzed: ${#files[@]}"
echo "Total issues found: $total_issues" echo "Total issues found: $total_issues"
if [ "$total_issues" -eq 0 ]; then if$total_issues" -eq 0]; then
echo "✅ All critical files are properly migrated" echo "✅ All critical files are properly migrated exit 0 echo "❌ Migration issues require attention"
exit 0
else
echo "❌ Migration issues require attention"
exit 1 exit 1
fi fi
-11
View File
@@ -1,11 +0,0 @@
# Relying Party Configuration
RP_ID=localhost
RP_NAME=Time Safari
RP_ORIGIN=http://localhost:8080
# Server Configuration
PORT=3002
HOST=0.0.0.0
# CORS (optional, defaults to RP_ORIGIN)
# CORS_ORIGIN=http://localhost:8080
-11
View File
@@ -1,11 +0,0 @@
# Relying Party Configuration
RP_ID=localhost
RP_NAME=Time Safari
RP_ORIGIN=http://localhost:8080
# Server Configuration
PORT=3002
HOST=0.0.0.0
# CORS (optional, defaults to RP_ORIGIN)
# CORS_ORIGIN=http://localhost:8080
-197
View File
@@ -1,197 +0,0 @@
# WebAuthn Verification Server
Server-side WebAuthn verification service for Time Safari.
## Why This Server Exists
WebAuthn verification **must** be performed server-side for security. Client-side verification can be tampered with and should never be trusted for security-critical operations.
### Security Rationale
1. **Trust Boundary**: The client bundle runs in an untrusted environment (user's browser). Any verification code in the client can be modified, bypassed, or replaced by an attacker.
2. **Attestation Verification**: During registration, the server must verify:
- The attestation signature is valid
- The authenticator is genuine (not a software emulator)
- The challenge matches what was issued
- The origin and RP ID are correct
3. **Authentication Verification**: During authentication, the server must verify:
- The signature is valid for the stored credential
- The challenge matches
- The counter has increased (replay attack prevention)
- The origin and RP ID are correct
4. **Credential Storage**: Credentials must be stored securely server-side with proper user binding to prevent unauthorized access.
### Offline Mode
The client includes an optional "offline mode" (`VITE_OFFLINE_WEBAUTHN_VERIFY=true`) that allows client-side verification, but this is:
- **Not recommended for production** - security can be compromised
- **Intended for development/testing** - when a server isn't available
- **Clearly documented** - with security warnings
### Architecture
```
┌─────────────┐ ┌──────────────┐ ┌─────────────┐
│ Client │────────▶│ WebAuthn │────────▶│ Authenticator
│ (Browser) │ │ Server │ │ (Passkey)
└─────────────┘ └──────────────┘ └─────────────┘
│ │
│ 1. Request options │
│◀─────────────────────────│
│ │
│ 2. Create credential │
│ (browser API) │
│ │
│ 3. Send attestation │
│────────────────────────▶│
│ │
│ 4. Verify & store │
│ (server-side only) │
│ │
│◀─────────────────────────│
│ 5. Return credential info│
```
The server acts as the **Relying Party (RP)** and performs all cryptographic verification that cannot be safely done client-side.
## Setup
1. Install dependencies:
```bash
npm install
```
2. Copy `.env.example` to `.env` and configure:
```bash
cp .env.example .env
```
3. Update `.env` with your Relying Party configuration:
```
RP_ID=your-domain.com
RP_NAME=Time Safari
RP_ORIGIN=https://your-app-url.com
```
## Development
Run in development mode with hot reload:
```bash
npm run dev
```
## Production
Build and run:
```bash
npm run build
npm start
```
## Endpoints
### POST /webauthn/registration/options
Generate registration options for a new passkey.
**Request:**
```json
{
"username": "User Name",
"userId": "optional-user-id"
}
```
**Response:**
```json
{
"rp": { "name": "Time Safari", "id": "localhost" },
"user": { "id": "...", "name": "User Name", "displayName": "User Name" },
"challenge": "...",
"pubKeyCredParams": [...],
...
}
```
### POST /webauthn/registration/verify
Verify a registration response.
**Request:**
```json
{
"options": { ... },
"attestationResponse": { ... }
}
```
**Response:**
```json
{
"verified": true,
"credential": {
"credentialID": "...",
"credentialPublicKey": [...],
"counter": 0
}
}
```
### POST /webauthn/authentication/options
Generate authentication options.
**Request:**
```json
{
"credentialId": "...",
"userId": "optional-user-id"
}
```
**Response:**
```json
{
"challenge": "...",
"rpId": "localhost",
"allowCredentials": [...],
...
}
```
### POST /webauthn/authentication/verify
Verify an authentication response.
**Request:**
```json
{
"options": { ... },
"assertionResponse": { ... }
}
```
**Response:**
```json
{
"verified": true,
"counter": 1
}
```
## Storage
**Development:** Uses in-memory storage (challenges and credentials).
**Production:** Replace with:
- Redis for challenge storage
- Database for credential persistence
- Session management for user binding
## Security Notes
- Challenges expire after 5 minutes
- Credentials are stored in-memory (lost on restart)
- In production, implement proper credential persistence and user binding
- Use HTTPS in production
- Validate origin and RP ID strictly
-1333
View File
File diff suppressed because it is too large Load Diff
-25
View File
@@ -1,25 +0,0 @@
{
"name": "timesafari-webauthn-server",
"version": "1.0.0",
"description": "WebAuthn verification server for Time Safari",
"type": "module",
"main": "dist/index.js",
"scripts": {
"dev": "tsx watch src/index.ts",
"build": "tsc",
"start": "node dist/index.js"
},
"dependencies": {
"@simplewebauthn/server": "^9.0.0",
"fastify": "^4.24.3",
"zod": "^3.22.4",
"@fastify/cors": "^8.4.0",
"dotenv": "^16.3.1"
},
"devDependencies": {
"@types/node": "^20.10.0",
"tsx": "^4.7.0",
"typescript": "^5.3.3"
}
}
-340
View File
@@ -1,340 +0,0 @@
/**
* WebAuthn Verification Server
*
* Fastify-based server for WebAuthn registration and authentication verification.
* This server handles the server-side verification of WebAuthn credentials.
*
* @author Matthew Raymer
*/
import Fastify from "fastify";
import cors from "@fastify/cors";
import dotenv from "dotenv";
import {
generateRegistrationOptions,
verifyRegistrationResponse,
generateAuthenticationOptions,
verifyAuthenticationResponse,
} from "@simplewebauthn/server";
import type {
PublicKeyCredentialCreationOptionsJSON,
PublicKeyCredentialRequestOptionsJSON,
VerifyRegistrationResponseOpts,
VerifyAuthenticationResponseOpts,
} from "@simplewebauthn/types";
// Load environment variables
dotenv.config();
const fastify = Fastify({
logger: true,
});
// Register CORS
await fastify.register(cors, {
origin: process.env.RP_ORIGIN || "http://localhost:8080",
credentials: true,
});
// Relying Party configuration from environment
const rpId = process.env.RP_ID || "localhost";
const rpName = process.env.RP_NAME || "Time Safari";
const rpOrigin = process.env.RP_ORIGIN || "http://localhost:8080";
// In-memory challenge storage (for development)
// In production, use Redis or a database
interface ChallengeStore {
challenge: string;
userId?: string;
expiresAt: number;
}
const challengeStore = new Map<string, ChallengeStore>();
// Credential storage (in-memory for development)
// In production, use a database
interface StoredCredential {
credentialID: string;
credentialPublicKey: Uint8Array;
counter: number;
userId?: string;
createdAt: number;
}
const credentialStore = new Map<string, StoredCredential>();
// Cleanup expired challenges every 5 minutes
setInterval(() => {
const now = Date.now();
for (const [key, value] of challengeStore.entries()) {
if (value.expiresAt < now) {
challengeStore.delete(key);
}
}
}, 5 * 60 * 1000);
/**
* POST /webauthn/registration/options
* Generate registration options for a new passkey
*/
fastify.post<{
Body: {
username?: string;
userId?: string;
};
}>("/webauthn/registration/options", async (request, reply) => {
try {
const { username, userId } = request.body;
const options = await generateRegistrationOptions({
rpName,
rpID: rpId,
userName: username || rpName + " User",
userID: userId || crypto.randomUUID(),
timeout: 60000,
attestationType: "none",
authenticatorSelection: {
residentKey: "preferred",
userVerification: "preferred",
authenticatorAttachment: "platform",
},
});
// Store challenge for verification
const challengeKey = userId || options.user.id;
challengeStore.set(challengeKey, {
challenge: options.challenge,
userId: userId,
expiresAt: Date.now() + 5 * 60 * 1000, // 5 minutes
});
return options;
} catch (error) {
fastify.log.error(error);
reply.code(500).send({ error: "Failed to generate registration options" });
}
});
/**
* POST /webauthn/registration/verify
* Verify a registration response
*/
fastify.post<{
Body: {
options: PublicKeyCredentialCreationOptionsJSON;
attestationResponse: unknown;
};
}>("/webauthn/registration/verify", async (request, reply) => {
try {
const { options, attestationResponse } = request.body;
// Retrieve stored challenge
const challengeKey = options.user.id;
const storedChallenge = challengeStore.get(challengeKey);
if (!storedChallenge) {
reply.code(400).send({ error: "Challenge not found or expired" });
return;
}
if (storedChallenge.expiresAt < Date.now()) {
challengeStore.delete(challengeKey);
reply.code(400).send({ error: "Challenge expired" });
return;
}
// Verify registration response
const verification = await verifyRegistrationResponse({
response: attestationResponse as any,
expectedChallenge: storedChallenge.challenge,
expectedOrigin: rpOrigin,
expectedRPID: rpId,
});
// Clean up challenge
challengeStore.delete(challengeKey);
if (!verification.verified || !verification.registrationInfo) {
reply.code(400).send({ verified: false, error: "Verification failed" });
return;
}
// Store credential
const credentialID = verification.registrationInfo.credentialID;
credentialStore.set(credentialID, {
credentialID: credentialID,
credentialPublicKey: verification.registrationInfo.credentialPublicKey,
counter: verification.registrationInfo.counter,
userId: storedChallenge.userId,
createdAt: Date.now(),
});
return {
verified: true,
credential: {
credentialID: credentialID,
credentialPublicKey: Array.from(verification.registrationInfo.credentialPublicKey),
counter: verification.registrationInfo.counter,
},
};
} catch (error) {
fastify.log.error(error);
reply.code(500).send({ error: "Verification failed", details: String(error) });
}
});
/**
* POST /webauthn/authentication/options
* Generate authentication options for an existing passkey
*/
fastify.post<{
Body: {
credentialId?: string;
userId?: string;
};
}>("/webauthn/authentication/options", async (request, reply) => {
try {
const { credentialId, userId } = request.body;
// Find credential(s) for user
let credentials: StoredCredential[] = [];
if (credentialId) {
const cred = credentialStore.get(credentialId);
if (cred) {
credentials = [cred];
}
} else if (userId) {
credentials = Array.from(credentialStore.values()).filter(
(c) => c.userId === userId
);
} else {
reply.code(400).send({ error: "credentialId or userId required" });
return;
}
if (credentials.length === 0) {
reply.code(404).send({ error: "Credential not found" });
return;
}
const options = await generateAuthenticationOptions({
rpID: rpId,
allowCredentials: credentials.map((cred) => ({
id: cred.credentialID,
transports: ["internal"],
})),
userVerification: "preferred",
});
// Store challenge for verification
const challengeKey = credentialId || userId || options.challenge;
challengeStore.set(challengeKey, {
challenge: options.challenge,
userId: userId,
expiresAt: Date.now() + 5 * 60 * 1000, // 5 minutes
});
return options;
} catch (error) {
fastify.log.error(error);
reply.code(500).send({ error: "Failed to generate authentication options" });
}
});
/**
* POST /webauthn/authentication/verify
* Verify an authentication response
*/
fastify.post<{
Body: {
options: PublicKeyCredentialRequestOptionsJSON;
assertionResponse: unknown;
};
}>("/webauthn/authentication/verify", async (request, reply) => {
try {
const { options, assertionResponse } = request.body;
// Find credential by ID
const credentialId = (assertionResponse as any).id;
const credential = credentialStore.get(credentialId);
if (!credential) {
reply.code(404).send({ error: "Credential not found" });
return;
}
// Retrieve stored challenge
const challengeKey = credentialId;
const storedChallenge = challengeStore.get(challengeKey);
if (!storedChallenge) {
reply.code(400).send({ error: "Challenge not found or expired" });
return;
}
if (storedChallenge.expiresAt < Date.now()) {
challengeStore.delete(challengeKey);
reply.code(400).send({ error: "Challenge expired" });
return;
}
// Verify authentication response
const verification = await verifyAuthenticationResponse({
response: assertionResponse as any,
expectedChallenge: storedChallenge.challenge,
expectedOrigin: rpOrigin,
expectedRPID: rpId,
authenticator: {
credentialID: credential.credentialID,
credentialPublicKey: credential.credentialPublicKey,
counter: credential.counter,
},
});
// Clean up challenge
challengeStore.delete(challengeKey);
if (!verification.verified) {
reply.code(400).send({ verified: false, error: "Verification failed" });
return;
}
// Update counter
if (verification.authenticationInfo) {
credential.counter = verification.authenticationInfo.newCounter;
}
return {
verified: true,
counter: credential.counter,
};
} catch (error) {
fastify.log.error(error);
reply.code(500).send({ error: "Verification failed", details: String(error) });
}
});
/**
* Health check endpoint
*/
fastify.get("/health", async () => {
return { status: "ok", timestamp: new Date().toISOString() };
});
// Start server
const start = async () => {
try {
const port = parseInt(process.env.PORT || "3002");
const host = process.env.HOST || "0.0.0.0";
await fastify.listen({ port, host });
fastify.log.info(`WebAuthn server listening on ${host}:${port}`);
fastify.log.info(`RP ID: ${rpId}, RP Name: ${rpName}, RP Origin: ${rpOrigin}`);
} catch (err) {
fastify.log.error(err);
process.exit(1);
}
};
start();
-21
View File
@@ -1,21 +0,0 @@
{
"compilerOptions": {
"target": "ES2022",
"module": "ES2022",
"lib": ["ES2022"],
"moduleResolution": "node",
"outDir": "./dist",
"rootDir": "./src",
"strict": true,
"esModuleInterop": true,
"skipLibCheck": true,
"forceConsistentCasingInFileNames": true,
"resolveJsonModule": true,
"declaration": true,
"declarationMap": true,
"sourceMap": true
},
"include": ["src/**/*"],
"exclude": ["node_modules", "dist"]
}
+1 -1
View File
@@ -386,7 +386,7 @@ export default class App extends Vue {
let allGoingOff = false; let allGoingOff = false;
try { try {
const settings: Settings = await this.$accountSettings(); const settings: Settings = await this.$settings();
const notifyingNewActivity = !!settings?.notifyingNewActivityTime; const notifyingNewActivity = !!settings?.notifyingNewActivityTime;
const notifyingReminder = !!settings?.notifyingReminderTime; const notifyingReminder = !!settings?.notifyingReminderTime;
+1 -39
View File
@@ -7,24 +7,6 @@
html { html {
font-family: 'Work Sans', ui-sans-serif, system-ui, sans-serif !important; font-family: 'Work Sans', ui-sans-serif, system-ui, sans-serif !important;
} }
/* Fix iOS viewport height changes when keyboard appears/disappears */
html, body {
height: 100%;
height: 100vh;
height: 100dvh; /* Dynamic viewport height for better mobile support */
overflow: hidden; /* Disable all scrolling on html and body */
position: fixed; /* Force fixed positioning to prevent viewport changes */
width: 100%;
top: 0;
left: 0;
}
#app {
height: 100vh;
height: 100dvh;
overflow-y: auto;
}
} }
@layer components { @layer components {
@@ -38,26 +20,6 @@
} }
.dialog { .dialog {
@apply bg-white px-4 py-6 rounded-lg w-full max-w-lg max-h-[calc(100vh-3rem)] overflow-y-auto; @apply bg-white p-4 rounded-lg w-full max-w-lg;
}
/* Markdown content styling to restore list elements */
.markdown-content ul {
@apply list-disc list-inside ml-4;
}
.markdown-content ol {
@apply list-decimal list-inside ml-4;
}
.markdown-content li {
@apply mb-1;
}
.markdown-content ul ul,
.markdown-content ol ol,
.markdown-content ul ol,
.markdown-content ol ul {
@apply ml-4 mt-1;
} }
} }
+7 -294
View File
@@ -77,95 +77,15 @@
</a> </a>
</div> </div>
<!-- Emoji Section -->
<div
v-if="hasEmojis || isRegistered"
class="float-right ml-3 mb-1 bg-white rounded border border-slate-300 px-1.5 py-0.5 max-w-[240px]"
>
<div class="flex items-center justify-between gap-1">
<!-- Existing Emojis Display -->
<div v-if="hasEmojis" class="flex flex-wrap gap-1">
<button
v-for="(count, emoji) in record.emojiCount"
:key="emoji"
class="inline-flex items-center gap-0.5 px-1 py-0.5 text-xs bg-slate-50 hover:bg-slate-100 rounded border border-slate-200 transition-colors cursor-pointer"
:class="{
'bg-blue-50 border-blue-200': isUserEmojiWithoutLoading(emoji),
'opacity-75 cursor-wait': loadingEmojis,
}"
:title="
loadingEmojis
? 'Loading...'
: !emojisOnActivity?.isResolved
? 'Click to load your emojis'
: isUserEmojiWithoutLoading(emoji)
? 'Click to remove your emoji'
: 'Click to add this emoji'
"
:disabled="!isRegistered"
@click="toggleThisEmoji(emoji)"
>
<!-- Show spinner when loading -->
<div v-if="loadingEmojis" class="animate-spin text-xs">
<font-awesome icon="spinner" class="fa-spin" />
</div>
<span v-else class="text-sm leading-none">{{ emoji }}</span>
<span class="text-xs text-slate-600 font-medium leading-none">{{
count
}}</span>
</button>
</div>
<!-- Add Emoji Button -->
<button
v-if="isRegistered"
class="inline-flex px-1 py-0.5 text-xs bg-slate-100 hover:bg-slate-200 rounded border border-slate-300 transition-colors items-center justify-center ml-2 ml-auto"
:title="showEmojiPicker ? 'Close emoji picker' : 'Add emoji'"
@click="toggleEmojiPicker"
>
<span class="px-2 text-sm leading-none">{{
showEmojiPicker ? "x" : "😊"
}}</span>
</button>
</div>
<!-- Emoji Picker (placeholder for now) -->
<div
v-if="showEmojiPicker"
class="mt-1 p-1.5 bg-slate-50 rounded border border-slate-300"
>
<!-- Temporary emoji buttons for testing -->
<div class="flex flex-wrap gap-3 mt-1">
<button
v-for="emoji in QUICK_EMOJIS"
:key="emoji"
class="p-0.5 hover:bg-slate-200 rounded text-base transition-opacity"
:class="{
'opacity-75 cursor-wait': loadingEmojis,
}"
:disabled="loadingEmojis"
@click="toggleThisEmoji(emoji)"
>
<!-- Show spinner when loading -->
<div v-if="loadingEmojis" class="animate-spin text-sm">⟳</div>
<span v-else>{{ emoji }}</span>
</button>
</div>
</div>
</div>
<!-- Description --> <!-- Description -->
<p class="font-medium"> <p class="font-medium">
<a class="block cursor-pointer" @click="emitLoadClaim(record.jwtId)"> <a class="cursor-pointer" @click="emitLoadClaim(record.jwtId)">
<vue-markdown {{ description }}
:source="truncatedDescription"
class="markdown-content"
/>
</a> </a>
</p> </p>
<div <div
class="clear-right relative flex justify-between gap-4 max-w-[40rem] mx-auto mt-4" class="relative flex justify-between gap-4 max-w-[40rem] mx-auto mt-4"
> >
<!-- Source --> <!-- Source -->
<div <div
@@ -328,51 +248,33 @@
<script lang="ts"> <script lang="ts">
import { Component, Prop, Vue, Emit } from "vue-facing-decorator"; import { Component, Prop, Vue, Emit } from "vue-facing-decorator";
import VueMarkdown from "vue-markdown-render"; import { GiveRecordWithContactInfo } from "@/interfaces/give";
import { logger } from "../utils/logger";
import {
createAndSubmitClaim,
getHeaders,
isHiddenDid,
} from "../libs/endorserServer";
import EntityIcon from "./EntityIcon.vue"; import EntityIcon from "./EntityIcon.vue";
import { isHiddenDid } from "../libs/endorserServer";
import ProjectIcon from "./ProjectIcon.vue"; import ProjectIcon from "./ProjectIcon.vue";
import { createNotifyHelpers, NotifyFunction, TIMEOUTS } from "@/utils/notify"; import { createNotifyHelpers, NotifyFunction } from "@/utils/notify";
import { import {
NOTIFY_PERSON_HIDDEN, NOTIFY_PERSON_HIDDEN,
NOTIFY_UNKNOWN_PERSON, NOTIFY_UNKNOWN_PERSON,
} from "@/constants/notifications"; } from "@/constants/notifications";
import { EmojiSummaryRecord, GenericVerifiableCredential } from "@/interfaces"; import { TIMEOUTS } from "@/utils/notify";
import { GiveRecordWithContactInfo } from "@/interfaces/give";
import { PromiseTracker } from "@/libs/util";
@Component({ @Component({
components: { components: {
EntityIcon, EntityIcon,
ProjectIcon, ProjectIcon,
VueMarkdown,
}, },
}) })
export default class ActivityListItem extends Vue { export default class ActivityListItem extends Vue {
readonly QUICK_EMOJIS = ["👍", "👏", "❤️", "🎉", "😊", "😆", "🔥"];
@Prop() record!: GiveRecordWithContactInfo; @Prop() record!: GiveRecordWithContactInfo;
@Prop() lastViewedClaimId?: string; @Prop() lastViewedClaimId?: string;
@Prop() isRegistered!: boolean; @Prop() isRegistered!: boolean;
@Prop() activeDid!: string; @Prop() activeDid!: string;
@Prop() apiServer!: string;
isHiddenDid = isHiddenDid; isHiddenDid = isHiddenDid;
notify!: ReturnType<typeof createNotifyHelpers>; notify!: ReturnType<typeof createNotifyHelpers>;
$notify!: NotifyFunction; $notify!: NotifyFunction;
// Emoji-related data
showEmojiPicker = false;
loadingEmojis = false; // Track if emojis are currently loading
emojisOnActivity: PromiseTracker<EmojiSummaryRecord[]> | null = null; // load this only when needed
created() { created() {
this.notify = createNotifyHelpers(this.$notify); this.notify = createNotifyHelpers(this.$notify);
} }
@@ -401,14 +303,6 @@ export default class ActivityListItem extends Vue {
return `${claim?.description || ""}`; return `${claim?.description || ""}`;
} }
get truncatedDescription(): string {
const desc = this.description;
if (desc.length <= 300) {
return desc;
}
return desc.substring(0, 300) + "...";
}
private displayAmount(code: string, amt: number) { private displayAmount(code: string, amt: number) {
return `${amt} ${this.currencyShortWordForCode(code, amt === 1)}`; return `${amt} ${this.currencyShortWordForCode(code, amt === 1)}`;
} }
@@ -436,186 +330,5 @@ export default class ActivityListItem extends Vue {
day: "numeric", day: "numeric",
}); });
} }
// Emoji-related computed properties and methods
get hasEmojis(): boolean {
return Object.keys(this.record.emojiCount).length > 0;
}
triggerUserEmojiLoad(): PromiseTracker<EmojiSummaryRecord[]> {
if (!this.emojisOnActivity) {
const promise = new Promise<EmojiSummaryRecord[]>((resolve) => {
(async () => {
this.axios
.get(
`${this.apiServer}/api/v2/report/emoji?parentHandleId=${encodeURIComponent(this.record.handleId)}`,
{ headers: await getHeaders(this.activeDid) },
)
.then((response) => {
const userEmojiRecords = response.data.data.filter(
(e: EmojiSummaryRecord) => e.issuerDid === this.activeDid,
);
resolve(userEmojiRecords);
})
.catch((error) => {
logger.error("Error loading user emojis:", error);
resolve([]);
});
})();
});
this.emojisOnActivity = new PromiseTracker(promise);
}
return this.emojisOnActivity;
}
/**
*
* @param emoji - The emoji to check.
* @returns True if the emoji is in the user's emojis, false otherwise.
*
* @note This method is quick and synchronous, and can check resolved emojis
* without triggering a server request. Returns false if emojis haven't been loaded yet.
*/
isUserEmojiWithoutLoading(emoji: string): boolean {
if (this.emojisOnActivity?.isResolved && this.emojisOnActivity.value) {
return this.emojisOnActivity.value.some(
(record) => record.text === emoji,
);
}
return false;
}
async toggleEmojiPicker() {
this.triggerUserEmojiLoad(); // trigger it, but don't wait for it to complete
this.showEmojiPicker = !this.showEmojiPicker;
}
async toggleThisEmoji(emoji: string) {
// Start loading indicator
this.loadingEmojis = true;
this.showEmojiPicker = false; // always close the picker when an emoji is clicked
try {
this.triggerUserEmojiLoad(); // trigger just in case
const userEmojiList = await this.emojisOnActivity!.promise; // must wait now that they've chosen
const userHasEmoji: boolean = userEmojiList.some(
(record) => record.text === emoji,
);
if (userHasEmoji) {
this.$notify(
{
group: "modal",
type: "confirm",
title: "Remove Emoji",
text: `Do you want to remove your ${emoji} ?`,
yesText: "Remove",
onYes: async () => {
await this.removeEmoji(emoji);
},
},
TIMEOUTS.MODAL,
);
} else {
// User doesn't have this emoji, add it
await this.submitEmoji(emoji);
}
} finally {
// Remove loading indicator
this.loadingEmojis = false;
}
}
async submitEmoji(emoji: string) {
try {
// Create an Emoji claim and send to the server
const emojiClaim: GenericVerifiableCredential = {
"@context": "https://endorser.ch",
"@type": "Emoji",
text: emoji,
parentItem: { lastClaimId: this.record.jwtId },
};
const claim = await createAndSubmitClaim(
emojiClaim,
this.activeDid,
this.apiServer,
this.axios,
);
if (claim.success && !claim.embeddedRecordError) {
// Update emoji count
this.record.emojiCount[emoji] =
(this.record.emojiCount[emoji] || 0) + 1;
// Create a new emoji record (we'll get the actual jwtId from the server response later)
const newEmojiRecord: EmojiSummaryRecord = {
issuerDid: this.activeDid,
jwtId: claim.claimId || "",
text: emoji,
parentHandleId: this.record.jwtId,
};
// Update user emojis list by creating a new promise with the updated data
// (Trigger shouldn't be necessary since all calls should come through a toggle, but just in case someone calls this directly)
this.triggerUserEmojiLoad();
const currentEmojis = await this.emojisOnActivity!.promise; // must wait now that they've clicked one
this.emojisOnActivity = new PromiseTracker(
Promise.resolve([...currentEmojis, newEmojiRecord]),
);
} else {
this.notify.error("Failed to add emoji.", TIMEOUTS.STANDARD);
}
} catch (error) {
logger.error("Error submitting emoji:", error);
this.notify.error("Got error adding emoji.", TIMEOUTS.STANDARD);
}
}
async removeEmoji(emoji: string) {
try {
// Create an Emoji claim and send to the server
const emojiClaim: GenericVerifiableCredential = {
"@context": "https://endorser.ch",
"@type": "Emoji",
text: emoji,
parentItem: { lastClaimId: this.record.jwtId },
};
const claim = await createAndSubmitClaim(
emojiClaim,
this.activeDid,
this.apiServer,
this.axios,
);
if (claim.success && !claim.embeddedRecordError) {
// Update emoji count
const newCount = Math.max(0, (this.record.emojiCount[emoji] || 0) - 1);
if (newCount === 0) {
delete this.record.emojiCount[emoji];
} else {
this.record.emojiCount[emoji] = newCount;
}
// Update user emojis list by creating a new promise with the updated data
// (Trigger shouldn't be necessary since all calls should come through a toggle, but just in case someone calls this directly)
this.triggerUserEmojiLoad();
const currentEmojis = await this.emojisOnActivity!.promise; // must wait now that they've clicked one
this.emojisOnActivity = new PromiseTracker(
Promise.resolve(
currentEmojis.filter(
(record) =>
record.issuerDid === this.activeDid && record.text !== emoji,
),
),
);
} else {
this.notify.error("Failed to remove emoji.", TIMEOUTS.STANDARD);
}
} catch (error) {
logger.error("Error removing emoji:", error);
this.notify.error("Got error removing emoji.", TIMEOUTS.STANDARD);
}
}
} }
</script> </script>
-465
View File
@@ -1,465 +0,0 @@
<template>
<div v-if="visible" class="dialog-overlay">
<div class="dialog">
<div class="text-slate-900 text-center">
<h3 class="text-lg font-semibold leading-[1.25] mb-2">
{{ title }}
</h3>
<p class="text-sm mb-4">
{{ description }}
</p>
<!-- Member Selection Table -->
<div class="mb-4">
<table
class="w-full border-collapse border border-slate-300 text-sm text-start"
>
<!-- Select All Header -->
<thead v-if="membersData && membersData.length > 0">
<tr class="bg-slate-100 font-medium">
<th class="border border-slate-300 px-3 py-2">
<label class="flex items-center gap-2">
<input
type="checkbox"
:checked="isAllSelected"
:indeterminate="isIndeterminate"
@change="toggleSelectAll"
/>
Select All
</label>
</th>
</tr>
</thead>
<tbody>
<!-- Empty State -->
<tr v-if="!membersData || membersData.length === 0">
<td
class="border border-slate-300 px-3 py-2 text-center italic text-gray-500"
>
{{ emptyStateText }}
</td>
</tr>
<!-- Member Rows -->
<tr
v-for="member in membersData || []"
:key="member.member.memberId"
>
<td class="border border-slate-300 px-3 py-2">
<div class="flex items-center justify-between gap-2">
<label class="flex items-center gap-2">
<input
type="checkbox"
:checked="isMemberSelected(member.did)"
@change="toggleMemberSelection(member.did)"
/>
<div class="">
<div class="text-sm font-semibold">
{{ member.name || SOMEONE_UNNAMED }}
</div>
<div
class="flex items-center gap-0.5 text-xs text-slate-500"
>
<span class="font-semibold sm:hidden">DID:</span>
<span
class="w-[35vw] sm:w-auto truncate text-left"
style="direction: rtl"
>{{ member.did }}</span
>
</div>
</div>
</label>
<!-- Contact indicator - only show if they are already a contact -->
<font-awesome
v-if="member.isContact"
icon="user-circle"
class="fa-fw ms-auto text-slate-400 cursor-pointer hover:text-slate-600"
@click="showContactInfo"
/>
</div>
</td>
</tr>
</tbody>
<!-- Select All Footer -->
<tfoot v-if="membersData && membersData.length > 0">
<tr class="bg-slate-100 font-medium">
<th class="border border-slate-300 px-3 py-2">
<label class="flex items-center gap-2">
<input
type="checkbox"
:checked="isAllSelected"
:indeterminate="isIndeterminate"
@change="toggleSelectAll"
/>
Select All
</label>
</th>
</tr>
</tfoot>
</table>
</div>
<!-- Action Buttons -->
<div class="space-y-2">
<!-- Main Action Button -->
<button
v-if="membersData && membersData.length > 0"
:disabled="!hasSelectedMembers"
:class="[
'block w-full text-center text-md font-bold uppercase px-2 py-2 rounded-md',
hasSelectedMembers
? 'bg-blue-600 text-white cursor-pointer'
: 'bg-slate-400 text-slate-200 cursor-not-allowed',
]"
@click="processSelectedMembers"
>
{{ buttonText }}
</button>
<!-- Cancel Button -->
<button
class="block w-full text-center text-md font-bold uppercase bg-slate-600 text-white px-2 py-2 rounded-md"
@click="cancel"
>
Maybe Later
</button>
</div>
</div>
</div>
</div>
</template>
<script lang="ts">
import { Vue, Component, Prop } from "vue-facing-decorator";
import { PlatformServiceMixin } from "@/utils/PlatformServiceMixin";
import { SOMEONE_UNNAMED } from "@/constants/entities";
import { MemberData } from "@/interfaces";
import { setVisibilityUtil, getHeaders, register } from "@/libs/endorserServer";
import { createNotifyHelpers } from "@/utils/notify";
import { Contact } from "@/db/tables/contacts";
@Component({
mixins: [PlatformServiceMixin],
emits: ["close"],
})
export default class BulkMembersDialog extends Vue {
@Prop({ default: "" }) activeDid!: string;
@Prop({ default: "" }) apiServer!: string;
// isOrganizer: true = organizer mode (admit members), false = member mode (set visibility)
@Prop({ required: true }) isOrganizer!: boolean;
// Vue notification system
$notify!: (
notification: { group: string; type: string; title: string; text: string },
timeout?: number,
) => void;
// Notification system
notify!: ReturnType<typeof createNotifyHelpers>;
// Component state
membersData: MemberData[] = [];
selectedMembers: string[] = [];
visible = false;
// Constants
// In Vue templates, imported constants need to be explicitly made available to the template
readonly SOMEONE_UNNAMED = SOMEONE_UNNAMED;
get hasSelectedMembers() {
return this.selectedMembers.length > 0;
}
get isAllSelected() {
if (!this.membersData || this.membersData.length === 0) return false;
return this.membersData.every((member) =>
this.selectedMembers.includes(member.did),
);
}
get isIndeterminate() {
if (!this.membersData || this.membersData.length === 0) return false;
const selectedCount = this.membersData.filter((member) =>
this.selectedMembers.includes(member.did),
).length;
return selectedCount > 0 && selectedCount < this.membersData.length;
}
get title() {
return this.isOrganizer
? "Admit Pending Members"
: "Add Members to Contacts";
}
get description() {
return this.isOrganizer
? "Would you like to admit these members to the meeting and add them to your contacts?"
: "Would you like to add these members to your contacts?";
}
get buttonText() {
return this.isOrganizer ? "Admit + Add to Contacts" : "Add to Contacts";
}
get emptyStateText() {
return this.isOrganizer
? "No pending members to admit"
: "No members are not in your contacts";
}
created() {
this.notify = createNotifyHelpers(this.$notify);
}
open(members: MemberData[]) {
this.visible = true;
this.membersData = members;
// Select all by default
this.selectedMembers = this.membersData.map((member) => member.did);
}
close(notSelectedMemberDids: string[]) {
this.visible = false;
this.$emit("close", { notSelectedMemberDids: notSelectedMemberDids });
}
cancel() {
this.close(this.membersData.map((member) => member.did));
}
toggleSelectAll() {
if (!this.membersData || this.membersData.length === 0) return;
if (this.isAllSelected) {
// Deselect all
this.selectedMembers = [];
} else {
// Select all
this.selectedMembers = this.membersData.map((member) => member.did);
}
}
toggleMemberSelection(memberDid: string) {
const index = this.selectedMembers.indexOf(memberDid);
if (index > -1) {
this.selectedMembers.splice(index, 1);
} else {
this.selectedMembers.push(memberDid);
}
}
isMemberSelected(memberDid: string) {
return this.selectedMembers.includes(memberDid);
}
async processSelectedMembers() {
try {
const selectedMembers: MemberData[] = this.membersData.filter((member) =>
this.selectedMembers.includes(member.did),
);
const notSelectedMembers: MemberData[] = this.membersData.filter(
(member) => !this.selectedMembers.includes(member.did),
);
let admittedCount = 0;
let contactAddedCount = 0;
let errors = 0;
for (const member of selectedMembers) {
try {
// Organizer mode: admit and register the member first
if (this.isOrganizer) {
await this.admitMember(member);
await this.registerMember(member);
admittedCount++;
}
// If they're not a contact yet, add them as a contact
if (!member.isContact) {
// Organizer mode: set isRegistered to true, member mode: undefined
await this.addAsContact(
member,
this.isOrganizer ? true : undefined,
);
contactAddedCount++;
}
// Set their seesMe to true
await this.updateContactVisibility(member.did, true);
} catch (error) {
// eslint-disable-next-line no-console
console.error(`Error processing member ${member.did}:`, error);
// Continue with other members even if one fails
errors++;
}
}
// Show success notification
if (this.isOrganizer) {
if (admittedCount > 0) {
this.$notify(
{
group: "alert",
type: "success",
title: "Members Admitted Successfully",
text: `${admittedCount} member${admittedCount === 1 ? "" : "s"} admitted and registered${contactAddedCount === 0 ? "" : admittedCount === contactAddedCount ? " and" : `, ${contactAddedCount}`}${contactAddedCount === 0 ? "" : ` added as contact${contactAddedCount === 1 ? "" : "s"}`}.`,
},
5000,
);
}
if (errors > 0) {
this.$notify(
{
group: "alert",
type: "danger",
title: "Error",
text: "Failed to fully admit some members. Work with them individually below.",
},
5000,
);
}
} else {
// Member mode: show contacts added notification
if (contactAddedCount > 0) {
this.$notify(
{
group: "alert",
type: "success",
title: "Contacts Added Successfully",
text: `${contactAddedCount} member${contactAddedCount === 1 ? "" : "s"} added as contact${contactAddedCount === 1 ? "" : "s"}.`,
},
5000,
);
}
}
this.close(notSelectedMembers.map((member) => member.did));
} catch (error) {
// eslint-disable-next-line no-console
console.error(
`Error ${this.isOrganizer ? "admitting members" : "adding contacts"}:`,
error,
);
this.$notify(
{
group: "alert",
type: "danger",
title: "Error",
text: "Some errors occurred. Work with members individually below.",
},
5000,
);
}
}
async admitMember(member: {
did: string;
name: string;
member: { memberId: string };
}) {
try {
const headers = await getHeaders(this.activeDid);
await this.axios.put(
`${this.apiServer}/api/partner/groupOnboardMember/${member.member.memberId}`,
{ admitted: true },
{ headers },
);
} catch (err) {
// eslint-disable-next-line no-console
console.error("Error admitting member:", err);
throw err;
}
}
async registerMember(member: MemberData) {
try {
const contact: Contact = { did: member.did };
const result = await register(
this.activeDid,
this.apiServer,
this.axios,
contact,
);
if (result.success) {
if (result.embeddedRecordError) {
throw new Error(result.embeddedRecordError);
}
await this.$updateContact(member.did, { registered: true });
} else {
throw result;
}
} catch (err) {
// eslint-disable-next-line no-console
console.error("Error registering member:", err);
throw err;
}
}
async addAsContact(
member: { did: string; name: string },
isRegistered?: boolean,
) {
try {
const newContact: Contact = {
did: member.did,
name: member.name,
registered: isRegistered,
};
await this.$insertContact(newContact);
} catch (err) {
// eslint-disable-next-line no-console
console.error("Error adding contact:", err);
if (err instanceof Error && err.message?.indexOf("already exists") > -1) {
// Contact already exists, continue
} else {
throw err; // Re-throw if it's not a duplicate error
}
}
}
async updateContactVisibility(did: string, seesMe: boolean) {
try {
// Get the contact object
const contact = await this.$getContact(did);
if (!contact) {
throw new Error(`Contact not found for DID: ${did}`);
}
// Use the proper API to set visibility on the server
const result = await setVisibilityUtil(
this.activeDid,
this.apiServer,
this.axios,
contact,
seesMe,
);
if (!result.success) {
throw new Error(result.error || "Failed to set visibility");
}
} catch (err) {
// eslint-disable-next-line no-console
console.error("Error updating contact visibility:", err);
throw err;
}
}
showContactInfo() {
// isOrganizer: true = admit mode, false = visibility mode
const message = this.isOrganizer
? "This user is already your contact, but they are not yet admitted to the meeting."
: "This user is already your contact, but your activities are not visible to them yet.";
this.$notify(
{
group: "alert",
type: "info",
title: "Contact Info",
text: message,
},
5000,
);
}
}
</script>
+1 -1
View File
@@ -46,7 +46,7 @@
<span class="text-xs truncate">{{ contact.did }}</span> <span class="text-xs truncate">{{ contact.did }}</span>
</div> </div>
<div class="text-sm truncate"> <div class="text-sm">
{{ contact.notes }} {{ contact.notes }}
</div> </div>
</div> </div>
+10 -167
View File
@@ -10,18 +10,12 @@ messages * - Conditional UI based on platform capabilities * * @component *
<template> <template>
<div id="sectionDataExport" :class="containerClasses"> <div id="sectionDataExport" :class="containerClasses">
<div :class="titleClasses">Data Management</div> <div :class="titleClasses">Data Export</div>
<router-link <router-link
v-if="activeDid" v-if="activeDid"
:to="{ name: 'seed-backup' }" :to="{ name: 'seed-backup' }"
:class="backupButtonClasses" :class="backupButtonClasses"
> >
<!-- Notification dot - show while the user has not yet backed up their seed phrase -->
<font-awesome
v-if="showRedNotificationDot"
icon="circle"
class="absolute -right-[8px] -top-[8px] text-rose-500 text-[14px] border border-white rounded-full"
></font-awesome>
Backup Identifier Seed Backup Identifier Seed
</router-link> </router-link>
@@ -30,7 +24,7 @@ messages * - Conditional UI based on platform capabilities * * @component *
:class="exportButtonClasses" :class="exportButtonClasses"
@click="exportDatabase()" @click="exportDatabase()"
> >
{{ isExporting ? "Exporting..." : "Export Contacts" }} {{ isExporting ? "Exporting..." : "Download Contacts" }}
</button> </button>
<div <div
@@ -55,54 +49,11 @@ messages * - Conditional UI based on platform capabilities * * @component *
</li> </li>
</ul> </ul>
</div> </div>
<!-- Import Contacts -->
<div id="sectionImportContactsSettings" class="mt-4">
<h2 class="text-slate-500 text-sm font-bold">Import Contacts</h2>
<div class="mt-2">
<input
type="file"
class="w-full bg-white rounded-md pe-2 file:border-0 file:bg-gradient-to-b file:from-blue-400 file:to-blue-700 file:shadow-[inset_0_-1px_0_0_rgba(0,0,0,0.5)] file:text-white file:px-3 file:py-2 file:me-2 file:rounded-s-md"
@change="uploadImportFile"
/>
<transition
enter-active-class="transform ease-out duration-300 transition"
enter-from-class="translate-y-2 opacity-0 sm:translate-y-4"
enter-to-class="translate-y-0 opacity-100 sm:translate-y-0"
leave-active-class="transition ease-in duration-500"
leave-from-class="opacity-100"
leave-to-class="opacity-0"
>
<div v-if="showContactImport()" class="mt-2">
<!-- Bulk import has an error
<div class="flex justify-center">
<button
class="block text-center text-md 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-1.5 py-2 rounded-md mb-6"
@click="confirmSubmitImportFile()"
>
Overwrite Settings & Contacts
<br />
(which doesn't include Identifier Data)
</button>
</div>
-->
<button
class="block w-full text-center text-md 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-1.5 py-2 rounded-md"
@click="checkContactImports()"
>
Import Contacts
</button>
</div>
</transition>
</div>
</div>
</div> </div>
</template> </template>
<script lang="ts"> <script lang="ts">
import { Component, Prop, Vue } from "vue-facing-decorator"; import { Component, Prop, Vue } from "vue-facing-decorator";
import { Router } from "vue-router";
import * as R from "ramda"; import * as R from "ramda";
import { AppString, NotificationIface } from "../constants/app"; import { AppString, NotificationIface } from "../constants/app";
@@ -110,10 +61,8 @@ import { Contact } from "../db/tables/contacts";
import { logger } from "../utils/logger"; import { logger } from "../utils/logger";
import { contactsToExportJson } from "../libs/util"; import { contactsToExportJson } from "../libs/util";
import { createNotifyHelpers, TIMEOUTS } from "@/utils/notify"; import { createNotifyHelpers } from "@/utils/notify";
import { PlatformServiceMixin } from "@/utils/PlatformServiceMixin"; import { PlatformServiceMixin } from "@/utils/PlatformServiceMixin";
import { ACCOUNT_VIEW_CONSTANTS } from "@/constants/accountView";
import { ImportContent } from "@/interfaces/accountView";
/** /**
* @vue-component * @vue-component
@@ -136,12 +85,6 @@ export default class DataExportSection extends Vue {
*/ */
$notify!: (notification: NotificationIface, timeout?: number) => void; $notify!: (notification: NotificationIface, timeout?: number) => void;
/**
* Router instance injected by Vue
* Used for navigation
*/
$router!: Router;
/** /**
* Active DID (Decentralized Identifier) of the user * Active DID (Decentralized Identifier) of the user
* Controls visibility of seed backup option * Controls visibility of seed backup option
@@ -155,18 +98,6 @@ export default class DataExportSection extends Vue {
*/ */
isExporting = false; isExporting = false;
/**
* Flag indicating if the user has backed up their seed phrase
* Used to control the visibility of the notification dot
*/
showRedNotificationDot = false;
/**
* Reference to the selected import file
* Used to store the file selected by the user for import
*/
private inputImportFileName: Blob | undefined;
/** /**
* Notification helper for consistent notification patterns * Notification helper for consistent notification patterns
* Created as a getter to ensure $notify is available when called * Created as a getter to ensure $notify is available when called
@@ -198,7 +129,7 @@ export default class DataExportSection extends Vue {
* CSS classes for the backup button (router link) * CSS classes for the backup button (router link)
*/ */
get backupButtonClasses(): string { get backupButtonClasses(): string {
return "block relative w-full text-center text-md 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-1.5 py-2 rounded-md mb-2 mt-2"; return "block w-full text-center text-md 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-1.5 py-2 rounded-md mb-2 mt-2";
} }
/** /**
@@ -257,30 +188,12 @@ export default class DataExportSection extends Vue {
// first remove the contactMethods field, mostly to cast to a clear type (that will end up with JSON objects) // first remove the contactMethods field, mostly to cast to a clear type (that will end up with JSON objects)
const exContact: Contact = R.omit(["contactMethods"], contact); const exContact: Contact = R.omit(["contactMethods"], contact);
// now add contactMethods as a true array of ContactMethod objects // now add contactMethods as a true array of ContactMethod objects
// $contacts() returns normalized contacts where contactMethods is already an array, exContact.contactMethods = contact.contactMethods
// but we handle both array and string cases for robustness ? typeof contact.contactMethods === "string" &&
if (contact.contactMethods) { contact.contactMethods.trim() !== ""
if (Array.isArray(contact.contactMethods)) { ? JSON.parse(contact.contactMethods)
// Already an array, use it directly : []
exContact.contactMethods = contact.contactMethods; : [];
} else {
// Check if it's a string that needs parsing (shouldn't happen with normalized contacts, but handle for robustness)
const contactMethodsValue = contact.contactMethods as unknown;
if (
typeof contactMethodsValue === "string" &&
contactMethodsValue.trim() !== ""
) {
// String that needs parsing
exContact.contactMethods = JSON.parse(contactMethodsValue);
} else {
// Invalid data, use empty array
exContact.contactMethods = [];
}
}
} else {
// No contactMethods, use empty array
exContact.contactMethods = [];
}
return exContact; return exContact;
}); });
@@ -305,76 +218,6 @@ export default class DataExportSection extends Vue {
created() { created() {
this.notify = createNotifyHelpers(this.$notify); this.notify = createNotifyHelpers(this.$notify);
this.loadSeedBackupStatus();
}
/**
* Loads the seed backup status from account settings
* Updates the hasBackedUpSeed flag to control notification dot visibility
*/
private async loadSeedBackupStatus(): Promise<void> {
try {
const settings = await this.$accountSettings();
this.showRedNotificationDot =
!!settings.isRegistered && !settings.hasBackedUpSeed;
} catch (err: unknown) {
logger.error("Failed to load seed backup status:", err);
// Default to false (show notification dot) if we can't load the setting
this.showRedNotificationDot = false;
}
}
/**
* Handles file selection for contact import
* Stores the selected file for later processing
*/
async uploadImportFile(event: Event): Promise<void> {
this.inputImportFileName = (event.target as HTMLInputElement).files?.[0];
}
/**
* Checks if a contact import file has been selected
* Used to conditionally show the import button
*/
showContactImport(): boolean {
return !!this.inputImportFileName;
}
/**
* Processes the selected import file and navigates to the contact import view
* Parses the JSON file and extracts contact data for import
*/
async checkContactImports(): Promise<void> {
if (!this.inputImportFileName) {
return;
}
const reader = new FileReader();
reader.onload = (event) => {
const fileContent: string = (event.target?.result as string) || "{}";
try {
const contents: ImportContent = JSON.parse(fileContent);
const contactTableRows: Array<Contact> = (
contents.data?.data as [{ tableName: string; rows: Array<Contact> }]
)?.find((table) => table.tableName === "contacts")
?.rows as Array<Contact>;
const contactRows = contactTableRows.map(
// @ts-expect-error for omitting this field that is found in the Dexie format
(contact) => R.omit(["$types"], contact) as Contact,
);
this.$router.push({
name: "contact-import",
query: { contacts: JSON.stringify(contactRows) },
});
} catch (error) {
logger.error("Error checking contact imports:", error);
this.notify.error(
ACCOUNT_VIEW_CONSTANTS.ERRORS.IMPORT_ERROR,
TIMEOUTS.STANDARD,
);
}
};
reader.readAsText(this.inputImportFileName);
} }
} }
</script> </script>
File diff suppressed because it is too large Load Diff
+85 -44
View File
@@ -3,35 +3,31 @@ from GiftedDialog.vue to handle the complete step 1 * entity selection interface
with dynamic labeling and grid display. * * Features: * - Dynamic step labeling with dynamic labeling and grid display. * * Features: * - Dynamic step labeling
based on context * - EntityGrid integration for unified entity display * - based on context * - EntityGrid integration for unified entity display * -
Conflict detection and prevention * - Special entity handling (You, Unnamed) * - Conflict detection and prevention * - Special entity handling (You, Unnamed) * -
Cancel functionality * - Event delegation for entity selection * - Warning Show All navigation with context preservation * - Cancel functionality * - Event
notifications for conflicted entities * - Template streamlined with computed CSS delegation for entity selection * - Warning notifications for conflicted
properties * * @author Matthew Raymer */ entities * - Template streamlined with computed CSS properties * * @author
Matthew Raymer */
<template> <template>
<div id="sectionGiftedGiver"> <div id="sectionGiftedGiver">
<label class="block font-bold mb-1"> <label class="block font-bold mb-4">
{{ stepLabel }} {{ stepLabel }}
</label> </label>
<!-- Toggle link for entity type selection -->
<div class="text-right mb-4">
<button
type="button"
class="text-sm text-blue-600 hover:text-blue-800 underline font-medium"
@click="handleToggleEntityType"
>
{{ toggleLinkText }}
</button>
</div>
<EntityGrid <EntityGrid
:entity-type="shouldShowProjects ? 'projects' : 'people'" :entity-type="shouldShowProjects ? 'projects' : 'people'"
:entities="shouldShowProjects ? projects || undefined : allContacts" :entities="shouldShowProjects ? projects : allContacts"
:max-items="10"
:active-did="activeDid" :active-did="activeDid"
:all-my-dids="allMyDids" :all-my-dids="allMyDids"
:all-contacts="allContacts" :all-contacts="allContacts"
:conflict-checker="conflictChecker" :conflict-checker="conflictChecker"
:show-you-entity="shouldShowYouEntity"
:you-selectable="youSelectable" :you-selectable="youSelectable"
:show-all-route="showAllRoute"
:show-all-query-params="showAllQueryParams"
:notify="notify" :notify="notify"
:conflict-context="conflictContext" :conflict-context="conflictContext"
:hide-show-all="hideShowAll"
@entity-selected="handleEntitySelected" @entity-selected="handleEntitySelected"
/> />
@@ -72,6 +68,7 @@ interface EntitySelectionEvent {
* - EntityGrid integration for unified entity display * - EntityGrid integration for unified entity display
* - Conflict detection and prevention * - Conflict detection and prevention
* - Special entity handling (You, Unnamed) * - Special entity handling (You, Unnamed)
* - Show All navigation with context preservation
* - Cancel functionality * - Cancel functionality
* - Event delegation for entity selection * - Event delegation for entity selection
* - Warning notifications for conflicted entities * - Warning notifications for conflicted entities
@@ -99,9 +96,13 @@ export default class EntitySelectionStep extends Vue {
@Prop({ default: false }) @Prop({ default: false })
showProjects!: boolean; showProjects!: boolean;
/** Array of available projects (optional - EntityGrid loads internally if not provided) */ /** Whether this is from a project view */
@Prop({ required: false }) @Prop({ default: false })
projects?: PlanData[]; isFromProjectView!: boolean;
/** Array of available projects */
@Prop({ required: true })
projects!: PlanData[];
/** Array of available contacts */ /** Array of available contacts */
@Prop({ required: true }) @Prop({ required: true })
@@ -153,6 +154,10 @@ export default class EntitySelectionStep extends Vue {
@Prop() @Prop()
notify?: (notification: NotificationIface, timeout?: number) => void; notify?: (notification: NotificationIface, timeout?: number) => void;
/** Whether to hide the "Show All" navigation */
@Prop({ default: false })
hideShowAll!: boolean;
/** /**
* CSS classes for the cancel button * CSS classes for the cancel button
*/ */
@@ -165,19 +170,15 @@ export default class EntitySelectionStep extends Vue {
*/ */
get stepLabel(): string { get stepLabel(): string {
if (this.stepType === "recipient") { if (this.stepType === "recipient") {
return "Choose who received the gift:";
} else if (this.stepType === "giver") {
if (this.shouldShowProjects) { if (this.shouldShowProjects) {
return "Choose recipient project"; return "Choose a project benefitted from:";
} else { } else {
return "Choose recipient person"; return "Choose a person received from:";
}
} else {
// this.stepType === "giver"
if (this.shouldShowProjects) {
return "Choose giving project";
} else {
return "Choose giving person";
} }
} }
return "Choose entity:";
} }
/** /**
@@ -204,6 +205,16 @@ export default class EntitySelectionStep extends Vue {
return false; return false;
} }
/**
* Whether to show the "You" entity
*/
get shouldShowYouEntity(): boolean {
return (
this.stepType === "recipient" ||
(this.stepType === "giver" && this.isFromProjectView)
);
}
/** /**
* Whether the "You" entity is selectable * Whether the "You" entity is selectable
*/ */
@@ -212,14 +223,56 @@ export default class EntitySelectionStep extends Vue {
} }
/** /**
* Text for the toggle link * Route name for "Show All" navigation
*/ */
get toggleLinkText(): string { get showAllRoute(): string {
if (this.shouldShowProjects) { if (this.shouldShowProjects) {
return "... or choose a person instead →"; return "discover";
} else { } else if (this.allContacts.length > 0) {
return "... or choose a project instead →"; return "contact-gift";
} }
return "";
}
/**
* Query parameters for "Show All" navigation
*/
get showAllQueryParams(): Record<string, string> {
const baseParams = {
stepType: this.stepType,
giverEntityType: this.giverEntityType,
recipientEntityType: this.recipientEntityType,
// Form field values to preserve
description: this.description,
amountInput: this.amountInput,
unitCode: this.unitCode,
offerId: this.offerId,
fromProjectId: this.fromProjectId,
toProjectId: this.toProjectId,
showProjects: this.showProjects.toString(),
isFromProjectView: this.isFromProjectView.toString(),
};
if (this.shouldShowProjects) {
// For project contexts, still pass entity type information
return baseParams;
}
return {
...baseParams,
// Always pass both giver and recipient info for context preservation
giverProjectId: this.fromProjectId || "",
giverProjectName: this.giver?.name || "",
giverProjectImage: this.giver?.image || "",
giverProjectHandleId: this.giver?.handleId || "",
giverDid: this.giverEntityType === "person" ? this.giver?.did || "" : "",
recipientProjectId: this.toProjectId || "",
recipientProjectName: this.receiver?.name || "",
recipientProjectImage: this.receiver?.image || "",
recipientProjectHandleId: this.receiver?.handleId || "",
recipientDid:
this.recipientEntityType === "person" ? this.receiver?.did || "" : "",
};
} }
/** /**
@@ -232,13 +285,6 @@ export default class EntitySelectionStep extends Vue {
}); });
} }
/**
* Handle toggle entity type button click
*/
handleToggleEntityType(): void {
this.emitToggleEntityType();
}
/** /**
* Handle cancel button click * Handle cancel button click
*/ */
@@ -259,11 +305,6 @@ export default class EntitySelectionStep extends Vue {
emitCancel(): void { emitCancel(): void {
// No return value needed // No return value needed
} }
@Emit("toggle-entity-type")
emitToggleEntityType(): void {
// No return value needed
}
} }
</script> </script>
+45 -14
View File
@@ -1,6 +1,16 @@
/* EntitySummaryButton.vue - Displays selected entity with edit capability */ /** * EntitySummaryButton.vue - Displays selected entity with edit capability *
* Extracted from GiftedDialog.vue to handle entity summary display in the gift *
details step with edit functionality. * * Features: * - Shows entity avatar
(person or project) * - Displays entity name and role label * - Handles editable
vs locked states * - Function props for parent control over edit behavior * -
Supports both person and project entity types * - Template streamlined with
computed CSS properties * * @author Matthew Raymer */
<template> <template>
<button :class="containerClasses" @click="handleClick"> <component
:is="editable ? 'button' : 'div'"
:class="containerClasses"
@click="handleClick"
>
<!-- Entity Icon/Avatar --> <!-- Entity Icon/Avatar -->
<div> <div>
<template v-if="entityType === 'project'"> <template v-if="entityType === 'project'">
@@ -37,11 +47,14 @@
</h3> </h3>
</div> </div>
<!-- Edit Icon --> <!-- Edit/Lock Icon -->
<p class="ms-auto text-sm pe-1 text-blue-500"> <p class="ms-auto text-sm pe-1" :class="iconClasses">
<font-awesome icon="pen" title="Change" /> <font-awesome
:icon="editable ? 'pen' : 'lock'"
:title="editable ? 'Change' : 'Can\'t be changed'"
/>
</p> </p>
</button> </component>
</template> </template>
<script lang="ts"> <script lang="ts">
@@ -62,12 +75,12 @@ interface EntityData {
} }
/** /**
* EntitySummaryButton - Displays selected entity with edit capability * EntitySummaryButton - Displays selected entity with optional edit capability
* *
* Features: * Features:
* - Shows entity avatar (person or project) * - Shows entity avatar (person or project)
* - Displays entity name and role label * - Displays entity name and role label
* - Always editable - click to change entity * - Handles editable vs locked states
* - Function props for parent control over edit behavior * - Function props for parent control over edit behavior
* - Supports both person and project entity types * - Supports both person and project entity types
* - Template streamlined with computed CSS properties * - Template streamlined with computed CSS properties
@@ -91,9 +104,13 @@ export default class EntitySummaryButton extends Vue {
@Prop({ required: true }) @Prop({ required: true })
label!: string; label!: string;
/** Whether the entity can be edited */
@Prop({ default: true })
editable!: boolean;
/** /**
* Function prop for handling edit requests * Function prop for handling edit requests
* Called when the button is clicked, allowing parent to control edit behavior * Called when the button is clicked and editable, allowing parent to control edit behavior
*/ */
@Prop({ type: Function, default: () => {} }) @Prop({ type: Function, default: () => {} })
onEditRequested!: (data: { onEditRequested!: (data: {
@@ -115,6 +132,13 @@ export default class EntitySummaryButton extends Vue {
return this.entity !== null && "profileImageUrl" in this.entity; return this.entity !== null && "profileImageUrl" in this.entity;
} }
/**
* Computed CSS classes for the edit/lock icon
*/
get iconClasses(): string {
return this.editable ? "text-blue-500" : "text-slate-400";
}
/** /**
* Computed CSS classes for the entity name * Computed CSS classes for the entity name
*/ */
@@ -148,13 +172,16 @@ export default class EntitySummaryButton extends Vue {
} }
/** /**
* Handle click event - call function prop to allow parent to control edit behavior * Handle click event - only call function prop if editable
* Allows parent to control edit behavior and validation
*/ */
handleClick(): void { handleClick(): void {
this.onEditRequested({ if (this.editable) {
entityType: this.entityType, this.onEditRequested({
entity: this.entity, entityType: this.entityType,
}); entity: this.entity,
});
}
} }
} }
</script> </script>
@@ -168,4 +195,8 @@ button {
button:hover { button:hover {
background-color: #f1f5f9; /* hover:bg-slate-100 */ background-color: #f1f5f9; /* hover:bg-slate-100 */
} }
div {
cursor: default;
}
</style> </style>
+4 -2
View File
@@ -211,6 +211,8 @@ export default class FeedFilters extends Vue {
} }
</script> </script>
<style scoped> <style>
/* Component-specific styles if needed */ #dialogFeedFilters.dialog-overlay {
overflow: scroll;
}
</style> </style>
+20
View File
@@ -16,6 +16,7 @@ control over updates and validation * * @author Matthew Raymer */
:entity="giver" :entity="giver"
:entity-type="giverEntityType" :entity-type="giverEntityType"
:label="giverLabel" :label="giverLabel"
:editable="canEditGiver"
:on-edit-requested="handleEditGiver" :on-edit-requested="handleEditGiver"
/> />
@@ -24,6 +25,7 @@ control over updates and validation * * @author Matthew Raymer */
:entity="receiver" :entity="receiver"
:entity-type="recipientEntityType" :entity-type="recipientEntityType"
:label="recipientLabel" :label="recipientLabel"
:editable="canEditRecipient"
:on-edit-requested="handleEditRecipient" :on-edit-requested="handleEditRecipient"
/> />
</div> </div>
@@ -170,6 +172,10 @@ export default class GiftDetailsStep extends Vue {
@Prop({ default: "" }) @Prop({ default: "" })
prompt!: string; prompt!: string;
/** Whether this is from a project view */
@Prop({ default: false })
isFromProjectView!: boolean;
/** Whether there's a conflict between giver and receiver */ /** Whether there's a conflict between giver and receiver */
@Prop({ default: false }) @Prop({ default: false })
hasConflict!: boolean; hasConflict!: boolean;
@@ -271,6 +277,20 @@ export default class GiftDetailsStep extends Vue {
: "Given to:"; : "Given to:";
} }
/**
* Whether the giver can be edited
*/
get canEditGiver(): boolean {
return !(this.isFromProjectView && this.giverEntityType === "project");
}
/**
* Whether the recipient can be edited
*/
get canEditRecipient(): boolean {
return this.recipientEntityType === "person";
}
/** /**
* Computed CSS classes for submit button * Computed CSS classes for submit button
*/ */
+85 -142
View File
@@ -3,18 +3,19 @@
<div <div
class="dialog" class="dialog"
data-testid="gifted-dialog" data-testid="gifted-dialog"
:data-recipient-entity-type="currentRecipientEntityType" :data-recipient-entity-type="recipientEntityType"
> >
<!-- Step 1: Entity Selection --> <!-- Step 1: Entity Selection -->
<EntitySelectionStep <EntitySelectionStep
v-show="firstStep" v-show="firstStep"
:step-type="stepType" :step-type="stepType"
:giver-entity-type="currentGiverEntityType" :giver-entity-type="giverEntityType"
:recipient-entity-type="currentRecipientEntityType" :recipient-entity-type="recipientEntityType"
:show-projects=" :show-projects="
currentGiverEntityType === 'project' || giverEntityType === 'project' || recipientEntityType === 'project'
currentRecipientEntityType === 'project'
" "
:is-from-project-view="isFromProjectView"
:projects="projects"
:all-contacts="allContacts" :all-contacts="allContacts"
:active-did="activeDid" :active-did="activeDid"
:all-my-dids="allMyDids" :all-my-dids="allMyDids"
@@ -28,8 +29,8 @@
:unit-code="unitCode" :unit-code="unitCode"
:offer-id="offerId" :offer-id="offerId"
:notify="$notify" :notify="$notify"
:hide-show-all="hideShowAll"
@entity-selected="handleEntitySelected" @entity-selected="handleEntitySelected"
@toggle-entity-type="handleToggleEntityType"
@cancel="cancel" @cancel="cancel"
/> />
@@ -38,12 +39,13 @@
v-show="!firstStep" v-show="!firstStep"
:giver="giver" :giver="giver"
:receiver="receiver" :receiver="receiver"
:giver-entity-type="currentGiverEntityType" :giver-entity-type="giverEntityType"
:recipient-entity-type="currentRecipientEntityType" :recipient-entity-type="recipientEntityType"
:description="description" :description="description"
:amount="parseFloat(amountInput) || 0" :amount="parseFloat(amountInput) || 0"
:unit-code="unitCode" :unit-code="unitCode"
:prompt="prompt" :prompt="prompt"
:is-from-project-view="isFromProjectView"
:has-conflict="hasPersonConflict" :has-conflict="hasPersonConflict"
:offer-id="offerId" :offer-id="offerId"
:from-project-id="fromProjectId" :from-project-id="fromProjectId"
@@ -67,6 +69,7 @@ import {
createAndSubmitGive, createAndSubmitGive,
didInfo, didInfo,
serverMessageForUser, serverMessageForUser,
getHeaders,
} from "../libs/endorserServer"; } from "../libs/endorserServer";
import * as libsUtil from "../libs/util"; import * as libsUtil from "../libs/util";
import { Contact } from "../db/tables/contacts"; import { Contact } from "../db/tables/contacts";
@@ -79,7 +82,6 @@ import GiftDetailsStep from "../components/GiftDetailsStep.vue";
import { PlanData } from "../interfaces/records"; import { PlanData } from "../interfaces/records";
import { PlatformServiceMixin } from "@/utils/PlatformServiceMixin"; import { PlatformServiceMixin } from "@/utils/PlatformServiceMixin";
import { createNotifyHelpers, TIMEOUTS, NotifyFunction } from "@/utils/notify"; import { createNotifyHelpers, TIMEOUTS, NotifyFunction } from "@/utils/notify";
import { showSeedPhraseReminder } from "@/utils/seedPhraseReminder";
import { import {
NOTIFY_GIFT_ERROR_NEGATIVE_AMOUNT, NOTIFY_GIFT_ERROR_NEGATIVE_AMOUNT,
NOTIFY_GIFT_ERROR_NO_DESCRIPTION, NOTIFY_GIFT_ERROR_NO_DESCRIPTION,
@@ -113,10 +115,12 @@ export default class GiftedDialog extends Vue {
@Prop() fromProjectId = ""; @Prop() fromProjectId = "";
@Prop() toProjectId = ""; @Prop() toProjectId = "";
@Prop({ default: "person" }) initialGiverEntityType = "person" as @Prop() isFromProjectView = false;
@Prop() hideShowAll = false;
@Prop({ default: "person" }) giverEntityType = "person" as
| "person" | "person"
| "project"; | "project";
@Prop({ default: "person" }) initialRecipientEntityType = "person" as @Prop({ default: "person" }) recipientEntityType = "person" as
| "person" | "person"
| "project"; | "project";
@@ -130,9 +134,8 @@ export default class GiftedDialog extends Vue {
description = ""; description = "";
firstStep = true; // true = Step 1 (giver/recipient selection), false = Step 2 (amount/description) firstStep = true; // true = Step 1 (giver/recipient selection), false = Step 2 (amount/description)
giver?: libsUtil.GiverReceiverInputInfo; // undefined means no identified giver agent giver?: libsUtil.GiverReceiverInputInfo; // undefined means no identified giver agent
currentGiverEntityType: "person" | "project" = "person"; // Mutable version (can be toggled)
currentRecipientEntityType: "person" | "project" = "person"; // Mutable version (can be toggled)
offerId = ""; offerId = "";
projects: PlanData[] = [];
prompt = ""; prompt = "";
receiver?: libsUtil.GiverReceiverInputInfo; receiver?: libsUtil.GiverReceiverInputInfo;
stepType = "giver"; stepType = "giver";
@@ -143,12 +146,20 @@ export default class GiftedDialog extends Vue {
didInfo = didInfo; didInfo = didInfo;
// Computed property to help debug template logic
get shouldShowProjects() {
const result =
(this.stepType === "giver" && this.giverEntityType === "project") ||
(this.stepType === "recipient" && this.recipientEntityType === "project");
return result;
}
// Computed property to check if current selection would create a conflict // Computed property to check if current selection would create a conflict
get hasPersonConflict() { get hasPersonConflict() {
// Only check for conflicts when both entities are persons // Only check for conflicts when both entities are persons
if ( if (
this.currentGiverEntityType !== "person" || this.giverEntityType !== "person" ||
this.currentRecipientEntityType !== "person" this.recipientEntityType !== "person"
) { ) {
return false; return false;
} }
@@ -165,56 +176,22 @@ export default class GiftedDialog extends Vue {
return false; return false;
} }
// Computed property to check if current selection would create a project conflict // Computed property to check if a contact would create a conflict when selected
get hasProjectConflict() { wouldCreateConflict(contactDid: string) {
// Only check for conflicts when both entities are projects // Only check for conflicts when both entities are persons
if ( if (
this.currentGiverEntityType !== "project" || this.giverEntityType !== "person" ||
this.currentRecipientEntityType !== "project" this.recipientEntityType !== "person"
) { ) {
return false; return false;
} }
// Check if giver and recipient are the same project if (this.stepType === "giver") {
if ( // If selecting as giver, check if it conflicts with current recipient
this.giver?.handleId && return this.receiver?.did === contactDid;
this.receiver?.handleId && } else if (this.stepType === "recipient") {
this.giver.handleId === this.receiver.handleId // If selecting as recipient, check if it conflicts with current giver
) { return this.giver?.did === contactDid;
return true;
}
return false;
}
// Computed property to check if a contact or project would create a conflict when selected
wouldCreateConflict(identifier: string) {
// Check for person conflicts when both entities are persons
if (
this.currentGiverEntityType === "person" &&
this.currentRecipientEntityType === "person"
) {
if (this.stepType === "giver") {
// If selecting as giver, check if it conflicts with current recipient
return this.receiver?.did === identifier;
} else if (this.stepType === "recipient") {
// If selecting as recipient, check if it conflicts with current giver
return this.giver?.did === identifier;
}
}
// Check for project conflicts when both entities are projects
if (
this.currentGiverEntityType === "project" &&
this.currentRecipientEntityType === "project"
) {
if (this.stepType === "giver") {
// If selecting as giver, check if it conflicts with current recipient
return this.receiver?.handleId === identifier;
} else if (this.stepType === "recipient") {
// If selecting as recipient, check if it conflicts with current giver
return this.giver?.handleId === identifier;
}
} }
return false; return false;
@@ -238,35 +215,27 @@ export default class GiftedDialog extends Vue {
this.amountInput = amountInput || "0"; this.amountInput = amountInput || "0";
this.unitCode = unitCode || "HUR"; this.unitCode = unitCode || "HUR";
this.callbackOnSuccess = callbackOnSuccess; this.callbackOnSuccess = callbackOnSuccess;
// Initialize current entity types from initial prop values this.firstStep = !giver;
this.currentGiverEntityType = this.initialGiverEntityType; this.stepType = "giver";
this.currentRecipientEntityType = this.initialRecipientEntityType;
try { try {
const settings = await this.$accountSettings(); const settings = await this.$settings();
this.apiServer = settings.apiServer || ""; this.apiServer = settings.apiServer || "";
this.activeDid = settings.activeDid || "";
// Get activeDid from active_identity table (single source of truth) this.allContacts = await this.$contacts();
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const activeIdentity = await (this as any).$getActiveIdentity();
this.activeDid = activeIdentity.activeDid || "";
// Skip Step 1 if both giver and receiver are provided
const hasGiver = giver && (!!giver.did || !!giver.handleId);
const hasReceiver = receiver && (!!receiver.did || !!receiver.handleId);
this.firstStep = !hasGiver || !hasReceiver;
if (this.firstStep) {
this.stepType = giver ? "receiver" : "giver";
}
logger.debug("[GiftedDialog] Settings received:", {
activeDid: this.activeDid,
apiServer: this.apiServer,
});
this.allContacts = await this.$contactsByDateAdded();
this.allMyDids = await retrieveAccountDids(); this.allMyDids = await retrieveAccountDids();
if (
this.giverEntityType === "project" ||
this.recipientEntityType === "project"
) {
await this.loadProjects();
} else {
// Clear projects array when not needed
this.projects = [];
}
} catch (err: unknown) { } catch (err: unknown) {
logger.error("Error retrieving settings from database:", err); logger.error("Error retrieving settings from database:", err);
this.safeNotify.error( this.safeNotify.error(
@@ -314,8 +283,6 @@ export default class GiftedDialog extends Vue {
this.prompt = ""; this.prompt = "";
this.unitCode = "HUR"; this.unitCode = "HUR";
this.firstStep = true; this.firstStep = true;
// Reset to initial prop values
this.currentGiverEntityType = this.initialGiverEntityType;
} }
async confirm() { async confirm() {
@@ -353,15 +320,6 @@ export default class GiftedDialog extends Vue {
return; return;
} }
// Check for project conflict
if (this.hasProjectConflict) {
this.safeNotify.error(
"You cannot select the same project as both giver and recipient.",
TIMEOUTS.STANDARD,
);
return;
}
this.close(); this.close();
this.safeNotify.toast( this.safeNotify.toast(
NOTIFY_GIFTED_DETAILS_RECORDING_GIVE.message, NOTIFY_GIFTED_DETAILS_RECORDING_GIVE.message,
@@ -403,8 +361,8 @@ export default class GiftedDialog extends Vue {
let providerPlanHandleId: string | undefined; let providerPlanHandleId: string | undefined;
if ( if (
this.currentGiverEntityType === "project" && this.giverEntityType === "project" &&
this.currentRecipientEntityType === "person" this.recipientEntityType === "person"
) { ) {
// Project-to-person gift // Project-to-person gift
fromDid = undefined; // No person giver fromDid = undefined; // No person giver
@@ -412,8 +370,8 @@ export default class GiftedDialog extends Vue {
fulfillsProjectHandleId = undefined; // No project recipient fulfillsProjectHandleId = undefined; // No project recipient
providerPlanHandleId = this.giver?.handleId; // Project giver providerPlanHandleId = this.giver?.handleId; // Project giver
} else if ( } else if (
this.currentGiverEntityType === "person" && this.giverEntityType === "person" &&
this.currentRecipientEntityType === "project" this.recipientEntityType === "project"
) { ) {
// Person-to-project gift // Person-to-project gift
fromDid = giverDid as string; // Person giver fromDid = giverDid as string; // Person giver
@@ -453,15 +411,6 @@ export default class GiftedDialog extends Vue {
); );
} else { } else {
this.safeNotify.success("That gift was recorded.", TIMEOUTS.VERY_LONG); this.safeNotify.success("That gift was recorded.", TIMEOUTS.VERY_LONG);
// Show seed phrase backup reminder if needed
try {
const settings = await this.$accountSettings();
showSeedPhraseReminder(!!settings.hasBackedUpSeed, this.$notify);
} catch (error) {
logger.error("Error checking seed backup status:", error);
}
if (this.callbackOnSuccess) { if (this.callbackOnSuccess) {
this.callbackOnSuccess(amount); this.callbackOnSuccess(amount);
} }
@@ -523,6 +472,27 @@ export default class GiftedDialog extends Vue {
this.firstStep = false; this.firstStep = false;
} }
async loadProjects() {
try {
const response = await fetch(this.apiServer + "/api/v2/report/plans", {
method: "GET",
headers: await getHeaders(this.activeDid),
});
if (response.status !== 200) {
throw new Error("Failed to load projects");
}
const results = await response.json();
if (results.data) {
this.projects = results.data;
}
} catch (error) {
logger.error("Error loading projects:", error);
this.safeNotify.error("Failed to load projects", TIMEOUTS.STANDARD);
}
}
selectProject(project: PlanData) { selectProject(project: PlanData) {
this.giver = { this.giver = {
did: project.handleId, did: project.handleId,
@@ -530,13 +500,10 @@ export default class GiftedDialog extends Vue {
image: project.image, image: project.image,
handleId: project.handleId, handleId: project.handleId,
}; };
// Only set receiver to "You" if no receiver has been selected yet this.receiver = {
if (!this.receiver || !this.receiver.did) { did: this.activeDid,
this.receiver = { name: "You",
did: this.activeDid, };
name: "You",
};
}
this.firstStep = false; this.firstStep = false;
} }
@@ -573,22 +540,17 @@ export default class GiftedDialog extends Vue {
return { return {
amountInput: this.amountInput, amountInput: this.amountInput,
description: this.description, description: this.description,
giverDid: giverDid: this.giverEntityType === "person" ? this.giver?.did : undefined,
this.currentGiverEntityType === "person" ? this.giver?.did : undefined,
giverName: this.giver?.name, giverName: this.giver?.name,
offerId: this.offerId, offerId: this.offerId,
fulfillsProjectId: fulfillsProjectId:
this.currentRecipientEntityType === "project" this.recipientEntityType === "project" ? this.toProjectId : undefined,
? this.toProjectId
: undefined,
providerProjectId: providerProjectId:
this.currentGiverEntityType === "project" this.giverEntityType === "project"
? this.giver?.handleId ? this.giver?.handleId
: this.fromProjectId, : this.fromProjectId,
recipientDid: recipientDid:
this.currentRecipientEntityType === "person" this.recipientEntityType === "person" ? this.receiver?.did : undefined,
? this.receiver?.did
: undefined,
recipientName: this.receiver?.name, recipientName: this.receiver?.name,
unitCode: this.unitCode, unitCode: this.unitCode,
}; };
@@ -648,7 +610,6 @@ export default class GiftedDialog extends Vue {
entityType: string; entityType: string;
currentEntity: { did: string; name: string }; currentEntity: { did: string; name: string };
}) { }) {
// Always allow editing - go back to Step 1 to select a new entity
this.goBackToStep1(data.entityType); this.goBackToStep1(data.entityType);
} }
@@ -659,24 +620,6 @@ export default class GiftedDialog extends Vue {
this.confirm(); this.confirm();
} }
/**
* Handle toggle entity type request from EntitySelectionStep
*/
handleToggleEntityType() {
// Toggle the appropriate entity type based on current step
if (this.stepType === "giver") {
this.currentGiverEntityType =
this.currentGiverEntityType === "person" ? "project" : "person";
// Clear any selected giver when toggling
this.giver = undefined;
} else if (this.stepType === "recipient") {
this.currentRecipientEntityType =
this.currentRecipientEntityType === "person" ? "project" : "person";
// Clear any selected receiver when toggling
this.receiver = undefined;
}
}
/** /**
* Handle amount update from GiftDetailsStep * Handle amount update from GiftDetailsStep
*/ */
+12 -17
View File
@@ -74,7 +74,7 @@
If you'd like an introduction, If you'd like an introduction,
<a <a
class="text-blue-500" class="text-blue-500"
@click="copyTextToClipboard('A link to this page', deepLinkUrl)" @click="copyToClipboard('A link to this page', deepLinkUrl)"
>click here to copy this page, paste it into a message, and ask if >click here to copy this page, paste it into a message, and ask if
they'll tell you more about the {{ roleName }}.</a they'll tell you more about the {{ roleName }}.</a
> >
@@ -110,7 +110,7 @@
* @since 2024-12-19 * @since 2024-12-19
*/ */
import { Component, Vue } from "vue-facing-decorator"; import { Component, Vue } from "vue-facing-decorator";
import { copyToClipboard } from "../services/ClipboardService"; import { useClipboard } from "@vueuse/core";
import * as R from "ramda"; import * as R from "ramda";
import * as serverUtil from "../libs/endorserServer"; import * as serverUtil from "../libs/endorserServer";
import { Contact } from "../db/tables/contacts"; import { Contact } from "../db/tables/contacts";
@@ -197,24 +197,19 @@ export default class HiddenDidDialog extends Vue {
); );
} }
async copyTextToClipboard(name: string, text: string) { copyToClipboard(name: string, text: string) {
try { useClipboard()
await copyToClipboard(text); .copy(text)
this.notify.success( .then(() => {
NOTIFY_COPIED_TO_CLIPBOARD.message(name || "That"), this.notify.success(
TIMEOUTS.SHORT, NOTIFY_COPIED_TO_CLIPBOARD.message(name || "That"),
); TIMEOUTS.SHORT,
} catch (error) { );
this.$logAndConsole( });
`Error copying ${name || "content"} to clipboard: ${error}`,
true,
);
this.notify.error(`Failed to copy ${name || "content"} to clipboard.`);
}
} }
onClickShareClaim() { onClickShareClaim() {
this.copyTextToClipboard("A link to this page", this.deepLinkUrl); this.copyToClipboard("A link to this page", this.deepLinkUrl);
window.navigator.share({ window.navigator.share({
title: "Help Connect Me", title: "Help Connect Me",
text: "I'm trying to find the people who recorded this. Can you help me?", text: "I'm trying to find the people who recorded this. Can you help me?",
+4 -9
View File
@@ -132,7 +132,7 @@
v-if="shouldMirrorVideo" v-if="shouldMirrorVideo"
class="absolute top-2 left-2 bg-black/50 text-white px-2 py-1 rounded text-xs" class="absolute top-2 left-2 bg-black/50 text-white px-2 py-1 rounded text-xs"
> >
<font-awesome icon="circle-user" class="w-[1em] mr-1" /> <font-awesome icon="mirror" class="w-[1em] mr-1" />
Mirrored Mirrored
</div> </div>
<div :class="cameraControlsClasses"> <div :class="cameraControlsClasses">
@@ -293,7 +293,7 @@ const inputImageFileNameRef = ref<Blob>();
export default class ImageMethodDialog extends Vue { export default class ImageMethodDialog extends Vue {
$notify!: NotifyFunction; $notify!: NotifyFunction;
$router!: Router; $router!: Router;
notify!: ReturnType<typeof createNotifyHelpers>; notify = createNotifyHelpers(this.$notify);
/** Active DID for user authentication */ /** Active DID for user authentication */
activeDid = ""; activeDid = "";
@@ -498,14 +498,9 @@ export default class ImageMethodDialog extends Vue {
* @throws {Error} When settings retrieval fails * @throws {Error} When settings retrieval fails
*/ */
async mounted() { async mounted() {
// Initialize notification helpers
this.notify = createNotifyHelpers(this.$notify);
try { try {
// Get activeDid from active_identity table (single source of truth) const settings = await this.$accountSettings();
// eslint-disable-next-line @typescript-eslint/no-explicit-any this.activeDid = settings.activeDid || "";
const activeIdentity = await (this as any).$getActiveIdentity();
this.activeDid = activeIdentity.activeDid || "";
} catch (error) { } catch (error) {
logger.error("Error retrieving settings from database:", error); logger.error("Error retrieving settings from database:", error);
this.notify.error( this.notify.error(
-130
View File
@@ -1,130 +0,0 @@
<template>
<div v-if="visible" class="dialog-overlay">
<div class="dialog">
<!-- Header -->
<h2 class="text-lg font-semibold leading-[1.25] mb-4">Select Project</h2>
<!-- EntityGrid for projects -->
<EntityGrid
:entity-type="'projects'"
:active-did="activeDid"
:all-my-dids="allMyDids"
:all-contacts="allContacts"
:conflict-checker="() => false"
:show-you-entity="false"
:show-unnamed-entity="false"
:notify="notify"
:conflict-context="'project'"
@entity-selected="handleEntitySelected"
/>
<!-- Cancel Button -->
<div class="flex gap-2 mt-4">
<button
class="block w-full text-center text-md uppercase bg-gradient-to-b from-slate-400 to-slate-700 shadow-[inset_0_-1px_0_0_rgba(0,0,0,0.5)] text-white px-2 py-2 rounded-md"
@click="handleCancel"
>
Cancel
</button>
</div>
</div>
</div>
</template>
<script lang="ts">
import { Component, Prop, Vue, Emit } from "vue-facing-decorator";
import EntityGrid from "./EntityGrid.vue";
import { Contact } from "../db/tables/contacts";
import { PlanData } from "../interfaces/records";
import { NotificationIface } from "../constants/app";
/**
* MeetingProjectDialog - Dialog for selecting a project link for a meeting
*
* Features:
* - EntityGrid integration for project selection
* - No special entities (You, Unnamed)
* - Immediate assignment on project selection
* - Cancel button to close without selection
*/
@Component({
components: {
EntityGrid,
},
})
export default class MeetingProjectDialog extends Vue {
/** Whether the dialog is visible */
visible = false;
/** Active user's DID */
@Prop({ required: true })
activeDid!: string;
/** All user's DIDs */
@Prop({ required: true })
allMyDids!: string[];
/** All contacts */
@Prop({ required: true })
allContacts!: Contact[];
/** Notification function from parent component */
@Prop()
notify?: (notification: NotificationIface, timeout?: number) => void;
/**
* Handle entity selection from EntityGrid
* Immediately assigns the selected project and closes the dialog
*/
handleEntitySelected(event: {
type: "person" | "project";
data: Contact | PlanData;
}) {
const project = event.data as PlanData;
this.emitAssign(project);
this.close();
}
/**
* Handle cancel button click
*/
handleCancel(): void {
this.close();
}
/**
* Open the dialog
*/
open(): void {
this.visible = true;
this.emitOpen();
}
/**
* Close the dialog
*/
close(): void {
this.visible = false;
this.emitClose();
}
// Emit methods using @Emit decorator
@Emit("assign")
emitAssign(project: PlanData): PlanData {
return project;
}
@Emit("open")
emitOpen(): void {
// Emit when dialog opens
}
@Emit("close")
emitClose(): void {
// Emit when dialog closes
}
}
</script>
<style scoped></style>

Some files were not shown because too many files have changed in this diff Show More