Skip to content

feat(web): default call to action for every video - #3545

Open
AatmanAJ wants to merge 2 commits into
CapSoftware:mainfrom
AatmanAJ:feat/default-call-to-action
Open

AatmanAJ wants to merge 2 commits into
CapSoftware:mainfrom
AatmanAJ:feat/default-call-to-action

Conversation

@AatmanAJ

@AatmanAJ AatmanAJ commented Oct 29, 2026 •

Copy link
Copy Markdown
Contributor

Adds a default call to action. Set it once and it shows on every video that doesn't have its own call to action, so people no longer have to add the same "Book a call" button to each video by hand. It was asked for a few times on Discord, Loom-style.

How it works

  • Where it's set: the call to action dialog has a new Use on all my videos switch. Saving with it on makes this call to action your default.
  • Settings: Account settings has a Default call to action card where you can see, edit or remove the default without opening a video. It reuses the same dialog in a "default" mode.
  • Which call to action a viewer sees:
    1. The video's own call to action, if it has one.
    2. Nothing, if you turned calls to action off for that video.
    3. Otherwise, your default.
  • Retroactive: it's resolved when the page loads (share page and embed), like defaultPlaybackSpeed falls back to the organization setting. That means it covers every way a video gets created (desktop, web upload, Loom import, API) without touching any of them, it applies to videos you've already shared, and editing the default updates every video that uses it.
  • Turning it off for one video: a video using the default shows Default in the Manage Cap menu instead of "On", and its dialog offers Turn off for this video. That stores callToAction: false on the video.
  • Coming back to the default: opening the dialog on a video with no call to action pre-fills it from your default with the switch on, so saving puts the video back on the default.
  • Pro: gated the same as today, on the owner's plan. Removing a default doesn't need Pro.
  • Storage: the default lives in users.preferences.defaultCallToAction, so there's no migration. It's only sent to the page for the owner; viewers just get the resolved call to action.

Two things worth knowing

  • "Remove" now stores false instead of deleting the key. Without that, a default would come back on a video where you'd removed the call to action. Videos whose call to action was removed before this PR have no key, so they'll pick up a default if their owner sets one.
  • Writes use JSON_TYPE instead of COALESCE. New users' preferences is a JSON null, not SQL NULL. COALESCE keeps a JSON null, and JSON_SET can't add a key to it, so the first save silently did nothing. All writes in the action now treat anything that isn't a JSON object as {}. There's a test for it.

Testing

  • Unit tests:
    • New tests for which call to action wins and for parsing the default.
    • The share page shows the owner's default on a video with no call to action of its own. A video that's turned off stays off. A non-Pro owner shows nothing. Only the owner receives their default in the page data.
    • The actions turn a video off with false. Saving as default writes the user's preferences and clears the video's own copy. Setting a default requires Pro, but removing one doesn't.
    • The 4 call to action test files pass, 92 tests.
  • Full web unit suite: 4,815 passed. The one failure, homepage-seo.test.ts (it expects no FAQPage on the homepage), also fails on main.
  • bun run typecheck and Biome: clean.
  • By hand, locally (Next dev server, MySQL, a Pro test account and three videos, one with its own call to action):
    • Saving with Use on all my videos on wrote the default. Logged-out viewers then saw it on the edited video and on a video that was never touched, in both the share page and the embed. The video with its own call to action kept it.
    • The Manage Cap menu shows Default. The dialog opens pre-filled with the switch on.
    • Turn off for this video stored {"callToAction": false}, and viewers saw no call to action on it.
    • Editing the default's colour in Settings updated the videos that use it.
    • Remove default cleared it. Videos without their own call to action then showed nothing, and the one with its own kept it.
    • With the owner not on Pro, no default was served. It came back once the owner was Pro again.

