Commit Graph

39 Commits

Author SHA1 Message Date
Víctor Falcón 2cca291773
fix(pwa): keep mobile OAuth consent from being swallowed by the installed app (#701)
## Problem

On mobile, starting an OAuth connection from ChatGPT (MCP connector)
opens the **installed Whisper Money PWA** and lands on the **dashboard**
instead of the OAuth consent screen — so the connection can never be
approved.

## Cause

The web app manifest declares `scope: "/"`, so the installed PWA claims
the entire origin, including `/oauth/authorize`. On Android the OAuth
link gets captured into the app, and the default launch behavior just
**focuses the already-open window (the dashboard) and drops the incoming
URL** — the consent page never loads.

Narrowing `scope` to exclude `/oauth` isn't viable: the app's routes are
flat (`/dashboard`, `/transactions`, `/accounts`, …) and share no
prefix, so a narrower scope would push half the app out to the browser.

## Fix

- `site.webmanifest`: add `launch_handler.client_mode: "focus-existing"`
so the launcher hands the app the captured target URL via the launch
queue.
- `app.tsx`: a `launchQueue` consumer that navigates to the target URL
when it's an `/oauth/` path. Every other launch is a no-op (keeps its
place).

Low blast radius — additive manifest field + a guard scoped to `/oauth/`
paths; existing (desktop) OAuth is unaffected.

## Verification

Cannot be tested off-device. After deploy, on the phone:
1. **Reinstall the PWA** (remove from home screen, re-add) so Android
re-fetches the manifest.
2. Retry the ChatGPT OAuth connect → should land on the "Connect ChatGPT
to Whisper Money" consent screen.

Depends on `launch_handler`/`launchQueue` (Chrome/Edge 102+, covers
nearly all Android).
2026-07-18 19:37:04 +02:00
Víctor Falcón 578a9b44d8
fix(banking): keep the native green Wise logo, not the aggregator's (#590)
## Context

Follow-up to #589. In production, the deduped Wise entry rendered with
the **Enable Banking blue wordmark** instead of our own green "W" mark,
even though clicking it correctly starts our native (personal-token)
integration.

## Cause

