Skip to content

feat(tiktok): photo carousel support + Content Sharing API UX compliance#25

Merged
paulocastellano merged 10 commits into
mainfrom
feature/tiktok-photo-and-ux-compliance
May 10, 2026
Merged

feat(tiktok): photo carousel support + Content Sharing API UX compliance#25
paulocastellano merged 10 commits into
mainfrom
feature/tiktok-photo-and-ux-compliance

Conversation

@paulocastellano
Copy link
Copy Markdown
Contributor

Summary

  • Adds TikTok photo carousel support end-to-end (ContentType::TikTokPhoto, variant picker UI, publisher fix to use the description field which has a 4000-char cap vs the 90-char title cap)
  • Brings the post editor into compliance with TikTok's Content Sharing UX Guideline Points 1–5, addressing the rejection feedback "showcase full interactions between privacy settings and interaction settings"
  • New resources/js/enums/platform.ts mirrors the PHP Platform enum; Edit.vue and ScheduleTab.vue now use the enum instead of string literals
  • Backend hardening: FormRequest rejects publishing TikTok posts without explicit privacy_level; publisher throws instead of silently defaulting

TikTok UX Guideline checklist

Point Requirement Status
1 creator_info fetched on render + nickname displayed + max duration validated ✅ already in place
2 Title editable, privacy dropdown with no default, interaction toggles unchecked + greyed when creator-disabled
2 Music Usage Confirmation declaration always visible ✅ fixed (was conditional)
2c Photo posts show only "Allow Comment" — Duet/Stitch hidden
2b privacy_level required at backend (no silent default) ✅ FormRequest + publisher throw
3a Disclosure toggle defaults OFF; Your Brand / Branded Content sub-checkboxes; publish disabled with exact tooltip when toggle on without sub-selection
3b SELF_ONLY shown disabled with tooltip when Branded Content checked (not filtered out) + toast on auto-clear
4 Compliance text varies between Music Usage / Branded Content Policy + Music Usage based on selection
5 Preview, editable caption, explicit publish consent, status polling ✅ already in place

Test plan

  • Photo posts — connect a TikTok account, attach 2-5 images, pick "Photo carousel" variant, set privacy, publish; verify the carousel appears on TikTok with caption in description
  • Video posts — attach a video, leave variant on "Video", verify Duet/Stitch/AIGC controls visible, Auto Add Music hidden
  • Disclosure flow — open Disclose toggle without picking a sub-toggle; verify publish button disabled with tooltip "You need to indicate if your content promotes yourself, a third party, or both."
  • Branded Content interaction — pick "Only me" privacy, then enable Branded Content; verify privacy clears AND a toast surfaces "Privacy was cleared because Branded Content cannot be private."
  • Branded Content disabled state — with Branded Content on, open privacy dropdown; verify "Only me" appears disabled with tooltip "Branded content visibility cannot be set to private."; amber persistent warning visible below the dropdown
  • Always-visible Music Usage — with no toggles touched, verify the line "By posting, you agree to TikTok's Music Usage Confirmation." is visible at the bottom of the panel from initial render
  • Compliance text variance — toggle Branded Content; verify line becomes "Branded Content Policy and Music Usage Confirmation"
  • Backend hardening — try to publish via direct API request without meta.privacy_level; verify 422 with platforms.0.meta.privacy_level error; draft saves still work without it
  • Run full Pest suite — 1490 tests pass

Files changed

13 files modified + 3 new files:

Backend (PHP): ContentType.php, TikTokPublisher.php, UpdatePostRequest.php, PostPlatformFactory.php
Frontend (Vue/TS): TikTokSettings.vue, ScheduleTab.vue, Edit.vue, content-type.ts, new platform.ts
i18n: lang/{en,pt-BR,es}/posts.php
Tests: new TikTokPhotoContentTypeTest.php, new UpdatePostRequestTest.php, modified TikTokPublisherTest.php

Resubmission to TikTok

Recording a screencast walking through the interactions in order is the next step before resubmitting to TikTok review:

  1. Connect account → 2. Open editor → 3. Show variant pills + privacy placeholder + interaction toggles + always-visible Music Usage line → 4. Open Disclose, try publishing (disabled w/ tooltip) → 5. Toggle Branded Content (privacy clears + toast + amber warning) → 6. Open privacy dropdown (Only me disabled w/ tooltip) → 7. Pick Public, publish → 8. Switch to Photo carousel variant, attach images, verify Duet/Stitch hidden, Auto Add Music visible