Open questions

  • Per-user or per-organization: I made the default per user, because the call to action is owner-only and Pro-gated by the owner, and a booking link is usually personal. An organization-wide default could sit on top of this later.
  • Existing videos: the default applies to videos you'd already shared. The settings copy says so. If you'd rather it only covered new videos, I can store the date the default was set and only apply it to videos created after that.

RetriggerView in GreptileConfidence Score: 5/5

The PR appears safe to merge; both previous findings are addressed.

Summary

Adds a personal default call to action for videos without their own.

  • Owners can save one call to action as the default for their videos.
  • Share pages and embeds choose a video's call to action or its owner's default.

No new actionable issues were found. Checks were limited to reading code; tests and browser checks were not run.

Reviews (3) · Last reviewed commit: "fix(web): save a default call to action ..." · Reviewed by Greptile

@vercel
vercel Bot temporarily deployed to Preview – cap-status October 29, 2026 14:14 Inactive
@vercel

vercel Bot commented Oct 29, 2026 •

Copy link
Copy Markdown
Contributor

The latest updates on your projects. Learn more about Vercel for GitHub.

1 Skipped Deployment
Project Deployment Actions Updated
cap-status Skipped Skipped Oct 29, 2026 2:32pm UTC

@AatmanAJ

Copy link
Copy Markdown
Contributor Author

@greptileai review

Comment on lines +31 to +34
if (!user.isPro) {
setUpgradeModalOpen(true);
return;
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Downgraded owners cannot remove defaults

openDialog opens the upgrade prompt before mounting the dialog for owners without Pro. They cannot reach “Remove default,” even though updateDefaultCallToAction(null) allows removal without Pro. Allow owners with an existing default to open the dialog; keep the Pro check on saving.

Prompt To Fix With AI
This is a comment left during a code review.
Path: apps/web/app/(org)/dashboard/settings/account/components/DefaultCallToAction.tsx
Line: 31-34

Comment:
**Downgraded owners cannot remove defaults**

`openDialog` opens the upgrade prompt before mounting the dialog for owners without Pro. They cannot reach “Remove default,” even though `updateDefaultCallToAction(null)` allows removal without Pro. Allow owners with an existing default to open the dialog; keep the Pro check on saving.

---

For each issue above, determine whether it is valid and should be fixed. If so, fix it directly.

Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!

Comment on lines +83 to +90
if (options.asDefault) {
await storeDefaultCallToAction(user.id, result.value);
await db()
.update(videos)
.set({
settings: sql`JSON_REMOVE(${jsonObject(videos.settings)}, '$.callToAction')`,
})
.where(ownedVideo);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Failed saves change other videos

Saving with asDefault commits the account default before clearing the video's own value. If the second write fails, other videos pick up the new default while this video keeps its old button—or stays off—and the dialog reports that saving failed. Put both writes in one database transaction so a failed save leaves both values unchanged.

Prompt To Fix With AI
This is a comment left during a code review.
Path: apps/web/actions/videos/call-to-action.ts
Line: 83-90

Comment:
**Failed saves change other videos**

Saving with `asDefault` commits the account default before clearing the video's own value. If the second write fails, other videos pick up the new default while this video keeps its old button—or stays off—and the dialog reports that saving failed. Put both writes in one database transaction so a failed save leaves both values unchanged.

---

For each issue above, determine whether it is valid and should be fixed. If so, fix it directly.

@AatmanAJ

Copy link
Copy Markdown
Contributor Author

@greptileai review

@vercel
vercel Bot temporarily deployed to Preview – cap-status October 29, 2026 14:32 Inactive
@AatmanAJ
AatmanAJ marked this pull request as ready for review October 29, 2026 14:37
@AatmanAJ

Copy link
Copy Markdown
Contributor Author

@richiemcilroy this one's ready too - a default call to action for every video, which a few people asked for on Discord. Greptile's at 5/5 and I tested it end to end locally (share page, embed, settings, Pro gating).

This branch was previously deployed

1 inactive deployment
Preview – cap-status — 43ada333 Deployed Oct 29, 2026 by vercel[bot]
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