SMS Code Review — Pre-Push Gate
Scoped to: web/js/src/Main/Sms/** and web/js/src/Main/SmsBulk/**
Run this before pushing or raising a PR for any SMS frontend changes. Output a PASS ✅ or FAIL ❌ verdict per category. If any category fails, the push should be blocked until fixed.
Scope options
The skill accepts an optional argument to narrow the diff scope:
| Invocation |
Scope |
/sms-review |
Both Sms/ and SmsBulk/ (default) |
/sms-review sms |
web/js/src/Main/Sms/** only |
/sms-review bulk |
web/js/src/Main/SmsBulk/** only |
Steps
1. Determine scope and get the diff
Check whether the user passed sms or bulk as an argument:
sms argument — diff only Sms/:
git diff origin/main -- "web/js/src/Main/Sms/**"
git diff --name-only origin/main -- "web/js/src/Main/Sms/**"
bulk argument — diff only SmsBulk/:
git diff origin/main -- "web/js/src/Main/SmsBulk/**"
git diff --name-only origin/main -- "web/js/src/Main/SmsBulk/**"
No argument (default) — diff both:
git diff origin/main -- "web/js/src/Main/Sms/**" "web/js/src/Main/SmsBulk/**"
git diff --name-only origin/main -- "web/js/src/Main/Sms/**" "web/js/src/Main/SmsBulk/**"
Read each changed file fully where necessary to verify the checks below.
2. Run all checks
For each category, output: category name, verdict (PASS ✅ / FAIL ❌), and specific file + line citations for any failures.
Check 1 — Feature Flag Wrapping
FAIL if any of the following are found:
- A flag check is commented out and replaced with a hardcoded
true or false:// ❌ Bypasses experiment entirely
shouldShowX: true, // MC_Flag::isOn(SOME_FLAG) ? $result : false
- A flag check lives at the call site rather than inside the component or function it gates. The gated component should own its own eligibility:
// ❌ Flag leaks to parent — parent shouldn't know about child's internals
{isOn(FLAGS.SOME_FLAG) && <MyComponent />}
// ✅ Component self-contains the guard
function MyComponent() {
if (!isOn(FLAGS.SOME_FLAG)) return null;
...
}
- A new UI feature or behaviour has no flag at all, but is not behind an existing entry-point gate. New unbounded features must be flagged.
- Variables instantiated for experiment/flag evaluation (e.g.
$experiment->shouldShow()) whose result is never actually used in the return value.
- Duplicate flag gating: the same flag checked both in a parent and inside the child component without a clear reason.
Check 2 — Exposed / Debug Code
FAIL if any of the following are found:
console.log, console.warn, console.error left in production code paths (not inside catch blocks or test files).
- Commented-out code blocks that contain real logic (not explanatory comments), especially commented-out flag checks or experiment guards:
// ❌ Real conditional left as comment
"shouldShow" => true, // MC_Flag::isOn(...) ? $result : false
- Hardcoded external URLs inline in JSX or component logic. URLs must be named constants or come from config:
// ❌
href="https://mailchimp.com/help/about-sms-marketing-credits/#..."
// ✅
const CREDITS_HELP_URL = 'https://mailchimp.com/help/about-sms-marketing-credits/...';
href={CREDITS_HELP_URL}
- Hardcoded API keys, auth tokens, or credentials anywhere in non-test code.
- Debug-only props (
debug={true}, verbose, trace) left on components in production paths.
TODO/FIXME comments that indicate incomplete or unsafe logic that must be resolved before shipping.
Check 3 — React Bad Practices
FAIL if any of the following are found:
Functions recreated every render: plain functions or objects defined inside a component body that have no dependency on props/state should be moved outside the component. Functions that do depend on state/props must be wrapped in useCallback or useMemo:
// ❌ Recreated on every render
function MyComp() {
const formatPhone = (n: string) => n.replace(...);
const parseDate = (d: unknown): number | null => { ... };
...
}
// ✅ Outside the component — no deps
const formatPhone = (n: string) => n.replace(...);
Magic number initial state: useState initialised with a raw number instead of a named constant:
useState(1) // ❌ what does 1 mean?
useState(BannerState.InitialHold) // ✅
IIFEs inside JSX: immediately-invoked function expressions in the render tree make JSX unreadable. Extract to a variable before return:
// ❌
<Text>{(() => { if (x) return 'a'; return 'b'; })()}</Text>
// ✅
const label = x ? 'a' : 'b';
<Text>{label}</Text>
Missing cleanup in async useEffect: any useEffect that fires an async operation (fetch, promise) must use a cancelled guard to prevent setState on unmounted components:
// ❌ — no cleanup
useEffect(() => {
fetchData().then((res) => setState(res));
}, []);
// ✅
useEffect(() => {
let cancelled = false;
fetchData().then((res) => { if (!cancelled) setState(res); });
return () => { cancelled = true; };
}, []);
Stale or unused deps in hooks: a variable listed in useEffect/useCallback/useMemo dependency arrays that is never read inside the hook body. Also flag deps that are missing but should be present.
Same hook called multiple times: if a hook like usePricingMessages or pricingFlaggedMessage is called more than once in the same component with overlapping parameters, the calls must be merged into one.
Derived state stored in useState: if a value can be computed from existing props or state without side effects, it must not be stored in useState. Use a regular variable or useMemo.
null vs undefined inequality inconsistency: use != null (loose) to guard both null and undefined. Using !== null (strict) misses the undefined case when a TypeScript type allows it:
// ❌ undefined passes through as truthy
const hasHold = phoneTimerSetDate !== null;
// ✅
const hasHold = phoneTimerSetDate != null;
Check 4 — Dangerous Linter / Type Disabling
FAIL if any of the following are found without an explanatory comment immediately above the line:
// eslint-disable (line, next-line, or block) — especially for security rules:
no-eval, no-script-url, react/no-danger
no-unused-expressions, eqeqeq
// @ts-ignore or // @ts-nocheck — type errors must be fixed, not suppressed. If a third-party type is wrong, use // @ts-expect-error with a description.
/* eslint-disable */ block-level disables with no scope comment explaining why.
Any disable that exists solely to silence a real problem (rather than a false positive) is a FAIL.
Check 5 — DRY (Don't Repeat Yourself)
FAIL if any of the following are found:
The same JSX block (>10 lines) appears more than once. Extract to a component or shared variable:
// ❌ — autoRefillContent JSX block duplicated in both branches
{showSection ? (
<div>{/* 100 lines */}</div>
) : (
<div>{/* same 100 lines */}</div>
)}
// ✅ — use the variable in both branches
const autoRefillContent = <div>...</div>;
{showSection ? <>{subscriptionCard}{autoRefillContent}</> : autoRefillContent}
The same computation appears twice under different variable names with the same inputs:
// ❌ — identical computation, different names
const selectedTierPriceNum = getTotalPrice(formattedPricing[selectedMonthlyTier]?.[currency?.id]);
const recurringTierPriceNum = getTotalPrice(formattedPricing[selectedMonthlyTier]?.[currency?.id]);
Magic strings used in multiple places instead of a shared constant (e.g. step names, event names, CSS class names used conditionally in multiple spots).
The same conditional expression copy-pasted in more than two locations — extract to a named boolean.
Check 6 — XSS, Semantics, Accessibility
FAIL if any of the following are found:
XSS:
dangerouslySetInnerHTML used without DOMPurify.sanitize() wrapping the value
innerHTML assigned directly without sanitisation
- User-controlled string concatenated into a URL
href without sanitisation — use DOMPurify.sanitize() or restrict to a safe allow-list of origins
eval() or new Function() called with any dynamic input
Semantic HTML:
<div> or <span> used where a semantic element is correct: <button>, <a>, <nav>, <main>, <header>, <section>, <article>, <aside>, <ul>/<ol>/<li>
- Click handlers attached to non-interactive elements (
div, span) without both role and a keyboard event handler (onKeyDown / onKeyUp covering Enter/Space)
<table> used for layout rather than tabular data
Accessibility (WCAG 2.2 Level AA):
- Interactive elements (buttons, links, inputs, toggles) missing an accessible label:
aria-label, aria-labelledby, or visible inner text
- Images missing
alt attributes (decorative images must have alt="")
tabIndex={-1} on a visually clickable element without an explanatory comment confirming intent
- Form inputs missing an associated
<label> (either wrapping or via htmlFor / id pairing)
hideLabel prop used on a Wink component without an accessible label alternative supplied via a separate prop (e.g. label on Select, ToggleButton)
- Colour or icon used as the sole indicator of state — must be paired with text or an
aria-* attribute
Check 7 — Custom CSS Bypassing MCDS
FAIL if any of the following are found:
Inline style={{ }} props that use raw colour hex values or pixel values that map to design tokens. Use @mc/wink components or CSS variables:
// ❌ Bypasses MCDS token
style={{ color: '#21262A' }}
style={{ color: '#C84F00', display: 'flex' }}
// ✅ Use MCDS component or token variable
style={{ color: 'var(--color-content-primary)' }}
Custom .css / .less rules that override MCDS component internals using deep selectors (e.g. .wink-button .inner { ... }).
New CSS class names for spacing, typography, or colour that duplicate values already available via MCDS tokens or utility classes.
!important declarations used to override MCDS styles.
3. Output format
## SMS Pre-Push Review
**Branch:** <branch-name>
**Scope:** Sms + SmsBulk / Sms only / SmsBulk only ← use whichever applies
**Files changed in scope:** <count>
| # | Check | Verdict |
|---|-------|---------|
| 1 | Feature Flag Wrapping | ✅ PASS / ❌ FAIL |
| 2 | Exposed / Debug Code | ✅ PASS / ❌ FAIL |
| 3 | React Bad Practices | ✅ PASS / ❌ FAIL |
| 4 | Linter Disabling | ✅ PASS / ❌ FAIL |
| 5 | DRY | ✅ PASS / ❌ FAIL |
| 6 | XSS / Semantics / A11y | ✅ PASS / ❌ FAIL |
| 7 | Custom CSS / MCDS | ✅ PASS / ❌ FAIL |
### Failures
For each ❌: list the file, line number, a one-line description, and a concise code fix.
### Verdict
**PUSH BLOCKED** — fix all ❌ failures before pushing.
— or —
**CLEAR TO PUSH** — no blocking failures found.
Notes
- Files in
**/__tests__/, **.test.ts, **.test.tsx, **.test.js are excluded from checks 2, 3, and 7.
- NLS translation files (
data/nls/**) are always skipped.
- If no SMS/SmsBulk files are changed in the diff, output: "No SMS/SmsBulk files changed — review not required."
1---2name: sms-review3description: Pre-push code review for SMS and SmsBulk changes. Checks feature flag wrapping, exposed/debug code, React bad practices, dangerous linter disabling, DRY violations, and MCDS CSS compliance. Scoped to web/js/src/Main/Sms and web/js/src/Main/SmsBulk. Use before pushing or creating a PR for any SMS-related frontend changes, or whenever the user says "review my SMS changes", "check my SMS code", or asks if their SMS/SmsBulk changes are ready to push.4---5# SMS Code Review — Pre-Push Gate67Scoped to: `web/js/src/Main/Sms/**` and `web/js/src/Main/SmsBulk/**`89Run this before pushing or raising a PR for any SMS frontend changes. Output a **PASS ✅** or **FAIL ❌** verdict per category. If any category fails, the push should be blocked until fixed.1011## Scope options1213The skill accepts an optional argument to narrow the diff scope:1415| Invocation | Scope |16|---|---|17| `/sms-review` | Both `Sms/` and `SmsBulk/` (default) |18| `/sms-review sms` | `web/js/src/Main/Sms/**` only |19| `/sms-review bulk` | `web/js/src/Main/SmsBulk/**` only |2021---2223## Steps2425### 1. Determine scope and get the diff2627Check whether the user passed `sms` or `bulk` as an argument:2829- **`sms` argument** — diff only `Sms/`:30 ```bash31 git diff origin/main -- "web/js/src/Main/Sms/**"32 git diff --name-only origin/main -- "web/js/src/Main/Sms/**"33 ```3435- **`bulk` argument** — diff only `SmsBulk/`:36 ```bash37 git diff origin/main -- "web/js/src/Main/SmsBulk/**"38 git diff --name-only origin/main -- "web/js/src/Main/SmsBulk/**"39 ```4041- **No argument (default)** — diff both:42 ```bash43 git diff origin/main -- "web/js/src/Main/Sms/**" "web/js/src/Main/SmsBulk/**"44 git diff --name-only origin/main -- "web/js/src/Main/Sms/**" "web/js/src/Main/SmsBulk/**"45 ```4647Read each changed file fully where necessary to verify the checks below.4849---5051### 2. Run all checks5253For each category, output: category name, verdict (PASS ✅ / FAIL ❌), and specific file + line citations for any failures.5455---5657## Check 1 — Feature Flag Wrapping5859**FAIL if any of the following are found:**6061- A flag check is commented out and replaced with a hardcoded `true` or `false`:62 ```js63 // ❌ Bypasses experiment entirely64 shouldShowX: true, // MC_Flag::isOn(SOME_FLAG) ? $result : false65 ```66- A flag check lives at the **call site** rather than inside the component or function it gates. The gated component should own its own eligibility:67 ```jsx68 // ❌ Flag leaks to parent — parent shouldn't know about child's internals69 {isOn(FLAGS.SOME_FLAG) && <MyComponent />}7071 // ✅ Component self-contains the guard72 function MyComponent() {73 if (!isOn(FLAGS.SOME_FLAG)) return null;74 ...75 }76 ```77- A new UI feature or behaviour has no flag at all, but is not behind an existing entry-point gate. New unbounded features must be flagged.78- Variables instantiated for experiment/flag evaluation (e.g. `$experiment->shouldShow()`) whose result is never actually used in the return value.79- Duplicate flag gating: the same flag checked both in a parent and inside the child component without a clear reason.8081---8283## Check 2 — Exposed / Debug Code8485**FAIL if any of the following are found:**8687- `console.log`, `console.warn`, `console.error` left in production code paths (not inside `catch` blocks or test files).88- Commented-out code blocks that contain real logic (not explanatory comments), especially commented-out flag checks or experiment guards:89 ```php90 // ❌ Real conditional left as comment91 "shouldShow" => true, // MC_Flag::isOn(...) ? $result : false92 ```93- Hardcoded external URLs inline in JSX or component logic. URLs must be named constants or come from config:94 ```jsx95 // ❌96 href="https://mailchimp.com/help/about-sms-marketing-credits/#..."9798 // ✅99 const CREDITS_HELP_URL = 'https://mailchimp.com/help/about-sms-marketing-credits/...';100 href={CREDITS_HELP_URL}101 ```102- Hardcoded API keys, auth tokens, or credentials anywhere in non-test code.103- Debug-only props (`debug={true}`, `verbose`, `trace`) left on components in production paths.104- `TODO`/`FIXME` comments that indicate incomplete or unsafe logic that must be resolved before shipping.105106---107108## Check 3 — React Bad Practices109110**FAIL if any of the following are found:**111112- **Functions recreated every render**: plain functions or objects defined inside a component body that have no dependency on props/state should be moved outside the component. Functions that do depend on state/props must be wrapped in `useCallback` or `useMemo`:113 ```tsx114 // ❌ Recreated on every render115 function MyComp() {116 const formatPhone = (n: string) => n.replace(...);117 const parseDate = (d: unknown): number | null => { ... };118 ...119 }120121 // ✅ Outside the component — no deps122 const formatPhone = (n: string) => n.replace(...);123 ```124125- **Magic number initial state**: `useState` initialised with a raw number instead of a named constant:126 ```tsx127 useState(1) // ❌ what does 1 mean?128 useState(BannerState.InitialHold) // ✅129 ```130131- **IIFEs inside JSX**: immediately-invoked function expressions in the render tree make JSX unreadable. Extract to a variable before `return`:132 ```tsx133 // ❌134 <Text>{(() => { if (x) return 'a'; return 'b'; })()}</Text>135136 // ✅137 const label = x ? 'a' : 'b';138 <Text>{label}</Text>139 ```140141- **Missing cleanup in async `useEffect`**: any `useEffect` that fires an async operation (fetch, promise) must use a `cancelled` guard to prevent setState on unmounted components:142 ```tsx143 // ❌ — no cleanup144 useEffect(() => {145 fetchData().then((res) => setState(res));146 }, []);147148 // ✅149 useEffect(() => {150 let cancelled = false;151 fetchData().then((res) => { if (!cancelled) setState(res); });152 return () => { cancelled = true; };153 }, []);154 ```155156- **Stale or unused deps in hooks**: a variable listed in `useEffect`/`useCallback`/`useMemo` dependency arrays that is never read inside the hook body. Also flag deps that are missing but should be present.157158- **Same hook called multiple times**: if a hook like `usePricingMessages` or `pricingFlaggedMessage` is called more than once in the same component with overlapping parameters, the calls must be merged into one.159160- **Derived state stored in `useState`**: if a value can be computed from existing props or state without side effects, it must not be stored in `useState`. Use a regular variable or `useMemo`.161162- **`null` vs `undefined` inequality inconsistency**: use `!= null` (loose) to guard both `null` and `undefined`. Using `!== null` (strict) misses the `undefined` case when a TypeScript type allows it:163 ```ts164 // ❌ undefined passes through as truthy165 const hasHold = phoneTimerSetDate !== null;166167 // ✅168 const hasHold = phoneTimerSetDate != null;169 ```170171---172173## Check 4 — Dangerous Linter / Type Disabling174175**FAIL if any of the following are found without an explanatory comment immediately above the line:**176177- `// eslint-disable` (line, next-line, or block) — especially for security rules:178 - `no-eval`, `no-script-url`, `react/no-danger`179 - `no-unused-expressions`, `eqeqeq`180181- `// @ts-ignore` or `// @ts-nocheck` — type errors must be fixed, not suppressed. If a third-party type is wrong, use `// @ts-expect-error` with a description.182183- `/* eslint-disable */` block-level disables with no scope comment explaining why.184185Any disable that exists solely to silence a real problem (rather than a false positive) is a FAIL.186187---188189## Check 5 — DRY (Don't Repeat Yourself)190191**FAIL if any of the following are found:**192193- The same JSX block (>10 lines) appears more than once. Extract to a component or shared variable:194 ```tsx195 // ❌ — autoRefillContent JSX block duplicated in both branches196 {showSection ? (197 <div>{/* 100 lines */}</div>198 ) : (199 <div>{/* same 100 lines */}</div>200 )}201202 // ✅ — use the variable in both branches203 const autoRefillContent = <div>...</div>;204 {showSection ? <>{subscriptionCard}{autoRefillContent}</> : autoRefillContent}205 ```206207- The same computation appears twice under different variable names with the same inputs:208 ```tsx209 // ❌ — identical computation, different names210 const selectedTierPriceNum = getTotalPrice(formattedPricing[selectedMonthlyTier]?.[currency?.id]);211 const recurringTierPriceNum = getTotalPrice(formattedPricing[selectedMonthlyTier]?.[currency?.id]);212 ```213214- Magic strings used in multiple places instead of a shared constant (e.g. step names, event names, CSS class names used conditionally in multiple spots).215216- The same conditional expression copy-pasted in more than two locations — extract to a named boolean.217218---219220## Check 6 — XSS, Semantics, Accessibility221222**FAIL if any of the following are found:**223224**XSS:**225- `dangerouslySetInnerHTML` used without `DOMPurify.sanitize()` wrapping the value226- `innerHTML` assigned directly without sanitisation227- User-controlled string concatenated into a URL `href` without sanitisation — use `DOMPurify.sanitize()` or restrict to a safe allow-list of origins228- `eval()` or `new Function()` called with any dynamic input229230**Semantic HTML:**231- `<div>` or `<span>` used where a semantic element is correct: `<button>`, `<a>`, `<nav>`, `<main>`, `<header>`, `<section>`, `<article>`, `<aside>`, `<ul>`/`<ol>`/`<li>`232- Click handlers attached to non-interactive elements (`div`, `span`) without both `role` and a keyboard event handler (`onKeyDown` / `onKeyUp` covering Enter/Space)233- `<table>` used for layout rather than tabular data234235**Accessibility (WCAG 2.2 Level AA):**236- Interactive elements (buttons, links, inputs, toggles) missing an accessible label: `aria-label`, `aria-labelledby`, or visible inner text237- Images missing `alt` attributes (decorative images must have `alt=""`)238- `tabIndex={-1}` on a visually clickable element without an explanatory comment confirming intent239- Form inputs missing an associated `<label>` (either wrapping or via `htmlFor` / `id` pairing)240- `hideLabel` prop used on a Wink component without an accessible label alternative supplied via a separate prop (e.g. `label` on `Select`, `ToggleButton`)241- Colour or icon used as the sole indicator of state — must be paired with text or an `aria-*` attribute242243---244245## Check 7 — Custom CSS Bypassing MCDS246247**FAIL if any of the following are found:**248249- Inline `style={{ }}` props that use raw colour hex values or pixel values that map to design tokens. Use `@mc/wink` components or CSS variables:250 ```tsx251 // ❌ Bypasses MCDS token252 style={{ color: '#21262A' }}253 style={{ color: '#C84F00', display: 'flex' }}254255 // ✅ Use MCDS component or token variable256 style={{ color: 'var(--color-content-primary)' }}257 ```258259- Custom `.css` / `.less` rules that override MCDS component internals using deep selectors (e.g. `.wink-button .inner { ... }`).260261- New CSS class names for spacing, typography, or colour that duplicate values already available via MCDS tokens or utility classes.262263- `!important` declarations used to override MCDS styles.264265---266267## 3. Output format268269```270## SMS Pre-Push Review271272**Branch:** <branch-name>273**Scope:** Sms + SmsBulk / Sms only / SmsBulk only ← use whichever applies274**Files changed in scope:** <count>275276| # | Check | Verdict |277|---|-------|---------|278| 1 | Feature Flag Wrapping | ✅ PASS / ❌ FAIL |279| 2 | Exposed / Debug Code | ✅ PASS / ❌ FAIL |280| 3 | React Bad Practices | ✅ PASS / ❌ FAIL |281| 4 | Linter Disabling | ✅ PASS / ❌ FAIL |282| 5 | DRY | ✅ PASS / ❌ FAIL |283| 6 | XSS / Semantics / A11y | ✅ PASS / ❌ FAIL |284| 7 | Custom CSS / MCDS | ✅ PASS / ❌ FAIL |285286### Failures287288For each ❌: list the file, line number, a one-line description, and a concise code fix.289290### Verdict291292**PUSH BLOCKED** — fix all ❌ failures before pushing.293— or —294**CLEAR TO PUSH** — no blocking failures found.295```296297---298299## Notes300301- Files in `**/__tests__/`, `**.test.ts`, `**.test.tsx`, `**.test.js` are excluded from checks 2, 3, and 7.302- NLS translation files (`data/nls/**`) are always skipped.303- If no SMS/SmsBulk files are changed in the diff, output: "No SMS/SmsBulk files changed — review not required."