@m13v/seo-components 0.38.3 → 0.38.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@m13v/seo-components",
3
- "version": "0.38.3",
3
+ "version": "0.38.4",
4
4
  "scripts": {
5
5
  "build:css": "tailwind -i src/_build.css -o dist/styles.css --minify",
6
6
  "lint:mobile-spans": "node scripts/lint-mobile-spans.mjs",
@@ -82,6 +82,21 @@ export interface InstallEmailGateProps {
82
82
  sentTitle?: string;
83
83
  /** Stage 2 (sent) body copy when emailOnly is true. Receives the submitted email. */
84
84
  sentDescription?: (email: string) => React.ReactNode;
85
+ /**
86
+ * Redirect-after-submit mode. When true, the submit POST is expected to
87
+ * return JSON `{ url: string }` (e.g. a Stripe Checkout session URL). On
88
+ * success the browser is redirected to that URL instead of advancing the
89
+ * modal to "command" or "sent". Used by paid-checkout flows where the
90
+ * email gate hands off to an external billing page. When set, takes
91
+ * precedence over `emailOnly`.
92
+ */
93
+ redirectOnSuccess?: boolean;
94
+ /**
95
+ * Extra fields merged into the submit POST body alongside `{ email }`.
96
+ * Useful for passing `section`, UTMs, or product identifiers to the
97
+ * checkout endpoint.
98
+ */
99
+ submitExtras?: Record<string, unknown>;
85
100
  }
86
101
 
87
102
  type Stage = "closed" | "email" | "command" | "sent";
@@ -108,6 +123,8 @@ export function InstallEmailGate({
108
123
  emailOnly = false,
109
124
  sentTitle = "Check your inbox",
110
125
  sentDescription,
126
+ redirectOnSuccess = false,
127
+ submitExtras,
111
128
  }: InstallEmailGateProps) {
112
129
  const capture = useCapture();
113
130
  const [stage, setStage] = useState<Stage>("closed");
@@ -166,10 +183,12 @@ export function InstallEmailGate({
166
183
  setSubmitting(true);
167
184
  setError("");
168
185
  try {
186
+ const body: Record<string, unknown> = { email: trimmed };
187
+ if (submitExtras) Object.assign(body, submitExtras);
169
188
  const res = await fetch(newsletterPath, {
170
189
  method: "POST",
171
190
  headers: { "Content-Type": "application/json" },
172
- body: JSON.stringify({ email: trimmed }),
191
+ body: JSON.stringify(body),
173
192
  });
174
193
  if (res.status >= 400 && res.status < 500) {
175
194
  const data = await res.json().catch(() => ({}));
@@ -178,9 +197,14 @@ export function InstallEmailGate({
178
197
  return;
179
198
  }
180
199
  if (!res.ok) {
181
- console.warn("[InstallEmailGate] newsletter POST failed", res.status);
200
+ console.warn("[InstallEmailGate] submit POST failed", res.status);
182
201
  }
183
- if (remember) markInstallEmailCaptured(trimmed, storageKey);
202
+ const delivery = redirectOnSuccess
203
+ ? "redirect"
204
+ : emailOnly
205
+ ? "email_only"
206
+ : "page_reveal";
207
+ if (remember && !redirectOnSuccess) markInstallEmailCaptured(trimmed, storageKey);
184
208
  capture("newsletter_subscribed", {
185
209
  component: "InstallEmailGate",
186
210
  email: trimmed,
@@ -188,25 +212,51 @@ export function InstallEmailGate({
188
212
  site,
189
213
  section,
190
214
  source: "install_gate",
191
- delivery: emailOnly ? "email_only" : "page_reveal",
215
+ delivery,
192
216
  });
193
217
  trackGetStartedClick({
194
- destination: emailOnly ? "email:install" : "modal:command",
218
+ destination: redirectOnSuccess
219
+ ? "redirect:checkout"
220
+ : emailOnly
221
+ ? "email:install"
222
+ : "modal:command",
195
223
  site,
196
224
  section,
197
225
  text: "email-submitted",
198
226
  component: "InstallEmailGate",
199
- extra: { email: trimmed, delivery: emailOnly ? "email_only" : "page_reveal" },
227
+ extra: { email: trimmed, delivery },
200
228
  });
229
+
230
+ if (redirectOnSuccess) {
231
+ const data = (await res.json().catch(() => ({}))) as { url?: string };
232
+ if (data.url && typeof window !== "undefined") {
233
+ window.location.href = data.url;
234
+ // Keep the modal in the submitting state while the browser navigates
235
+ // away so the user does not see a flash of stale UI.
236
+ return;
237
+ }
238
+ setError("No checkout URL returned. Try again.");
239
+ setSubmitting(false);
240
+ return;
241
+ }
242
+
201
243
  setSubmittedEmail(trimmed);
202
244
  setStage(emailOnly ? "sent" : "command");
203
245
  } catch (err) {
204
- console.warn("[InstallEmailGate] newsletter POST network error", err);
246
+ console.warn("[InstallEmailGate] submit POST network error", err);
247
+ if (redirectOnSuccess) {
248
+ setError("Network error. Try again.");
249
+ setSubmitting(false);
250
+ return;
251
+ }
205
252
  if (remember) markInstallEmailCaptured(trimmed, storageKey);
206
253
  setSubmittedEmail(trimmed);
207
254
  setStage(emailOnly ? "sent" : "command");
208
255
  } finally {
209
- setSubmitting(false);
256
+ // For redirect mode we returned early above on success; the browser
257
+ // is navigating away so we intentionally do not flip submitting back
258
+ // to false. For other modes, the stage transition is enough.
259
+ if (!redirectOnSuccess) setSubmitting(false);
210
260
  }
211
261
  };
212
262