feat(remotion): WhatsApp template management demo video for Meta app review
16s landscape (1920x1080): shows SquareMCP chat prompt triggering an animated cURL call to the Business Management API creating a message template (HEADER/BODY/FOOTER components with variables), then the right panel renders a WhatsApp phone UI previewing the template bubble with variable placeholders highlighted and a PENDING status badge. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
BIN
product/site/whatsapp-template-demo-video.mp4
Normal file
BIN
product/site/whatsapp-template-demo-video.mp4
Normal file
Binary file not shown.
@@ -12,6 +12,7 @@ import {
|
||||
SquareMCPTikTokProof,
|
||||
} from "./SquareMCPTikTok";
|
||||
import { SquareMCPWhatsApp } from "./SquareMCPWhatsApp";
|
||||
import { SquareMCPWhatsAppTemplate } from "./SquareMCPWhatsAppTemplate";
|
||||
|
||||
export const RemotionRoot = () => {
|
||||
return (
|
||||
@@ -96,6 +97,14 @@ export const RemotionRoot = () => {
|
||||
width={1920}
|
||||
height={1080}
|
||||
/>
|
||||
<Composition
|
||||
id="SquareMCPWhatsAppTemplate"
|
||||
component={SquareMCPWhatsAppTemplate}
|
||||
durationInFrames={16 * 30}
|
||||
fps={30}
|
||||
width={1920}
|
||||
height={1080}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
44
videos/remotion-demo/src/SquareMCPWhatsAppTemplate.tsx
Normal file
44
videos/remotion-demo/src/SquareMCPWhatsAppTemplate.tsx
Normal file
@@ -0,0 +1,44 @@
|
||||
import { AbsoluteFill, Sequence, useVideoConfig } from "remotion";
|
||||
import { WhatsAppBackground } from "./scenes/whatsapp/WhatsAppBackground";
|
||||
import { WhatsAppTemplateScreen } from "./scenes/whatsapp/WhatsAppTemplateScreen";
|
||||
import { FONT, SPRING_CFG } from "./styles";
|
||||
import { spring, useCurrentFrame } from "remotion";
|
||||
|
||||
const WA_GREEN = "#25D366";
|
||||
|
||||
const Intro = () => {
|
||||
const frame = useCurrentFrame();
|
||||
const { fps } = useVideoConfig();
|
||||
const logoIn = spring({ fps, frame, config: SPRING_CFG });
|
||||
const textIn = spring({ fps, frame: Math.max(0, frame - 18), config: SPRING_CFG });
|
||||
const subIn = spring({ fps, frame: Math.max(0, frame - 36), config: SPRING_CFG });
|
||||
|
||||
return (
|
||||
<AbsoluteFill style={{ alignItems: "center", justifyContent: "center", display: "flex", flexDirection: "column", gap: 28 }}>
|
||||
<div style={{ opacity: logoIn, transform: `scale(${0.7 + logoIn * 0.3})`, fontSize: 80 }}>📋</div>
|
||||
<div style={{ fontFamily: FONT, color: "#ffffff", fontSize: 60, fontWeight: 800, textAlign: "center", lineHeight: 1.1, opacity: textIn, transform: `translateY(${(1 - textIn) * 36}px)` }}>
|
||||
WhatsApp Template Management
|
||||
</div>
|
||||
<div style={{ fontFamily: FONT, color: WA_GREEN, fontSize: 30, fontWeight: 600, opacity: subIn, transform: `translateY(${(1 - subIn) * 20}px)` }}>
|
||||
Creating message templates via the Business API
|
||||
</div>
|
||||
</AbsoluteFill>
|
||||
);
|
||||
};
|
||||
|
||||
// Total: 3s intro + 13s template screen = 16s @ 30fps = 480 frames
|
||||
export const SquareMCPWhatsAppTemplate = () => {
|
||||
const { fps } = useVideoConfig();
|
||||
|
||||
return (
|
||||
<AbsoluteFill>
|
||||
<WhatsAppBackground />
|
||||
<Sequence durationInFrames={3 * fps}>
|
||||
<Intro />
|
||||
</Sequence>
|
||||
<Sequence from={3 * fps}>
|
||||
<WhatsAppTemplateScreen />
|
||||
</Sequence>
|
||||
</AbsoluteFill>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,348 @@
|
||||
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_HEADER = "#202c33";
|
||||
|
||||
const TEMPLATE_REQUEST_LINES = [
|
||||
"curl -X POST \\",
|
||||
" https://graph.facebook.com/v18.0/ \\",
|
||||
" {BUSINESS_ACCOUNT_ID}/message_templates \\",
|
||||
" -H 'Authorization: Bearer {TOKEN}' \\",
|
||||
" -H 'Content-Type: application/json' \\",
|
||||
" -d '{",
|
||||
' "name": "pilot_request_alert",',
|
||||
' "language": "en_US",',
|
||||
' "category": "MARKETING",',
|
||||
' "components": [',
|
||||
' { "type": "HEADER", "format": "TEXT",',
|
||||
' "text": "New Pilot Request 🚀" },',
|
||||
' { "type": "BODY",',
|
||||
' "text": "{{1}} from {{2}} wants to connect.\\nUse case: {{3}}" },',
|
||||
' { "type": "FOOTER",',
|
||||
' "text": "Sent via SquareMCP" }',
|
||||
" ]",
|
||||
" }'",
|
||||
];
|
||||
|
||||
const RESPONSE_LINES = [
|
||||
"{",
|
||||
' "id": "1234567890123456",',
|
||||
' "status": "PENDING",',
|
||||
' "category": "MARKETING"',
|
||||
"}",
|
||||
];
|
||||
|
||||
// ── Left: API call panel ──────────────────────────────────────────────────────
|
||||
|
||||
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 });
|
||||
|
||||
const reqProgress = interpolate(frame, [40, 170], [0, TEMPLATE_REQUEST_LINES.length], { extrapolateRight: "clamp" });
|
||||
const resProgress = interpolate(frame, [185, 230], [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)`,
|
||||
}}
|
||||
>
|
||||
<div style={{ fontFamily: FONT, color: "rgba(255,255,255,0.4)", fontSize: 20, fontWeight: 600, letterSpacing: 2, textTransform: "uppercase" }}>
|
||||
SquareMCP — Creating message template
|
||||
</div>
|
||||
|
||||
{/* Chat prompt */}
|
||||
<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%",
|
||||
}}
|
||||
>
|
||||
Create a WhatsApp message template<br />
|
||||
for new pilot request alerts.
|
||||
</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)`,
|
||||
overflow: "hidden",
|
||||
}}
|
||||
>
|
||||
<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>
|
||||
|
||||
<div style={{ fontFamily: "ui-monospace, monospace", color: "#7ee787", fontSize: 15, marginBottom: 4 }}>$</div>
|
||||
|
||||
{TEMPLATE_REQUEST_LINES.slice(0, Math.ceil(reqProgress)).map((line, i) => (
|
||||
<div
|
||||
key={i}
|
||||
style={{
|
||||
fontFamily: "ui-monospace, monospace",
|
||||
color: line.includes('"type"') || line.includes('"text"') || line.includes('"format"') || line.includes('"category"') || line.includes('"name"') || line.includes('"language"')
|
||||
? "#79c0ff"
|
||||
: line.includes("HEADER") || line.includes("BODY") || line.includes("FOOTER") || line.includes("MARKETING")
|
||||
? "#ffa657"
|
||||
: "#e6edf3",
|
||||
fontSize: 14,
|
||||
lineHeight: 1.55,
|
||||
opacity: i < reqProgress - 1 ? 1 : reqProgress - i,
|
||||
}}
|
||||
>
|
||||
{line}
|
||||
</div>
|
||||
))}
|
||||
|
||||
{resProgress > 0 && (
|
||||
<div style={{ marginTop: 10 }}>
|
||||
<div style={{ fontFamily: "ui-monospace, monospace", color: WA_GREEN, fontSize: 13, marginBottom: 3 }}>
|
||||
# Response (200 OK) — template submitted for review
|
||||
</div>
|
||||
{RESPONSE_LINES.slice(0, Math.ceil(resProgress)).map((line, i) => (
|
||||
<div
|
||||
key={i}
|
||||
style={{
|
||||
fontFamily: "ui-monospace, monospace",
|
||||
color: line.includes("PENDING") ? "#ffa657" : line.includes("id") ? WA_GREEN : "#e6edf3",
|
||||
fontSize: 14,
|
||||
lineHeight: 1.55,
|
||||
opacity: i < resProgress - 1 ? 1 : resProgress - i,
|
||||
}}
|
||||
>
|
||||
{line}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
// ── Right: template preview panel ────────────────────────────────────────────
|
||||
|
||||
const RightPanel = ({ frame, fps }: { frame: number; fps: number }) => {
|
||||
const panelIn = spring({ fps, frame, config: SPRING_SOFT });
|
||||
const labelIn = spring({ fps, frame: Math.max(0, frame - 15), config: SPRING_CFG });
|
||||
const headerIn = spring({ fps, frame: Math.max(0, frame - 220), config: SPRING_CFG });
|
||||
const bodyIn = spring({ fps, frame: Math.max(0, frame - 245), config: SPRING_CFG });
|
||||
const footerIn = spring({ fps, frame: Math.max(0, frame - 265), config: SPRING_CFG });
|
||||
const badgeIn = spring({ fps, frame: Math.max(0, frame - 290), config: SPRING_CFG });
|
||||
|
||||
return (
|
||||
<div
|
||||
style={{
|
||||
width: 460,
|
||||
padding: "40px 48px 40px 24px",
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
gap: 24,
|
||||
opacity: panelIn,
|
||||
transform: `translateX(${(1 - panelIn) * 60}px)`,
|
||||
}}
|
||||
>
|
||||
<div
|
||||
style={{
|
||||
fontFamily: FONT,
|
||||
color: "rgba(255,255,255,0.4)",
|
||||
fontSize: 20,
|
||||
fontWeight: 600,
|
||||
letterSpacing: 2,
|
||||
textTransform: "uppercase",
|
||||
opacity: labelIn,
|
||||
}}
|
||||
>
|
||||
Template Preview
|
||||
</div>
|
||||
|
||||
{/* Phone showing template */}
|
||||
<div
|
||||
style={{
|
||||
borderRadius: 28,
|
||||
border: "6px solid #2a2a2a",
|
||||
background: WA_DARK,
|
||||
overflow: "hidden",
|
||||
boxShadow: "0 32px 80px rgba(0,0,0,0.7)",
|
||||
flex: 1,
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
}}
|
||||
>
|
||||
{/* Header bar */}
|
||||
<div style={{ background: WA_HEADER, padding: "12px 16px", display: "flex", alignItems: "center", gap: 10 }}>
|
||||
<div style={{ width: 32, height: 32, borderRadius: 999, background: WA_GREEN, display: "flex", alignItems: "center", justifyContent: "center" }}>
|
||||
<div style={{ fontFamily: FONT, color: "white", fontSize: 14, fontWeight: 700 }}>S</div>
|
||||
</div>
|
||||
<div>
|
||||
<div style={{ fontFamily: FONT, color: "white", fontSize: 13, fontWeight: 600 }}>SquareMCP</div>
|
||||
<div style={{ fontFamily: FONT, color: "rgba(255,255,255,0.45)", fontSize: 11 }}>Business Account</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Chat body */}
|
||||
<div style={{ flex: 1, background: "#0b141a", padding: "16px 12px", display: "flex", flexDirection: "column", justifyContent: "flex-end", gap: 8 }}>
|
||||
|
||||
{/* Template message bubble */}
|
||||
<div style={{ alignSelf: "flex-end", maxWidth: "90%" }}>
|
||||
{/* Header component */}
|
||||
<div
|
||||
style={{
|
||||
background: "#005c4b",
|
||||
borderRadius: "16px 16px 0 0",
|
||||
padding: "10px 14px 8px",
|
||||
opacity: headerIn,
|
||||
transform: `translateY(${(1 - headerIn) * 20}px)`,
|
||||
}}
|
||||
>
|
||||
<div style={{ fontFamily: FONT, color: "rgba(255,255,255,0.55)", fontSize: 10, marginBottom: 4, textTransform: "uppercase", letterSpacing: 1 }}>
|
||||
Header
|
||||
</div>
|
||||
<div style={{ fontFamily: FONT, color: "white", fontSize: 15, fontWeight: 700 }}>
|
||||
New Pilot Request 🚀
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Body component */}
|
||||
<div
|
||||
style={{
|
||||
background: "#005c4b",
|
||||
padding: "8px 14px",
|
||||
borderTop: "1px solid rgba(255,255,255,0.08)",
|
||||
opacity: bodyIn,
|
||||
transform: `translateY(${(1 - bodyIn) * 16}px)`,
|
||||
}}
|
||||
>
|
||||
<div style={{ fontFamily: FONT, color: "rgba(255,255,255,0.55)", fontSize: 10, marginBottom: 4, textTransform: "uppercase", letterSpacing: 1 }}>
|
||||
Body
|
||||
</div>
|
||||
<div style={{ fontFamily: FONT, color: "white", fontSize: 14, lineHeight: 1.5 }}>
|
||||
<span style={{ background: "rgba(37,211,102,0.2)", borderRadius: 4, padding: "0 4px" }}>{"{{1}}"}</span>
|
||||
{" from "}
|
||||
<span style={{ background: "rgba(37,211,102,0.2)", borderRadius: 4, padding: "0 4px" }}>{"{{2}}"}</span>
|
||||
{" wants to connect.\nUse case: "}
|
||||
<span style={{ background: "rgba(37,211,102,0.2)", borderRadius: 4, padding: "0 4px" }}>{"{{3}}"}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Footer component */}
|
||||
<div
|
||||
style={{
|
||||
background: "#005c4b",
|
||||
borderRadius: "0 0 4px 16px",
|
||||
padding: "6px 14px 20px",
|
||||
borderTop: "1px solid rgba(255,255,255,0.08)",
|
||||
position: "relative",
|
||||
opacity: footerIn,
|
||||
transform: `translateY(${(1 - footerIn) * 12}px)`,
|
||||
}}
|
||||
>
|
||||
<div style={{ fontFamily: FONT, color: "rgba(255,255,255,0.55)", fontSize: 10, marginBottom: 3, textTransform: "uppercase", letterSpacing: 1 }}>
|
||||
Footer
|
||||
</div>
|
||||
<div style={{ fontFamily: FONT, color: "rgba(255,255,255,0.5)", fontSize: 12 }}>
|
||||
Sent via SquareMCP
|
||||
</div>
|
||||
<div style={{ position: "absolute", bottom: 6, right: 10, fontFamily: FONT, color: "rgba(255,255,255,0.4)", fontSize: 11 }}>
|
||||
9:41 AM
|
||||
</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: 13 }}>
|
||||
Message
|
||||
</div>
|
||||
<div style={{ width: 36, height: 36, borderRadius: 999, background: WA_GREEN, display: "flex", alignItems: "center", justifyContent: "center" }}>
|
||||
<svg width="16" height="16" viewBox="0 0 24 24" fill="white"><path d="M2 21l21-9L2 3v7l15 2-15 2v7z" /></svg>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Status badge */}
|
||||
<div
|
||||
style={{
|
||||
borderRadius: 16,
|
||||
border: "1px solid rgba(255,166,87,0.4)",
|
||||
background: "rgba(255,166,87,0.08)",
|
||||
padding: "14px 20px",
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
gap: 12,
|
||||
opacity: badgeIn,
|
||||
transform: `translateY(${(1 - badgeIn) * 16}px)`,
|
||||
}}
|
||||
>
|
||||
<div style={{ width: 10, height: 10, borderRadius: 999, background: "#ffa657", flexShrink: 0 }} />
|
||||
<div style={{ fontFamily: FONT, color: "#ffa657", fontSize: 14, fontWeight: 600 }}>
|
||||
Status: PENDING — submitted to Meta for review
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const Divider = () => (
|
||||
<div style={{ width: 1, alignSelf: "stretch", background: "linear-gradient(to bottom, transparent, rgba(37,211,102,0.25), transparent)", margin: "40px 0" }} />
|
||||
);
|
||||
|
||||
export const WhatsAppTemplateScreen = () => {
|
||||
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>
|
||||
);
|
||||
};
|
||||
Reference in New Issue
Block a user