appfunnel 0.13.0 → 0.14.0
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/dist/index.js +54 -31
- package/dist/index.js.map +1 -1
- package/package.json +4 -4
- package/templates/default/README.md +0 -69
- package/templates/default/appfunnel.config.ts +0 -17
- package/templates/default/gitignore +0 -3
- package/templates/default/locales/en.json +0 -3
- package/templates/default/src/app.css +0 -1
- package/templates/default/src/components/ConsentDrawer.tsx +0 -70
- package/templates/default/src/components/Header.tsx +0 -37
- package/templates/default/src/components/paywall/PaymentCheckoutDialog.tsx +0 -76
- package/templates/default/src/funnel.tsx +0 -9
- package/templates/default/src/pages/birthday.tsx +0 -66
- package/templates/default/src/pages/download.tsx +0 -67
- package/templates/default/src/pages/email.tsx +0 -95
- package/templates/default/src/pages/intro.tsx +0 -109
- package/templates/default/src/pages/multi-select.tsx +0 -79
- package/templates/default/src/pages/name.tsx +0 -48
- package/templates/default/src/pages/paywall.tsx +0 -191
- package/templates/default/src/pages/single-select.tsx +0 -61
- package/templates/default/src/pages/upsell.tsx +0 -158
- package/templates/default/template.json +0 -10
- package/templates/default/tsconfig.json +0 -16
|
@@ -1,79 +0,0 @@
|
|
|
1
|
-
import { definePage, useResponse, useNavigation } from '@appfunnel-dev/sdk'
|
|
2
|
-
import { MultiSelect, motion } from '@appfunnel-dev/sdk/elements'
|
|
3
|
-
import { Header } from '../components/Header'
|
|
4
|
-
|
|
5
|
-
export const page = definePage({
|
|
6
|
-
name: 'Interests',
|
|
7
|
-
routes: [{ to: 'name' }],
|
|
8
|
-
})
|
|
9
|
-
|
|
10
|
-
export default function Interests() {
|
|
11
|
-
const [interests] = useResponse<string[]>('interests')
|
|
12
|
-
const { goToNextPage } = useNavigation()
|
|
13
|
-
|
|
14
|
-
const hasSelection = interests && interests.length > 0
|
|
15
|
-
|
|
16
|
-
return (
|
|
17
|
-
<div className="min-h-screen bg-white flex flex-col max-w-[672px] mx-auto w-full">
|
|
18
|
-
<Header />
|
|
19
|
-
<div className="flex-1 flex flex-col px-4 pt-8 pb-6">
|
|
20
|
-
<h1 className="text-[30px] leading-tight font-bold text-[#171717] mb-2">
|
|
21
|
-
What are you most interested in?
|
|
22
|
-
</h1>
|
|
23
|
-
<p className="text-lg text-[#727272] mb-6">Select all that apply</p>
|
|
24
|
-
|
|
25
|
-
<MultiSelect
|
|
26
|
-
responseKey="interests"
|
|
27
|
-
className="flex flex-col gap-3"
|
|
28
|
-
min={1}
|
|
29
|
-
options={[
|
|
30
|
-
{ label: 'Fitness & Health', value: 'fitness' },
|
|
31
|
-
{ label: 'Nutrition & Diet', value: 'nutrition' },
|
|
32
|
-
{ label: 'Mindfulness', value: 'mindfulness' },
|
|
33
|
-
{ label: 'Productivity', value: 'productivity' },
|
|
34
|
-
{ label: 'Finance', value: 'finance' },
|
|
35
|
-
{ label: 'Learning', value: 'learning' },
|
|
36
|
-
]}
|
|
37
|
-
renderItem={({ item, active }) => (
|
|
38
|
-
<div
|
|
39
|
-
className={`w-full px-4 py-4 rounded-2xl text-left transition-all border-2 ${
|
|
40
|
-
active
|
|
41
|
-
? 'border-blue-600 bg-blue-50'
|
|
42
|
-
: 'border-transparent bg-[#F5F5F5]'
|
|
43
|
-
}`}
|
|
44
|
-
>
|
|
45
|
-
<div className="flex items-center gap-3">
|
|
46
|
-
<div
|
|
47
|
-
className={`w-5 h-5 rounded-lg border-2 flex items-center justify-center shrink-0 ${
|
|
48
|
-
active ? 'border-blue-600 bg-blue-600' : 'border-gray-300'
|
|
49
|
-
}`}
|
|
50
|
-
>
|
|
51
|
-
{active && (
|
|
52
|
-
<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="white" strokeWidth="3">
|
|
53
|
-
<polyline points="20 6 9 17 4 12" />
|
|
54
|
-
</svg>
|
|
55
|
-
)}
|
|
56
|
-
</div>
|
|
57
|
-
<span className="text-base font-semibold text-[#171717]">{item.label}</span>
|
|
58
|
-
</div>
|
|
59
|
-
</div>
|
|
60
|
-
)}
|
|
61
|
-
/>
|
|
62
|
-
|
|
63
|
-
<motion.button
|
|
64
|
-
whileTap={hasSelection ? { scale: 0.95 } : {}}
|
|
65
|
-
transition={{ duration: 0.15 }}
|
|
66
|
-
onClick={goToNextPage}
|
|
67
|
-
disabled={!hasSelection}
|
|
68
|
-
className={`w-full py-4 rounded-2xl text-base font-bold transition-colors mt-6 ${
|
|
69
|
-
hasSelection
|
|
70
|
-
? 'bg-blue-600 text-white'
|
|
71
|
-
: 'bg-gray-100 text-gray-400'
|
|
72
|
-
}`}
|
|
73
|
-
>
|
|
74
|
-
Continue
|
|
75
|
-
</motion.button>
|
|
76
|
-
</div>
|
|
77
|
-
</div>
|
|
78
|
-
)
|
|
79
|
-
}
|
|
@@ -1,48 +0,0 @@
|
|
|
1
|
-
import { definePage, useUser, useNavigation } from '@appfunnel-dev/sdk'
|
|
2
|
-
import { motion } from '@appfunnel-dev/sdk/elements'
|
|
3
|
-
import { Header } from '../components/Header'
|
|
4
|
-
|
|
5
|
-
export const page = definePage({
|
|
6
|
-
name: 'Name',
|
|
7
|
-
routes: [{ to: 'birthday' }],
|
|
8
|
-
})
|
|
9
|
-
|
|
10
|
-
export default function Name() {
|
|
11
|
-
const { name, setName } = useUser()
|
|
12
|
-
const { goToNextPage } = useNavigation()
|
|
13
|
-
|
|
14
|
-
const enabled = !!name?.trim()
|
|
15
|
-
|
|
16
|
-
return (
|
|
17
|
-
<div className="min-h-screen bg-white flex flex-col max-w-[672px] mx-auto w-full">
|
|
18
|
-
<Header />
|
|
19
|
-
<div className="flex-1 flex flex-col justify-between px-4 pt-6 pb-6">
|
|
20
|
-
<div>
|
|
21
|
-
<h1 className="text-[30px] leading-tight font-bold text-[#171717] mb-6">
|
|
22
|
-
What's your name?
|
|
23
|
-
</h1>
|
|
24
|
-
<input
|
|
25
|
-
type="text"
|
|
26
|
-
value={name || ''}
|
|
27
|
-
onChange={(e) => setName(e.target.value)}
|
|
28
|
-
placeholder="Your name"
|
|
29
|
-
className="w-full px-4 py-4 rounded-2xl border border-gray-200 text-base text-[#171717] placeholder-gray-400 outline-none focus:border-blue-600 transition-colors"
|
|
30
|
-
/>
|
|
31
|
-
</div>
|
|
32
|
-
<motion.button
|
|
33
|
-
whileTap={enabled ? { scale: 0.95 } : {}}
|
|
34
|
-
transition={{ duration: 0.15 }}
|
|
35
|
-
onClick={goToNextPage}
|
|
36
|
-
disabled={!enabled}
|
|
37
|
-
className={`w-full py-4 rounded-2xl text-base font-bold transition-colors ${
|
|
38
|
-
enabled
|
|
39
|
-
? 'bg-blue-600 text-white'
|
|
40
|
-
: 'bg-gray-100 text-gray-400'
|
|
41
|
-
}`}
|
|
42
|
-
>
|
|
43
|
-
Continue
|
|
44
|
-
</motion.button>
|
|
45
|
-
</div>
|
|
46
|
-
</div>
|
|
47
|
-
)
|
|
48
|
-
}
|
|
@@ -1,191 +0,0 @@
|
|
|
1
|
-
import {
|
|
2
|
-
definePage,
|
|
3
|
-
useProducts,
|
|
4
|
-
useNavigation,
|
|
5
|
-
usePayment,
|
|
6
|
-
} from '@appfunnel-dev/sdk'
|
|
7
|
-
import { motion } from '@appfunnel-dev/sdk/elements'
|
|
8
|
-
import {
|
|
9
|
-
PaymentCheckoutDialog,
|
|
10
|
-
type PaymentCheckoutDialogHandle,
|
|
11
|
-
} from '../components/paywall/PaymentCheckoutDialog'
|
|
12
|
-
import { useRef } from 'react'
|
|
13
|
-
|
|
14
|
-
export const page = definePage({
|
|
15
|
-
name: 'Paywall',
|
|
16
|
-
type: 'paywall',
|
|
17
|
-
routes: [{ to: 'upsell' }],
|
|
18
|
-
})
|
|
19
|
-
|
|
20
|
-
export default function Paywall() {
|
|
21
|
-
const { products, selected, select } = useProducts()
|
|
22
|
-
const { goToNextPage } = useNavigation()
|
|
23
|
-
const { loading } = usePayment()
|
|
24
|
-
const checkoutRef = useRef<PaymentCheckoutDialogHandle>(null)
|
|
25
|
-
|
|
26
|
-
const plans = products.filter((p) => !p.id.startsWith('upsell'))
|
|
27
|
-
|
|
28
|
-
const handleContinue = () => {
|
|
29
|
-
checkoutRef.current?.open()
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
const canContinue = selected && !loading
|
|
33
|
-
|
|
34
|
-
return (
|
|
35
|
-
<div className="min-h-screen bg-white flex flex-col items-center">
|
|
36
|
-
<div className="flex flex-col max-w-[672px] mx-auto w-full px-4 pb-32">
|
|
37
|
-
{/* Header */}
|
|
38
|
-
<div className="pt-8 pb-6 text-center">
|
|
39
|
-
<h1 className="text-[28px] font-bold text-[#171717]">
|
|
40
|
-
Choose your plan
|
|
41
|
-
</h1>
|
|
42
|
-
<p className="text-base text-[#727272] mt-2">
|
|
43
|
-
Start your journey today
|
|
44
|
-
</p>
|
|
45
|
-
</div>
|
|
46
|
-
|
|
47
|
-
{/* Features */}
|
|
48
|
-
<div className="flex flex-col gap-3 mb-8">
|
|
49
|
-
{[
|
|
50
|
-
'Personalized plan based on your answers',
|
|
51
|
-
'Track your progress over time',
|
|
52
|
-
'Access to all premium features',
|
|
53
|
-
'Cancel anytime, no questions asked',
|
|
54
|
-
].map((text) => (
|
|
55
|
-
<div key={text} className="flex items-center gap-3">
|
|
56
|
-
<div className="w-6 h-6 rounded-full bg-blue-600 flex items-center justify-center shrink-0">
|
|
57
|
-
<svg
|
|
58
|
-
width="14"
|
|
59
|
-
height="14"
|
|
60
|
-
viewBox="0 0 24 24"
|
|
61
|
-
fill="none"
|
|
62
|
-
stroke="white"
|
|
63
|
-
strokeWidth="2.75"
|
|
64
|
-
strokeLinecap="round"
|
|
65
|
-
strokeLinejoin="round"
|
|
66
|
-
>
|
|
67
|
-
<polyline points="20 6 9 17 4 12" />
|
|
68
|
-
</svg>
|
|
69
|
-
</div>
|
|
70
|
-
<span className="text-[#171717] text-[15px]">
|
|
71
|
-
{text}
|
|
72
|
-
</span>
|
|
73
|
-
</div>
|
|
74
|
-
))}
|
|
75
|
-
</div>
|
|
76
|
-
|
|
77
|
-
{/* Plan cards */}
|
|
78
|
-
<div className="space-y-3 mb-6">
|
|
79
|
-
{plans.map((product) => {
|
|
80
|
-
const isSelected = selected?.id === product.id
|
|
81
|
-
|
|
82
|
-
return (
|
|
83
|
-
<button
|
|
84
|
-
key={product.id}
|
|
85
|
-
onClick={() => select(product.id)}
|
|
86
|
-
className={`w-full flex items-center justify-between text-left transition-all rounded-2xl px-5 py-4 border-2 ${
|
|
87
|
-
isSelected
|
|
88
|
-
? 'border-blue-600 bg-blue-50'
|
|
89
|
-
: 'border-gray-200 bg-transparent'
|
|
90
|
-
}`}
|
|
91
|
-
>
|
|
92
|
-
<div className="flex items-center gap-3">
|
|
93
|
-
<div
|
|
94
|
-
className={`w-[22px] h-[22px] rounded-full border-2 flex items-center justify-center shrink-0 ${
|
|
95
|
-
isSelected
|
|
96
|
-
? 'border-blue-600 bg-blue-600'
|
|
97
|
-
: 'border-gray-300 bg-transparent'
|
|
98
|
-
}`}
|
|
99
|
-
>
|
|
100
|
-
{isSelected && (
|
|
101
|
-
<svg
|
|
102
|
-
width="14"
|
|
103
|
-
height="14"
|
|
104
|
-
viewBox="0 0 24 24"
|
|
105
|
-
fill="none"
|
|
106
|
-
stroke="white"
|
|
107
|
-
strokeWidth="2.75"
|
|
108
|
-
strokeLinecap="round"
|
|
109
|
-
strokeLinejoin="round"
|
|
110
|
-
>
|
|
111
|
-
<polyline points="20 6 9 17 4 12" />
|
|
112
|
-
</svg>
|
|
113
|
-
)}
|
|
114
|
-
</div>
|
|
115
|
-
<div className="flex flex-col">
|
|
116
|
-
<span className="text-[#171717] text-base font-bold">
|
|
117
|
-
{product.displayName}
|
|
118
|
-
</span>
|
|
119
|
-
<span className="text-[#727272] text-sm">
|
|
120
|
-
{product.price}
|
|
121
|
-
{product.period
|
|
122
|
-
? ` / ${product.period}`
|
|
123
|
-
: ''}
|
|
124
|
-
</span>
|
|
125
|
-
</div>
|
|
126
|
-
</div>
|
|
127
|
-
{product.hasTrial && (
|
|
128
|
-
<span className="text-xs font-bold text-blue-600 bg-blue-50 px-2 py-1 rounded-full">
|
|
129
|
-
{product.trialDays}-day trial
|
|
130
|
-
</span>
|
|
131
|
-
)}
|
|
132
|
-
</button>
|
|
133
|
-
)
|
|
134
|
-
})}
|
|
135
|
-
</div>
|
|
136
|
-
|
|
137
|
-
{/* Legal text */}
|
|
138
|
-
<p className="text-center text-gray-400 text-[11px] leading-4 mb-6">
|
|
139
|
-
By continuing, you agree that your subscription will
|
|
140
|
-
automatically renew at the regular price at the end of your
|
|
141
|
-
plan period. You can cancel at any time.
|
|
142
|
-
</p>
|
|
143
|
-
|
|
144
|
-
{/* Money-back guarantee */}
|
|
145
|
-
<div className="flex items-center gap-3 justify-center py-4">
|
|
146
|
-
<svg
|
|
147
|
-
width="28"
|
|
148
|
-
height="28"
|
|
149
|
-
viewBox="0 0 24 24"
|
|
150
|
-
fill="#2563EB"
|
|
151
|
-
stroke="none"
|
|
152
|
-
>
|
|
153
|
-
<path d="M12 2L3 5v6.09c0 5.05 3.41 9.76 9 10.91 5.59-1.15 9-5.86 9-10.91V5l-9-3zm-2 15l-4-4 1.41-1.41L10 14.17l6.59-6.59L18 9l-8 8z" />
|
|
154
|
-
</svg>
|
|
155
|
-
<div className="flex flex-col">
|
|
156
|
-
<span className="font-bold text-[#171717] text-sm">
|
|
157
|
-
Money-back guarantee
|
|
158
|
-
</span>
|
|
159
|
-
<span className="text-[#727272] text-xs">
|
|
160
|
-
Not satisfied? Get a full refund within 30 days.
|
|
161
|
-
</span>
|
|
162
|
-
</div>
|
|
163
|
-
</div>
|
|
164
|
-
</div>
|
|
165
|
-
|
|
166
|
-
{/* Sticky continue */}
|
|
167
|
-
<div className="fixed bottom-0 left-0 right-0 z-30 bg-white px-4 py-3 pb-[max(12px,env(safe-area-inset-bottom))] border-t border-gray-200">
|
|
168
|
-
<div className="max-w-[672px] mx-auto">
|
|
169
|
-
<motion.button
|
|
170
|
-
whileTap={canContinue ? { scale: 0.95 } : {}}
|
|
171
|
-
transition={{ duration: 0.15 }}
|
|
172
|
-
onClick={handleContinue}
|
|
173
|
-
disabled={!canContinue}
|
|
174
|
-
className={`w-full py-4 rounded-2xl text-lg font-bold border-none ${
|
|
175
|
-
canContinue
|
|
176
|
-
? 'bg-blue-600 text-white cursor-pointer'
|
|
177
|
-
: 'bg-gray-400 text-white cursor-not-allowed'
|
|
178
|
-
}`}
|
|
179
|
-
>
|
|
180
|
-
Continue
|
|
181
|
-
</motion.button>
|
|
182
|
-
</div>
|
|
183
|
-
</div>
|
|
184
|
-
|
|
185
|
-
<PaymentCheckoutDialog
|
|
186
|
-
ref={checkoutRef}
|
|
187
|
-
onSuccess={goToNextPage}
|
|
188
|
-
/>
|
|
189
|
-
</div>
|
|
190
|
-
)
|
|
191
|
-
}
|
|
@@ -1,61 +0,0 @@
|
|
|
1
|
-
import { definePage } from '@appfunnel-dev/sdk'
|
|
2
|
-
import { SingleSelect } from '@appfunnel-dev/sdk/elements'
|
|
3
|
-
import { Header } from '../components/Header'
|
|
4
|
-
|
|
5
|
-
export const page = definePage({
|
|
6
|
-
name: 'Goal',
|
|
7
|
-
routes: [{ to: 'multi-select' }],
|
|
8
|
-
})
|
|
9
|
-
|
|
10
|
-
export default function Goal() {
|
|
11
|
-
return (
|
|
12
|
-
<div className="min-h-screen bg-white flex flex-col max-w-[672px] mx-auto w-full">
|
|
13
|
-
<Header showBack={false} />
|
|
14
|
-
<div className="flex-1 flex flex-col px-4 pt-8 pb-6">
|
|
15
|
-
<h1 className="text-[30px] leading-tight font-bold text-[#171717] mb-6">
|
|
16
|
-
What's your main goal?
|
|
17
|
-
</h1>
|
|
18
|
-
<SingleSelect
|
|
19
|
-
responseKey="goal"
|
|
20
|
-
className="flex flex-col gap-3"
|
|
21
|
-
options={[
|
|
22
|
-
{ label: 'Get started quickly', sublabel: 'Jump right in', value: 'get_started' },
|
|
23
|
-
{ label: 'Learn something new', sublabel: 'Explore and grow', value: 'learn' },
|
|
24
|
-
{ label: 'Improve my routine', sublabel: 'Level up daily habits', value: 'improve' },
|
|
25
|
-
{ label: 'Track my progress', sublabel: 'Stay on top of goals', value: 'track' },
|
|
26
|
-
{ label: 'Something else', sublabel: 'Tell us more later', value: 'other' },
|
|
27
|
-
]}
|
|
28
|
-
renderItem={({ item, active }) => (
|
|
29
|
-
<div
|
|
30
|
-
className={`w-full px-4 py-4 rounded-2xl text-left transition-all border-2 ${
|
|
31
|
-
active
|
|
32
|
-
? 'border-blue-600 bg-blue-50'
|
|
33
|
-
: 'border-transparent bg-[#F5F5F5]'
|
|
34
|
-
}`}
|
|
35
|
-
>
|
|
36
|
-
<div className="flex items-center gap-3">
|
|
37
|
-
<div
|
|
38
|
-
className={`w-5 h-5 rounded-full border-2 flex items-center justify-center shrink-0 ${
|
|
39
|
-
active ? 'border-blue-600 bg-blue-600' : 'border-gray-300'
|
|
40
|
-
}`}
|
|
41
|
-
>
|
|
42
|
-
{active && (
|
|
43
|
-
<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="white" strokeWidth="3">
|
|
44
|
-
<polyline points="20 6 9 17 4 12" />
|
|
45
|
-
</svg>
|
|
46
|
-
)}
|
|
47
|
-
</div>
|
|
48
|
-
<div>
|
|
49
|
-
<div className="text-base font-semibold text-[#171717]">{item.label}</div>
|
|
50
|
-
{item.sublabel && (
|
|
51
|
-
<div className="text-sm text-[#727272]">{item.sublabel}</div>
|
|
52
|
-
)}
|
|
53
|
-
</div>
|
|
54
|
-
</div>
|
|
55
|
-
</div>
|
|
56
|
-
)}
|
|
57
|
-
/>
|
|
58
|
-
</div>
|
|
59
|
-
</div>
|
|
60
|
-
)
|
|
61
|
-
}
|
|
@@ -1,158 +0,0 @@
|
|
|
1
|
-
import {
|
|
2
|
-
definePage,
|
|
3
|
-
useProducts,
|
|
4
|
-
useNavigation,
|
|
5
|
-
usePayment,
|
|
6
|
-
} from '@appfunnel-dev/sdk'
|
|
7
|
-
import { Loading, motion } from '@appfunnel-dev/sdk/elements'
|
|
8
|
-
|
|
9
|
-
export const page = definePage({
|
|
10
|
-
name: 'Upsell',
|
|
11
|
-
type: 'upsell',
|
|
12
|
-
routes: [{ to: 'download' }],
|
|
13
|
-
})
|
|
14
|
-
|
|
15
|
-
export default function Upsell() {
|
|
16
|
-
const { products } = useProducts()
|
|
17
|
-
const { goToNextPage } = useNavigation()
|
|
18
|
-
const { loading, purchase } = usePayment()
|
|
19
|
-
|
|
20
|
-
const upsellProduct = products.find((p) => p.id.startsWith('upsell'))
|
|
21
|
-
|
|
22
|
-
const handleAdd = () => {
|
|
23
|
-
if (!upsellProduct) return
|
|
24
|
-
purchase(upsellProduct.id, {
|
|
25
|
-
onSuccess: goToNextPage,
|
|
26
|
-
})
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
return (
|
|
30
|
-
<div className="min-h-screen bg-white flex flex-col items-center">
|
|
31
|
-
{/* Skip header */}
|
|
32
|
-
<div className="w-full max-w-[672px] flex justify-end px-4 pt-4">
|
|
33
|
-
<button
|
|
34
|
-
onClick={goToNextPage}
|
|
35
|
-
className="p-2 text-gray-400 hover:text-gray-600"
|
|
36
|
-
>
|
|
37
|
-
<svg
|
|
38
|
-
width="24"
|
|
39
|
-
height="24"
|
|
40
|
-
viewBox="0 0 24 24"
|
|
41
|
-
fill="none"
|
|
42
|
-
stroke="currentColor"
|
|
43
|
-
strokeWidth="2"
|
|
44
|
-
strokeLinecap="round"
|
|
45
|
-
strokeLinejoin="round"
|
|
46
|
-
>
|
|
47
|
-
<line x1="18" y1="6" x2="6" y2="18" />
|
|
48
|
-
<line x1="6" y1="6" x2="18" y2="18" />
|
|
49
|
-
</svg>
|
|
50
|
-
</button>
|
|
51
|
-
</div>
|
|
52
|
-
|
|
53
|
-
{/* Content */}
|
|
54
|
-
<div className="max-w-[672px] w-full flex flex-col gap-6 px-4 pb-32">
|
|
55
|
-
{/* Badge */}
|
|
56
|
-
<div className="flex justify-center">
|
|
57
|
-
<span className="bg-blue-50 text-blue-600 text-sm font-bold px-4 py-1.5 rounded-full">
|
|
58
|
-
Special one-time offer
|
|
59
|
-
</span>
|
|
60
|
-
</div>
|
|
61
|
-
|
|
62
|
-
{/* Title */}
|
|
63
|
-
<div className="text-center">
|
|
64
|
-
<h1 className="text-[28px] font-bold text-[#171717]">
|
|
65
|
-
Unlock the full experience
|
|
66
|
-
</h1>
|
|
67
|
-
<p className="text-base text-[#727272] mt-2">
|
|
68
|
-
Add premium features to supercharge your results
|
|
69
|
-
</p>
|
|
70
|
-
</div>
|
|
71
|
-
|
|
72
|
-
{/* Features */}
|
|
73
|
-
<div className="flex flex-col gap-4">
|
|
74
|
-
{[
|
|
75
|
-
{
|
|
76
|
-
title: 'Advanced analytics',
|
|
77
|
-
desc: 'Get detailed insights into your progress',
|
|
78
|
-
},
|
|
79
|
-
{
|
|
80
|
-
title: 'Priority support',
|
|
81
|
-
desc: 'Get help when you need it most',
|
|
82
|
-
},
|
|
83
|
-
{
|
|
84
|
-
title: 'Exclusive content',
|
|
85
|
-
desc: 'Access members-only resources',
|
|
86
|
-
},
|
|
87
|
-
].map((item) => (
|
|
88
|
-
<div
|
|
89
|
-
key={item.title}
|
|
90
|
-
className="flex items-start gap-3"
|
|
91
|
-
>
|
|
92
|
-
<div className="w-6 h-6 rounded-full bg-blue-600 flex items-center justify-center shrink-0 mt-0.5">
|
|
93
|
-
<svg
|
|
94
|
-
width="12"
|
|
95
|
-
height="12"
|
|
96
|
-
viewBox="0 0 24 24"
|
|
97
|
-
fill="none"
|
|
98
|
-
stroke="white"
|
|
99
|
-
strokeWidth="3"
|
|
100
|
-
>
|
|
101
|
-
<polyline points="20 6 9 17 4 12" />
|
|
102
|
-
</svg>
|
|
103
|
-
</div>
|
|
104
|
-
<div>
|
|
105
|
-
<span className="text-[15px] font-bold text-[#171717]">
|
|
106
|
-
{item.title}
|
|
107
|
-
</span>
|
|
108
|
-
<p className="text-sm text-[#727272]">
|
|
109
|
-
{item.desc}
|
|
110
|
-
</p>
|
|
111
|
-
</div>
|
|
112
|
-
</div>
|
|
113
|
-
))}
|
|
114
|
-
</div>
|
|
115
|
-
|
|
116
|
-
{/* Price */}
|
|
117
|
-
{upsellProduct && (
|
|
118
|
-
<div className="text-center py-4">
|
|
119
|
-
<span className="text-3xl font-bold text-[#171717]">
|
|
120
|
-
{upsellProduct.price}
|
|
121
|
-
</span>
|
|
122
|
-
{upsellProduct.period && (
|
|
123
|
-
<span className="text-base text-[#727272]">
|
|
124
|
-
{' '}
|
|
125
|
-
/ {upsellProduct.period}
|
|
126
|
-
</span>
|
|
127
|
-
)}
|
|
128
|
-
</div>
|
|
129
|
-
)}
|
|
130
|
-
|
|
131
|
-
{/* Legal */}
|
|
132
|
-
<p className="text-center text-gray-400 text-[11px] leading-4">
|
|
133
|
-
By continuing, you agree to be charged the amount shown
|
|
134
|
-
above. Cancel anytime.
|
|
135
|
-
</p>
|
|
136
|
-
</div>
|
|
137
|
-
|
|
138
|
-
{/* Sticky button */}
|
|
139
|
-
<div className="fixed bottom-0 left-0 right-0 z-30 bg-white px-4 py-3 pb-[max(12px,env(safe-area-inset-bottom))] border-t border-gray-200">
|
|
140
|
-
<div className="max-w-[672px] mx-auto flex flex-col gap-2">
|
|
141
|
-
<motion.button
|
|
142
|
-
whileTap={!loading ? { scale: 0.95 } : {}}
|
|
143
|
-
transition={{ duration: 0.15 }}
|
|
144
|
-
onClick={handleAdd}
|
|
145
|
-
disabled={loading}
|
|
146
|
-
className={`w-full py-4 rounded-2xl text-lg font-bold border-none ${
|
|
147
|
-
loading
|
|
148
|
-
? 'bg-gray-400 text-white cursor-not-allowed'
|
|
149
|
-
: 'bg-blue-600 text-white cursor-pointer'
|
|
150
|
-
}`}
|
|
151
|
-
>
|
|
152
|
-
{loading ? <Loading size="sm" color="#fff" /> : 'Add to my plan'}
|
|
153
|
-
</motion.button>
|
|
154
|
-
</div>
|
|
155
|
-
</div>
|
|
156
|
-
</div>
|
|
157
|
-
)
|
|
158
|
-
}
|
|
@@ -1,10 +0,0 @@
|
|
|
1
|
-
{
|
|
2
|
-
"name": "Default",
|
|
3
|
-
"description": "9-page funnel with quiz, account creation, paywall, upsell, and download screens",
|
|
4
|
-
"products": [
|
|
5
|
-
{ "id": "plan_1", "label": "Plan 1", "description": "The cheapest plan — typically a short-term or weekly option" },
|
|
6
|
-
{ "id": "plan_2", "label": "Plan 2", "description": "Mid-tier plan — typically monthly, best balance of value and price" },
|
|
7
|
-
{ "id": "plan_3", "label": "Plan 3", "description": "Premium plan — typically quarterly or annual, highest value" },
|
|
8
|
-
{ "id": "upsell_1", "label": "Upsell", "description": "One-time or add-on offer shown after the main purchase" }
|
|
9
|
-
]
|
|
10
|
-
}
|
|
@@ -1,16 +0,0 @@
|
|
|
1
|
-
{
|
|
2
|
-
"compilerOptions": {
|
|
3
|
-
"target": "ES2020",
|
|
4
|
-
"module": "ESNext",
|
|
5
|
-
"moduleResolution": "bundler",
|
|
6
|
-
"jsx": "react-jsx",
|
|
7
|
-
"strict": true,
|
|
8
|
-
"esModuleInterop": true,
|
|
9
|
-
"skipLibCheck": true,
|
|
10
|
-
"paths": {
|
|
11
|
-
"@/*": ["./src/*"]
|
|
12
|
-
},
|
|
13
|
-
"baseUrl": "."
|
|
14
|
-
},
|
|
15
|
-
"include": ["src"]
|
|
16
|
-
}
|