@funnelsgrove/cli 0.1.87 → 0.1.89

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.
Files changed (121) hide show
  1. package/dist/apiClient.d.ts +1 -0
  2. package/dist/apiClient.js +32 -1
  3. package/funnel-contract-compatibility.json +28 -28
  4. package/package.json +3 -3
  5. package/template_docs/.funnelsgrove-docs.json +3 -3
  6. package/template_docs/docs/funnelsgrove/migrations/step-contract-v3.md +1 -1
  7. package/template_docs/funnel-docs.config.json +1 -1
  8. package/template_scaffold/.funnelsgrove-docs.json +3 -3
  9. package/template_scaffold/.funnelsgrove-scaffold.json +26 -471
  10. package/template_scaffold/PLAN.md +15 -29
  11. package/template_scaffold/context/build-context.md +3 -3
  12. package/template_scaffold/docs/funnelsgrove/migrations/step-contract-v3.md +1 -1
  13. package/template_scaffold/funnel-docs.config.json +1 -1
  14. package/template_scaffold/package-lock.json +9 -9
  15. package/template_scaffold/package.json +2 -2
  16. package/template_scaffold/public/assets/email-capture-preview.svg +5 -0
  17. package/template_scaffold/public/assets/paywall-preview.svg +6 -0
  18. package/template_scaffold/src/app/sdk/page.tsx +14 -26
  19. package/template_scaffold/src/components/FunnelFlow.tsx +8 -1
  20. package/template_scaffold/src/config/billing.plans.ts +71 -21
  21. package/template_scaffold/src/config/funnel.manifest.ts +20 -59
  22. package/template_scaffold/src/runtime/checkout-runtime-config.ts +0 -7
  23. package/template_scaffold/src/runtime/step-registry.ts +0 -28
  24. package/template_scaffold/src/steps/content/step-01.content.ts +4 -9
  25. package/template_scaffold/src/steps/content/step-32-paywall.content.ts +12 -50
  26. package/template_scaffold/src/steps/step-01.tsx +43 -50
  27. package/template_scaffold/src/steps/step-31-email-capture.tsx +18 -1
  28. package/template_scaffold/src/steps/step-32-paywall.tsx +93 -299
  29. package/template_scaffold/src/steps/step-33-subscription-started.tsx +1 -1
  30. package/template_scaffold/src/steps/step-content.registry.ts +1 -26
  31. package/template_scaffold/public/claimbee/bee.png +0 -0
  32. package/template_scaffold/public/claimbee/device-iphone.png +0 -0
  33. package/template_scaffold/public/claimbee/device-macbook.png +0 -0
  34. package/template_scaffold/public/claimbee/device-none.png +0 -0
  35. package/template_scaffold/public/claimbee/device-watch.png +0 -0
  36. package/template_scaffold/public/claimbee/glow.svg +0 -3
  37. package/template_scaffold/public/claimbee/steps-sprite-alt.png +0 -0
  38. package/template_scaffold/public/claimbee/steps-sprite-default.png +0 -0
  39. package/template_scaffold/public/claimbee/testimonial-avatar.png +0 -0
  40. package/template_scaffold/public/claimbee/testimonial-star.svg +0 -3
  41. package/template_scaffold/public/paywall/ai-claim-assistant.png +0 -0
  42. package/template_scaffold/public/paywall/claim-history.png +0 -0
  43. package/template_scaffold/public/paywall/clear-progress.png +0 -0
  44. package/template_scaffold/public/paywall/hero.png +0 -0
  45. package/template_scaffold/public/paywall/money-alerts.png +0 -0
  46. package/template_scaffold/public/paywall/money-back-badge.png +0 -0
  47. package/template_scaffold/public/paywall/paywall-image.png +0 -0
  48. package/template_scaffold/public/paywall/r1.png +0 -0
  49. package/template_scaffold/public/paywall/r2.png +0 -0
  50. package/template_scaffold/public/paywall/r3.png +0 -0
  51. package/template_scaffold/public/paywall/r4.png +0 -0
  52. package/template_scaffold/public/paywall/r5.png +0 -0
  53. package/template_scaffold/public/paywall/r6.png +0 -0
  54. package/template_scaffold/public/paywall/special-offer-gift.png +0 -0
  55. package/template_scaffold/public/please/bee.png +0 -0
  56. package/template_scaffold/public/please/device-iphone.png +0 -0
  57. package/template_scaffold/public/please/device-macbook.png +0 -0
  58. package/template_scaffold/public/please/device-none.png +0 -0
  59. package/template_scaffold/public/please/device-watch.png +0 -0
  60. package/template_scaffold/public/please/glow.svg +0 -3
  61. package/template_scaffold/public/please/steps-sprite-alt.png +0 -0
  62. package/template_scaffold/public/please/steps-sprite-default.png +0 -0
  63. package/template_scaffold/public/please/testimonial-avatar.png +0 -0
  64. package/template_scaffold/public/please/testimonial-star.svg +0 -3
  65. package/template_scaffold/public/upsell/upgrade-hero.png +0 -0
  66. package/template_scaffold/src/assets/claimbee/bee.png +0 -0
  67. package/template_scaffold/src/assets/claimbee/device-iphone.png +0 -0
  68. package/template_scaffold/src/assets/claimbee/device-macbook.png +0 -0
  69. package/template_scaffold/src/assets/claimbee/device-none.png +0 -0
  70. package/template_scaffold/src/assets/claimbee/device-watch.png +0 -0
  71. package/template_scaffold/src/assets/claimbee/glow.svg +0 -3
  72. package/template_scaffold/src/assets/claimbee/steps-sprite-alt.png +0 -0
  73. package/template_scaffold/src/assets/claimbee/steps-sprite-default.png +0 -0
  74. package/template_scaffold/src/assets/claimbee/testimonial-avatar.png +0 -0
  75. package/template_scaffold/src/assets/claimbee/testimonial-star.svg +0 -3
  76. package/template_scaffold/src/components/FunnelStepImage.tsx +0 -28
  77. package/template_scaffold/src/config/offer-sets.generated.ts +0 -4
  78. package/template_scaffold/src/steps/content/manage-subscription.content.ts +0 -62
  79. package/template_scaffold/src/steps/content/paywall.content.ts +0 -191
  80. package/template_scaffold/src/steps/content/step-02.content.ts +0 -77
  81. package/template_scaffold/src/steps/content/step-03.content.ts +0 -40
  82. package/template_scaffold/src/steps/content/step-04.content.ts +0 -49
  83. package/template_scaffold/src/steps/editor/manage-subscription.editor.ts +0 -179
  84. package/template_scaffold/src/steps/editor/paywall.editor.ts +0 -232
  85. package/template_scaffold/src/steps/editor/step-02.editor.ts +0 -41
  86. package/template_scaffold/src/steps/editor/step-03.editor.ts +0 -58
  87. package/template_scaffold/src/steps/editor/step-04.editor.ts +0 -98
  88. package/template_scaffold/src/steps/pricing/paywall-b.pricing.ts +0 -12
  89. package/template_scaffold/src/steps/step-02.tsx +0 -140
  90. package/template_scaffold/src/steps/step-03.tsx +0 -262
  91. package/template_scaffold/src/steps/step-04.tsx +0 -673
  92. package/template_scaffold/src/steps/step-32b-paywall-b.tsx +0 -35
  93. package/template_scaffold/src/steps/step-35-manage-subscription.tsx +0 -46
  94. package/template_scaffold/src/steps/use-claimbee-step-content.ts +0 -15
  95. package/template_scaffold/tests/funnel-agent-docs.test.ts +0 -751
  96. package/template_scaffold/tests/package.test.ts +0 -37
  97. package/template_scaffold/tests/src/app/[stepId]/page.test.tsx +0 -59
  98. package/template_scaffold/tests/src/app/layout.test.ts +0 -26
  99. package/template_scaffold/tests/src/components/FunnelFlow.test.tsx +0 -38
  100. package/template_scaffold/tests/src/components/FunnelStepImage.test.ts +0 -15
  101. package/template_scaffold/tests/src/config/billing.plans.test.ts +0 -143
  102. package/template_scaffold/tests/src/config/funnel.manifest.test.ts +0 -166
  103. package/template_scaffold/tests/src/contract/funnel-project.validation.test.ts +0 -79
  104. package/template_scaffold/tests/src/contract/golden/golden-fixtures.test.ts +0 -190
  105. package/template_scaffold/tests/src/contract/validate-funnel.cli.test.ts +0 -191
  106. package/template_scaffold/tests/src/runtime/checkout-runtime-config.test.ts +0 -60
  107. package/template_scaffold/tests/src/runtime/funnel-runtime.test.ts +0 -25
  108. package/template_scaffold/tests/src/runtime/step-content-context.test.ts +0 -38
  109. package/template_scaffold/tests/src/runtime/use-funnel-flow-controller.integration.test.ts +0 -203
  110. package/template_scaffold/tests/src/runtime/use-funnel-flow-controller.test.ts +0 -76
  111. package/template_scaffold/tests/src/steps/paywall-checkout-options.test.ts +0 -104
  112. package/template_scaffold/tests/src/steps/shared-header-contract.test.ts +0 -29
  113. package/template_scaffold/tests/src/steps/step-01.test.ts +0 -38
  114. package/template_scaffold/tests/src/steps/step-02.test.ts +0 -43
  115. package/template_scaffold/tests/src/steps/step-03.test.ts +0 -15
  116. package/template_scaffold/tests/src/steps/step-04.test.ts +0 -32
  117. package/template_scaffold/tests/src/steps/step-31-email-capture.test.ts +0 -22
  118. package/template_scaffold/tests/src/steps/step-32-paywall.test.ts +0 -773
  119. package/template_scaffold/tests/src/steps/step-33-subscription-started.test.ts +0 -44
  120. package/template_scaffold/tests/src/steps/step-34-upsell-form.test.ts +0 -27
  121. package/template_scaffold/tests/src/steps/step-image-migration.test.ts +0 -20
