feat(remotion): WhatsApp Cloud API demo video for Meta app review

15s landscape (1920x1080) split-screen: left shows SquareMCP chat
prompt + animated cURL command + 200 response with wamid; right shows
a rendered WhatsApp phone UI with the message bubble appearing and blue
double-checkmarks. Also adds transparent-background logo PNG for Meta
Tech Provider icon upload.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Garfield
2026-05-14 20:02:22 -04:00
parent d7c55cb82b
commit 195ad0b3d1
7 changed files with 518 additions and 0 deletions

View File

@@ -11,6 +11,7 @@ import {
SquareMCPTikTokProblem,
SquareMCPTikTokProof,
} from "./SquareMCPTikTok";
import { SquareMCPWhatsApp } from "./SquareMCPWhatsApp";
export const RemotionRoot = () => {
return (
@@ -87,6 +88,14 @@ export const RemotionRoot = () => {
width={1920}
height={1080}
/>
<Composition
id="SquareMCPWhatsApp"
component={SquareMCPWhatsApp}
durationInFrames={15 * 30}
fps={30}
width={1920}
height={1080}
/>
</>
);
};

View File

@@ -0,0 +1,27 @@
import { AbsoluteFill, Sequence, useVideoConfig } from "remotion";
import { WhatsAppBackground } from "./scenes/whatsapp/WhatsAppBackground";
import { WhatsAppIntro } from "./scenes/whatsapp/WhatsAppIntro";
import { WhatsAppSplitScreen } from "./scenes/whatsapp/WhatsAppSplitScreen";
const Shell = ({ children }: { children: React.ReactNode }) => (
<AbsoluteFill>
<WhatsAppBackground />
{children}
</AbsoluteFill>
);
// Total: 3s intro + 12s split-screen = 15s @ 30fps = 450 frames
export const SquareMCPWhatsApp = () => {
const { fps } = useVideoConfig();
return (
<Shell>
<Sequence durationInFrames={3 * fps}>
<WhatsAppIntro />
</Sequence>
<Sequence from={3 * fps}>
<WhatsAppSplitScreen />
</Sequence>
</Shell>
);
};

View File

@@ -0,0 +1,25 @@
import { AbsoluteFill } from "remotion";
export const WhatsAppBackground = () => (
<AbsoluteFill
style={{
background: "linear-gradient(160deg, #0a1a10 0%, #060d0a 50%, #030806 100%)",
}}
>
{/* Subtle grid */}
<AbsoluteFill
style={{
backgroundImage:
"linear-gradient(rgba(37,211,102,0.04) 1px, transparent 1px), linear-gradient(90deg, rgba(37,211,102,0.04) 1px, transparent 1px)",
backgroundSize: "64px 64px",
}}
/>
{/* Glow */}
<AbsoluteFill
style={{
background:
"radial-gradient(ellipse 60% 40% at 50% 50%, rgba(37,211,102,0.07) 0%, transparent 70%)",
}}
/>
</AbsoluteFill>
);

View File

@@ -0,0 +1,88 @@
import { AbsoluteFill, spring, useCurrentFrame, useVideoConfig } from "remotion";
import { FONT, SPRING_CFG } from "../../styles";
const WA_GREEN = "#25D366";
const WhatsAppIcon = ({ size }: { size: number }) => (
<svg width={size} height={size} viewBox="0 0 64 64" fill="none">
<circle cx="32" cy="32" r="32" fill={WA_GREEN} />
<path
d="M32 10C19.85 10 10 19.85 10 32c0 3.9 1.05 7.55 2.87 10.7L10 54l11.6-2.83A21.87 21.87 0 0 0 32 54c12.15 0 22-9.85 22-22S44.15 10 32 10zm0 4c9.94 0 18 8.06 18 18s-8.06 18-18 18a17.93 17.93 0 0 1-9.1-2.47l-.65-.39-6.75 1.65 1.68-6.57-.43-.68A17.93 17.93 0 0 1 14 32c0-9.94 8.06-18 18-18zm-5.2 9.5c-.38 0-1 .14-1.52.7-.52.57-2 1.96-2 4.78s2.05 5.55 2.33 5.93c.29.38 4 6.2 9.77 8.45 1.36.52 2.42.83 3.24 1.06 1.36.38 2.6.33 3.58.2 1.09-.14 3.35-1.37 3.83-2.69.47-1.33.47-2.47.33-2.7-.14-.24-.52-.38-.9-.57l-3.96-1.95c-.38-.19-.67-.28-.95.29-.29.57-1.1 1.38-1.35 1.67-.24.29-.48.33-.86.14-.38-.19-1.6-.59-3.04-1.88-1.12-1-1.88-2.24-2.1-2.62-.22-.38-.02-.58.17-.77.17-.17.38-.43.57-.65.19-.22.24-.38.38-.67.14-.29.07-.52-.05-.72-.12-.19-1.05-2.52-1.43-3.45-.38-.91-.76-.76-1.05-.76h-.9z"
fill="white"
/>
</svg>
);
const SquareMCPIcon = ({ size }: { size: number }) => (
<svg width={size} height={size} viewBox="0 0 64 64" fill="none">
<defs>
<linearGradient id="wg" x1="10" y1="10" x2="54" y2="54" gradientUnits="userSpaceOnUse">
<stop stopColor="#7DB6FF" />
<stop offset="1" stopColor="#0E63F6" />
</linearGradient>
</defs>
<path
d="M10 12C10 10.9 10.9 10 12 10H31V17H17V31H10V12ZM33 10H52C53.1 10 54 10.9 54 12V31H47V17H33V10ZM10 33H17V47H31V54H12C10.9 54 10 53.1 10 52V33ZM47 33H54V52C54 53.1 53.1 54 52 54H33V47H47V33Z"
fill="url(#wg)"
/>
<path d="M24 24H33V31H40V40H31V33H24V24Z" fill="#0E63F6" opacity="0.92" />
</svg>
);
export const WhatsAppIntro = () => {
const frame = useCurrentFrame();
const { fps } = useVideoConfig();
const logoIn = spring({ fps, frame, config: SPRING_CFG });
const textIn = spring({ fps, frame: Math.max(0, frame - 20), config: SPRING_CFG });
const subIn = spring({ fps, frame: Math.max(0, frame - 40), config: SPRING_CFG });
return (
<AbsoluteFill style={{ alignItems: "center", justifyContent: "center", display: "flex", flexDirection: "column", gap: 32 }}>
{/* Logo pair */}
<div
style={{
display: "flex",
alignItems: "center",
gap: 40,
opacity: logoIn,
transform: `scale(${0.7 + logoIn * 0.3})`,
}}
>
<SquareMCPIcon size={120} />
<div style={{ fontFamily: FONT, color: "rgba(255,255,255,0.3)", fontSize: 64, fontWeight: 300 }}>×</div>
<WhatsAppIcon size={120} />
</div>
{/* Title */}
<div
style={{
fontFamily: FONT,
color: "#ffffff",
fontSize: 64,
fontWeight: 800,
textAlign: "center",
lineHeight: 1.1,
opacity: textIn,
transform: `translateY(${(1 - textIn) * 40}px)`,
}}
>
SquareMCP × WhatsApp Business
</div>
{/* Subtitle */}
<div
style={{
fontFamily: FONT,
color: "#25D366",
fontSize: 32,
fontWeight: 600,
opacity: subIn,
transform: `translateY(${(1 - subIn) * 24}px)`,
}}
>
Sending messages via the Cloud API
</div>
</AbsoluteFill>
);
};

View File

@@ -0,0 +1,369 @@
import {
AbsoluteFill,
interpolate,
spring,
useCurrentFrame,
useVideoConfig,
} from "remotion";
import { FONT, SPRING_CFG, SPRING_SOFT } from "../../styles";
const WA_GREEN = "#25D366";
const WA_DARK = "#111b21";
const WA_BUBBLE = "#005c4b";
const WA_HEADER = "#202c33";
// ── Left panel: SquareMCP chat + API call ─────────────────────────────────────
const CURL_LINES = [
"curl -X POST \\",
" https://graph.facebook.com/v18.0/ \\",
" {PHONE_NUMBER_ID}/messages \\",
" -H 'Authorization: Bearer {TOKEN}' \\",
" -H 'Content-Type: application/json' \\",
" -d '{",
' "messaging_product": "whatsapp",',
' "to": "+19548716341",',
' "type": "text",',
' "text": { "body": "Hello from SquareMCP!" }',
" }'",
];
const RESPONSE_LINES = [
"{",
' "messaging_product": "whatsapp",',
' "messages": [{',
' "id": "wamid.HBgLMTk1NDg3MTYzNDEV..."',
" }]",
"}",
];
const LeftPanel = ({ frame, fps }: { frame: number; fps: number }) => {
const panelIn = spring({ fps, frame, config: SPRING_SOFT });
const chatIn = spring({ fps, frame: Math.max(0, frame - 10), config: SPRING_CFG });
const termIn = spring({ fps, frame: Math.max(0, frame - 30), config: SPRING_CFG });
// Animate curl lines appearing one by one
const curlProgress = interpolate(frame, [40, 140], [0, CURL_LINES.length], { extrapolateRight: "clamp" });
// Response appears after curl finishes
const responseProgress = interpolate(frame, [155, 210], [0, RESPONSE_LINES.length], { extrapolateRight: "clamp" });
return (
<div
style={{
flex: 1,
padding: "40px 32px 40px 48px",
display: "flex",
flexDirection: "column",
gap: 24,
opacity: panelIn,
transform: `translateX(${(1 - panelIn) * -60}px)`,
}}
>
{/* Section label */}
<div style={{ fontFamily: FONT, color: "rgba(255,255,255,0.4)", fontSize: 20, fontWeight: 600, letterSpacing: 2, textTransform: "uppercase" }}>
SquareMCP Sending message
</div>
{/* Chat prompt bubble */}
<div
style={{
borderRadius: 20,
border: "1px solid rgba(14,99,246,0.35)",
background: "rgba(9,12,22,0.94)",
padding: "20px 24px",
opacity: chatIn,
transform: `translateY(${(1 - chatIn) * 30}px)`,
}}
>
<div style={{ fontFamily: FONT, color: "rgba(255,255,255,0.4)", fontSize: 16, marginBottom: 10 }}>Chat</div>
<div
style={{
marginLeft: "auto",
borderRadius: 18,
background: "linear-gradient(135deg, rgba(14,99,246,0.95), rgba(125,182,255,0.9))",
padding: "16px 20px",
color: "#08111f",
fontFamily: FONT,
fontSize: 22,
fontWeight: 700,
lineHeight: 1.3,
maxWidth: "90%",
}}
>
Send a WhatsApp message to +1 954 871 6341:<br />
"Hello from SquareMCP!"
</div>
</div>
{/* Terminal */}
<div
style={{
borderRadius: 16,
background: "#0d1117",
border: "1px solid rgba(37,211,102,0.2)",
padding: "18px 20px",
flex: 1,
opacity: termIn,
transform: `translateY(${(1 - termIn) * 30}px)`,
}}
>
{/* Terminal header */}
<div style={{ display: "flex", alignItems: "center", gap: 8, marginBottom: 14 }}>
{["#ff5f57", "#febc2e", "#28c840"].map((c) => (
<div key={c} style={{ width: 12, height: 12, borderRadius: 999, background: c }} />
))}
<div style={{ marginLeft: 8, fontFamily: "ui-monospace, monospace", color: "rgba(255,255,255,0.3)", fontSize: 14 }}>
terminal curl
</div>
</div>
{/* Prompt */}
<div style={{ fontFamily: "ui-monospace, monospace", color: "#7ee787", fontSize: 15, marginBottom: 6 }}>
$ {/* cursor blink when not yet started */}
</div>
{/* Curl lines */}
{CURL_LINES.slice(0, Math.ceil(curlProgress)).map((line, i) => (
<div
key={i}
style={{
fontFamily: "ui-monospace, monospace",
color: line.startsWith(" -d") || line.includes('"') ? "#79c0ff" : "#e6edf3",
fontSize: 15,
lineHeight: 1.6,
opacity: i < curlProgress - 1 ? 1 : curlProgress - i,
}}
>
{line}
</div>
))}
{/* Response */}
{responseProgress > 0 && (
<div style={{ marginTop: 12 }}>
<div style={{ fontFamily: "ui-monospace, monospace", color: WA_GREEN, fontSize: 14, marginBottom: 4 }}>
# Response (200 OK)
</div>
{RESPONSE_LINES.slice(0, Math.ceil(responseProgress)).map((line, i) => (
<div
key={i}
style={{
fontFamily: "ui-monospace, monospace",
color: line.includes("wamid") ? WA_GREEN : "#e6edf3",
fontSize: 15,
lineHeight: 1.55,
opacity: i < responseProgress - 1 ? 1 : responseProgress - i,
}}
>
{line}
</div>
))}
</div>
)}
</div>
</div>
);
};
// ── Right panel: WhatsApp phone UI ────────────────────────────────────────────
const RightPanel = ({ frame, fps }: { frame: number; fps: number }) => {
const panelIn = spring({ fps, frame, config: SPRING_SOFT });
// Message bubble appears at frame 220 (after API response shown)
const msgIn = spring({ fps, frame: Math.max(0, frame - 220), config: SPRING_CFG });
const checkIn = spring({ fps, frame: Math.max(0, frame - 260), config: SPRING_CFG });
const showMsg = frame >= 220;
return (
<div
style={{
width: 440,
padding: "40px 48px 40px 24px",
display: "flex",
alignItems: "center",
justifyContent: "center",
opacity: panelIn,
transform: `translateX(${(1 - panelIn) * 60}px)`,
}}
>
{/* Phone shell */}
<div
style={{
width: 340,
height: 680,
borderRadius: 44,
border: "8px solid #2a2a2a",
background: WA_DARK,
overflow: "hidden",
boxShadow: "0 40px 120px rgba(0,0,0,0.8), 0 0 0 1px rgba(255,255,255,0.06)",
display: "flex",
flexDirection: "column",
}}
>
{/* Status bar */}
<div style={{ background: WA_HEADER, padding: "10px 20px 8px", display: "flex", justifyContent: "space-between", alignItems: "center" }}>
<div style={{ fontFamily: FONT, color: "white", fontSize: 13, fontWeight: 600 }}>9:41</div>
<div style={{ display: "flex", gap: 6, alignItems: "center" }}>
{/* Signal bars */}
<div style={{ display: "flex", gap: 2, alignItems: "flex-end" }}>
{[8, 12, 16, 20].map((h, i) => (
<div key={i} style={{ width: 4, height: h, background: "white", borderRadius: 2, opacity: 0.9 }} />
))}
</div>
{/* Battery */}
<div style={{ width: 22, height: 11, border: "1.5px solid white", borderRadius: 3, padding: 1.5, display: "flex" }}>
<div style={{ flex: 1, background: WA_GREEN, borderRadius: 1 }} />
</div>
</div>
</div>
{/* Chat header */}
<div
style={{
background: WA_HEADER,
borderBottom: "1px solid rgba(255,255,255,0.05)",
padding: "12px 16px",
display: "flex",
alignItems: "center",
gap: 12,
}}
>
<div style={{ width: 40, height: 40, borderRadius: 999, background: WA_GREEN, display: "flex", alignItems: "center", justifyContent: "center" }}>
<div style={{ fontFamily: FONT, color: "white", fontSize: 18, fontWeight: 700 }}>S</div>
</div>
<div>
<div style={{ fontFamily: FONT, color: "white", fontSize: 15, fontWeight: 600 }}>SquareMCP</div>
<div style={{ fontFamily: FONT, color: "rgba(255,255,255,0.45)", fontSize: 12 }}>Business Account</div>
</div>
</div>
{/* Chat body */}
<div
style={{
flex: 1,
background: `url("data:image/svg+xml,%3Csvg width='60' height='60' viewBox='0 0 60 60' xmlns='http://www.w3.org/2000/svg'%3E%3Cg fill='none' fill-rule='evenodd'%3E%3Cg fill='%23182229' fill-opacity='0.4'%3E%3Cpath d='M36 34v-4h-2v4h-4v2h4v4h2v-4h4v-2h-4zm0-30V0h-2v4h-4v2h4v4h2V6h4V4h-4zM6 34v-4H4v4H0v2h4v4h2v-4h4v-2H6zM6 4V0H4v4H0v2h4v4h2V6h4V4H6z'/%3E%3C/g%3E%3C/g%3E%3C/svg%3E")`,
backgroundColor: "#0b141a",
padding: "16px 12px",
display: "flex",
flexDirection: "column",
justifyContent: "flex-end",
}}
>
{/* Incoming message bubble */}
{showMsg && (
<div
style={{
alignSelf: "flex-end",
maxWidth: "82%",
opacity: msgIn,
transform: `translateY(${(1 - msgIn) * 24}px) scale(${0.9 + msgIn * 0.1})`,
}}
>
<div
style={{
background: WA_BUBBLE,
borderRadius: "18px 4px 18px 18px",
padding: "10px 14px 22px",
position: "relative",
}}
>
<div style={{ fontFamily: FONT, color: "white", fontSize: 15, lineHeight: 1.45 }}>
Hello from SquareMCP!
</div>
{/* Timestamp + checks */}
<div
style={{
position: "absolute",
bottom: 6,
right: 10,
display: "flex",
alignItems: "center",
gap: 4,
}}
>
<div style={{ fontFamily: FONT, color: "rgba(255,255,255,0.5)", fontSize: 11 }}>9:41 AM</div>
{/* Double check marks */}
<div style={{ opacity: checkIn, display: "flex" }}>
<svg width="16" height="11" viewBox="0 0 16 11" fill="none">
<path d="M1 5.5L5 9.5L10 1" stroke={WA_GREEN} strokeWidth="1.8" strokeLinecap="round" strokeLinejoin="round" />
<path d="M6 5.5L10 9.5L15 1" stroke={WA_GREEN} strokeWidth="1.8" strokeLinecap="round" strokeLinejoin="round" />
</svg>
</div>
</div>
</div>
</div>
)}
</div>
{/* Input bar */}
<div
style={{
background: WA_HEADER,
padding: "8px 12px",
display: "flex",
alignItems: "center",
gap: 8,
}}
>
<div
style={{
flex: 1,
background: "#2a3942",
borderRadius: 24,
padding: "8px 16px",
fontFamily: FONT,
color: "rgba(255,255,255,0.3)",
fontSize: 14,
}}
>
Message
</div>
<div
style={{
width: 38,
height: 38,
borderRadius: 999,
background: WA_GREEN,
display: "flex",
alignItems: "center",
justifyContent: "center",
}}
>
<svg width="18" height="18" viewBox="0 0 24 24" fill="white">
<path d="M2 21l21-9L2 3v7l15 2-15 2v7z" />
</svg>
</div>
</div>
</div>
</div>
);
};
// ── Divider ───────────────────────────────────────────────────────────────────
const Divider = () => (
<div
style={{
width: 1,
alignSelf: "stretch",
background: "linear-gradient(to bottom, transparent, rgba(37,211,102,0.25), transparent)",
margin: "40px 0",
}}
/>
);
// ── Split screen scene ────────────────────────────────────────────────────────
export const WhatsAppSplitScreen = () => {
const frame = useCurrentFrame();
const { fps } = useVideoConfig();
return (
<AbsoluteFill style={{ display: "flex", flexDirection: "row", alignItems: "stretch" }}>
<LeftPanel frame={frame} fps={fps} />
<Divider />
<RightPanel frame={frame} fps={fps} />
</AbsoluteFill>
);
};