From 4da66b41a9b53c73989000c60cf0b0ae7c066af1 Mon Sep 17 00:00:00 2001 From: metatroncubeswdev Date: Tue, 20 Oct 2026 19:14:28 -0400 Subject: [PATCH] Add privacy/terms pages and TikTok posting-screen requirements - Public /privacy and /terms pages (Quorla, operated by Metatroncube Software Solutions) for platform app listings; includes Google API Limited Use statement and TikTok data use - TikTok post panel follows the Content Posting API guidelines: shows the creator's name/avatar, audience options from creator info with no default, comment/Duet/Stitch off by default and locked when the creator disabled them, max video length, commercial content disclosure (Your brand / Branded content, branded content can't be private), Music Usage Confirmation / Branded Content Policy consent - Placeholder app icon (branding/quorla-icon-1024.png) Co-Authored-By: Claude Opus 5.5 --- branding/quorla-icon-1024.png | Bin 0 -> 88105 bytes connector/src/platforms/index.js | 9 +++ connector/src/platforms/tiktok.js | 29 +++++++++- connector/test/platforms.test.mjs | 15 +++++ dashboard/server/index.js | 8 +++ dashboard/server/legal/legal.css | 13 +++++ dashboard/server/legal/privacy.html | 75 ++++++++++++++++++++++++ dashboard/server/legal/terms.html | 68 ++++++++++++++++++++++ dashboard/web/src/PlatformPicker.jsx | 83 +++++++++++++++++++++++++++ 9 files changed, 297 insertions(+), 3 deletions(-) create mode 100644 branding/quorla-icon-1024.png create mode 100644 dashboard/server/legal/legal.css create mode 100644 dashboard/server/legal/privacy.html create mode 100644 dashboard/server/legal/terms.html diff --git a/branding/quorla-icon-1024.png b/branding/quorla-icon-1024.png new file mode 100644 index 0000000000000000000000000000000000000000..714afa75c4fe5e1a60c4c3d715f9275451b57ca0 GIT binary patch literal 88105 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
UiK_O@*OeUn_7pfSX4wQMVloa`QB)8pHd8 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 SHXwq(WlLXr?)ZPL+wp&8Z$+K} literal 0 HcmV?d00001 diff --git a/connector/src/platforms/index.js b/connector/src/platforms/index.js index fc5b98f..b232dfa 100644 --- a/connector/src/platforms/index.js +++ b/connector/src/platforms/index.js @@ -107,6 +107,14 @@ export async function checkConnection(connId) { } } +/** Platform-specific live account settings shown on the post screen (TikTok's creator info). */ +export async function creatorInfo(connId) { + const { conn, tokens } = await tokensFor(connId); + const a = adapter(conn.platform); + if (!a.creatorInfo) throw err(`${a.name} has no creator settings.`); + return a.creatorInfo(tokens); +} + // ---------- posting ---------- /** Problems that stop a post going to this platform (checked when the draft is saved). */ export function validate(platform, client, post) { @@ -121,6 +129,7 @@ export function validate(platform, client, post) { if (f.required && (opts[f.key] === undefined || opts[f.key] === "")) throw err(`${a.name}: ${f.label.toLowerCase()} is required.`); if (f.max && String(opts[f.key] || "").length > f.max) throw err(`${a.name}: ${f.label.toLowerCase()} is longer than ${f.max} characters.`); } + if (a.checkOptions) a.checkOptions(opts); const cap = a.limits?.caption; const caption = opts.caption ?? post.caption ?? ""; if (cap && caption.length > cap) throw err(`${a.name}: the caption is ${caption.length} characters; the limit is ${cap}. Shorten it or add a ${a.name} caption.`); diff --git a/connector/src/platforms/tiktok.js b/connector/src/platforms/tiktok.js index 709f2a3..9551df3 100644 --- a/connector/src/platforms/tiktok.js +++ b/connector/src/platforms/tiktok.js @@ -50,12 +50,32 @@ export default { limits: { caption: 2200, images: 35 }, fields: [ { key: "privacy", label: "Who can view", type: "select", options: PRIVACY, required: true, hint: "TikTok requires you to choose. Unaudited apps can only use “Only me”." }, - { key: "allowComments", label: "Allow comments", type: "checkbox", default: true }, + { key: "allowComments", label: "Allow comments", type: "checkbox", default: false }, { key: "allowDuet", label: "Allow Duet", type: "checkbox", default: false, onlyFor: ["REELS"] }, { key: "allowStitch", label: "Allow Stitch", type: "checkbox", default: false, onlyFor: ["REELS"] }, { key: "title", label: "Photo post title", type: "text", max: 90, onlyFor: ["IMAGE", "CAROUSEL"], hint: "Optional, up to 90 characters. The caption becomes the description." }, + { key: "discloseCommercial", label: "Disclose commercial content", type: "checkbox", default: false, hint: "Turn on if this promotes the client's own business or a third party." }, + { key: "brandOrganic", label: "Your brand (labelled “Promotional content”)", type: "checkbox", default: false }, + { key: "brandedContent", label: "Branded content (labelled “Paid partnership”)", type: "checkbox", default: false }, ], + /** What this creator allows right now (TikTok requires the post screen to follow it). */ + async creatorInfo(tokens) { + const { data } = await request("TikTok", `${API}/post/publish/creator_info/query/`, { token: tokens.accessToken, method: "POST", json: {} }); + const c = data.data || {}; + return { + nickname: c.creator_nickname || null, username: c.creator_username || null, avatar: c.creator_avatar_url || null, + privacyOptions: c.privacy_level_options || [], commentDisabled: Boolean(c.comment_disabled), duetDisabled: Boolean(c.duet_disabled), + stitchDisabled: Boolean(c.stitch_disabled), maxVideoSeconds: c.max_video_post_duration_sec || null, + }; + }, + + /** TikTok's commercial-content rules, checked when the draft is saved and again before posting. */ + checkOptions(opts) { + if (opts.discloseCommercial && !opts.brandOrganic && !opts.brandedContent) throw new Error("TikTok: you turned on commercial content disclosure. Choose “Your brand”, “Branded content” or both."); + if (opts.brandedContent && opts.privacy === "SELF_ONLY") throw new Error("TikTok: branded content can't be private (“Only me”). Choose another audience."); + }, + async identify(tokens) { const { data } = await request("TikTok", `${API}/user/info/`, { token: tokens.accessToken, query: { fields: "open_id,avatar_url,display_name,username,follower_count,profile_deep_link" } }); const u = data.data?.user || {}; @@ -65,7 +85,10 @@ export default { async publish({ tokens, conn, post, opts, caption }) { const token = tokens.accessToken; if (!opts.privacy) throw new Error("TikTok: choose who can view the post."); + this.checkOptions(opts); const creator = (await request("TikTok", `${API}/post/publish/creator_info/query/`, { token, method: "POST", json: {} })).data.data || {}; + const brand = { brand_content_toggle: Boolean(opts.discloseCommercial && opts.brandedContent), brand_organic_toggle: Boolean(opts.discloseCommercial && opts.brandOrganic) }; + const noComment = opts.allowComments !== true || Boolean(creator.comment_disabled); if (creator.privacy_level_options && !creator.privacy_level_options.includes(opts.privacy)) { throw new Error(`TikTok: this account can't post as “${opts.privacy}”. Allowed: ${creator.privacy_level_options.join(", ")}.`); } @@ -77,7 +100,7 @@ export default { const { data } = await request("TikTok", `${API}/post/publish/video/init/`, { token, method: "POST", json: { - post_info: { title: String(caption || "").slice(0, 2200), privacy_level: opts.privacy, disable_comment: opts.allowComments === false, disable_duet: !opts.allowDuet, disable_stitch: !opts.allowStitch }, + post_info: { title: String(caption || "").slice(0, 2200), privacy_level: opts.privacy, disable_comment: noComment, disable_duet: !opts.allowDuet || Boolean(creator.duet_disabled), disable_stitch: !opts.allowStitch || Boolean(creator.stitch_disabled), ...brand }, source_info: { source: "FILE_UPLOAD", video_size: bytes.length, chunk_size: plan.chunkSize, total_chunk_count: plan.count }, }, }); @@ -93,7 +116,7 @@ export default { const { data } = await request("TikTok", `${API}/post/publish/content/init/`, { token, method: "POST", json: { - post_info: { title: String(opts.title || "").slice(0, 90), description: String(caption || "").slice(0, 4000), privacy_level: opts.privacy, disable_comment: opts.allowComments === false, auto_add_music: true }, + post_info: { title: String(opts.title || "").slice(0, 90), description: String(caption || "").slice(0, 4000), privacy_level: opts.privacy, disable_comment: noComment, auto_add_music: true, ...brand }, source_info: { source: "PULL_FROM_URL", photo_cover_index: 0, photo_images: urls }, post_mode: "DIRECT_POST", media_type: "PHOTO", }, diff --git a/connector/test/platforms.test.mjs b/connector/test/platforms.test.mjs index c641363..0ca4c33 100644 --- a/connector/test/platforms.test.mjs +++ b/connector/test/platforms.test.mjs @@ -214,6 +214,21 @@ test("TikTok: connect, privacy is enforced, video uploads and photo posts pull f assert.equal(init.source_info.total_chunk_count, 1); assert.equal(init.post_info.privacy_level, "SELF_ONLY"); assert.equal(sent(/upload\.tiktok/)[0].headers["Content-Range"], "bytes 0-1023/1024"); + assert.equal(init.post_info.disable_comment, true, "comments are off unless the person turns them on"); + assert.equal(init.post_info.brand_content_toggle, false); + // commercial content disclosure rules + assert.throws(() => P.validate("tiktok", "acme", { type: "REELS", media: [VID], caption: "x", options: { tiktok: { privacy: "SELF_ONLY", discloseCommercial: true } } }), /Choose “Your brand”/); + assert.throws(() => P.validate("tiktok", "acme", { type: "REELS", media: [VID], caption: "x", options: { tiktok: { privacy: "SELF_ONLY", discloseCommercial: true, brandedContent: true } } }), /can't be private/); + routes = routes.filter((x) => !String(x.match).includes("creator_info")); + route("POST", "https://open.tiktokapis.com/v2/post/publish/creator_info/query/", json({ data: { creator_nickname: "Acme", creator_username: "acme", privacy_level_options: ["PUBLIC_TO_EVERYONE", "SELF_ONLY"], comment_disabled: true, max_video_post_duration_sec: 600 }, error: { code: "ok" } })); + const info = await P.creatorInfo(conn.id); + assert.deepEqual([info.nickname, info.commentDisabled, info.maxVideoSeconds, info.privacyOptions.length], ["Acme", true, 600, 2]); + calls = []; + await P.publish("tiktok", "acme", { type: "REELS", media: [VID], caption: "Promo", options: { tiktok: { privacy: "PUBLIC_TO_EVERYONE", allowComments: true, discloseCommercial: true, brandOrganic: true } } }); + const promo = bodyJson(sent(/video\/init/)[0]).post_info; + assert.equal(promo.brand_organic_toggle, true); + assert.equal(promo.brand_content_toggle, false); + assert.equal(promo.disable_comment, true, "creator turned comments off in TikTok, so they stay off"); route("POST", "https://open.tiktokapis.com/v2/post/publish/content/init/", json({ data: { publish_id: "pub2" }, error: { code: "ok" } })); await P.publish("tiktok", "acme", { type: "CAROUSEL", media: [IMG, IMG2], caption: "Menu", options: { tiktok: { privacy: "SELF_ONLY", title: "New menu" } } }); const photo = bodyJson(sent(/content\/init/)[0]); diff --git a/dashboard/server/index.js b/dashboard/server/index.js index 2df5dbf..2afd37f 100644 --- a/dashboard/server/index.js +++ b/dashboard/server/index.js @@ -2,6 +2,7 @@ import fs from "node:fs"; import path from "node:path"; import express from "express"; +import { fileURLToPath } from "node:url"; import { PORT, WEB_DIST, AGENCY_NAME, BRANDING_DIR, ODOO } from "./config.js"; import { login, logout, currentUser, requireAuth, requireAdmin, setupFirstAdmin, issueSession } from "./auth.js"; import * as users from "./users.js"; @@ -49,6 +50,12 @@ app.get("/api/auth/me", (req, res) => res.json({ user: currentUser(req), agency: app.get("/brand/logo.png", (_req, res) => res.sendFile(path.join(BRANDING_DIR, "metatroncube-logo.png"))); app.get("/brand/mark.png", (_req, res) => res.sendFile(path.join(BRANDING_DIR, "metatroncube-mark.png"))); +// Public legal pages (linked from platform app listings: TikTok, Google, LinkedIn, Pinterest, Meta). +const LEGAL_DIR = fileURLToPath(new URL("./legal/", import.meta.url)); +app.get("/privacy", (_req, res) => res.sendFile(path.join(LEGAL_DIR, "privacy.html"))); +app.get("/terms", (_req, res) => res.sendFile(path.join(LEGAL_DIR, "terms.html"))); +app.get("/legal/legal.css", (_req, res) => res.sendFile(path.join(LEGAL_DIR, "legal.css"))); + // Signed, expiring media links that platforms (TikTok, Threads, Google Business Profile) fetch posts from. app.get("/m/:token/:name", (req, res) => { const f = resolveMediaToken(req.params.token); @@ -99,6 +106,7 @@ app.get("/api/oauth/:platform/start", requireAdmin, h(async (req) => { return { url: extra.startConnect({ platform: req.params.platform, client, redirectUri: redirectUri(req, req.params.platform), user: req.user }) }; })); app.post("/api/connections/:id/check", requireAdmin, h(async (req) => ({ connection: await extra.checkConnection(req.params.id) }))); +app.get("/api/connections/:id/creator-info", h(async (req) => ({ creator: await extra.creatorInfo(req.params.id) }))); app.get("/api/connections/:id/targets", h(async (req) => ({ targets: await extra.targetsFor(req.params.id) }))); app.patch("/api/connections/:id", requireAdmin, h(async (req) => ({ connection: await extra.setTarget(req.params.id, String(req.body?.targetId || "")) }))); app.delete("/api/connections/:id", requireAdmin, h(async (req) => { extra.vault.removeConnection(req.params.id); return { ok: true }; })); diff --git a/dashboard/server/legal/legal.css b/dashboard/server/legal/legal.css new file mode 100644 index 0000000..81d52a0 --- /dev/null +++ b/dashboard/server/legal/legal.css @@ -0,0 +1,13 @@ +:root { --bg: #ffffff; --fg: #1f2024; --muted: #6b6e78; --line: #e4e7ec; --accent: #2c5f96; color-scheme: light dark; } +@media (prefers-color-scheme: dark) { :root { --bg: #14171b; --fg: #e8eaed; --muted: #9aa0a8; --line: #2a2f36; --accent: #7fb2e5; } } +* { box-sizing: border-box; } +body { margin: 0; background: var(--bg); color: var(--fg); font: 16px/1.65 system-ui, -apple-system, "Segoe UI", Roboto, sans-serif; } +main { max-width: 720px; margin: 0 auto; padding: 48px 20px 80px; } +.brand { font-weight: 800; letter-spacing: .02em; color: var(--accent); margin: 0 0 24px; } +h1 { font-size: 32px; line-height: 1.2; margin: 0 0 6px; } +.meta { color: var(--muted); font-size: 14px; margin: 0 0 28px; } +h2 { font-size: 19px; margin: 32px 0 8px; padding-top: 20px; border-top: 1px solid var(--line); } +ul { padding-left: 1.2em; } +li { margin: 6px 0; } +a { color: var(--accent); } +.links { margin-top: 40px; font-size: 14px; } diff --git a/dashboard/server/legal/privacy.html b/dashboard/server/legal/privacy.html new file mode 100644 index 0000000..d36883d --- /dev/null +++ b/dashboard/server/legal/privacy.html @@ -0,0 +1,75 @@ + + + + + +Privacy Policy · Quorla + + + +
+

Quorla

+

Privacy Policy

+

Effective 20 October 2026 · Operated by Metatroncube Software Solutions, Waterloo, Ontario, Canada

+ +

Quorla is a social media management service used by marketing agencies to plan, approve and publish content for the businesses they work with, and to see how that content performs. This policy explains what information Quorla handles, why, and the choices you have.

+ +

Who this covers

+
    +
  • Agency users: people who sign in to Quorla (agency owners, managers, designers, editors).
  • +
  • Connected account owners: businesses and creators whose social media accounts an agency connects to Quorla with the owner's permission.
  • +
+ +

Information we collect

+
    +
  • Account details: name, work email and role of agency users. Passwords are stored only as one-way hashes.
  • +
  • Content: images, videos, captions, plans and reports that agency users upload or create.
  • +
  • Connected platform data: when an account is connected, Quorla receives access tokens and the data each platform allows for the permissions granted, such as the account's ID, name, username, profile picture, follower count, posts, post insights and comments.
  • +
  • Technical logs: sign-in times, actions such as approvals and publishing results, and errors, used to run and secure the service.
  • +
+ +

Data from connected platforms

+

Quorla only uses platform data to provide the features the account owner and their agency use, and only within each platform's terms:

+
    +
  • TikTok (Login Kit, Content Posting API): basic profile information (open ID, display name, username, avatar) and follower counts to show which account is connected; and permission to upload and publish videos and photo posts that a person has reviewed and approved in Quorla. Quorla does not post anything without that approval and does not add watermarks or promotional branding to content.
  • +
  • Meta (Facebook, Instagram, Threads): Page and account details, insights, posts and comments for reporting and replies, and publishing of approved posts.
  • +
  • Google (YouTube, Google Business Profile): channel and business location details, and uploading or posting approved content. Quorla's use and transfer of information received from Google APIs adheres to the Google API Services User Data Policy, including the Limited Use requirements.
  • +
  • LinkedIn, Pinterest and Reddit: profile, Page or board details needed to post, and publishing of approved content.
  • +
+

We never sell platform data, never use it for advertising, and never use it to train AI models.

+ +

How we use information

+
    +
  • To run Quorla: sign-in, content library, scheduling, approvals, publishing and reports.
  • +
  • To produce AI-assisted drafts (content plans, captions, reports). Relevant content and performance data are processed by our AI provider, Anthropic, under terms that do not allow it to train models on that data.
  • +
  • To keep the service secure and to fix problems.
  • +
+ +

Sharing

+

Information is shared only with the agency that controls the account in Quorla, with the social platforms when you publish to them, with service providers that host or process data for us (hosting, AI processing, email), or when the law requires it. Each provider may use the data only to provide its service to us.

+ +

Security and storage

+

Quorla is served over HTTPS. Platform access tokens and app keys are encrypted at rest. Access is limited to the agency's signed-in users, and only admins can connect accounts or approve posts.

+ +

Retention and deletion

+
    +
  • Disconnecting an account in Quorla deletes its stored access tokens immediately. You can also remove Quorla's access in the platform's own settings (for example TikTok: Settings and privacy → Security → Manage app permissions).
  • +
  • Content and reports are kept while the agency uses Quorla, then deleted within 90 days after the agency's account closes, unless the law requires us to keep them longer.
  • +
  • To ask for a copy or deletion of your information, email us at the address below. We respond within 30 days.
  • +
+ +

Your rights

+

Depending on where you live (for example under Canada's PIPEDA or the EU/UK GDPR), you may have the right to access, correct, delete or move your information, and to object to some processing. Contact us to use these rights.

+ +

Children

+

Quorla is a business tool and is not intended for anyone under 18.

+ +

Changes

+

We will post changes on this page and update the date at the top. For significant changes we will notify agency admins.

+ +

Contact

+

Metatroncube Software Solutions, Waterloo, Ontario, Canada
Email: support@quorlasocial.com

+ +
+ + diff --git a/dashboard/server/legal/terms.html b/dashboard/server/legal/terms.html new file mode 100644 index 0000000..7c00fea --- /dev/null +++ b/dashboard/server/legal/terms.html @@ -0,0 +1,68 @@ + + + + + +Terms of Service · Quorla + + + +
+

Quorla

+

Terms of Service

+

Effective 20 October 2026 · Operated by Metatroncube Software Solutions, Waterloo, Ontario, Canada

+ +

These terms apply to anyone who uses Quorla, the social media management service operated by Metatroncube Software Solutions ("we", "us"). By signing in to or using Quorla you agree to them.

+ +

1. The service

+

Quorla lets agencies plan, review, approve and publish content to social media accounts they manage, and see performance. Posts are published only after a person with admin rights approves them.

+ +

2. Accounts

+
    +
  • You must give accurate details and keep your password private. You are responsible for activity under your account.
  • +
  • Agency admins decide who has access and what role they have.
  • +
+ +

3. Connecting social accounts

+
    +
  • You may only connect accounts you own or that the owner has authorized you to manage, and only for the purposes the owner agreed to.
  • +
  • When you connect or post to a platform, that platform's own terms and policies also apply, including the TikTok Terms of Service, TikTok's Music Usage Confirmation and Branded Content Policy, Meta's terms, the YouTube Terms of Service, and the terms of LinkedIn, Pinterest and Reddit.
  • +
  • You can disconnect any account at any time in Quorla, or remove Quorla's access in the platform's settings.
  • +
+ +

4. Your content

+

You keep all rights to the content you upload or create. You give us permission to store, process and publish it only as needed to provide Quorla to you. You confirm you have the rights to everything you publish, including music, images and people's likenesses, and that paid partnerships and promotional content are disclosed as the law and each platform require.

+ +

5. Acceptable use

+

You must not use Quorla to:

+
    +
  • post spam, misleading content, fake reviews or testimonials, or anything illegal;
  • +
  • buy followers or engagement, or run engagement schemes;
  • +
  • break any platform's rules or rate limits, or try to get around their restrictions;
  • +
  • access accounts or data you aren't authorized to use, or interfere with the service's security.
  • +
+ +

6. AI features

+

AI-generated drafts are suggestions. A person must review them before they are published, and you are responsible for what you approve.

+ +

7. Fees

+

Fees, if any, are set out in your plan or agreement with us.

+ +

8. Availability and changes

+

We work to keep Quorla available but don't guarantee it will be uninterrupted. Platforms can change or limit their APIs at any time, which may affect features. We may update Quorla and these terms; we'll post changes here and notify agency admins of significant ones.

+ +

9. Disclaimer and liability

+

Quorla is provided "as is". To the extent the law allows, we are not liable for indirect or consequential losses, for actions taken by social platforms, or for content you approve. Our total liability for any claim is limited to the fees you paid us in the 12 months before the claim.

+ +

10. Ending use

+

You can stop using Quorla at any time. We may suspend accounts that break these terms. Our Privacy Policy explains what happens to data afterwards.

+ +

11. Governing law

+

These terms are governed by the laws of the Province of Ontario and the federal laws of Canada that apply there.

+ +

Contact

+

Metatroncube Software Solutions, Waterloo, Ontario, Canada
Email: support@quorlasocial.com

+ +
+ + diff --git a/dashboard/web/src/PlatformPicker.jsx b/dashboard/web/src/PlatformPicker.jsx index 2e5d0ec..30fc634 100644 --- a/dashboard/web/src/PlatformPicker.jsx +++ b/dashboard/web/src/PlatformPicker.jsx @@ -47,7 +47,90 @@ function TargetSelect({ conn, value, onChange, id }) { ); } +const TT_PRIVACY = { PUBLIC_TO_EVERYONE: "Everyone", MUTUAL_FOLLOW_FRIENDS: "Friends", FOLLOWER_OF_CREATOR: "Followers", SELF_ONLY: "Only me" }; + +/** TikTok's post screen, following its Content Posting API guidelines (creator info, no default audience, disclosure, consent). */ +function TikTokFields({ platform, type, options, setOption, caption }) { + const [creator, setCreator] = useState(null); + const [error, setError] = useState(null); + useEffect(() => { api(`/api/connections/${platform.conn.id}/creator-info`).then((r) => setCreator(r.creator)).catch(setError); }, [platform.conn.id]); + const video = type === "REELS"; + const branded = Boolean(options.discloseCommercial && options.brandedContent); + const organic = Boolean(options.discloseCommercial && options.brandOrganic); + const check = (key, label, disabled, note) => ( + + ); + const limit = platform.limits?.caption; + const used = (options.caption ?? caption ?? "").length; + + return ( +
+
+
+ {error &&
Couldn't load this TikTok account's settings: {error.message}. Reconnect TikTok on the Connections page.
} + {!creator && !error &&
Loading the account's TikTok settings…
} + {creator && ( + <> +
+ + + + {!video && ( + + setOption("title", e.target.value)} /> + + )} +
+
+ {check("allowComments", "Allow comments", creator.commentDisabled, "off in this account's TikTok settings")} + {video && check("allowDuet", "Allow Duet", creator.duetDisabled, "off in TikTok settings")} + {video && check("allowStitch", "Allow Stitch", creator.stitchDisabled, "off in TikTok settings")} +
+ {video && creator.maxVideoSeconds &&
This account can post videos up to {Math.floor(creator.maxVideoSeconds / 60)} min {creator.maxVideoSeconds % 60 ? `${creator.maxVideoSeconds % 60} s` : ""}.
} +
+ +

Turn on if this post promotes the client's own business, a product or a third party.

+ {options.discloseCommercial && ( +
+ {check("brandOrganic", "Your brand: you're promoting yourself or your own business")} + {check("brandedContent", "Branded content: you're promoting another brand or a third party", options.privacy === "SELF_ONLY", "not available for “Only me”")} + {(organic || branded) &&
The post will be labelled “{branded ? "Paid partnership" : "Promotional content"}”.
} + {!organic && !branded &&
Choose at least one option.
} +
+ )} +
+ + )} + +