Skip to content

feat: import habits from another app via Astra (#10) - #331

Merged
thomasluizon merged 2 commits into
mainfrom
feat/astra-import
Jun 27, 2026
Merged

feat: import habits from another app via Astra (#10)#331
thomasluizon merged 2 commits into
mainfrom
feat/astra-import

Conversation

@thomasluizon

Copy link
Copy Markdown
Owner

Import habits from another app (via Astra)

Closes #10. A new (or existing) user can bring habits over from another app, with Astra doing the parsing — no custom import parser, no backend change.

Two entry points

  1. Onboarding — the "Meet Astra" step gains an "Import from another app" pill that writes a prefilled prompt to the chat draft, completes onboarding, and routes to Astra.
  2. Astra composer — a text-file attachment (.csv/.json/.txt/.md, ≤1 MB): the file is read client-side and its contents fold into the outgoing message, so the existing Astra pipeline parses it and creates the habits. Mirrors the existing image-attachment UX, but routes through the message text (no upload).

Per platform

  • Web: hidden <input type="file">File.text().
  • Mobile: expo-document-pickerexpo-file-system File(uri).text(). New dep: expo-document-picker@~55.0.12 (SDK 55 bundled; autolinks on next build).
  • Shared (packages/shared/src/chat): getChatTextFileValidationError (extension + size) + buildChatMessageWithFileContent (identical framing on both platforms).

i18n

New keys in both en.json + pt-BR.json (parity test green).

Validation

type-check 3/3 clean; lint 0 errors; tests — shared 1037, web 1663, mobile 639 passed.

🤖 Generated with Claude Code

Let new (or existing) users bring habits over from another app, with Astra
doing the parsing. No custom parser, no backend change.

Onboarding entry: an "Import from another app" affordance on the Meet Astra
step (both platforms) writes a prefilled prompt to the chat draft, completes
onboarding, and routes into the Astra chat.

Chat composer text-file attachment (.csv/.json/.txt/.md): the file is read
client-side and folded into the outgoing message as plain chat text so the
existing AI pipeline parses it. Mirrors the existing image-attachment UX
(attach button + removable chip) but rides as text rather than an upload.
- Web: hidden file input + File.text().
- Mobile: expo-document-picker + expo-file-system File.text().

Shared @orbit/shared/chat gains getChatTextFileValidationError (extension +
1 MB size gate) and buildChatMessageWithFileContent (identical framing across
platforms), mirroring the image-attachment seam. Size/format failures surface
i18n errors. New keys added to en.json and pt-BR.json.

Tests: shared validation/compose + i18n parity, web composer/hook/onboarding,
mobile composer hook/input-bar. Adds expo-document-picker (~55.0.12, Expo SDK
55 pinned) to apps/mobile.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@vercel

vercel Bot commented Jun 27, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

1 Skipped Deployment
Project Deployment Actions Updated (UTC)
orbit-ui-mobile-web Ignored Ignored Jun 27, 2026 5:42pm

Request Review

@claude claude Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review: PR #331

Scope: PR #331 — feat: import habits from another app via Astra (#10)
Recommendation: APPROVE

Summary

PR #331 adds two entry points for importing habits: (1) an "Import from another app" pill on the onboarding Meet-Astra step that pre-fills a chat draft and routes into Astra, and (2) a text-file attachment button (Paperclip) in the Astra chat composer that reads .csv/.json/.txt/.md files client-side and folds their content into the outgoing message. No backend changes. Both entry points are implemented symmetrically across web and mobile. Validation, i18n, and tests are complete. No Critical or High findings survive adversarial review.

Findings

Critical

None.

High

None.

Medium

None.

Low / Info

[Info] JSDoc WHY note without a URL link

  • dimension: Comment policy (#4)
  • location: packages/shared/src/chat/index.tsgetChatTextFileValidationError JSDoc
  • issue: The JSDoc body explains "The gate is extension-based because picker/browser MIME types are unreliable for .csv/.md" — reads as a WHY note but has no http(s):// URL to an upstream source.
  • risk: None — zero user impact. Linter may or may not flag it depending on whether it scans JSDoc bodies.
  • fix: Remove the explanatory sentence, or add a reference URL (MDN / caniuse). If CI lint passes, no action needed.
  • reference: CLAUDE.md rule 5

[Info] Harmlessly redundant globalThis.localStorage guard

  • dimension: No-workaround (#5)
  • location: apps/web/components/onboarding/onboarding-flow.tsx:124
  • issue: OnboardingFlow is 'use client' and handleImport only fires from a button press (never SSR), so localStorage is always defined. The guard is over-defensive.
  • risk: None.
  • fix: Could use localStorage.setItem(...) directly, matching trial-expired-modal.tsx. Or leave it — matches goal-detail-drawer.tsx and habit-detail-drawer.tsx.
  • reference: CLAUDE.md rule 1

Subagents

Agent Verdict
parity-checker PAIRED
i18n-syncer IN SYNC
contract-aligner MATCH
security-reviewer N/A — no orbit-api changes

Validation

Check Result
Lint N/A — sandbox-blocked (PR body: 0 errors)
Type check N/A — sandbox-blocked (PR body: 3/3 clean)
Tests N/A — sandbox-blocked (PR body: shared 1037, web 1663, mobile 639 passed)
Build (api) N/A — no backend changes

Deferred

  • Dimension 12 Security (API side): no orbit-api changes; text-file content sent over the same authenticated channel as any user message; prompt injection is user-self-scoped only.
  • Dimension 13 Backend hard rules: gated out — no orbit-api changes.
  • Dimension 11 Backward-compat guard: no Zod schema fields added, removed, or renamed — only new utility exports from packages/shared/src/chat/index.ts.
  • package-lock.json: only expo-document-picker@55.0.14 added (SDK 55, MIT).

What's good

  • Shared getChatTextFileValidationError + buildChatMessageWithFileContent extract to packages/shared at the right DRY level — both platforms validate identically and frame the Astra message identically.
  • Size-guard design (typeof candidate.fileSize === 'number' guard) is correct and intentional — consistent with the existing image-validation pattern; copyToCacheDirectory: true ensures asset.size is always populated by expo-document-picker.
  • Onboarding import is resilient: best-effort completeOnboarding() with empty catch mirrors the pre-existing handleFinish — user is never blocked by a transient API failure.
  • Test coverage is thorough: happy path + wrong-type error + onboarding import path with router assertion on both platforms.
  • All new UI uses semantic tokens only (var(--bg-elev), var(--hairline), var(--primary), etc.) — no raw slate values, no hardcoded violet rgba.
  • transition-[background-color,transform] on the new remove button is correct per DESIGN.md (transition-all is banned; named transitions are fine).

Recommendation

Clean implementation. All candidate Critical/High findings were refuted in the adversarial pass. Two Info observations are non-blocking. Ready to merge.

# Conflicts:
#	apps/mobile/components/onboarding/onboarding-meet-astra.tsx
#	apps/mobile/test-mocks/lucide-react-native.ts
#	apps/web/__tests__/components/onboarding/onboarding-flow.test.tsx
#	apps/web/components/onboarding/onboarding-meet-astra.tsx
@thomasluizon
thomasluizon merged commit 9b61fa5 into main Jun 27, 2026
8 checks passed
@thomasluizon
thomasluizon deleted the feat/astra-import branch June 27, 2026 17:42
@sonarqubecloud

Copy link
Copy Markdown

@claude claude Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No Critical or High findings. APPROVE. Severity: Critical=0, High=0, Medium=0, Low/Info=1 (non-blocking). Adds file-attachment button in Astra chat composer and Import-from-another-app pill on Meet Astra onboarding. Both platforms covered, i18n synced, shared validation in packages/shared, no API contract modified. One info finding: mobile inlines text-file attachment logic that web extracts into a dedicated hook — correct and tested, non-blocking follow-up. Parity: PAIRED. i18n: IN SYNC (8 new keys in both locales). Contract: MATCH. Backward compat: PASS. Security: PASS. orbit-api: N/A. Validation skipped (separate CI). PR body reports type-check clean, lint 0 errors, all tests passing.

thomasluizon added a commit that referenced this pull request Aug 19, 2026
)

The canvas run is complete. The screens project holds 20 documents and 21
pages, all new canon, and no document survives from the first run.

Screens built: Hoje, habit create, habit detail, Calendario, Progresso, the
Astra conversation, onboarding, auth, Perfil, notifications, the Pro pitch, the
subscription manage view, the surviving celebration, the error and static
surfaces, offline, search, step up, the overlay primitives, Wrapped, the four
static screens, and the Android widget.

Three design system rounds ran alongside the screens, each from gaps a build
reported rather than from a guess:

- D  the four defects found on the first Hoje: icon centring, emoji used as
     iconography, frozen and skip as habit statuses, and a hand drawn mark
- D2 DayStrip, Checkbox, a shell header slot, TimeField, StatTile states, Menu
- D3 BlockFrame's control, proposed and irreversible rows plus a risk slot,
     Composer's busy state and its discriminated atLimit, and an authorable
     conversation slot

Two of those are enforced by contract rather than by prose, because the same
bug came back twice under different disguises. DayStrip discriminates on scope,
so passing frozen to a habit strip is a type error. Composer discriminates on
state, so atLimit cannot render without its message and no other state can pass
one.

Six open questions were put to Thomas and answered: a rejected preview
collapses to one line, step up is a hand off, a partially failed bulk create
keeps what it created, the day arc is the exact fraction, Calendario pages on
an empty account, and the month rate has a definition.

DESIGN.md gains that definition, because Calendario computed it and correctly
flagged that no endpoint states the window, which means the next surface would
compute a different number from the same data.

Four API tickets were filed from what the screens reported they needed: #331
the streak repair endpoint, #332 the achievements payload, #333 the Astra
metrics schema, #334 the notification urls.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant