Commit graph

21 commits

Author SHA1 Message Date
Paulo Castellano
dba6346226 refactor(auth): split workspace gate into EnsureHasWorkspace middleware
EnsureAccountReady bundled a subscription gate (redirects to onboarding,
SaaS only) with a workspace gate (redirects to workspace creation). The
connect routes can't sit behind it because connecting/disconnecting
happens during onboarding, before a subscription exists.

Split the workspace gate into a standalone EnsureHasWorkspace middleware:

- EnsureAccountReady is now subscription-only.
- EnsureHasWorkspace redirects to workspace creation when there is no
  current workspace, in both SaaS and self-hosted modes.
- The social connect group gains EnsureHasWorkspace; the main app group
  gains it alongside EnsureAccountReady (listed after it, so the
  subscription gate still runs first — no custom middleware priority).
- The repeated `if (! $workspace) redirect()` guard is removed from the
  connect/store/authorize/disconnect/index/toggle handlers, and their
  return types are tightened (no more dangling RedirectResponse).

LinkedIn connect's no-workspace path changes from a popup callback to the
same redirect as the other platforms.
2026-06-25 14:30:15 -03:00
Paulo Castellano
cd2798fd8d refactor(social): use native Inertia for all connect popup flows
Replace the per-platform native form POST + manual CSRF + JSON/Blade
popup callback with a single Inertia mechanism:

- popupCallback() renders the accounts/PopupCallback Inertia page (notifies
  the opener + closes the popup) for both the GET OAuth callbacks and the
  selection submits. Drops the auth.social-callback Blade view, the
  expectsJson JSON branch, and useHttp/useSocialConnect on the frontend.
- Selection/credential pages (LinkedIn, Facebook, Instagram, Bluesky,
  Mastodon) use Inertia useForm: automatic CSRF + native validation errors.
- Unify the three selection screens on one row + View/Choose layout; the
  LinkedIn company tag now uses a building icon.
- Bluesky auth failures throw ValidationException (422 for XHR, redirect
  back with errors otherwise).

Controller tests updated from assertViewIs/assertViewHas to assertInertia.
2026-06-25 13:54:16 -03:00
Paulo Castellano
2d07002325 chore: remove vestigial onboarding OAuth flag and legacy plan env vars
The onboarding/connect and accounts screens now share the same NetworkConnectGrid
with the same popup-close-and-reload flow, so the social_connect_onboarding flag
no longer affects anything in the normal path. Remove the frontend plumbing
(useOAuthPopup query param, the grid's onboarding prop) and the dormant backend
scaffolding (the session flag in 6 OAuth controllers, getRedirectRoute, and the
ignored isOnboarding arg); the YouTube no-channels error now redirects to
app.accounts directly. Also drop the unused legacy plan price-id env vars from
.env.example (only the Workspace plan remains).
2026-06-22 13:54:42 -03:00
Paulo Castellano
0619f2a0f4 refactor: address code review for per-workspace pricing
Bug fix
- Surface the localized "network already connected" message on the
  Facebook/Instagram/InstagramFacebook/LinkedInPage/Threads/YouTube OAuth
  callbacks: catch NetworkAlreadyConnectedException before the generic catch
  so the conflict no longer falls through to a generic error + Log::error.

Scope / dead code
- Remove the orphaned BillingController::checkout() + app.billing.checkout route
  (onboarding starts checkout directly); delete the now-dead DiscordWidget and
  useFeatureAccess composable; prune orphaned i18n keys left by removing the
  plan picker / upgrade dialog / count limits (billing.subscribe.*,
  accounts.limit_reached, workspaces.limit_reached, common.discord.*).
- Drop the unused `plan` prop from the usage page and the unused `label` from
  the onboarding persona payload (labels come from i18n); remove
  Persona::options()/label().

Conventions
- declare(strict_types=1) on the two new migrations.
- Extract autofill validation into AutofillBrandRequest (FormRequest).
- Drop the unused $plan param from AccountPolicy::swapPlan.
- CreateUser: drop the stale config('cashier.trial_days', 7) fallback (now 8).
- Rename LimitEnforcementTest to InvitePermissionTest; use Pest mock() helper in
  WorkspaceQuantitySyncTest; move shared test helpers into Pest.php.
- Memoize BillingCycle window() + subscription lookup.
2026-06-21 21:28:47 -03:00
Paulo Castellano
cbf8fb283c feat: per-workspace pricing, onboarding, and billing overhaul
Pricing
- Bill per workspace ($12/mo or $120/yr each); Stripe quantity tracks the
  workspace count and syncs on workspace create/delete.
- 2,500 AI credits per workspace, pooled at the account level; monthly reset
  on the billing anniversary, annual granted upfront (no rollover).
- One social account per network per workspace; remove all count-based limits
  (workspace/social/member) and the legacy plan tiers (single Workspace plan).

Onboarding (cloud only: SELF_HOSTED=false + PostHog)
- Replace the /subscribe plan picker with /onboarding persona selection
  (Creator/Freelancer/Startup/Agency/Small business/Other), saved on the user
  (users.persona) and mirrored to PostHog, then Stripe Checkout on the monthly
  price. 8-day trial so Stripe displays 7.

Billing screen
- Remove the Change Plan dialog (dead with a single plan); add an annual-upgrade
  banner for monthly subscribers (swapToYearly).
- Current-plan card shows the workspace count instead of the plan name.

System AI
- Brand analyzer / workspace autofill is always allowed and never debits credits
  (system feature, not the user's usage).

Self-hosted (SELF_HOSTED=true) bypasses all billing, credit, limit, network,
and onboarding logic.
2026-06-21 20:40:03 -03:00
Paulo Castellano
cbea8394fc feat: localize OAuth popup callback messages across 12 controllers
User reported the social-account OAuth callback popup ('Threads account
connected!') stayed in English regardless of the active locale. The
hardcoded message was wired through SocialController and 11 platform
controllers (Bluesky, Facebook, Instagram, InstagramFacebook, LinkedIn,
LinkedInPage, Mastodon, Pinterest, Threads, TikTok, YouTube), plus the
Blade view that the popup renders.

Adds an accounts.popup_callback i18n block (en/pt-BR/es) covering:
- The popup chrome (title, closing/close-now status text).
- Generic success/reconnect messages (one shared 'Account connected!'
  / 'Account reconnected!' line — the popup already shows a checkmark
  and lives for ~2s so platform-specific wording wasn't pulling weight).
- Error variants (account/page/channel) and contextual edge cases
  (page not found, no Facebook pages, no YouTube channels, etc.).

Updates every popupCallback() callsite to read from these keys, plus
the Blade view's title and submessage. Regenerates the JSON locale
bundle so laravel-vue-i18n stays in sync.
2026-05-07 14:03:52 -03:00
Paulo Castellano
f4fd8a2aab refactor: centralize social API base URLs into the configuration file to improve maintainability and environment flexibility 2026-05-02 13:49:20 -03:00
Paulo Castellano
a8bb44f0e5 fix: self-host billing bypass, Facebook API metrics migration, X token refresh, and duplicate social accounts
- Block all billing/usage/subscribe routes in self-hosted mode (redirect to calendar)
- Hide billing_email field in account settings when self-hosted
- Migrate deprecated Facebook Page Insights metrics to new Media Views API (v25.0)
- Fix X analytics token refresh to use Basic Auth (matching XPublisher/ConnectionVerifier)
- Prevent duplicate social accounts by using updateOrCreate across all OAuth controllers
2026-04-15 09:46:18 -03:00
Paulo Castellano
8caa3d55c7 feat: redesign accounts page with multi-account support
- New card-based layout showing connected accounts with avatar, platform
  badge, brand label, and connection date
- "Add Social" button opens dialog with platform grid for connecting
- Removed unique constraint on workspace_id+platform to allow multiple
  accounts per platform
- Removed "already connected" checks from all 13 OAuth controllers
- Updated tests to verify multi-account connection works
2026-04-14 19:48:35 -03:00
Paulo Castellano
c867faa2ee fix: address code review issues for billing migration 2026-04-14 18:44:47 -03:00
Paulo Castellano
4cf601e618 feat: add YouTube Analytics with 7 channel metrics
Integrates YouTube Analytics API v2 to display channel-level metrics
(views, minutes watched, avg view duration, avg view percentage,
subscribers gained/lost, likes) with date range support and caching.
2026-04-14 13:10:27 -03:00
Paulo Castellano
c020419db1 feat: social account toggle action, API, MCP + full test coverage
- Extract ToggleSocialAccount action from SocialController
- Add API endpoints: GET /social-accounts, PUT /social-accounts/{id}/toggle
- Add MCP tools: ListSocialAccountsTool, ToggleSocialAccountTool
- Fix all MCP tools: findOrFail → find + Response::error for graceful errors
- Fix MCP tools using $request->validated() without validate() call
- Fix return types to Response|ResponseFactory for error paths
- Add SocialAccountResource is_active/status fields (no tokens exposed)
- Add 43 MCP tests covering all 18 tools (CRUD, validation, cross-workspace)
- Add API response structure tests for posts, hashtags, labels, workspace
- Add API validation tests for post create/update, api-key expiry, label color
- Add API cross-workspace delete tests for hashtags and labels
- Add app validation tests for hashtag/label update, invite fields, password
- Add auth required tests for notifications, profile delete, api-keys index
- Add media reorder validation tests
2026-03-31 01:42:39 -03:00
Paulo Castellano
74c6442728 refactor: code review fixes — policies, enums, data_get, tests
- Refactor WorkspacePolicy to use pivot role instead of workspace.user_id
- Add manageBilling policy (owner only) to BillingController
- Fix ApiKeyController authorization (view → manageTeam for store/destroy)
- Fix WorkspaceInviteController using workspace.user_id for owner checks
- Fix WorkspaceController settings is_owner using workspace.user_id
- Create PostAction enum for UpdatePost/PostController action strings
- Create ApiToken\Status enum
- Add User::SUBSCRIPTION_NAME constant, replace all hardcoded 'default'
- Convert wantsEmailFor to accept NotificationType enum
- Convert all $data[] to data_get() across publishers, controllers, jobs
- Fix SocialLoginController callback missing try/catch
- Fix SocialController::toggleActive missing workspace null check
- Fix UpdatePost NPE on meta merge when postPlatform not found
- Remove HTML5 required attributes from form inputs
- Convert function declarations to arrow functions in Vue components
- Replace hardcoded URLs with Wayfinder route helpers
- Replace new Date() with dayjs
- Add 16 new test files covering policies, authorization, publishing
2026-03-31 00:40:18 -03:00
Paulo Castellano
56b8c92e72 refactor: settings redesign, Spanish translations, language system, strict_types
Settings pages:
- Redesign layout to match Sendkit (max-w-4xl, space-y-12, Separator sections)
- Merge Members page into Workspace settings with Table, invite Dialog, ConfirmDeleteModal
- Add workspace logo upload/delete routes and controller methods
- Translate all hardcoded strings in Workspace.vue modals

Language system:
- Drop languages table, replace language_id FK with locale string column on users
- Create config/languages.php for available languages and default locale
- Add Spanish (es) translations (13 files)
- Simplify HandleInertiaRequests, ProfileController, RegisteredUserController

Code quality:
- Add declare(strict_types=1) to all PHP files
- Fix MastodonPublisher using wrong attribute (filename -> original_filename)
- Fix HasMediaTest for new has_photo/photo_url accessors
- Fix PublishToSocialPlatformTest type error revealed by strict_types
- Remove orphaned Language model from AppServiceProvider morph map
- Update User TypeScript interface (has_photo, photo_url, locale)
- Eager load media relation on workspaces to prevent N+1
- Add 8 new tests for workspace logo upload/delete
- Update workspace settings test to assert members/invitations props

All 710 tests passing.
2026-03-30 00:20:43 -03:00
Paulo Castellano
8689e54e55 refactor: restructure to Actions, subdomain routes (app/api), API tokens
- Extract business logic from controllers into Action classes:
  Post/, Workspace/, Hashtag/, Label/, Invite/, ApiKey/
- Create subdomain routing: app.trypost.test (Inertia dashboard),
  api.trypost.test (REST API with token auth)
- Add ApiToken model with tp_ prefix, token_lookup/hash auth
- Add AuthenticateApiToken middleware for API authentication
- Create Api controllers with JSON Resources for all entities
- Create App controllers that use Actions + Inertia responses
- Organize Form Requests into Api/ and App/ directories
- Add api_tokens migration
- Update all route names with app. prefix
- Update all tests to use new route names (684 passing)
2026-03-29 19:24:28 -03:00
Paulo Castellano
4fa1cb32dc feat: add include_granted_scopes to Google OAuth
Enables incremental authorization so previously granted scopes
are preserved when requesting new permissions.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-26 13:41:15 -03:00
Paulo Castellano
2239d2480c feat: translate billing page and fix flash messages
- Move billing routes from /billing to /settings/billing
- Add billing translations (en/pt-br)
- Update billing/Index.vue to use $t() for all strings
- Fix all controllers using route('dashboard') to use route('calendar') or route('accounts')
- Replace ->with('error', ...) pattern with session()->flash('flash.banner', ...)
- Add self-hosted mode support for hasActiveSubscription() and workspace methods
- Add flash translations for account connection errors

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-26 12:01:53 -03:00
Paulo Castellano
7042bcb5ae feat: Implement soft deletes for workspace hashtags and labels, and integrate content_type into the initial post platforms table creation. 2026-01-17 14:44:37 -03:00
Paulo Castellano
7c00c3387e feat: Implement user onboarding, subscription management, and refactor social integrations with new UI components and mail templates. 2026-01-16 23:46:30 -03:00
Paulo Castellano
7867fcce02 feat: Implement real-time post status updates and configurable social platform enabling/disabling. 2026-01-16 12:36:52 -03:00
Paulo Castellano
7c80d717b1 chore: first commit 2026-01-15 14:24:39 -03:00