@@ -1,673 +0,0 @@
1
- import {
2
- useEffect,
3
- useRef,
4
- useState,
5
- type KeyboardEvent,
6
- type PointerEvent as ReactPointerEvent,
7
- } from 'react';
8
- import { FunnelStepHeader } from '@/components/step/FunnelStepHeader';
9
- import {
10
- usePreviewStepLocalizedContent,
11
- usePreviewVariableValues,
12
- useFunnel,
13
- type FunnelStepMeta,
14
- } from '@funnelsgrove/runtime';
15
- import { getStepContentLocale } from '@/runtime/step-content-context';
16
- import { stepFourContent } from '@/steps/content/step-04.content';
17
- import { stepFourEditor } from '@/steps/editor/step-04.editor';
18
-
19
- export const stepFourId = 'step-4';
20
- const scratchThreshold = 0.34;
21
- const scratchBrushRadius = 18;
22
- const rewardRevealDelayMs = 520;
23
-
24
- type ScratchPoint = {
25
- x: number;
26
- y: number;
27
- };
28
-
29
- const clamp = (value: number, min: number, max: number): number => {
30
- return Math.min(Math.max(value, min), max);
31
- };
32
-
33
- const drawScratchOverlay = (
34
- context: CanvasRenderingContext2D,
35
- width: number,
36
- height: number,
37
- ): void => {
38
- const gradient = context.createLinearGradient(0, 0, 0, height);
39
- gradient.addColorStop(0, '#ffd460');
40
- gradient.addColorStop(0.45, '#efbc41');
41
- gradient.addColorStop(1, '#cb8b3e');
42
- context.clearRect(0, 0, width, height);
43
- context.fillStyle = gradient;
44
- context.fillRect(0, 0, width, height);
45
-
46
- context.globalAlpha = 0.16;
47
- for (let index = 0; index < 1200; index += 1) {
48
- const x = Math.random() * width;
49
- const y = Math.random() * height;
50
- const size = 1 + Math.random() * 1.8;
51
- context.fillStyle = index % 2 === 0 ? '#fff5b3' : '#b36f18';
52
- context.fillRect(x, y, size, size);
53
- }
54
-
55
- context.globalAlpha = 0.12;
56
- context.strokeStyle = '#fff7d3';
57
- context.lineWidth = 1.5;
58
- for (let index = 0; index < 10; index += 1) {
59
- const y = ((index + 1) / 12) * height;
60
- context.beginPath();
61
- context.moveTo(18, y);
62
- context.bezierCurveTo(width * 0.3, y - 8, width * 0.7, y + 8, width - 18, y);
63
- context.stroke();
64
- }
65
-
66
- context.globalAlpha = 1;
67
- };
68
-
69
- const eraseScratch = (
70
- context: CanvasRenderingContext2D,
71
- fromPoint: ScratchPoint,
72
- toPoint: ScratchPoint,
73
- radius: number,
74
- ): void => {
75
- context.save();
76
- context.globalCompositeOperation = 'destination-out';
77
- context.strokeStyle = '#000000';
78
- context.lineCap = 'round';
79
- context.lineJoin = 'round';
80
- context.lineWidth = radius * 2;
81
- context.beginPath();
82
- context.moveTo(fromPoint.x, fromPoint.y);
83
- context.lineTo(toPoint.x, toPoint.y);
84
- context.stroke();
85
- context.restore();
86
- };
87
-
88
- const measureScratchProgress = (
89
- context: CanvasRenderingContext2D,
90
- width: number,
91
- height: number,
92
- ): number => {
93
- const imageData = context.getImageData(0, 0, width, height).data;
94
- const sampleStride = Math.max(4, Math.floor(width / 24));
95
- let transparentPixels = 0;
96
- let sampledPixels = 0;
97
-
98
- for (let y = 0; y < height; y += sampleStride) {
99
- for (let x = 0; x < width; x += sampleStride) {
100
- const alpha = imageData[(y * width + x) * 4 + 3];
101
- sampledPixels += 1;
102
- if (alpha < 32) {
103
- transparentPixels += 1;
104
- }
105
- }
106
- }
107
-
108
- if (sampledPixels === 0) {
109
- return 0;
110
- }
111
-
112
- return transparentPixels / sampledPixels;
113
- };
114
-
115
- export const stepFour: FunnelStepMeta = {
116
- id: stepFourId,
117
- name: 'scratchpad-entry',
118
- type: 'progress_interstitial',
119
- figmaNodeId: '7390-1456',
120
- title: 'Scratchcard Entry',
121
- description: 'Scratch-to-reveal discount step before the canonical paywall.',
122
- actionBar: {
123
- hidden: true,
124
- },
125
- };
126
-
127
- export function StepFour() {
128
- const { attributes, goNext } = useFunnel();
129
- const variableValues = {};
130
- usePreviewVariableValues(stepFourId, variableValues);
131
- const content = usePreviewStepLocalizedContent(
132
- stepFourId,
133
- stepFourContent,
134
- getStepContentLocale(attributes),
135
- {
136
- editorSections: stepFourEditor,
137
- variableValues,
138
- },
139
- );
140
- const canvasRef = useRef<HTMLCanvasElement | null>(null);
141
- const measurementFrameRef = useRef<number | null>(null);
142
- const rewardTimeoutRef = useRef<number | null>(null);
143
- const pointerActiveRef = useRef(false);
144
- const lastPointRef = useRef<ScratchPoint | null>(null);
145
- const [scratchProgress, setScratchProgress] = useState(0);
146
- const [scratchComplete, setScratchComplete] = useState(false);
147
- const [rewardVisible, setRewardVisible] = useState(false);
148
-
149
- useEffect(() => {
150
- const canvas = canvasRef.current;
151
- if (!canvas) {
152
- return;
153
- }
154
-
155
- const devicePixelRatio = window.devicePixelRatio || 1;
156
- const rect = canvas.getBoundingClientRect();
157
- const width = Math.max(1, Math.round(rect.width * devicePixelRatio));
158
- const height = Math.max(1, Math.round(rect.height * devicePixelRatio));
159
-
160
- canvas.width = width;
161
- canvas.height = height;
162
- const context = canvas.getContext('2d');
163
- if (!context) {
164
- return;
165
- }
166
-
167
- context.setTransform(1, 0, 0, 1, 0, 0);
168
- context.scale(devicePixelRatio, devicePixelRatio);
169
- drawScratchOverlay(context, rect.width, rect.height);
170
- pointerActiveRef.current = false;
171
- lastPointRef.current = null;
172
-
173
- return () => {
174
- if (measurementFrameRef.current !== null) {
175
- window.cancelAnimationFrame(measurementFrameRef.current);
176
- measurementFrameRef.current = null;
177
- }
178
- if (rewardTimeoutRef.current !== null) {
179
- window.clearTimeout(rewardTimeoutRef.current);
180
- rewardTimeoutRef.current = null;
181
- }
182
- };
183
- }, [content.ticket.code, content.ticket.offerLabel]);
184
-
185
- useEffect(() => {
186
- if (!scratchComplete || rewardVisible) {
187
- return;
188
- }
189
-
190
- rewardTimeoutRef.current = window.setTimeout(() => {
191
- setRewardVisible(true);
192
- rewardTimeoutRef.current = null;
193
- }, rewardRevealDelayMs);
194
-
195
- return () => {
196
- if (rewardTimeoutRef.current !== null) {
197
- window.clearTimeout(rewardTimeoutRef.current);
198
- rewardTimeoutRef.current = null;
199
- }
200
- };
201
- }, [rewardVisible, scratchComplete]);
202
-
203
- const queueProgressMeasurement = (): void => {
204
- if (measurementFrameRef.current !== null) {
205
- return;
206
- }
207
-
208
- measurementFrameRef.current = window.requestAnimationFrame(() => {
209
- measurementFrameRef.current = null;
210
- const canvas = canvasRef.current;
211
- if (!canvas) {
212
- return;
213
- }
214
-
215
- const context = canvas.getContext('2d');
216
- if (!context) {
217
- return;
218
- }
219
-
220
- const nextProgress = measureScratchProgress(context, canvas.width, canvas.height);
221
- setScratchProgress(nextProgress);
222
- if (nextProgress >= scratchThreshold) {
223
- setScratchComplete(true);
224
- }
225
- });
226
- };
227
-
228
- const getPointFromEvent = (
229
- event: ReactPointerEvent<HTMLDivElement>,
230
- ): { point: ScratchPoint; context: CanvasRenderingContext2D } | null => {
231
- const canvas = canvasRef.current;
232
- if (!canvas) {
233
- return null;
234
- }
235
-
236
- const context = canvas.getContext('2d');
237
- if (!context) {
238
- return null;
239
- }
240
-
241
- const rect = canvas.getBoundingClientRect();
242
- const point = {
243
- x: clamp(event.clientX - rect.left, 0, rect.width),
244
- y: clamp(event.clientY - rect.top, 0, rect.height),
245
- };
246
-
247
- return { point, context };
248
- };
249
-
250
- const handleReveal = (): void => {
251
- const canvas = canvasRef.current;
252
- if (canvas) {
253
- const context = canvas.getContext('2d');
254
- if (context) {
255
- context.clearRect(0, 0, canvas.width, canvas.height);
256
- }
257
- }
258
-
259
- setScratchProgress(1);
260
- setScratchComplete(true);
261
- };
262
-
263
- const handlePointerDown = (event: ReactPointerEvent<HTMLDivElement>): void => {
264
- if (scratchComplete) {
265
- return;
266
- }
267
-
268
- const scratchState = getPointFromEvent(event);
269
- if (!scratchState) {
270
- return;
271
- }
272
-
273
- event.currentTarget.setPointerCapture(event.pointerId);
274
- pointerActiveRef.current = true;
275
- lastPointRef.current = scratchState.point;
276
- eraseScratch(
277
- scratchState.context,
278
- scratchState.point,
279
- scratchState.point,
280
- scratchBrushRadius,
281
- );
282
- queueProgressMeasurement();
283
- };
284
-
285
- const handlePointerMove = (event: ReactPointerEvent<HTMLDivElement>): void => {
286
- if (!pointerActiveRef.current || scratchComplete) {
287
- return;
288
- }
289
-
290
- const scratchState = getPointFromEvent(event);
291
- if (!scratchState) {
292
- return;
293
- }
294
-
295
- eraseScratch(
296
- scratchState.context,
297
- lastPointRef.current ?? scratchState.point,
298
- scratchState.point,
299
- scratchBrushRadius,
300
- );
301
- lastPointRef.current = scratchState.point;
302
- queueProgressMeasurement();
303
- };
304
-
305
- const endScratch = (): void => {
306
- pointerActiveRef.current = false;
307
- lastPointRef.current = null;
308
- };
309
-
310
- const handleTicketKeyDown = (event: KeyboardEvent<HTMLDivElement>): void => {
311
- if (event.key !== 'Enter' && event.key !== ' ') {
312
- return;
313
- }
314
-
315
- event.preventDefault();
316
- handleReveal();
317
- };
318
-
319
- return (
320
- <>
321
- <section className='scratchpad-step' data-node-id='7390:1456'>
322
- <div className='scratchpad-shell'>
323
- {rewardVisible ? (
324
- <article className='scratchpad-reward-card' data-node-id='6992:6168'>
325
- <div className='scratchpad-reward-copy'>
326
- <p className='scratchpad-reward-kicker'>{content.reward.kicker}</p>
327
- <p className='scratchpad-reward-description'>{content.reward.description}</p>
328
- <p className='scratchpad-reward-offer'>{content.reward.offerLabel}</p>
329
- <p className='scratchpad-reward-applied'>{content.reward.appliedLabel}</p>
330
- </div>
331
- <button
332
- type='button'
333
- className='scratchpad-reward-cta'
334
- onClick={() => goNext()}
335
- >
336
- {content.reward.ctaLabel}
337
- </button>
338
- </article>
339
- ) : (
340
- <article className='scratchpad-card-panel'>
341
- <FunnelStepHeader
342
- className='scratchpad-header'
343
- titleClassName='scratchpad-title'
344
- subtitleClassName='scratchpad-description'
345
- title={
346
- <>
347
- {content.header.title[0]}
348
- <br />
349
- {content.header.title[1]}
350
- </>
351
- }
352
- subtitle={
353
- <div className='scratchpad-description-group'>
354
- {content.header.description.map((line) => (
355
- <p key={line} className='scratchpad-description-line'>
356
- {line}
357
- </p>
358
- ))}
359
- </div>
360
- }
361
- subtitleAs='div'
362
- />
363
-
364
- <div className='scratchpad-ticket-block'>
365
- <p className='scratchpad-instruction'>{content.header.instructionLabel}</p>
366
- <div
367
- className={scratchComplete ? 'scratchpad-ticket is-revealed' : 'scratchpad-ticket'}
368
- data-node-id='7390:1455'
369
- role='button'
370
- tabIndex={0}
371
- onKeyDown={handleTicketKeyDown}
372
- onPointerDown={handlePointerDown}
373
- onPointerMove={handlePointerMove}
374
- onPointerUp={endScratch}
375
- onPointerLeave={endScratch}
376
- onPointerCancel={endScratch}
377
- aria-label={content.header.scratchAriaLabel}
378
- >
379
- <div className='scratchpad-ticket-face'>
380
- <p className='scratchpad-ticket-offer'>{content.ticket.offerLabel}</p>
381
- <div className='scratchpad-ticket-code'>{content.ticket.code}</div>
382
- </div>
383
- <canvas
384
- ref={canvasRef}
385
- className={scratchComplete ? 'scratchpad-overlay is-hidden' : 'scratchpad-overlay'}
386
- />
387
- {!scratchComplete ? (
388
- <div className='scratchpad-overlay-icon' aria-hidden>
389
- 〰︎
390
- </div>
391
- ) : null}
392
- <span className='scratchpad-ticket-notch scratchpad-ticket-notch--left' aria-hidden />
393
- <span className='scratchpad-ticket-notch scratchpad-ticket-notch--right' aria-hidden />
394
- </div>
395
- {!scratchComplete ? (
396
- <button
397
- type='button'
398
- className='scratchpad-reveal-button'
399
- onClick={handleReveal}
400
- >
401
- Reveal instantly
402
- </button>
403
- ) : null}
404
- <p className='scratchpad-progress-copy'>
405
- {scratchComplete
406
- ? 'Discount unlocked'
407
- : `${Math.round(scratchProgress * 100)}% revealed`}
408
- </p>
409
- </div>
410
- </article>
411
- )}
412
- </div>
413
- </section>
414
-
415
- {stepFourStyles ? <style>{stepFourStyles}</style> : null}
416
- </>
417
- );
418
- }
419
-
420
- const stepFourStyles = `
421
- .scratchpad-step {
422
- min-height: 100%;
423
- background:
424
- radial-gradient(circle at 50% -18%, rgba(255, 255, 255, 0.92) 0%, rgba(255, 255, 255, 0.2) 36%, transparent 58%),
425
- linear-gradient(180deg, #f6f8fb 0%, #edf2f8 100%);
426
- padding: calc(env(safe-area-inset-top, 0px) + 48px) var(--padding) 96px;
427
- display: flex;
428
- align-items: center;
429
- justify-content: center;
430
- }
431
-
432
- .scratchpad-shell {
433
- width: 100%;
434
- display: flex;
435
- justify-content: center;
436
- }
437
-
438
- .scratchpad-card-panel,
439
- .scratchpad-reward-card {
440
- width: min(100%, 382px);
441
- border-radius: 20px;
442
- background: var(--color-surface);
443
- box-shadow: 0 20px 44px rgba(31, 41, 51, 0.14);
444
- padding: 16px 24px 24px;
445
- }
446
-
447
- .scratchpad-header {
448
- text-align: center;
449
- }
450
-
451
- .scratchpad-title {
452
- margin: 0;
453
- color: #1b1557;
454
- font-size: 24px;
455
- line-height: 1.35;
456
- font-weight: 800;
457
- }
458
-
459
- .scratchpad-description {
460
- margin: 0;
461
- }
462
-
463
- .scratchpad-description-group {
464
- display: flex;
465
- flex-direction: column;
466
- gap: 2px;
467
- }
468
-
469
- .scratchpad-description-line {
470
- margin: 0;
471
- color: #262729;
472
- font-size: 14px;
473
- line-height: 1.35;
474
- }
475
-
476
- .scratchpad-ticket-block {
477
- margin-top: 24px;
478
- display: flex;
479
- flex-direction: column;
480
- align-items: center;
481
- gap: 16px;
482
- }
483
-
484
- .scratchpad-instruction {
485
- margin: 0;
486
- color: #1b1557;
487
- font-size: 16px;
488
- font-weight: 700;
489
- line-height: 1.35;
490
- text-transform: uppercase;
491
- }
492
-
493
- .scratchpad-ticket {
494
- position: relative;
495
- border-radius: 16px;
496
- width: 160px;
497
- height: 160px;
498
- cursor: pointer;
499
- outline: none;
500
- isolation: isolate;
501
- }
502
-
503
- .scratchpad-ticket:focus-visible {
504
- box-shadow: 0 0 0 3px rgba(63, 81, 181, 0.24);
505
- }
506
-
507
- .scratchpad-ticket-face {
508
- position: absolute;
509
- inset: 0;
510
- border-radius: 16px;
511
- background: linear-gradient(180deg, #ffd15f 0%, #cf8f45 100%);
512
- display: flex;
513
- flex-direction: column;
514
- align-items: center;
515
- justify-content: center;
516
- gap: 18px;
517
- box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.45), 0 10px 24px rgba(86, 51, 12, 0.24);
518
- }
519
-
520
- .scratchpad-ticket-offer {
521
- margin: 0;
522
- color: #56330c;
523
- font-size: 24px;
524
- line-height: 1.35;
525
- font-weight: 800;
526
- }
527
-
528
- .scratchpad-ticket-code {
529
- border-radius: 6px;
530
- background: rgba(255, 255, 255, 0.94);
531
- color: #56330c;
532
- font-size: 11px;
533
- line-height: 1.35;
534
- font-weight: 700;
535
- text-transform: uppercase;
536
- padding: 7px 10px;
537
- }
538
-
539
- .scratchpad-overlay {
540
- position: absolute;
541
- inset: 0;
542
- width: 100%;
543
- height: 100%;
544
- border-radius: 16px;
545
- transition: opacity 180ms ease;
546
- }
547
-
548
- .scratchpad-overlay.is-hidden {
549
- opacity: 0;
550
- }
551
-
552
- .scratchpad-overlay-icon {
553
- position: absolute;
554
- inset: 0;
555
- display: flex;
556
- align-items: center;
557
- justify-content: center;
558
- color: rgba(255, 245, 211, 0.84);
559
- font-size: 44px;
560
- transform: rotate(-12deg);
561
- pointer-events: none;
562
- }
563
-
564
- .scratchpad-ticket-notch {
565
- position: absolute;
566
- top: 50%;
567
- width: 28px;
568
- height: 28px;
569
- margin-top: -14px;
570
- border-radius: 999px;
571
- background: var(--color-surface);
572
- box-shadow: inset 0 0 0 1px rgba(31, 41, 51, 0.04);
573
- }
574
-
575
- .scratchpad-ticket-notch--left {
576
- left: -14px;
577
- }
578
-
579
- .scratchpad-ticket-notch--right {
580
- right: -14px;
581
- }
582
-
583
- .scratchpad-reveal-button {
584
- border: none;
585
- background: transparent;
586
- color: #3f51b5;
587
- font-size: 13px;
588
- line-height: 1.35;
589
- font-weight: 700;
590
- padding: 0;
591
- }
592
-
593
- .scratchpad-progress-copy {
594
- margin: 0;
595
- color: #5b647a;
596
- font-size: 12px;
597
- line-height: 1.35;
598
- font-weight: 600;
599
- }
600
-
601
- .scratchpad-reward-card {
602
- padding-top: 24px;
603
- display: flex;
604
- flex-direction: column;
605
- gap: 24px;
606
- }
607
-
608
- .scratchpad-reward-copy {
609
- display: flex;
610
- flex-direction: column;
611
- align-items: center;
612
- gap: 8px;
613
- text-align: center;
614
- }
615
-
616
- .scratchpad-reward-kicker {
617
- margin: 0;
618
- color: #1b1557;
619
- font-size: 28px;
620
- line-height: 1.35;
621
- font-weight: 800;
622
- text-transform: uppercase;
623
- }
624
-
625
- .scratchpad-reward-description {
626
- margin: 0;
627
- color: #262729;
628
- font-size: 16px;
629
- line-height: 1.35;
630
- }
631
-
632
- .scratchpad-reward-offer {
633
- margin: 0;
634
- color: #1b1557;
635
- font-size: 36px;
636
- line-height: 1.2;
637
- font-weight: 800;
638
- text-transform: uppercase;
639
- }
640
-
641
- .scratchpad-reward-applied {
642
- margin: 0;
643
- color: #262729;
644
- font-size: 15px;
645
- line-height: 1.35;
646
- font-weight: 600;
647
- }
648
-
649
- .scratchpad-reward-cta {
650
- border: none;
651
- border-radius: 16px;
652
- background: #3f51b5;
653
- color: #ffffff;
654
- font-size: 16px;
655
- line-height: 1.35;
656
- font-weight: 700;
657
- padding: 18px 24px;
658
- }
659
-
660
- @media (hover: hover) {
661
- .scratchpad-reveal-button:hover,
662
- .scratchpad-reward-cta:hover {
663
- opacity: 0.92;
664
- }
665
- }
666
-
667
- @media (max-width: 430px) {
668
- .scratchpad-card-panel,
669
- .scratchpad-reward-card {
670
- width: 100%;
671
- }
672
- }
673
- `;
@@ -1,35 +0,0 @@
1
- 'use client';
2
-
3
- import { useEffect } from 'react';
4
- import { StepPaywall, stepPaywall } from './step-32-paywall';
5
- import type { FunnelStepMeta } from '@funnelsgrove/runtime';
6
-
7
- export const stepPaywallBId = 'paywall-b';
8
-
9
- export const stepPaywallB: FunnelStepMeta = {
10
- ...stepPaywall,
11
- id: stepPaywallBId,
12
- name: 'paywall-b',
13
- type: 'paywall_offer',
14
- title: 'Paywall B',
15
- description: 'Duplicate ClaimBee paywall variant for A/B testing.',
16
- };
17
-
18
- export function StepPaywallB() {
19
- useEffect(() => {
20
- document.documentElement.dataset.paywallVariant = 'b';
21
-
22
- return () => {
23
- if (document.documentElement.dataset.paywallVariant === 'b') {
24
- delete document.documentElement.dataset.paywallVariant;
25
- }
26
- };
27
- }, []);
28
-
29
- return (
30
- <StepPaywall
31
- analyticsStepId={stepPaywallBId}
32
- analyticsStepName={stepPaywallB.name || stepPaywallB.title}
33
- />
34
- );
35
- }