Commit graph

12 commits

Author SHA1 Message Date
Paulo Castellano
173a1e4c61
Fix Facebook Page connect pagination (#212) (#253)
* Fix Facebook and Instagram-via-Facebook Page connect pagination.

Follow Graph API paging.next on /me/accounts so authorized non-first Pages are found and multi-Page accounts get the picker instead of silently connecting the first result.

Co-authored-by: Cursor <cursoragent@cursor.com>

* Paginate Meta accounts until paging.next is exhausted.

Drop the artificial 50-page cap and stop only when there is no next URL, or the same request URL repeats (broken pagination loop).

Co-authored-by: Cursor <cursoragent@cursor.com>

* Redact tokens in Graph pagination logs and harden test coverage.

Cover happy-path and failure cases for Meta /me/accounts pagination, including mid-loop failures, invalid paging.next, and Instagram pages without a linked IG account.

Co-authored-by: Cursor <cursoragent@cursor.com>

* Fail closed on incomplete Meta accounts pagination.

If a later /me/accounts page fails after earlier pages succeeded, throw instead of returning a truncated list that could auto-connect the wrong Page. Also revert the IG detail timeout that could wipe the whole connect list.

Co-authored-by: Cursor <cursoragent@cursor.com>

* Simplify Graph pagination helpers and page fetchers.

Bake the first request query into the URL, drop requestKey, and let IncompleteGraphPaginationException bubble from the controllers without catch/rethrow noise.

Co-authored-by: Cursor <cursoragent@cursor.com>

* Move incomplete pagination exception under Social\Meta.

Colocate it with GraphPaginator so the Meta scope is clear from the namespace instead of a generic Social exception name.

Co-authored-by: Cursor <cursoragent@cursor.com>

* Rename pagination exception to IncompleteMetaGraphPaginationException.

Keep it under Exceptions/Social with Meta in the class name instead of moving it into Services.

Co-authored-by: Cursor <cursoragent@cursor.com>

* Make GraphPaginator results explicit before mapping pages.

Assign the paginated accounts to a variable first so the Facebook and Instagram-via-Facebook fetchers read more clearly.

Co-authored-by: Cursor <cursoragent@cursor.com>

* Build Meta Graph pagination URLs with Laravel Uri.

Replace manual http_build_query concatenation with Uri::of()->withQuery().

Co-authored-by: Cursor <cursoragent@cursor.com>

* Use Laravel HTTP and Uri helpers in Meta Graph pagination.

Prefer response collect/json key access, filled(), and Uri path parsing over manual array and parse_url handling.

Co-authored-by: Cursor <cursoragent@cursor.com>

* Simplify graphVersion using Uri path and str().

Drop basename and native string casts; Uri::path() already yields the Graph API version segment.

Co-authored-by: Cursor <cursoragent@cursor.com>

* Drop unnecessary str() around graph API config.

Uri: :of() already accepts the string returned by config().
Co-authored-by: Cursor <cursoragent@cursor.com>

* Simplify GraphPaginator with Laravel helpers.

Consolidate failure handling via abort(), and use collect, when, throw_if, and Uri::value() for a shorter pagination loop.

Co-authored-by: Cursor <cursoragent@cursor.com>

* Refactor social OAuth page/channel selection handling. Update selectPage and selectChannel methods in Facebook, Instagram, and YouTube controllers to return popup callbacks instead of redirecting on session expiration or workspace not found. Enhance HandleInertiaRequests middleware to prevent deferring onboarding progress on social OAuth popup routes. Add tests to verify behavior for expired sessions and onboarding progress.

* Unify Instagram connect behind one card with a method picker.

Hide the Instagram-via-Facebook grid card and offer Instagram Login vs Facebook Pages from a single network entry, matching LinkedIn.

Co-authored-by: Cursor <cursoragent@cursor.com>

* Move social popup onboarding assertions into connection tests.

Cover the deferred-prop popup regression on Facebook, Instagram, and YouTube select routes instead of a synthetic onboarding share check.

Co-authored-by: Cursor <cursoragent@cursor.com>

* Stop suppressing onboarding defer on all social routes.

Override onboardingProgress only in popupCallback so picker pages stay deferred and the close page does not re-hit select after session clear.

Co-authored-by: Cursor <cursoragent@cursor.com>

* Always open the Instagram method dialog on connect.

Drop connectMethods and the single-method OAuth shortcut; the picker always offers both Login and Facebook Pages.

Co-authored-by: Cursor <cursoragent@cursor.com>

* Filter Instagram dialog options by enabled platforms.

Keep always opening the method picker, but only list OAuth entry points that are turned on.

Co-authored-by: Cursor <cursoragent@cursor.com>

* Extract Instagram connect methods into a dedicated helper.

Keep connectableOptions focused on shaping grid options while the enabled OAuth list lives in instagramConnectMethods().

Co-authored-by: Cursor <cursoragent@cursor.com>

* Harden Meta Graph pagination and localize Instagram connect copy.

Fail closed on Graph request errors and pathological paging, keep Instagram connect going when profile detail lookups time out, and translate the Instagram method dialog strings.

Co-authored-by: Cursor <cursoragent@cursor.com>

---------

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-08-08 12:01:46 -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
17449bfe2a test: cover network_taken popup on OAuth conflict; use FormRequest accessor
- Add a controller-level test asserting the Facebook OAuth callback returns the
  localized network_taken popup (not a generic error) when the network is
  already connected — covers the bug-A fix across the OAuth callbacks.
- WorkspaceController::autofillBrand: use $request->validated('url') instead of
  data_get on the validated array (idiomatic FormRequest accessor).
2026-06-22 08:01:08 -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
2da15df96c feat: introduce Account entity as billing owner and refactor architecture
- Create Account model as Cashier Billable entity (stripe, plan, subscription)
- Account owns workspaces and has an owner_id (User)
- User belongs to one Account via account_id
- Workspace belongs to Account via account_id, no longer has billing fields
- Remove Brand model entirely (workspaces serve as grouping)
- Rename brand_limit to workspace_limit in plans
- Workspace roles simplified: admin/member/viewer (owner via Account)
- Invites now belong to Account with workspaces JSON array
- Pennant features scope changed from Workspace to Account
- EnsureSubscribed middleware checks Account subscription
- All controllers updated: BillingController, OnboardingController,
  WorkspaceInviteController, SocialController, StripeEventListener
- Frontend: extract GoogleAuthButton component, create WorkspaceRole
  enum for type-safe role checks, fix all views for new architecture
- All 1101 tests passing
2026-04-14 22:22:04 -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
2fcf0a7e7a fix: update Socialite mocks in social controller tests
The mock shorthand Mockery::mock([...]) was not compatible with the
Socialite facade mock. Replaced with explicit shouldReceive chains
matching each controller's actual Socialite method calls.

Also keeps Google OAuth routes always registered so Wayfinder can
generate TypeScript helpers regardless of GOOGLE_AUTH_ENABLED setting.
2026-04-14 14:44:15 -03:00
Paulo Castellano
cb91529964 chore: working 2026-04-02 17:57:06 -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
52650909a4 feat: Add comprehensive support for new social media platforms, including publisher and controller tests, configuration, and factory updates. 2026-01-18 15:56:51 -03:00