#589 set the native Wise provider's `logo` to the Enable Banking brand
URL (`https://enablebanking.com/brands/BE/Wise/`) and deleted our local
asset — based on the mistaken belief that the green mark came from the
aggregator. It was the other way around: our asset
(`public/images/banks/logos/wise.png`, added in #525) **was** the green
mark; the Enable Banking URL serves the old blue wordmark.

## Fix

- Restore the green Wise asset.
- Point the native provider's `logo` back at
`/images/banks/logos/wise.png`.
- Update the regression test's expected logo accordingly.

Only the `logo` field was ever wrong — the dedup and unique-key fixes
from #589 are unaffected, so a single Wise entry still surfaces our
native integration.
2026-06-24 09:34:37 +02:00
Víctor Falcón ed5aac0c4a
fix(banking): stop Wise appearing multiple times in the connect list (#589)
## Problem

In production, searching for a bank like **Wise** in the *Connect Bank
Account* picker showed it many times, and **the count grew every time
the search box changed** (4 → 6 → …). Only one entry — the real native
integration — should appear.

## Root cause

Two bugs stacked on top of each other:

1. **Non-unique React key.** The list rendered each institution with
`key={institution.name}`. Wise is offered both natively (our own
API-token integration) and by the Enable Banking aggregator, so two rows
shared the key `"Wise"`. On every re-render of the filtered list, React
couldn't reconcile the duplicate keys and **leaked orphaned DOM nodes**
instead of replacing them — so the entry multiplied with each keystroke
(and rendered out of alphabetical order). React even warned: *"two
children with the same key, Wise … may cause children to be
duplicated"*.
2. **No dedup against native integrations.** Even without the growth,
Wise showed twice (aggregator + native). It should only surface through
our own integration.

The bank list is built from the Enable Banking API merged with our
native providers — it does **not** read from the `banks` table, which
was a red herring (only 2 non-duplicated Wise rows there).

## Changes

- Unique key (`name-country-index`) in both the dialog and inline
pickers.
- Drop aggregator institutions we already integrate natively, so Wise
only surfaces through its own integration (general — also covers
Binance, Coinbase, etc.).
- Point the native Wise logo at the official brand mark; remove the
now-unused local asset.

## Tests

Added regression tests covering dedup and repeated filtering. Reproduced
the bug first (counts grew `[3,4,5,6,7]`), then confirmed the fix holds
it at `1`. Full JS suite green (242 tests).
2026-06-23 16:20:59 +02:00
Víctor Falcón f60e6d7035
feat(banking): add Interactive Brokers sync via Flex Web Service (#581)
## What

Adds **Interactive Brokers** as a banking sync provider (investment
account, balances only), mirroring the Indexa Capital integration.

It uses the **Flex Web Service** rather than IBKR's Web API: the Web API
requires registering as an IBKR third party (business entity, Compliance
approval, RSA-signed OAuth, ~3-5 weeks), which is overkill for read-only
balance sync. Flex is a read-only token + Query ID model that fits our
existing API-key provider shape.

### How the sync works
- The user creates an Activity Flex Query (NAV + Open Positions) and a
Flex Web Service token in their IBKR Client Portal, then pastes both.
- Client flow: `SendRequest` → reference code → poll `GetStatement` →
parse the XML statement.
- Mapping: `EquitySummaryByReportDateInBase@total` → `balance` (daily
rows give historical backfill on first sync); `Σ(OpenPosition
costBasisMoney × fxRateToBase) + cash` → `invested_amount`, so **profit
derives as `balance − invested_amount`** (unrealized P&L), like Indexa
Capital. Everything is already in base currency, so no FX conversion is
needed.
- One statement covers every account, so the syncer fetches once per
connection to respect IB's per-query rate limit.
- IB returns HTTP 200 with an error XML, so the client translates Flex
error codes into the exceptions the sync job already understands:
`RequestException(401)` for token problems, `RequestException(429)` for
throttling, `TransientBankingProviderException` otherwise.

### Connect flow
- New connect/update-credentials endpoints validate the credentials by
pulling a statement, then build pending accounts from it.
- Credentials reuse the encrypted `api_token` (Flex token) and
`api_secret` (Flex Query ID) columns — **no migration**.
- The IB option (two fields) is added to the connect dialog, inline
connect flow, and update-credentials dialog, with Spanish translations.

## Feature flag (why this is a draft)

Gated behind a Pennant feature `App\Features\InteractiveBrokers` (off by
default). It was built against documented/open-source Flex XML fixtures,
**not a live IBKR account** (we don't have one). Before enabling,
validate against a real account (beta tester or a free IBKR account):

```
php artisan feature:enable InteractiveBrokers user@example.com
```

If the parser needs tweaks against real XML, they should be minor
(field-name level).

## Tests
- Client + balance sync: NAV → balance, invested/profit, daily backfill,
since-date incremental, multi-account, GetStatement polling, token-401 /
rate-limit-429 mapping.
- Controller: feature-flag gate (403), valid/invalid credentials,
subscription gate, onboarding auto-create, validation.
- Factory wiring, enum cases, job-level sync, feature-flag visibility
(vitest).
- Spanish translations added (enforced by `LocalizationTest`).

All green: `pint --test`, `phpstan`, OpenBanking + localization suite,
vitest.
2026-06-23 11:39:24 +02:00
Toni Grunwald 1c5a76a3a4
feat: add Wise open banking integration with balance sync (#525)
## Summary
Adds **Wise** as an API-token banking provider — connect flow,
transaction sync, and per-wallet balance sync — and fixes two bugs that
kept business/extra profiles and balances from working.

## Changes
- **Balance sync** — new `WiseBalanceSyncService` pulls
borderless-account balances on every sync. Previously Wise balances were
never fetched, so the displayed balance went stale (frozen at the last
value).
- **Multi-profile connect** — `WiseController` now creates accounts for
**every** profile on the token (personal *and* business), not just one.
- **ID-format fix** — `external_account_id` is now
`"{profileId}:{currency}"`. The connect flow previously wrote the
**borderless-account id**, but both sync services parse the **profile
id** — so any UI-connected account (and every business profile) silently
failed to sync.
- **Tests** — `WiseBalanceSyncTest` (currency matching, idempotent
today-balance, skip-when-unmapped) and `WiseControllerTest`
(multi-profile pending accounts, onboarding auto-create, invalid token).

## Test plan
- [x] `php artisan test
tests/Feature/OpenBanking/WiseBalanceSyncTest.php
tests/Feature/OpenBanking/WiseControllerTest.php` — 6 passing / 28
assertions
- [x] `vendor/bin/pint` — clean
- [ ] CI (full pest + lint + build)

🤖 Generated with [Claude Code](https://claude.com/claude-code)

---------

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-authored-by: Víctor Falcón <victoor89@gmail.com>
2026-06-16 13:23:25 +00:00
Víctor Falcón 259a9a9712
chore: replace Caddy with Portless for local HTTPS proxy (#258)
## Summary

- Replace Caddy reverse proxy with [Portless](https://portless.sh) for
local HTTPS, eliminating manual cert generation and `/etc/hosts` editing
- Two URL strategies coexist: `composer run dev` uses worktree-aware
URLs (`https://<branch>.dev.whisper.money.localhost`), `whispermoney
start` uses a fixed alias (`https://whisper.money.localhost`)
- Remove Caddyfile, `docker/caddy/` directory, caddy service from
`compose.yaml`, and cert-related `.gitignore` entries
- Simplify `vite.config.ts` by removing caddy cert detection block (Vite
stays on plain HTTP since browsers treat localhost as secure context)
- Overhaul `public/setup.sh` to use `portless trust`, `portless proxy
start`, and `portless alias` instead of SSL cert generation and hosts
file editing
2026-04-02 16:39:44 +01:00
Víctor Falcón 1b7b147832
fix(ui): app icon visible on light wallpapers + country select overflow on mobile (#162)
## Why

### Problem
Two iOS-specific UX issues reported by users:

1. **App icon invisible on light wallpapers** — the home screen shortcut
icon was a black logo on a white background, making it nearly invisible
against light iOS wallpapers (appeared as a faint frosted outline).
2. **Country dropdown overflows viewport** — the country picker in the
Connect Account flow exceeded the mobile viewport height, making it
impossible to scroll through the full list on small screens.

### Root Cause
1. All icon PNGs were exported with a white background and a black/grey
logo. The PWA manifest `background_color` was also `#ffffff`. No dark
background was baked in.
2. `SelectContent` used a fixed `max-h-96` (384px) regardless of
available screen space. The `SelectPrimitive.Viewport` height was bound
to `--radix-select-trigger-height` (the trigger element's height)
instead of the actual available viewport space, preventing Radix's
internal scroll buttons from working correctly on mobile.

## What

### Changes
- Regenerated all icon PNGs (favicon, apple-touch-icon,
web-app-manifest, whispermoney_icon_x*) — white logo on `#1b1b18` dark
background via ImageMagick Screen composite
- Updated `site.webmanifest` `background_color` from `#ffffff` →
`#1b1b18`
- Fixed `SelectContent` max height: `max-h-96` →
`max-h-[min(24rem,var(--radix-select-content-available-height))]`
- Fixed `SelectPrimitive.Viewport` height:
`--radix-select-trigger-height` →
`--radix-select-content-available-height`

## Verification

### Tests
No automated tests cover static asset generation or CSS utility values —
these are visual/rendering fixes.

### Manual
- iOS: Remove and re-add the home screen shortcut from Safari to pick up
the new `apple-touch-icon.png`. Icon should show a white bird logo on a
dark background, visible on any wallpaper.
- Mobile (iOS/Android): Open Connect Account dialog and verify the
country dropdown fits within the viewport and is fully scrollable.
2026-02-28 13:41:41 +00:00
Víctor Falcón 0e0a5c25fb
Redesign landing page with real feature screenshots (#125)
## Summary
- Redesign the landing page with a screenshot-driven layout replacing
placeholder icons
- Add real screenshots (light + dark mode) for all feature sections:
accounts, transactions, privacy, import, budgets (list, detail, edit),
and cashflow
- Update fonts to IBM Plex and refine section spacing, testimonials,
pricing, and FAQ

## Test plan
- [ ] Verify all feature screenshots load correctly in both light and
dark mode
- [ ] Check responsive layout across mobile, tablet, and desktop
- [ ] Confirm lazy loading works for images below the fold
2026-02-16 09:24:07 +01:00
Víctor Falcón 1b013fa687 Update OG Image 2026-02-14 17:15:37 +01:00
Víctor Falcón 60ebe7e86f
Replace E2E encryption mentions with privacy-first messaging (#124)
## Summary

- Removes all references to "end-to-end encryption" and "E2E" across the
codebase
- Replaces with privacy-first messaging: data is never shared with third
parties, users own their data, app remains private and secure
- Updates all user-facing content: landing page, onboarding, billing,
terms, emails, setup script, subscription plans, and Coolify template
- Updates Spanish translations (`lang/es.json`) to match new messaging
- Updates `CLAUDE.md` and `README.md` project descriptions

## Files changed (12)

- `CLAUDE.md` — Project description
- `README.md` — Feature list
- `config/subscriptions.php` — Plan feature lists
- `lang/es.json` — Spanish translations (18 keys updated)
- `public/setup.sh` — CLI banner text
- `resources/js/components/onboarding/step-smart-rules.tsx` — Onboarding
step
- `resources/js/pages/settings/billing.tsx` — Billing benefits
- `resources/js/pages/terms.tsx` — Terms of service
- `resources/views/mail/drip/promo-code.blade.php` — Promo email
- `resources/views/mail/drip/welcome.blade.php` — Welcome email
- `resources/views/mail/user-lead-invitation.blade.php` — Lead
invitation email
- `templates/coolify/whisper-money.yaml` — Coolify slogan

## Test plan

- [x] All 492 tests pass
- [x] Pint formatting passes
- [x] Prettier formatting passes
- [x] ESLint passes
2026-02-14 16:59:50 +01:00
Víctor Falcón caccac6166
feat: Docker dev env with Caddy, PHP on host (#103)
## Summary

- Replaces Valet/Traefik with Docker-based infrastructure: Caddy (HTTPS
reverse proxy), MySQL, Redis, Mailhog
- PHP runs natively on the host via `composer run dev` — Caddy proxies
to `host.docker.internal:8000`
- PHP container available under `--profile full` for self-hosting via
`setup.sh`
- Vite dev server serves over HTTPS using mkcert certs to avoid
mixed-content issues
- `.env.example` defaults updated for host-based dev workflow (127.0.0.1
+ forwarded ports)
- Deletes obsolete `traefik/` directory

## Test plan

- [x] `docker compose up -d` starts caddy, mysql, redis, mailhog (NOT
php)
- [x] `composer run dev` serves PHP on :8000, Vite on :5173 with HTTPS
- [x] `https://whisper.money.local` loads correctly via Caddy
- [x] Vite HMR works (edit a React component, see hot reload)
- [x] `php artisan test --compact` passes
- [x] `docker compose --profile full up -d` also starts the PHP
container
2026-02-09 13:31:16 +01:00
Víctor Falcón 1930cf229e feat: Remove dev command from whispermoney 2026-02-07 18:31:31 +01:00
Víctor Falcón da328efe79 fix: Install script improvements 2026-02-07 17:46:33 +01:00
Víctor Falcón b4897ef425 feat: Improve PWA standalone experience and redirect to dashboard (#90)
## Summary
- Add service worker registration and `viewport-fit=cover` for proper
iOS standalone mode (no Safari chrome)
- Change manifest `start_url` to `/dashboard` so PWA users skip the
landing page
- Add tests for PWA configuration

## Test plan
- [x] Deploy and re-add PWA to iOS home screen — should open as
standalone (no address bar)
- [x] PWA launch should go to `/dashboard` instead of the landing page
- [x] Non-authenticated PWA users should be redirected to login

Closes #71
2026-02-01 11:33:10 +01:00
Víctor Falcón f03fcf5ac6 feat: Print sponsor message on whispermoney script 2026-01-28 19:57:28 +01:00
Víctor Falcón 439ec86722
chore: Simplify IndexedDB sync by moving to Inertia shared props (#63)
This PR simplifies the IndexedDB synchronization mechanism by removing
individual sync controllers and services, and instead using Inertia.js
shared props to provide data globally across the application.

## Benefits

1. **Simplified Architecture**: Removed complex sync logic for
static/semi-static data (accounts, categories, banks, labels, automation
rules)
2. **Better Performance**: Data is now shared via Inertia props,
eliminating unnecessary API calls and IndexedDB operations
3. **Reduced Complexity**: Significantly reduced codebase size (~2000
lines removed)
4. **Better UX**: Data is immediately available on page load without
waiting for sync operations
5. **Maintainability**: Fewer moving parts means easier to maintain and
debug

## Migration Notes

- Transaction syncing still uses IndexedDB for offline support
- All other data (accounts, categories, banks, labels, automation rules)
is now fetched via Inertia shared props
- Components automatically receive updated data on navigation without
manual sync operations
2026-01-19 19:15:26 +01:00
Víctor Falcón 16a331ab5f feat: Don't check upgrades if not in main branch or in DEV_MODE 2026-01-19 13:10:01 +01:00
Víctor Falcón cae804c588 fix: lsoft port checking was broken 2026-01-16 19:59:01 +01:00
Víctor Falcón c953dd694d feat: Check ports availavility automatically on setup script 2026-01-16 19:28:14 +01:00
Víctor Falcón b2ae8cdb07 fix: composer dependencies install 2026-01-16 19:25:18 +01:00
Víctor Falcón 7fa9d620b2 fix: better script suggestions 2026-01-16 19:22:47 +01:00
Víctor Falcón 7cec6c0053 fix: wrong script path 2026-01-16 19:21:22 +01:00
Víctor Falcón 44f2efab87 fix: APP_KEY generation on setup script 2026-01-16 19:06:56 +01:00
Víctor Falcón 2607861fce fix: Wrong whispermoney script path 2026-01-16 18:48:44 +01:00
Víctor Falcón 3ab920ee60 fix: wrong script path 2026-01-16 18:28:27 +01:00
Víctor Falcón ffd96949e5
feat: Add wispermoney local command (#59)
## Changes

- Moved `setup.sh` to `public/setup.sh` so it's accessible via
`https://whisper.money/setup.sh` for remote users
- Added `wispermoney` wrapper command in root directory for easy local
execution
- Updated README to document the new `wispermoney` command
- Removed duplicate `setup.sh` from root directory

## Benefits

- Remote users can still use: `bash <(curl -fsSL
https://whisper.money/setup.sh)`
- Local developers can now use: `./wispermoney install` (or
start/stop/upgrade)
- Single source of truth - no duplicate setup scripts
- Cleaner project structure
2026-01-16 16:40:15 +01:00
Víctor Falcón 1e3de4b1e0 landing: add encryption video 2025-12-30 15:35:10 +01:00
Víctor Falcón 1e866a514d remove unused images 2025-12-30 14:24:16 +01:00
Víctor Falcón 104723cbef chore: update landing images 2025-12-30 10:47:42 +01:00
Víctor Falcón 4bc24c7bb2 chore: update PWA maskable icons 2025-12-30 07:22:19 +01:00
Víctor Falcón 61160ee15e mail: whisper money logo on header 2025-12-30 07:22:18 +01:00
Víctor Falcón b79310ad19 New icons for PWA 2025-12-11 16:46:37 +01:00
Víctor Falcón 9431ba0e05
Set up PWA with maskable icons and native status bar integration (#22)
## Changes

- Updated `site.webmanifest` with proper PWA configuration:
  - Added all maskable icons (48, 72, 96, 128, 192, 384, 512)
  - Set theme_color and background_color for native feel
  - Added start_url, scope, id, description, and categories

- Enhanced iOS/Safari support in `app.blade.php`:
  - Added `apple-mobile-web-app-capable` for fullscreen mode
- Added `apple-mobile-web-app-status-bar-style: black-translucent` for
integrated status bar
  - Added `mobile-web-app-capable` for Android

The dynamic theme-color meta tags (`#ffffff` for light, `#383838` for
dark) match the app's background colors, ensuring the status bar blends
seamlessly with the app.
2025-12-09 17:02:20 +01:00
Víctor Falcón 2d92afa66d Add bank logos and improve transaction table UI
- Add bank logos for BBVA, Indexa Capital, and ING
- Move bank logo from Bank column to Account column
- Make bank logos rounded-full in both custom bank form and transaction table
- Adjust description column max width from 500px to 400px
- Improve Account column layout with flex container and min-width
2025-12-05 10:09:30 +01:00
Víctor Falcón 669646aacb SEO landing page 2025-11-25 11:33:11 +01:00
Víctor Falcón c5b3181731 Favicon 2025-11-25 09:53:13 +01:00
Víctor Falcón f5ca2c6bdd Add dark mode images and improve UI responsiveness 2025-11-24 13:43:24 +01:00
Víctor Falcón dffc5137ba
Merge pull request #1 from whisper-money/landing-page
Landing page
2025-11-24 12:11:35 +01:00
Víctor Falcón fb140bbece Set up a fresh Laravel app 2025-11-07 12:01:36 +00:00