Adds the enum case TikTokPhoto = 'tiktok_photo' with maxMediaCount 35,
supportsImage true, supportsVideo false, 1:1 aspect ratio. The publisher
service already calls /post/publish/content/init/ correctly — this
enum case is the missing data layer that lets validation, UI, and
defaults treat photo posts as a first-class content type.
TikTok's photo endpoint caps the 'title' field at 90 UTF-16 runes but
allows 'description' up to 4000. Today the publisher always sends the
caption in 'title', which would silently fail for any photo caption
longer than 90 chars. Split buildPostInfo into buildVideoPostInfo
(uses title) and buildPhotoPostInfo (uses description, omits Duet/
Stitch/AIGC since they don't apply to photos).
## Photo carousel support

- Adds `ContentType::TikTokPhoto` enum case (max 35 photos, 1:1 aspect,
  supportsImage true, supportsVideo false) and JS mirror in content-type.ts.
- Variant pill picker (Video / Photo carousel) at the top of TikTokSettings,
  mirroring the Instagram pattern. Wired through ScheduleTab to the parent
  editor's existing update:platformContentType emit.
- i18n keys for variant_label / variant.video / variant.photo in en/pt-BR/es.
- Publisher: split buildPostInfo into buildVideoPostInfo (uses `title`,
  TikTok cap 2200 chars) and buildPhotoPostInfo (uses `description`, cap
  4000 chars; omits Duet/Stitch/AIGC since they don't apply). Removed the
  no-longer-needed queryCreatorInfo() call from publishVideo/publishPhotos
  — its only previous consumer (silent privacy_level fallback) is gone.

## UX Content Sharing API compliance

Per TikTok review feedback citing
https://developers.tiktok.com/doc/content-sharing-guidelines#required_ux_implementation_in_your_app

Point 1 — already satisfied (creator_info fetch + nickname display).

Point 2/4 — Music Usage Confirmation declaration is now always visible
in TikTokSettings; text changes between "Music Usage Confirmation" and
"Branded Content Policy and Music Usage Confirmation" based on toggle
state. Previously the entire `<p>` block was conditional on a brand
toggle being selected, hiding the baseline declaration.

Point 2b — privacy_level may not have a default. UI was already correct;
backend hardened: UpdatePostRequest now requires meta.privacy_level for
tiktok platforms when status is publishing/scheduled (via withValidator);
TikTokPublisher::resolveRequiredPrivacyLevel throws TikTokPublishException
(ContentPolicy category) when missing instead of silently falling back to
the creator's preferred level.

Point 2c — interaction settings now condition on content type:
- Photo posts hide Duet/Stitch (they don't apply per TikTok docs).
- Photo posts hide AIGC (also video-only).
- Video posts hide Auto Add Music (photos-only feature).
- Max-duration warning hidden when not a video post.
Source of truth is the user-selected contentType prop, not inferred
from media — ensures the UI reacts immediately to the variant pill.

Point 3a — publish button stays disabled when Disclose toggle is on
without a sub-selection (already the case via tiktokComplianceValid).
The disabled tooltip now uses the verbatim TikTok-required text "You
need to indicate if your content promotes yourself, a third party, or
both." instead of the generic "Some platform settings are incomplete..."
when the only blocker is TikTok disclosure incompleteness.

Point 3b — SELF_ONLY (Only me) privacy option is no longer filtered out
when Branded Content is checked. It is rendered disabled with a hover
tooltip "Branded content visibility cannot be set to private." plus a
persistent amber warning paragraph below the dropdown. When the user
toggles Branded Content while privacy is SELF_ONLY, the privacy clears
and a vue-sonner warning toast surfaces the change.

## Cross-cutting

- New `resources/js/enums/platform.ts` mirrors the PHP Platform enum,
  used in Edit.vue (tiktokComplianceValid + tiktokDisclosureIncomplete)
  and ScheduleTab.vue (all selected*Platforms computeds) to replace
  string literal comparisons against `'tiktok'` / `'facebook'` / etc.
- PostPlatformFactory tiktok() state defaults meta.privacy_level to
  SELF_ONLY so existing test fixtures keep passing under the new
  publisher/FormRequest requirements.

## Tests

- New tests/Unit/Enums/PostPlatform/TikTokPhotoContentTypeTest.php
  covering the new enum case (4 tests).
- TikTokPublisherTest: added "video uses title not description" and
  "throws when meta.privacy_level missing" regression tests; renamed
  two existing tests that depended on the removed silent fallback.
- New tests/Feature/UpdatePostRequestTest.php with 3 tests covering
  the FormRequest's privacy_level enforcement (publish-rejected,
  publish-passes, draft-allowed).

Full Pest suite: 1490 passed, 2 skipped (pre-existing).
… error

- ContentType::description() now reads from posts.content_types.{value}.description
  instead of hardcoded English. Adds the missing tiktok_photo entries in en/pt-BR/es
  and syncs three Pinterest descriptions that had drifted between the i18n file and
  the previously-hardcoded enum strings (the enum strings were the user-visible source).
- UpdatePostRequest::withValidator surfaces the privacy_required validation error
  via posts.form.tiktok.privacy_required (added in en/pt-BR/es) instead of an
  English string.
- Drops a verbose 3-line comment in withValidator that explained obvious code.
Removes the messages() override on UpdatePostRequest. The 4 messages it
defined were hardcoded English strings that bypassed Laravel's built-in
validation translations. With the override gone, errors flow through
lang/{locale}/validation.php which is already translated for en/pt-BR/es.
Trade-off: default messages reference the field path (e.g.
'platforms.0.content_type'). Acceptable here because the editor renders
errors inline next to each field rather than as a flat list.
The TikTok platform tile was getting silently disabled when the user
attached images before selecting TikTok: a fresh post defaults TikTok
to tiktok_video, the compliance check rejects images for video posts,
and platformIssues marks the tile as un-clickable.

Watch the media ref. If the first attached item is an image and the
TikTok post-platform's content_type is still tiktok_video (or vice
versa for video media on tiktok_photo), flip the content_type to match.
The variant picker inside TikTokSettings can still override afterwards;
this only covers the gap between adding media and opening the panel.

Reproducer: new post → attach image → click TikTok tile → previously
disabled with 'Only videos are allowed for this format', now enabled
and content_type lands on tiktok_photo.
…viable variant

Reverts the previous reactive watcher approach. Replaces it with two
small, declarative changes:

1. platformIssues for TikTok now checks whether ANY variant
   (tiktok_video / tiktok_photo) accepts the current media. The tile
   only stays blocked when neither variant fits (e.g. a GIF, mixed
   media). This matches the platform's real capability — TikTok does
   accept both videos and photos — and stops disabling the tile based
   on the post-platform's stale default content_type.

2. togglePlatform, when SELECTING TikTok, switches the post-platform's
   content_type to the variant that matches the first attached media
   (tiktok_video for video, tiktok_photo for image). Runs only on
   selection, not as a watcher — no implicit reactivity, no side
   effects on subsequent media changes. The variant picker inside
   TikTokSettings remains the user's manual override.

Reproducer fixed: new post → attach image → TikTok tile is now
clickable instead of greyed out with 'Only videos are allowed for
this format'. After clicking, content_type is tiktok_photo.
…with variant pickers

Replaces two TikTok-specific code paths (one in platformIssues, one in
togglePlatform) with a single generic helper. PLATFORM_VARIANTS maps
each platform that exposes multiple content types via a variant picker
to the list of those content types; firstCompatibleVariant returns the
first variant that fits the current media (or null when none fits).

Both consumers — the tile-level compliance check and the on-select
content_type snap — now share the same helper. Adding another platform
later (Instagram already has multiple variants if its default ever
changes) only requires extending the map, no logic changes.

Net effect: -15 lines, no TikTok-specific branching in logic, only data.
…match as TikTok

Pinterest has the same UX bug: default content_type is pinterest_pin
(image-only, 1 file). If the user attaches a video, the tile would
silently disable; if they attach 2+ images, also disabled — even though
pinterest_video_pin and pinterest_carousel exist as alternatives.

List all three Pinterest variants so the tile's lenient compliance
check considers each, and togglePlatform snaps to whichever fits the
attached media on selection.

Instagram, Facebook, and LinkedIn intentionally stay out: their defaults
(feed/post) accept both images and videos, so there's no silent block.
Including them would mask real per-variant errors (e.g. Reel + image
incompatibility) — that's a user-visible mistake we want surfaced, not
hidden at the tile level. The map is documented to that effect.
Two related fixes that together eliminate the 'Loading your TikTok
account settings…' flicker users were seeing on every keystroke /
variant click in the post editor:

1. PostController::edit no longer wraps tiktokCreatorInfos in
   Inertia::defer. The map is computed during the initial render and
   shipped as a regular prop. Without defer, the prop never resets to
   null between Inertia visits, so the loading line never reappears.

2. TikTokCreatorInfo::fetch is now wrapped in a 5-minute Cache::remember
   keyed by social_account_id. Autosaves (which round-trip through
   PostController::update → back() → edit() again) used to issue a
   fresh TikTok API call for every connected account on every save —
   now the cache short-circuits them. Creator info changes very rarely
   (only when the user updates privacy settings on TikTok itself), so
   five minutes of staleness is acceptable; the worst case is a
   slightly out-of-date privacy-options list that corrects on next
   page load.

Frontend cleanup: dropped the creatorInfoLoading prop, the inline
loading <p>, and the now-orphaned posts.form.tiktok.creator_info_loading
i18n key in en/pt-BR/es. ScheduleTab no longer passes the prop.
@paulocastellano paulocastellano merged commit 479011b into main May 10, 2026
2 checks passed
@paulocastellano paulocastellano deleted the feature/tiktok-photo-and-ux-compliance branch May 10, 2026 17:05
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant