2026-01-19 00:49:13 +00:00
|
|
|
<?php
|
|
|
|
|
|
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 03:20:43 +00:00
|
|
|
declare(strict_types=1);
|
|
|
|
|
|
2026-01-19 00:49:13 +00:00
|
|
|
use App\Enums\Media\Type as MediaType;
|
|
|
|
|
use App\Enums\SocialAccount\Platform;
|
|
|
|
|
|
|
|
|
|
test('platform has correct labels', function () {
|
|
|
|
|
expect(Platform::LinkedIn->label())->toBe('LinkedIn');
|
|
|
|
|
expect(Platform::LinkedInPage->label())->toBe('LinkedIn Page');
|
|
|
|
|
expect(Platform::X->label())->toBe('X');
|
|
|
|
|
expect(Platform::TikTok->label())->toBe('TikTok');
|
|
|
|
|
expect(Platform::YouTube->label())->toBe('YouTube Shorts');
|
|
|
|
|
expect(Platform::Facebook->label())->toBe('Facebook Page');
|
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 15:01:46 +00:00
|
|
|
expect(Platform::Instagram->label())->toBe('Instagram');
|
2026-04-02 20:57:06 +00:00
|
|
|
expect(Platform::InstagramFacebook->label())->toBe('Instagram (Facebook Business)');
|
2026-01-19 00:49:13 +00:00
|
|
|
expect(Platform::Threads->label())->toBe('Threads');
|
|
|
|
|
expect(Platform::Pinterest->label())->toBe('Pinterest');
|
|
|
|
|
expect(Platform::Bluesky->label())->toBe('Bluesky');
|
|
|
|
|
expect(Platform::Mastodon->label())->toBe('Mastodon');
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
test('platform has correct colors', function () {
|
|
|
|
|
expect(Platform::LinkedIn->color())->toBe('#0A66C2');
|
|
|
|
|
expect(Platform::LinkedInPage->color())->toBe('#0A66C2');
|
|
|
|
|
expect(Platform::X->color())->toBe('#000000');
|
|
|
|
|
expect(Platform::TikTok->color())->toBe('#000000');
|
|
|
|
|
expect(Platform::YouTube->color())->toBe('#FF0000');
|
|
|
|
|
expect(Platform::Facebook->color())->toBe('#1877F2');
|
|
|
|
|
expect(Platform::Instagram->color())->toBe('#E4405F');
|
|
|
|
|
expect(Platform::Threads->color())->toBe('#000000');
|
|
|
|
|
expect(Platform::Pinterest->color())->toBe('#E60023');
|
|
|
|
|
expect(Platform::Bluesky->color())->toBe('#0085FF');
|
|
|
|
|
expect(Platform::Mastodon->color())->toBe('#6364FF');
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
test('platform has correct allowed media types', function () {
|
2026-06-24 20:36:39 +00:00
|
|
|
expect(Platform::LinkedIn->allowedMediaTypes())->toContain(MediaType::Image, MediaType::Video, MediaType::Document);
|
|
|
|
|
expect(Platform::LinkedInPage->allowedMediaTypes())->toContain(MediaType::Image, MediaType::Video, MediaType::Document);
|
2026-01-19 00:49:13 +00:00
|
|
|
expect(Platform::X->allowedMediaTypes())->toContain(MediaType::Image, MediaType::Video);
|
2026-06-24 20:36:39 +00:00
|
|
|
expect(Platform::X->allowedMediaTypes())->not->toContain(MediaType::Document);
|
2026-01-19 00:49:13 +00:00
|
|
|
expect(Platform::TikTok->allowedMediaTypes())->toBe([MediaType::Video]);
|
|
|
|
|
expect(Platform::YouTube->allowedMediaTypes())->toBe([MediaType::Video]);
|
|
|
|
|
expect(Platform::Instagram->allowedMediaTypes())->toContain(MediaType::Image, MediaType::Video);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
test('platform has correct max images', function () {
|
2026-06-25 14:45:11 +00:00
|
|
|
expect(Platform::LinkedIn->maxImages())->toBe(10);
|
2026-01-19 00:49:13 +00:00
|
|
|
expect(Platform::X->maxImages())->toBe(4);
|
|
|
|
|
expect(Platform::TikTok->maxImages())->toBe(0);
|
|
|
|
|
expect(Platform::YouTube->maxImages())->toBe(0);
|
|
|
|
|
expect(Platform::Facebook->maxImages())->toBe(10);
|
|
|
|
|
expect(Platform::Instagram->maxImages())->toBe(10);
|
|
|
|
|
expect(Platform::Threads->maxImages())->toBe(10);
|
|
|
|
|
expect(Platform::Pinterest->maxImages())->toBe(5);
|
|
|
|
|
expect(Platform::Bluesky->maxImages())->toBe(4);
|
|
|
|
|
expect(Platform::Mastodon->maxImages())->toBe(4);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
test('platform has correct max content length', function () {
|
|
|
|
|
expect(Platform::LinkedIn->maxContentLength())->toBe(3000);
|
|
|
|
|
expect(Platform::X->maxContentLength())->toBe(280);
|
|
|
|
|
expect(Platform::TikTok->maxContentLength())->toBe(2200);
|
2026-05-03 21:38:17 +00:00
|
|
|
expect(Platform::YouTube->maxContentLength())->toBe(100);
|
2026-05-11 23:06:44 +00:00
|
|
|
expect(Platform::Facebook->maxContentLength())->toBe(10000);
|
2026-01-19 00:49:13 +00:00
|
|
|
expect(Platform::Instagram->maxContentLength())->toBe(2200);
|
|
|
|
|
expect(Platform::Threads->maxContentLength())->toBe(500);
|
|
|
|
|
expect(Platform::Pinterest->maxContentLength())->toBe(800);
|
|
|
|
|
expect(Platform::Bluesky->maxContentLength())->toBe(300);
|
|
|
|
|
expect(Platform::Mastodon->maxContentLength())->toBe(500);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
test('platform supports text only correctly', function () {
|
|
|
|
|
expect(Platform::LinkedIn->supportsTextOnly())->toBeTrue();
|
|
|
|
|
expect(Platform::LinkedInPage->supportsTextOnly())->toBeTrue();
|
|
|
|
|
expect(Platform::X->supportsTextOnly())->toBeTrue();
|
|
|
|
|
expect(Platform::Facebook->supportsTextOnly())->toBeTrue();
|
|
|
|
|
expect(Platform::Threads->supportsTextOnly())->toBeTrue();
|
|
|
|
|
expect(Platform::Bluesky->supportsTextOnly())->toBeTrue();
|
|
|
|
|
expect(Platform::Mastodon->supportsTextOnly())->toBeTrue();
|
|
|
|
|
|
|
|
|
|
expect(Platform::TikTok->supportsTextOnly())->toBeFalse();
|
|
|
|
|
expect(Platform::YouTube->supportsTextOnly())->toBeFalse();
|
|
|
|
|
expect(Platform::Instagram->supportsTextOnly())->toBeFalse();
|
|
|
|
|
expect(Platform::Pinterest->supportsTextOnly())->toBeFalse();
|
|
|
|
|
});
|
|
|
|
|
|
2026-07-03 16:11:36 +00:00
|
|
|
test('platform exposes the correct default token TTL fallback', function () {
|
|
|
|
|
// X access tokens live 2 hours.
|
|
|
|
|
expect(Platform::X->defaultTokenTtlSeconds())->toBe(7200);
|
|
|
|
|
|
|
|
|
|
// Instagram and Threads use Meta's 60-day long-lived token.
|
|
|
|
|
expect(Platform::Instagram->defaultTokenTtlSeconds())->toBe(5184000);
|
|
|
|
|
expect(Platform::Threads->defaultTokenTtlSeconds())->toBe(5184000);
|
|
|
|
|
|
|
|
|
|
// Networks that always return expires_in, set a fixed lifetime directly, or
|
|
|
|
|
// never expire have no fallback here.
|
|
|
|
|
expect(Platform::LinkedIn->defaultTokenTtlSeconds())->toBeNull();
|
|
|
|
|
expect(Platform::TikTok->defaultTokenTtlSeconds())->toBeNull();
|
|
|
|
|
expect(Platform::YouTube->defaultTokenTtlSeconds())->toBeNull();
|
|
|
|
|
expect(Platform::Pinterest->defaultTokenTtlSeconds())->toBeNull();
|
|
|
|
|
expect(Platform::Bluesky->defaultTokenTtlSeconds())->toBeNull();
|
|
|
|
|
expect(Platform::Facebook->defaultTokenTtlSeconds())->toBeNull();
|
|
|
|
|
});
|
|
|
|
|
|
2026-09-03 16:51:03 +00:00
|
|
|
test('platform is enabled by default for every platform', function (Platform $platform) {
|
|
|
|
|
expect($platform->isEnabled())->toBeTrue();
|
|
|
|
|
})->with([
|
|
|
|
|
Platform::LinkedIn,
|
|
|
|
|
Platform::LinkedInPage,
|
|
|
|
|
Platform::X,
|
|
|
|
|
Platform::TikTok,
|
|
|
|
|
Platform::YouTube,
|
|
|
|
|
Platform::Facebook,
|
|
|
|
|
Platform::Instagram,
|
|
|
|
|
Platform::InstagramFacebook,
|
|
|
|
|
Platform::Threads,
|
|
|
|
|
Platform::Pinterest,
|
|
|
|
|
Platform::Bluesky,
|
|
|
|
|
Platform::Mastodon,
|
|
|
|
|
Platform::Telegram,
|
|
|
|
|
Platform::Discord,
|
|
|
|
|
]);
|
|
|
|
|
|
|
|
|
|
test('each platform can be disabled via config', function (Platform $platform) {
|
|
|
|
|
config(["trypost.platforms.{$platform->value}.enabled" => false]);
|
|
|
|
|
|
|
|
|
|
expect($platform->isEnabled())->toBeFalse();
|
|
|
|
|
})->with([
|
|
|
|
|
Platform::LinkedIn,
|
|
|
|
|
Platform::LinkedInPage,
|
|
|
|
|
Platform::X,
|
|
|
|
|
Platform::TikTok,
|
|
|
|
|
Platform::YouTube,
|
|
|
|
|
Platform::Facebook,
|
|
|
|
|
Platform::Instagram,
|
|
|
|
|
Platform::InstagramFacebook,
|
|
|
|
|
Platform::Threads,
|
|
|
|
|
Platform::Pinterest,
|
|
|
|
|
Platform::Bluesky,
|
|
|
|
|
Platform::Mastodon,
|
|
|
|
|
Platform::Telegram,
|
|
|
|
|
Platform::Discord,
|
|
|
|
|
]);
|
|
|
|
|
|
|
|
|
|
test('each platform maps to its publishing queue', function (Platform $platform, string $queue) {
|
|
|
|
|
expect($platform->queue())->toBe($queue);
|
|
|
|
|
})->with([
|
|
|
|
|
[Platform::LinkedIn, 'social-linkedin'],
|
|
|
|
|
[Platform::LinkedInPage, 'social-linkedin-page'],
|
|
|
|
|
[Platform::X, 'social-x'],
|
|
|
|
|
[Platform::TikTok, 'social-tiktok'],
|
|
|
|
|
[Platform::YouTube, 'social-youtube'],
|
|
|
|
|
[Platform::Facebook, 'social-facebook'],
|
|
|
|
|
[Platform::Instagram, 'social-instagram'],
|
|
|
|
|
[Platform::InstagramFacebook, 'social-instagram-facebook'],
|
|
|
|
|
[Platform::Threads, 'social-threads'],
|
|
|
|
|
[Platform::Pinterest, 'social-pinterest'],
|
|
|
|
|
[Platform::Bluesky, 'social-bluesky'],
|
|
|
|
|
[Platform::Mastodon, 'social-mastodon'],
|
|
|
|
|
[Platform::Telegram, 'social-telegram'],
|
|
|
|
|
[Platform::Discord, 'social-discord'],
|
|
|
|
|
]);
|
|
|
|
|
|
|
|
|
|
test('allQueues lists every platform publishing queue in enum order', function () {
|
|
|
|
|
expect(Platform::allQueues())->toBe([
|
|
|
|
|
'social-linkedin',
|
|
|
|
|
'social-linkedin-page',
|
|
|
|
|
'social-x',
|
|
|
|
|
'social-tiktok',
|
|
|
|
|
'social-youtube',
|
|
|
|
|
'social-facebook',
|
|
|
|
|
'social-instagram',
|
|
|
|
|
'social-instagram-facebook',
|
|
|
|
|
'social-threads',
|
|
|
|
|
'social-pinterest',
|
|
|
|
|
'social-bluesky',
|
|
|
|
|
'social-mastodon',
|
|
|
|
|
'social-telegram',
|
|
|
|
|
'social-discord',
|
|
|
|
|
])->and(Platform::allQueues())->toHaveCount(count(Platform::cases()));
|
2026-01-19 00:49:13 +00:00
|
|
|
});
|
|
|
|
|
|
2026-09-03 16:51:03 +00:00
|
|
|
test('enabledQueues matches allQueues when every platform is enabled', function () {
|
|
|
|
|
foreach (Platform::cases() as $platform) {
|
|
|
|
|
config(["trypost.platforms.{$platform->value}.enabled" => true]);
|
|
|
|
|
}
|
2026-01-19 00:49:13 +00:00
|
|
|
|
2026-09-03 16:51:03 +00:00
|
|
|
expect(Platform::enabledQueues())->toBe(Platform::allQueues());
|
2026-01-19 00:49:13 +00:00
|
|
|
});
|
|
|
|
|
|
2026-09-03 16:51:03 +00:00
|
|
|
test('disabling a platform removes only its queue from enabledQueues', function (Platform $disabled) {
|
|
|
|
|
foreach (Platform::cases() as $platform) {
|
|
|
|
|
config(["trypost.platforms.{$platform->value}.enabled" => true]);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
config(["trypost.platforms.{$disabled->value}.enabled" => false]);
|
|
|
|
|
|
|
|
|
|
$enabledQueues = Platform::enabledQueues();
|
|
|
|
|
|
|
|
|
|
expect($enabledQueues)
|
|
|
|
|
->not->toContain($disabled->queue())
|
|
|
|
|
->toHaveCount(count(Platform::cases()) - 1);
|
|
|
|
|
|
|
|
|
|
foreach (Platform::cases() as $platform) {
|
|
|
|
|
if ($platform === $disabled) {
|
|
|
|
|
continue;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
expect($enabledQueues)->toContain($platform->queue());
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
expect(Platform::allQueues())->toContain($disabled->queue());
|
|
|
|
|
})->with([
|
|
|
|
|
Platform::LinkedIn,
|
|
|
|
|
Platform::LinkedInPage,
|
|
|
|
|
Platform::X,
|
|
|
|
|
Platform::TikTok,
|
|
|
|
|
Platform::YouTube,
|
|
|
|
|
Platform::Facebook,
|
|
|
|
|
Platform::Instagram,
|
|
|
|
|
Platform::InstagramFacebook,
|
|
|
|
|
Platform::Threads,
|
|
|
|
|
Platform::Pinterest,
|
|
|
|
|
Platform::Bluesky,
|
|
|
|
|
Platform::Mastodon,
|
|
|
|
|
Platform::Telegram,
|
|
|
|
|
Platform::Discord,
|
|
|
|
|
]);
|
|
|
|
|
|
|
|
|
|
test('disabling every platform yields no enabled queues', function () {
|
|
|
|
|
foreach (Platform::cases() as $platform) {
|
|
|
|
|
config(["trypost.platforms.{$platform->value}.enabled" => false]);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
expect(Platform::enabledQueues())->toBe([]);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
test('enabledQueues is always a subset of allQueues', function () {
|
|
|
|
|
config([
|
|
|
|
|
'trypost.platforms.linkedin.enabled' => true,
|
|
|
|
|
'trypost.platforms.linkedin-page.enabled' => false,
|
|
|
|
|
'trypost.platforms.x.enabled' => false,
|
|
|
|
|
'trypost.platforms.tiktok.enabled' => true,
|
|
|
|
|
'trypost.platforms.youtube.enabled' => false,
|
|
|
|
|
'trypost.platforms.facebook.enabled' => true,
|
|
|
|
|
'trypost.platforms.instagram.enabled' => false,
|
|
|
|
|
'trypost.platforms.instagram-facebook.enabled' => true,
|
|
|
|
|
'trypost.platforms.threads.enabled' => false,
|
|
|
|
|
'trypost.platforms.pinterest.enabled' => true,
|
|
|
|
|
'trypost.platforms.bluesky.enabled' => false,
|
|
|
|
|
'trypost.platforms.mastodon.enabled' => true,
|
|
|
|
|
'trypost.platforms.telegram.enabled' => false,
|
|
|
|
|
'trypost.platforms.discord.enabled' => true,
|
|
|
|
|
]);
|
|
|
|
|
|
|
|
|
|
$enabledQueues = Platform::enabledQueues();
|
|
|
|
|
|
|
|
|
|
expect($enabledQueues)->toEqual(array_values(array_intersect(Platform::allQueues(), $enabledQueues)))
|
|
|
|
|
->and(array_diff($enabledQueues, Platform::allQueues()))->toBe([]);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
test('horizon social publishing queues match enabledQueues', function () {
|
|
|
|
|
expect(config('horizon.defaults.social-publishing.queue'))->toBe(Platform::enabledQueues());
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
test('isEnabled falls back to env when enabled config is missing', function (Platform $platform, string $envKey) {
|
|
|
|
|
$platforms = config('trypost.platforms');
|
|
|
|
|
unset($platforms[$platform->value]['enabled']);
|
|
|
|
|
config(['trypost.platforms' => $platforms]);
|
|
|
|
|
|
|
|
|
|
$original = getenv($envKey);
|
|
|
|
|
putenv("{$envKey}=false");
|
|
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
expect($platform->isEnabled())->toBeFalse();
|
|
|
|
|
} finally {
|
|
|
|
|
if ($original === false) {
|
|
|
|
|
putenv($envKey);
|
|
|
|
|
} else {
|
|
|
|
|
putenv("{$envKey}={$original}");
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
})->with([
|
|
|
|
|
[Platform::LinkedIn, 'LINKEDIN_ENABLED'],
|
|
|
|
|
[Platform::LinkedInPage, 'LINKEDIN_PAGE_ENABLED'],
|
|
|
|
|
[Platform::X, 'X_ENABLED'],
|
|
|
|
|
[Platform::InstagramFacebook, 'INSTAGRAM_FACEBOOK_ENABLED'],
|
|
|
|
|
[Platform::Telegram, 'TELEGRAM_ENABLED'],
|
|
|
|
|
[Platform::Discord, 'DISCORD_ENABLED'],
|
|
|
|
|
]);
|
|
|
|
|
|
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 15:01:46 +00:00
|
|
|
test('linkedin pages and instagram facebook are not directly connectable', function () {
|
2026-06-25 00:05:09 +00:00
|
|
|
expect(Platform::LinkedInPage->isConnectable())->toBeFalse();
|
|
|
|
|
expect(Platform::LinkedIn->isConnectable())->toBeTrue();
|
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 15:01:46 +00:00
|
|
|
expect(Platform::InstagramFacebook->isConnectable())->toBeFalse();
|
2026-06-25 00:05:09 +00:00
|
|
|
expect(Platform::Instagram->isConnectable())->toBeTrue();
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
test('the linkedin card is connectable while either capability is enabled', function () {
|
|
|
|
|
config(['trypost.platforms.linkedin.enabled' => true, 'trypost.platforms.linkedin-page.enabled' => false]);
|
|
|
|
|
expect(Platform::LinkedIn->isConnectable())->toBeTrue();
|
|
|
|
|
|
|
|
|
|
config(['trypost.platforms.linkedin.enabled' => false, 'trypost.platforms.linkedin-page.enabled' => true]);
|
|
|
|
|
expect(Platform::LinkedIn->isConnectable())->toBeTrue();
|
|
|
|
|
|
|
|
|
|
config(['trypost.platforms.linkedin.enabled' => false, 'trypost.platforms.linkedin-page.enabled' => false]);
|
|
|
|
|
expect(Platform::LinkedIn->isConnectable())->toBeFalse();
|
|
|
|
|
|
|
|
|
|
// A company page never gets its own card regardless of toggles.
|
|
|
|
|
config(['trypost.platforms.linkedin-page.enabled' => true]);
|
|
|
|
|
expect(Platform::LinkedInPage->isConnectable())->toBeFalse();
|
|
|
|
|
});
|
Activation checklist + MCP OAuth authorize UX (#239) (#250)
* Wire onboarding activation into Account, observers, and shared Inertia data
Add onboarding casts/hasFinishedOnboarding, AccessToken ObservedBy,
Platform::connectableOptions, Post/SocialAccount onboarding broadcast hooks,
and lazy onboardingResidual share + SharedData types.
* Register onboarding routes and post-checkout activation redirects.
Wire billing processing and the sidebar checklist so owners land on
activation after subscribe, with locale sidebar/uk onboarding strings.
* Align MCP grant usability with onboarding activation checks
Unbound MCP tokens fall back to the user's current workspace and require
createPost so viewer/unscoped grants neither unlock the checklist nor
broadcast onboarding status.
* Require bound MCP workspace for onboarding activation.
Drop current-workspace fallback from usable MCP grants so checklist
detection and broadcasts match Passport token scoping; viewers still
cannot unlock the MCP step.
Co-authored-by: Cursor <cursoragent@cursor.com>
* Harden onboarding review findings and tighten locale strings.
Fix Welcome/Persona/TrackPost suites broken by the activation route reuse
and PostObserver analytics side effects, restore Echo poll fallbacks,
reject unbound MCP grants in tests, and drop unused onboarding.mcp keys.
Co-authored-by: Cursor <cursoragent@cursor.com>
* Remove unused sidebar and MCP authorization locale keys.
Drop dead sidebar menu/theme strings (including the overwritten
workspace label and api_keys nav entry) and unused MCP authorize
app_title/approving copy across all locales.
Co-authored-by: Cursor <cursoragent@cursor.com>
* Fix SetLocale crashing on Passport Symfony OAuth responses.
OAuth errors return a raw Symfony Response without withCookie(); attach
the default locale cookie via headers so authorize no longer 500s.
Co-authored-by: Cursor <cursoragent@cursor.com>
* Prompt OAuth guests to log in before rejecting unknown clients.
MCP Inspector often reuses a stale client_id; validateAuthorizationRequest
was returning invalid_client JSON before the login redirect. Guests now
hit /login first, then client validation runs after authentication.
Co-authored-by: Cursor <cursoragent@cursor.com>
* Render Inertia OAuth authorize errors for browser logins.
After login, Inertia follows the intended authorize URL; raw invalid_client
JSON broke that visit. HTML/Inertia requests now get mcp/AuthorizeError
while API JSON clients still receive the OAuth error payload.
Co-authored-by: Cursor <cursoragent@cursor.com>
* Detect Inertia OAuth error pages via Request::inertia().
Use the framework helper so post-login authorize failures keep returning
an Inertia page instead of raw OAuth JSON.
Co-authored-by: Cursor <cursoragent@cursor.com>
* Simplify OAuth authorize error page detection to expectsJson.
Drop the X-Inertia header sniff; browser and Inertia visits already do
not expectsJson, while API clients still receive the OAuth JSON payload.
Co-authored-by: Cursor <cursoragent@cursor.com>
* Share MCP authorize layout and drop the error close button.
Keep authorize and authorize-error on the same centered card shell instead of the auth split layout.
Co-authored-by: Cursor <cursoragent@cursor.com>
* Simplify onboarding activation for reviewability and safety.
Use an exists-based MCP check, keep GETs read-only, move sync into
syncAndNotify, clear MCP skips on connect, restrict complete to owners,
and share Echo/poll via one composable.
Co-authored-by: Cursor <cursoragent@cursor.com>
* Move MCP OAuth authorize UX out of the onboarding PR.
Keep the activation checklist focused; OAuth guest/error-page work now
lives on fix/mcp-oauth-authorize-ux.
Co-authored-by: Cursor <cursoragent@cursor.com>
* Fix corrupted French MCP locale after OAuth key cleanup.
Co-authored-by: Cursor <cursoragent@cursor.com>
* Restore MCP OAuth authorize UX onto the onboarding branch.
Keep authorize error page, guest login-before-client validation, and
SetLocale Symfony cookie fix in #250.
Co-authored-by: Cursor <cursoragent@cursor.com>
* Fix OAuth prompt=none redirects and harden onboarding tests.
Keep login_required/consent_required as redirects instead of Inertia,
add regression coverage for owner-only activation, require invite email
confirmation, and align MCP connected apps with the sessions list UI.
Co-authored-by: Cursor <cursoragent@cursor.com>
* Simplify onboarding guards and dedupe viewed analytics.
Introduce isOnboardingOpen / belongsToAccount helpers, collapse
duplicated sync/dispatch paths, and capture onboarding.viewed once
per account.
Co-authored-by: Cursor <cursoragent@cursor.com>
* Simplify onboarding event, observers, and status helpers.
Tighten Account onboarding predicates, drop nullable broadcast/dispatch
APIs, and collapse repeated observer/controller guards.
Co-authored-by: Cursor <cursoragent@cursor.com>
* Treat in-app users as always having an account.
Add resolveAccount(), tighten belongsToAccount to string ids, and fold
guest residual handling into ResolveOnboardingStatus.
Co-authored-by: Cursor <cursoragent@cursor.com>
* Rename onboarding residual share test to progress.
Co-authored-by: Cursor <cursoragent@cursor.com>
* Simplify onboarding status and rename residual to progress.
Use accountOrFail, extract MCP onboarding scope, auto-leave the ready
screen, and send non-onboarding checkout back to accounts.
Co-authored-by: Cursor <cursoragent@cursor.com>
* Extract HasAccount and prefer data_get in onboarding flows.
Move account helpers off User, drop nullable sidebarProgress, and
read OAuth/onboarding payloads with data_get.
Co-authored-by: Cursor <cursoragent@cursor.com>
* Simplify onboarding checks and extract HasOnboarding.
Use Eloquent + policies for MCP/backfill paths, and move account
onboarding helpers into a dedicated trait.
Co-authored-by: Cursor <cursoragent@cursor.com>
* Add trait tests and tidy onboarding imports.
Cover HasAccount and HasOnboarding under Models/Traits, prefer filled() for checkout session ids, and import Throwable instead of FQCN.
Co-authored-by: Cursor <cursoragent@cursor.com>
* Simplify checkout session_id and OAuth error props.
Read session_id via request->string(), and take OAuth error details from the League exception instead of decoding the response body.
Co-authored-by: Cursor <cursoragent@cursor.com>
* Simplify PostObserver onboarding notify path.
Share one otherPosts check for first-create and last-delete instead of separate callbacks.
Co-authored-by: Cursor <cursoragent@cursor.com>
* Use post author as onboarding sync actor.
Drop Auth::user() preference in PostObserver; checklist sync attributes to $post->user.
Co-authored-by: Cursor <cursoragent@cursor.com>
* Simplify SocialAccountObserver and OAuth authorize flow.
Share create/delete onboarding notify, drop Auth actor fallback to owner, and inline Passport Inertia error handling.
Co-authored-by: Cursor <cursoragent@cursor.com>
* Use lazy Inertia props for onboarding partial reloads.
Drop partial-header branching; wrap page props in closures and always redirect completed/dismissed accounts to the calendar.
Co-authored-by: Cursor <cursoragent@cursor.com>
* Defer sidebar onboarding progress and stamp completion as owner-only.
Skip the MCP checklist work on full Inertia visits via deferred shared props,
early-exit token scans, and keep account completion stamps owner-gated.
Co-authored-by: Cursor <cursoragent@cursor.com>
* Simplify deferred onboarding progress share via canShowProgress.
Co-authored-by: Cursor <cursoragent@cursor.com>
* Add User firstName for shared auth and simplify onboarding page.
Co-authored-by: Cursor <cursoragent@cursor.com>
* Move User firstName coverage into UserTest.
Co-authored-by: Cursor <cursoragent@cursor.com>
* Use first_name directly without empty-name fallbacks.
Co-authored-by: Cursor <cursoragent@cursor.com>
* Resolve onboarding sample prompt on the frontend via i18n.
Co-authored-by: Cursor <cursoragent@cursor.com>
* Stamp onboarding completion via the account owner after teammate unlocks.
Co-authored-by: Cursor <cursoragent@cursor.com>
* Count only the account owner MCP grant toward onboarding activation.
Co-authored-by: Cursor <cursoragent@cursor.com>
* Fix OAuth consent auth-token mismatch for mid-activation owners.
Skip deferred onboardingProgress on Passport authorize so Inertia does not
rotate the session authToken, cover happy and stale-token paths in tests,
and polish MCP setup copy plus sidebar/onboarding layout.
Co-authored-by: Cursor <cursoragent@cursor.com>
* Keep users on onboarding after activation completes.
Stamp completion and re-render the finished checklist instead of
redirecting to the calendar so owners can review the done state.
Co-authored-by: Cursor <cursoragent@cursor.com>
* Clarify Passport consent-view opt-out and guard app-route deferral.
Rename the authorize-only route check and assert onboardingProgress still
defers on calendar, onboarding, and MCP settings.
Co-authored-by: Cursor <cursoragent@cursor.com>
* Harden onboarding completion and MCP consent workspace binding.
Reject OAuth approve without a workspace, retry auto-complete until
stamped, send dismissed complete straight to calendar, and cover the
device consent defer opt-out.
Co-authored-by: Cursor <cursoragent@cursor.com>
* Enable activation checklist for self-hosted installs.
Remove the self-hosted onboarding redirects, keep the SaaS-only dismiss backfill, and cover subscription-less owners plus skip/complete destinations.
Co-authored-by: Cursor <cursoragent@cursor.com>
* Add GitHub, Hacker News, and directories referral sources.
Expand the welcome referral step with open-source and directory discovery channels.
Co-authored-by: Cursor <cursoragent@cursor.com>
* Refine welcome referral sources and labels.
Split Instagram/Threads, add Founder, and shorten Google, GitHub, AI, and blog option labels.
Co-authored-by: Cursor <cursoragent@cursor.com>
* Sort accounts platforms alphabetically and drop connect hover plus.
Reuse connectableOptions for the accounts index and remove the unused plus badge on disconnected cards.
Co-authored-by: Cursor <cursoragent@cursor.com>
* Centralize PostHog once-capture so disabled installs don't burn dedupe keys.
Move isEnabled + Cache::add into PostHogService::captureOnce and route onboarding viewed/step events through it.
Co-authored-by: Cursor <cursoragent@cursor.com>
* Simplify onboarding backfill to complete every existing open account.
Drop self-hosted and subscription filters; down clears completed_at again.
Co-authored-by: Cursor <cursoragent@cursor.com>
* Drop PostHog captureOnce and use plain capture for onboarding.
Remove cache-based event dedupe; callers rely on PostHogService::capture gating.
Co-authored-by: Cursor <cursoragent@cursor.com>
---------
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-08-07 23:34:43 +00:00
|
|
|
|
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 15:01:46 +00:00
|
|
|
test('the instagram card is connectable while either capability is enabled', function () {
|
|
|
|
|
config(['trypost.platforms.instagram.enabled' => true, 'trypost.platforms.instagram-facebook.enabled' => false]);
|
|
|
|
|
expect(Platform::Instagram->isConnectable())->toBeTrue();
|
|
|
|
|
|
|
|
|
|
config(['trypost.platforms.instagram.enabled' => false, 'trypost.platforms.instagram-facebook.enabled' => true]);
|
|
|
|
|
expect(Platform::Instagram->isConnectable())->toBeTrue();
|
|
|
|
|
|
|
|
|
|
config(['trypost.platforms.instagram.enabled' => false, 'trypost.platforms.instagram-facebook.enabled' => false]);
|
|
|
|
|
expect(Platform::Instagram->isConnectable())->toBeFalse();
|
|
|
|
|
|
|
|
|
|
// Via-Facebook never gets its own card regardless of toggles.
|
|
|
|
|
config(['trypost.platforms.instagram-facebook.enabled' => true]);
|
|
|
|
|
expect(Platform::InstagramFacebook->isConnectable())->toBeFalse();
|
|
|
|
|
});
|
|
|
|
|
|
Activation checklist + MCP OAuth authorize UX (#239) (#250)
* Wire onboarding activation into Account, observers, and shared Inertia data
Add onboarding casts/hasFinishedOnboarding, AccessToken ObservedBy,
Platform::connectableOptions, Post/SocialAccount onboarding broadcast hooks,
and lazy onboardingResidual share + SharedData types.
* Register onboarding routes and post-checkout activation redirects.
Wire billing processing and the sidebar checklist so owners land on
activation after subscribe, with locale sidebar/uk onboarding strings.
* Align MCP grant usability with onboarding activation checks
Unbound MCP tokens fall back to the user's current workspace and require
createPost so viewer/unscoped grants neither unlock the checklist nor
broadcast onboarding status.
* Require bound MCP workspace for onboarding activation.
Drop current-workspace fallback from usable MCP grants so checklist
detection and broadcasts match Passport token scoping; viewers still
cannot unlock the MCP step.
Co-authored-by: Cursor <cursoragent@cursor.com>
* Harden onboarding review findings and tighten locale strings.
Fix Welcome/Persona/TrackPost suites broken by the activation route reuse
and PostObserver analytics side effects, restore Echo poll fallbacks,
reject unbound MCP grants in tests, and drop unused onboarding.mcp keys.
Co-authored-by: Cursor <cursoragent@cursor.com>
* Remove unused sidebar and MCP authorization locale keys.
Drop dead sidebar menu/theme strings (including the overwritten
workspace label and api_keys nav entry) and unused MCP authorize
app_title/approving copy across all locales.
Co-authored-by: Cursor <cursoragent@cursor.com>
* Fix SetLocale crashing on Passport Symfony OAuth responses.
OAuth errors return a raw Symfony Response without withCookie(); attach
the default locale cookie via headers so authorize no longer 500s.
Co-authored-by: Cursor <cursoragent@cursor.com>
* Prompt OAuth guests to log in before rejecting unknown clients.
MCP Inspector often reuses a stale client_id; validateAuthorizationRequest
was returning invalid_client JSON before the login redirect. Guests now
hit /login first, then client validation runs after authentication.
Co-authored-by: Cursor <cursoragent@cursor.com>
* Render Inertia OAuth authorize errors for browser logins.
After login, Inertia follows the intended authorize URL; raw invalid_client
JSON broke that visit. HTML/Inertia requests now get mcp/AuthorizeError
while API JSON clients still receive the OAuth error payload.
Co-authored-by: Cursor <cursoragent@cursor.com>
* Detect Inertia OAuth error pages via Request::inertia().
Use the framework helper so post-login authorize failures keep returning
an Inertia page instead of raw OAuth JSON.
Co-authored-by: Cursor <cursoragent@cursor.com>
* Simplify OAuth authorize error page detection to expectsJson.
Drop the X-Inertia header sniff; browser and Inertia visits already do
not expectsJson, while API clients still receive the OAuth JSON payload.
Co-authored-by: Cursor <cursoragent@cursor.com>
* Share MCP authorize layout and drop the error close button.
Keep authorize and authorize-error on the same centered card shell instead of the auth split layout.
Co-authored-by: Cursor <cursoragent@cursor.com>
* Simplify onboarding activation for reviewability and safety.
Use an exists-based MCP check, keep GETs read-only, move sync into
syncAndNotify, clear MCP skips on connect, restrict complete to owners,
and share Echo/poll via one composable.
Co-authored-by: Cursor <cursoragent@cursor.com>
* Move MCP OAuth authorize UX out of the onboarding PR.
Keep the activation checklist focused; OAuth guest/error-page work now
lives on fix/mcp-oauth-authorize-ux.
Co-authored-by: Cursor <cursoragent@cursor.com>
* Fix corrupted French MCP locale after OAuth key cleanup.
Co-authored-by: Cursor <cursoragent@cursor.com>
* Restore MCP OAuth authorize UX onto the onboarding branch.
Keep authorize error page, guest login-before-client validation, and
SetLocale Symfony cookie fix in #250.
Co-authored-by: Cursor <cursoragent@cursor.com>
* Fix OAuth prompt=none redirects and harden onboarding tests.
Keep login_required/consent_required as redirects instead of Inertia,
add regression coverage for owner-only activation, require invite email
confirmation, and align MCP connected apps with the sessions list UI.
Co-authored-by: Cursor <cursoragent@cursor.com>
* Simplify onboarding guards and dedupe viewed analytics.
Introduce isOnboardingOpen / belongsToAccount helpers, collapse
duplicated sync/dispatch paths, and capture onboarding.viewed once
per account.
Co-authored-by: Cursor <cursoragent@cursor.com>
* Simplify onboarding event, observers, and status helpers.
Tighten Account onboarding predicates, drop nullable broadcast/dispatch
APIs, and collapse repeated observer/controller guards.
Co-authored-by: Cursor <cursoragent@cursor.com>
* Treat in-app users as always having an account.
Add resolveAccount(), tighten belongsToAccount to string ids, and fold
guest residual handling into ResolveOnboardingStatus.
Co-authored-by: Cursor <cursoragent@cursor.com>
* Rename onboarding residual share test to progress.
Co-authored-by: Cursor <cursoragent@cursor.com>
* Simplify onboarding status and rename residual to progress.
Use accountOrFail, extract MCP onboarding scope, auto-leave the ready
screen, and send non-onboarding checkout back to accounts.
Co-authored-by: Cursor <cursoragent@cursor.com>
* Extract HasAccount and prefer data_get in onboarding flows.
Move account helpers off User, drop nullable sidebarProgress, and
read OAuth/onboarding payloads with data_get.
Co-authored-by: Cursor <cursoragent@cursor.com>
* Simplify onboarding checks and extract HasOnboarding.
Use Eloquent + policies for MCP/backfill paths, and move account
onboarding helpers into a dedicated trait.
Co-authored-by: Cursor <cursoragent@cursor.com>
* Add trait tests and tidy onboarding imports.
Cover HasAccount and HasOnboarding under Models/Traits, prefer filled() for checkout session ids, and import Throwable instead of FQCN.
Co-authored-by: Cursor <cursoragent@cursor.com>
* Simplify checkout session_id and OAuth error props.
Read session_id via request->string(), and take OAuth error details from the League exception instead of decoding the response body.
Co-authored-by: Cursor <cursoragent@cursor.com>
* Simplify PostObserver onboarding notify path.
Share one otherPosts check for first-create and last-delete instead of separate callbacks.
Co-authored-by: Cursor <cursoragent@cursor.com>
* Use post author as onboarding sync actor.
Drop Auth::user() preference in PostObserver; checklist sync attributes to $post->user.
Co-authored-by: Cursor <cursoragent@cursor.com>
* Simplify SocialAccountObserver and OAuth authorize flow.
Share create/delete onboarding notify, drop Auth actor fallback to owner, and inline Passport Inertia error handling.
Co-authored-by: Cursor <cursoragent@cursor.com>
* Use lazy Inertia props for onboarding partial reloads.
Drop partial-header branching; wrap page props in closures and always redirect completed/dismissed accounts to the calendar.
Co-authored-by: Cursor <cursoragent@cursor.com>
* Defer sidebar onboarding progress and stamp completion as owner-only.
Skip the MCP checklist work on full Inertia visits via deferred shared props,
early-exit token scans, and keep account completion stamps owner-gated.
Co-authored-by: Cursor <cursoragent@cursor.com>
* Simplify deferred onboarding progress share via canShowProgress.
Co-authored-by: Cursor <cursoragent@cursor.com>
* Add User firstName for shared auth and simplify onboarding page.
Co-authored-by: Cursor <cursoragent@cursor.com>
* Move User firstName coverage into UserTest.
Co-authored-by: Cursor <cursoragent@cursor.com>
* Use first_name directly without empty-name fallbacks.
Co-authored-by: Cursor <cursoragent@cursor.com>
* Resolve onboarding sample prompt on the frontend via i18n.
Co-authored-by: Cursor <cursoragent@cursor.com>
* Stamp onboarding completion via the account owner after teammate unlocks.
Co-authored-by: Cursor <cursoragent@cursor.com>
* Count only the account owner MCP grant toward onboarding activation.
Co-authored-by: Cursor <cursoragent@cursor.com>
* Fix OAuth consent auth-token mismatch for mid-activation owners.
Skip deferred onboardingProgress on Passport authorize so Inertia does not
rotate the session authToken, cover happy and stale-token paths in tests,
and polish MCP setup copy plus sidebar/onboarding layout.
Co-authored-by: Cursor <cursoragent@cursor.com>
* Keep users on onboarding after activation completes.
Stamp completion and re-render the finished checklist instead of
redirecting to the calendar so owners can review the done state.
Co-authored-by: Cursor <cursoragent@cursor.com>
* Clarify Passport consent-view opt-out and guard app-route deferral.
Rename the authorize-only route check and assert onboardingProgress still
defers on calendar, onboarding, and MCP settings.
Co-authored-by: Cursor <cursoragent@cursor.com>
* Harden onboarding completion and MCP consent workspace binding.
Reject OAuth approve without a workspace, retry auto-complete until
stamped, send dismissed complete straight to calendar, and cover the
device consent defer opt-out.
Co-authored-by: Cursor <cursoragent@cursor.com>
* Enable activation checklist for self-hosted installs.
Remove the self-hosted onboarding redirects, keep the SaaS-only dismiss backfill, and cover subscription-less owners plus skip/complete destinations.
Co-authored-by: Cursor <cursoragent@cursor.com>
* Add GitHub, Hacker News, and directories referral sources.
Expand the welcome referral step with open-source and directory discovery channels.
Co-authored-by: Cursor <cursoragent@cursor.com>
* Refine welcome referral sources and labels.
Split Instagram/Threads, add Founder, and shorten Google, GitHub, AI, and blog option labels.
Co-authored-by: Cursor <cursoragent@cursor.com>
* Sort accounts platforms alphabetically and drop connect hover plus.
Reuse connectableOptions for the accounts index and remove the unused plus badge on disconnected cards.
Co-authored-by: Cursor <cursoragent@cursor.com>
* Centralize PostHog once-capture so disabled installs don't burn dedupe keys.
Move isEnabled + Cache::add into PostHogService::captureOnce and route onboarding viewed/step events through it.
Co-authored-by: Cursor <cursoragent@cursor.com>
* Simplify onboarding backfill to complete every existing open account.
Drop self-hosted and subscription filters; down clears completed_at again.
Co-authored-by: Cursor <cursoragent@cursor.com>
* Drop PostHog captureOnce and use plain capture for onboarding.
Remove cache-based event dedupe; callers rely on PostHogService::capture gating.
Co-authored-by: Cursor <cursoragent@cursor.com>
---------
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-08-07 23:34:43 +00:00
|
|
|
test('connectable options are sorted alphabetically by label', function () {
|
|
|
|
|
$labels = array_column(Platform::connectableOptions(), 'label');
|
|
|
|
|
|
|
|
|
|
$sorted = $labels;
|
|
|
|
|
natcasesort($sorted);
|
|
|
|
|
|
|
|
|
|
expect($labels)->toBe(array_values($sorted));
|
|
|
|
|
});
|
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 15:01:46 +00:00
|
|
|
|
|
|
|
|
test('connectable options include a single instagram card and omit the facebook variant', function () {
|
|
|
|
|
$values = array_column(Platform::connectableOptions(), 'value');
|
|
|
|
|
|
|
|
|
|
expect($values)->toContain(Platform::Instagram->value)
|
|
|
|
|
->and($values)->not->toContain(Platform::InstagramFacebook->value);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
test('instagram connectable option lists only enabled connect methods', function () {
|
|
|
|
|
config(['trypost.platforms.instagram.enabled' => true, 'trypost.platforms.instagram-facebook.enabled' => false]);
|
|
|
|
|
|
|
|
|
|
expect(Platform::instagramConnectMethods())->toBe([Platform::Instagram->value]);
|
|
|
|
|
|
|
|
|
|
$instagram = collect(Platform::connectableOptions())->firstWhere('value', Platform::Instagram->value);
|
|
|
|
|
|
|
|
|
|
expect($instagram['connect_methods'])->toBe([Platform::Instagram->value]);
|
|
|
|
|
|
|
|
|
|
config(['trypost.platforms.instagram.enabled' => false, 'trypost.platforms.instagram-facebook.enabled' => true]);
|
|
|
|
|
|
|
|
|
|
expect(Platform::instagramConnectMethods())->toBe([Platform::InstagramFacebook->value]);
|
|
|
|
|
|
|
|
|
|
config(['trypost.platforms.instagram.enabled' => true, 'trypost.platforms.instagram-facebook.enabled' => true]);
|
|
|
|
|
|
|
|
|
|
expect(Platform::instagramConnectMethods())->toBe([
|
|
|
|
|
Platform::Instagram->value,
|
|
|
|
|
Platform::InstagramFacebook->value,
|
|
|
|
|
]);
|
|
|
|
|
});
|