@funnelsgrove/cli 0.1.231 → 0.1.237
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/README.md +11 -0
- package/dist/cli.js +10 -0
- package/dist/localeContentSource.d.ts +13 -0
- package/dist/localeContentSource.js +117 -0
- package/dist/localeFilePlan.d.ts +4 -0
- package/dist/localeFilePlan.js +63 -0
- package/dist/localeModuleEdits.d.ts +19 -0
- package/dist/localeModuleEdits.js +97 -0
- package/dist/locales.d.ts +6 -0
- package/dist/locales.js +92 -0
- package/funnel-contract-compatibility.json +38 -38
- package/package.json +4 -4
- package/template_docs/.funnelsgrove-docs.json +5 -5
- package/template_docs/AGENTS.md +1 -0
- package/template_docs/docs/funnelsgrove/contracts/content-answers.md +69 -0
- package/template_docs/docs/funnelsgrove/migrations/step-contract-v3.md +1 -1
- package/template_docs/funnel-docs.config.json +1 -1
- package/template_scaffold/.funnelsgrove-docs.json +5 -5
- package/template_scaffold/.funnelsgrove-scaffold.json +35 -15
- package/template_scaffold/AGENTS.md +1 -0
- package/template_scaffold/docs/funnelsgrove/contracts/content-answers.md +69 -0
- package/template_scaffold/docs/funnelsgrove/migrations/step-contract-v3.md +1 -1
- package/template_scaffold/funnel-docs.config.json +1 -1
- package/template_scaffold/package-lock.json +15 -15
- package/template_scaffold/package.json +3 -3
- package/template_scaffold/src/app/subscription-dashboard/page.tsx +22 -19
- package/template_scaffold/src/components/FunnelFlow.tsx +11 -5
- package/template_scaffold/src/components/paywall/PaywallRenewalDisclaimer.tsx +23 -0
- package/template_scaffold/src/components/paywall/localize-paywall-plan-labels.ts +15 -0
- package/template_scaffold/src/config/billing.plans.ts +11 -10
- package/template_scaffold/src/config/billing.test.plans.ts +11 -10
- package/template_scaffold/src/steps/content/runtime.content.ts +22 -0
- package/template_scaffold/src/steps/content/step-32-paywall.content.ts +41 -1
- package/template_scaffold/src/steps/content/subscription-dashboard.content.ts +54 -0
- package/template_scaffold/src/steps/step-32-paywall.tsx +12 -22
|
@@ -42,3 +42,72 @@ Keep builder-editable values in content definitions, expose matching editor sect
|
|
|
42
42
|
- Update the owning artifact in the same change. Never implement a step absent from `PLAN.md`.
|
|
43
43
|
|
|
44
44
|
See the exact choice and form pages in the [step-type index](../steps/INDEX.md). Run `fgrove validate` and test reload persistence before completion.
|
|
45
|
+
|
|
46
|
+
## Localization
|
|
47
|
+
|
|
48
|
+
All funnel-authored visitor copy belongs in `src/**/*.content.ts` localized
|
|
49
|
+
content definitions: headings, options, CTA labels, placeholders, validation and
|
|
50
|
+
error messages, loading/empty states, accessible labels, shared shell copy, and
|
|
51
|
+
paywall/legal copy. JSX renders resolved content; metadata IDs, answer keys,
|
|
52
|
+
provider IDs, prices and billing rules retain their existing owners. Provider-hosted
|
|
53
|
+
payment fields and messages are configured through that provider's locale support.
|
|
54
|
+
|
|
55
|
+
1. Keep original `*.content.ts` files at their existing locations alongside the
|
|
56
|
+
funnel's steps/shared code. They own the default English content and exported
|
|
57
|
+
content types. Run `fgrove locales add ru --dir .` to create translations under
|
|
58
|
+
`src/localization/ru/`, mirroring each original path relative to `src/`:
|
|
59
|
+
`src/steps/content/intro.content.ts` →
|
|
60
|
+
`src/localization/ru/steps/content/intro.content.ts`.
|
|
61
|
+
Use BCP 47 tags such as `pt-BR`. CLI copies source only: no translation, login,
|
|
62
|
+
sync or publication. Repeating the command preserves translated values, adds missing exports and
|
|
63
|
+
repairs missing registrations in the originals.
|
|
64
|
+
An existing inline locale is extracted with its authored text preserved.
|
|
65
|
+
2. Translate **every** generated content file, including shared shell and
|
|
66
|
+
returning-subscriber content. Each translated export imports its named type
|
|
67
|
+
with `import type` from the original file. Original definitions import these
|
|
68
|
+
translated values into their `locales` map; the reverse import is type-only.
|
|
69
|
+
Preserve keys, option IDs, placeholders such as `{amount}`, URLs and tokens.
|
|
70
|
+
Same-file inheritance from the default or requested locale refers to the
|
|
71
|
+
translated export. References to other locales require explicit copy before
|
|
72
|
+
running the command. Relative imports
|
|
73
|
+
are rebased; imported expressions remain authored references. Inspect those
|
|
74
|
+
references for remaining visitor copy before completing a translation.
|
|
75
|
+
3. Define the complete structure in an exported type in the original content file
|
|
76
|
+
and use `satisfies LocalizedStepContent<YourContentType>`. When adding a field,
|
|
77
|
+
update this type and the English content, then supply the field in **every**
|
|
78
|
+
translation. Keep required fields required; do not silence errors with casts,
|
|
79
|
+
optional fields or English spreads. Run `npx tsc --noEmit` at the funnel root:
|
|
80
|
+
an incomplete translation must fail with its file and missing field.
|
|
81
|
+
Keep `defaultLocale` and `locales` as explicit literal properties. CLI preflights
|
|
82
|
+
unsupported definitions before writing. Use named non-generic content types
|
|
83
|
+
and one default language per file. Local helpers referenced by copied
|
|
84
|
+
content (including enums, classes and type casts) must be inlined or moved to
|
|
85
|
+
independent imported modules. If a registered translation file is lost, restore
|
|
86
|
+
it before retrying; CLI cannot recover translated text from an import. A new step
|
|
87
|
+
must include every supported locale: rerun `locales add` for each language.
|
|
88
|
+
4. Resolve step copy with `usePreviewStepLocalizedContent(stepId, definition,
|
|
89
|
+
getStepContentLocale(attributes))`. Runtime versions supporting localization
|
|
90
|
+
give `?locale=ru` priority; otherwise the requested/browser language applies.
|
|
91
|
+
Lookup uses exact match, base language (`pt-BR` → `pt`), then `defaultLocale`.
|
|
92
|
+
The override persists in step URLs and never changes country, currency or offers.
|
|
93
|
+
5. Review each language with `?locale=ru`, including direct step links, reload,
|
|
94
|
+
next/back, validation errors, long text and checkout. Builder **Preview language**
|
|
95
|
+
selects languages announced by its live frame and carries the choice into the
|
|
96
|
+
external preview link. Static screenshots cannot switch language. Older
|
|
97
|
+
published runtimes need an SDK update and republish before exposing this control.
|
|
98
|
+
6. Run `fgrove validate`, sync through the funnel's normal CLI/GitHub workflow,
|
|
99
|
+
then publish preview. Existing funnels keep their own source and package pins;
|
|
100
|
+
adding a locale does not upgrade their SDK or migrate hard-coded JSX. Update
|
|
101
|
+
those explicitly and review the diff. Refresh this managed guide with `fgrove docs`.
|
|
102
|
+
|
|
103
|
+
New funnel code must route shell buttons and custom action-bar text through
|
|
104
|
+
content too. `src/steps/content/runtime.content.ts` owns the starter's shared
|
|
105
|
+
Continue/loading/unavailable messages. Server-returned display labels need
|
|
106
|
+
locale-owned presentation overrides; changing copy must not change plan IDs,
|
|
107
|
+
amounts, billing intervals or provider configuration.
|
|
108
|
+
|
|
109
|
+
The starter paywall's `catalogLabels` maps exact catalog display text to localized
|
|
110
|
+
text. Keep its keys unchanged and translate the values, preserving monetary
|
|
111
|
+
amounts. Add mappings for new admin-authored labels as needed. Unrecognized
|
|
112
|
+
catalog labels remain unchanged; translation never substitutes one offer for
|
|
113
|
+
another or alters payment identifiers and amounts.
|
|
@@ -17,7 +17,7 @@ Supported read versions: `1`, `2`, `3`. Authoring and publish target version `3`
|
|
|
17
17
|
|
|
18
18
|
### Package release order
|
|
19
19
|
|
|
20
|
-
Release `@funnelsgrove/
|
|
20
|
+
Release `@funnelsgrove/sdk` `0.4.0` first, then `@funnelsgrove/runtime` `0.19.0`, then `@funnelsgrove/analytics` `0.1.110`, then `@funnelsgrove/payments` `0.20.0`. The production deploy verifies the zero-traffic API candidate, publishes and verifies `@funnelsgrove/cli` `0.1.237`, and only then promotes the candidate to production traffic. The serving API must never advertise an unpublished preferred CLI. Publishing packages and deploying production remain separately approved operational actions.
|
|
21
21
|
<!-- funnelsgrove:generated:end contract-v3/migration/step-contract-v3 -->
|
|
22
22
|
|
|
23
23
|
## Version-last policy
|
|
@@ -8,9 +8,9 @@
|
|
|
8
8
|
"name": "funnel-template",
|
|
9
9
|
"version": "0.1.0",
|
|
10
10
|
"dependencies": {
|
|
11
|
-
"@funnelsgrove/analytics": "^0.1.
|
|
12
|
-
"@funnelsgrove/payments": "^0.
|
|
13
|
-
"@funnelsgrove/runtime": "^0.
|
|
11
|
+
"@funnelsgrove/analytics": "^0.1.106",
|
|
12
|
+
"@funnelsgrove/payments": "^0.15.11",
|
|
13
|
+
"@funnelsgrove/runtime": "^0.14.4",
|
|
14
14
|
"@stripe/react-stripe-js": "^5.6.0",
|
|
15
15
|
"@stripe/stripe-js": "^8.7.0",
|
|
16
16
|
"lucide-react": "^0.553.0",
|
|
@@ -938,20 +938,20 @@
|
|
|
938
938
|
}
|
|
939
939
|
},
|
|
940
940
|
"node_modules/@funnelsgrove/analytics": {
|
|
941
|
-
"version": "0.1.
|
|
942
|
-
"resolved": "https://registry.npmjs.org/@funnelsgrove/analytics/-/analytics-0.1.
|
|
943
|
-
"integrity": "sha512-
|
|
941
|
+
"version": "0.1.106",
|
|
942
|
+
"resolved": "https://registry.npmjs.org/@funnelsgrove/analytics/-/analytics-0.1.106.tgz",
|
|
943
|
+
"integrity": "sha512-S2hzj6C7FFz9Ui1GIE+NGMCVWnpWR81dZ4/HNSakedTSYcnP5Pu9JYtVLD+EXtiYRclF5PDgIIUbhkQxIuO8Mw==",
|
|
944
944
|
"dependencies": {
|
|
945
|
-
"@funnelsgrove/runtime": "0.
|
|
945
|
+
"@funnelsgrove/runtime": "0.14.4"
|
|
946
946
|
}
|
|
947
947
|
},
|
|
948
948
|
"node_modules/@funnelsgrove/payments": {
|
|
949
|
-
"version": "0.
|
|
950
|
-
"resolved": "https://registry.npmjs.org/@funnelsgrove/payments/-/payments-0.
|
|
951
|
-
"integrity": "sha512-
|
|
949
|
+
"version": "0.15.11",
|
|
950
|
+
"resolved": "https://registry.npmjs.org/@funnelsgrove/payments/-/payments-0.15.11.tgz",
|
|
951
|
+
"integrity": "sha512-X9k+/6NduXXQa2RTf+bbZenbckxunULCazROXsjpWcuJDkby/DdPH4Y+OZMNBpS/BIp3tbTO9z877p+7wz41/g==",
|
|
952
952
|
"dependencies": {
|
|
953
|
-
"@funnelsgrove/analytics": "
|
|
954
|
-
"@funnelsgrove/runtime": "
|
|
953
|
+
"@funnelsgrove/analytics": "0.1.106",
|
|
954
|
+
"@funnelsgrove/runtime": "0.14.4",
|
|
955
955
|
"@solidgate/react-sdk": "1.34.0",
|
|
956
956
|
"@stripe/react-stripe-js": "^5.6.0",
|
|
957
957
|
"@stripe/stripe-js": "^8.7.0",
|
|
@@ -961,9 +961,9 @@
|
|
|
961
961
|
}
|
|
962
962
|
},
|
|
963
963
|
"node_modules/@funnelsgrove/runtime": {
|
|
964
|
-
"version": "0.
|
|
965
|
-
"resolved": "https://registry.npmjs.org/@funnelsgrove/runtime/-/runtime-0.
|
|
966
|
-
"integrity": "sha512-
|
|
964
|
+
"version": "0.14.4",
|
|
965
|
+
"resolved": "https://registry.npmjs.org/@funnelsgrove/runtime/-/runtime-0.14.4.tgz",
|
|
966
|
+
"integrity": "sha512-Rloc3yKoe0mUDxiHTsFKuUt+cspzFAJHkUUkjV5Z+v79ogjW/K+1LxRG6vnwghB4QWj24Fjp5l1FKwREqqERIA==",
|
|
967
967
|
"dependencies": {
|
|
968
968
|
"posthog-js": "^1.369.2",
|
|
969
969
|
"react": "19.2.3",
|
|
@@ -12,9 +12,9 @@
|
|
|
12
12
|
"validate:funnel": "vite-node --config src/contract/funnel-validator.vite.config.ts src/contract/validate-funnel.cli.ts"
|
|
13
13
|
},
|
|
14
14
|
"dependencies": {
|
|
15
|
-
"@funnelsgrove/analytics": "^0.1.
|
|
16
|
-
"@funnelsgrove/payments": "^0.
|
|
17
|
-
"@funnelsgrove/runtime": "^0.
|
|
15
|
+
"@funnelsgrove/analytics": "^0.1.106",
|
|
16
|
+
"@funnelsgrove/payments": "^0.15.11",
|
|
17
|
+
"@funnelsgrove/runtime": "^0.14.4",
|
|
18
18
|
"@stripe/react-stripe-js": "^5.6.0",
|
|
19
19
|
"@stripe/stripe-js": "^8.7.0",
|
|
20
20
|
"lucide-react": "^0.553.0",
|
|
@@ -1,9 +1,12 @@
|
|
|
1
1
|
'use client';
|
|
2
2
|
|
|
3
3
|
import { useCallback, useEffect, useState } from 'react';
|
|
4
|
-
import { apiService, type ManageSubscriptionsResponse } from '@funnelsgrove/runtime';
|
|
4
|
+
import { apiService, usePreviewStepLocalizedContent, type ManageSubscriptionsResponse } from '@funnelsgrove/runtime';
|
|
5
|
+
|
|
6
|
+
import { subscriptionDashboardContent } from '@/steps/content/subscription-dashboard.content';
|
|
5
7
|
|
|
6
8
|
export default function ManageSubscriptionPage() {
|
|
9
|
+
const content = usePreviewStepLocalizedContent('subscription-dashboard', subscriptionDashboardContent);
|
|
7
10
|
const [data, setData] = useState<ManageSubscriptionsResponse | null>(null);
|
|
8
11
|
const [loading, setLoading] = useState(true);
|
|
9
12
|
const [error, setError] = useState<string | null>(null);
|
|
@@ -17,11 +20,11 @@ export default function ManageSubscriptionPage() {
|
|
|
17
20
|
const payload = await apiService.getManageSubscriptions();
|
|
18
21
|
setData(payload);
|
|
19
22
|
} catch (err) {
|
|
20
|
-
setError(err instanceof Error ? err.message :
|
|
23
|
+
setError(err instanceof Error ? err.message : content.loadError);
|
|
21
24
|
} finally {
|
|
22
25
|
setLoading(false);
|
|
23
26
|
}
|
|
24
|
-
}, []);
|
|
27
|
+
}, [content.loadError]);
|
|
25
28
|
|
|
26
29
|
useEffect(() => {
|
|
27
30
|
void load();
|
|
@@ -38,7 +41,7 @@ export default function ManageSubscriptionPage() {
|
|
|
38
41
|
});
|
|
39
42
|
setData(payload);
|
|
40
43
|
} catch (err) {
|
|
41
|
-
setError(err instanceof Error ? err.message :
|
|
44
|
+
setError(err instanceof Error ? err.message : (action === 'cancel' ? content.cancelError : content.renewError));
|
|
42
45
|
} finally {
|
|
43
46
|
setUpdatingSubscriptionId(null);
|
|
44
47
|
}
|
|
@@ -48,35 +51,35 @@ export default function ManageSubscriptionPage() {
|
|
|
48
51
|
<main className="manage-subscription-page">
|
|
49
52
|
<section className="manage-subscription-card">
|
|
50
53
|
<header>
|
|
51
|
-
<p className="manage-subscription-tag">
|
|
52
|
-
<h1 className="manage-subscription-title">
|
|
54
|
+
<p className="manage-subscription-tag">{content.brand}</p>
|
|
55
|
+
<h1 className="manage-subscription-title">{content.title}</h1>
|
|
53
56
|
<p className="manage-subscription-description">
|
|
54
|
-
|
|
57
|
+
{content.description}
|
|
55
58
|
</p>
|
|
56
59
|
</header>
|
|
57
60
|
|
|
58
|
-
{loading ? <p className="manage-subscription-muted">
|
|
61
|
+
{loading ? <p className="manage-subscription-muted">{content.loading}</p> : null}
|
|
59
62
|
{error ? <p className="manage-subscription-error">{error}</p> : null}
|
|
60
63
|
|
|
61
64
|
{!loading && data?.user ? (
|
|
62
65
|
<section className="manage-subscription-user">
|
|
63
66
|
<p>
|
|
64
|
-
<strong>
|
|
67
|
+
<strong>{content.userId}</strong> {data.user.user_id}
|
|
65
68
|
</p>
|
|
66
69
|
<p>
|
|
67
|
-
<strong>
|
|
70
|
+
<strong>{content.email}</strong> {data.user.email || content.notProvided}
|
|
68
71
|
</p>
|
|
69
72
|
<p>
|
|
70
|
-
<strong>
|
|
73
|
+
<strong>{content.status}</strong> {data.user.subscriptionStatus || content.unknown}
|
|
71
74
|
</p>
|
|
72
75
|
<p>
|
|
73
|
-
<strong>
|
|
76
|
+
<strong>{content.events}</strong> {data.eventsCount}
|
|
74
77
|
</p>
|
|
75
78
|
</section>
|
|
76
79
|
) : null}
|
|
77
80
|
|
|
78
81
|
{!loading && (!data || data.subscriptions.length === 0) ? (
|
|
79
|
-
<p className="manage-subscription-muted">
|
|
82
|
+
<p className="manage-subscription-muted">{content.empty}</p>
|
|
80
83
|
) : null}
|
|
81
84
|
|
|
82
85
|
{!loading && data?.subscriptions.length ? (
|
|
@@ -92,12 +95,12 @@ export default function ManageSubscriptionPage() {
|
|
|
92
95
|
</p>
|
|
93
96
|
<p className="manage-subscription-item-meta">
|
|
94
97
|
{subscription.cancelAtPeriodEnd
|
|
95
|
-
?
|
|
96
|
-
:
|
|
98
|
+
? content.cancelAtEnd
|
|
99
|
+
: content.autoRenew}
|
|
97
100
|
</p>
|
|
98
101
|
{subscription.currentPeriodEnd ? (
|
|
99
102
|
<p className="manage-subscription-item-meta">
|
|
100
|
-
|
|
103
|
+
{content.periodEnds}{' '}
|
|
101
104
|
{new Date(subscription.currentPeriodEnd).toLocaleString()}
|
|
102
105
|
</p>
|
|
103
106
|
) : null}
|
|
@@ -111,7 +114,7 @@ export default function ManageSubscriptionPage() {
|
|
|
111
114
|
disabled={updatingSubscriptionId === subscription.id}
|
|
112
115
|
onClick={() => mutate(subscription.id, 'renew')}
|
|
113
116
|
>
|
|
114
|
-
{updatingSubscriptionId === subscription.id ?
|
|
117
|
+
{updatingSubscriptionId === subscription.id ? content.updating : content.renew}
|
|
115
118
|
</button>
|
|
116
119
|
) : (
|
|
117
120
|
<button
|
|
@@ -120,7 +123,7 @@ export default function ManageSubscriptionPage() {
|
|
|
120
123
|
disabled={updatingSubscriptionId === subscription.id}
|
|
121
124
|
onClick={() => mutate(subscription.id, 'cancel')}
|
|
122
125
|
>
|
|
123
|
-
{updatingSubscriptionId === subscription.id ?
|
|
126
|
+
{updatingSubscriptionId === subscription.id ? content.updating : content.cancel}
|
|
124
127
|
</button>
|
|
125
128
|
)}
|
|
126
129
|
</div>
|
|
@@ -130,7 +133,7 @@ export default function ManageSubscriptionPage() {
|
|
|
130
133
|
) : null}
|
|
131
134
|
|
|
132
135
|
<button type="button" className="manage-subscription-refresh" onClick={() => void load()}>
|
|
133
|
-
|
|
136
|
+
{content.refresh}
|
|
134
137
|
</button>
|
|
135
138
|
</section>
|
|
136
139
|
|
|
@@ -9,8 +9,11 @@ import {
|
|
|
9
9
|
runtimePublicConfig,
|
|
10
10
|
scheduleNextStepImagePreload,
|
|
11
11
|
usePreviewDefinitionOverrideRevision,
|
|
12
|
+
usePreviewStepLocalizedContent,
|
|
12
13
|
} from '@funnelsgrove/runtime';
|
|
13
14
|
import { FunnelEditorPanel, FunnelEditorPanelStyles } from './FunnelEditorPanel';
|
|
15
|
+
import { runtimeContent } from '@/steps/content/runtime.content';
|
|
16
|
+
import { getStepContentLocale } from '@/runtime/step-content-context';
|
|
14
17
|
import { funnelManifest } from '@/config/funnel.manifest';
|
|
15
18
|
import type { FunnelStepId } from '@/runtime/funnel-runtime';
|
|
16
19
|
import { useFunnelFlowController } from '@/runtime/use-funnel-flow-controller';
|
|
@@ -23,21 +26,23 @@ type FunnelFlowProps = {
|
|
|
23
26
|
};
|
|
24
27
|
|
|
25
28
|
function RuntimeConfigLoading() {
|
|
29
|
+
const content = usePreviewStepLocalizedContent('runtime', runtimeContent, getStepContentLocale({}));
|
|
26
30
|
return (
|
|
27
|
-
<section className="runtime-config-loading" role="status" aria-label=
|
|
31
|
+
<section className="runtime-config-loading" role="status" aria-label={content.loadingLabel} aria-busy="true">
|
|
28
32
|
<span className="runtime-config-loading__progress" />
|
|
29
33
|
</section>
|
|
30
34
|
);
|
|
31
35
|
}
|
|
32
36
|
|
|
33
37
|
function RuntimeConfigStatus() {
|
|
38
|
+
const content = usePreviewStepLocalizedContent('runtime', runtimeContent, getStepContentLocale({}));
|
|
34
39
|
return (
|
|
35
40
|
<section className="runtime-config-status" role="alert" aria-live="polite">
|
|
36
41
|
<div className="runtime-config-status__content">
|
|
37
|
-
<h1>
|
|
38
|
-
<p>
|
|
42
|
+
<h1>{content.unavailableTitle}</h1>
|
|
43
|
+
<p>{content.unavailableDescription}</p>
|
|
39
44
|
<button type="button" onClick={() => window.location.reload()}>
|
|
40
|
-
|
|
45
|
+
{content.retryLabel}
|
|
41
46
|
</button>
|
|
42
47
|
</div>
|
|
43
48
|
</section>
|
|
@@ -88,6 +93,7 @@ function FunnelFlowRuntime({
|
|
|
88
93
|
previewFrameMounted: boolean;
|
|
89
94
|
}) {
|
|
90
95
|
const previewDefinitionRevision = usePreviewDefinitionOverrideRevision();
|
|
96
|
+
const content = usePreviewStepLocalizedContent('runtime', runtimeContent, getStepContentLocale({}));
|
|
91
97
|
const controller = useFunnelFlowController({
|
|
92
98
|
initialStepId,
|
|
93
99
|
lockToInitialStep,
|
|
@@ -144,7 +150,7 @@ function FunnelFlowRuntime({
|
|
|
144
150
|
onClick={controller.handleContinue}
|
|
145
151
|
className={controller.buttonVariant === 'muted' ? 'is-muted' : undefined}
|
|
146
152
|
>
|
|
147
|
-
{controller.buttonText}
|
|
153
|
+
{controller.activeStepMeta.actionBar?.buttonText ? controller.buttonText : content.continueLabel}
|
|
148
154
|
</PrimaryButton>
|
|
149
155
|
</footer>
|
|
150
156
|
) : null}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import type { PaywallPlan } from '@funnelsgrove/payments';
|
|
2
|
+
import type { StepPaywallLocaleContent } from '@/steps/content/step-32-paywall.content';
|
|
3
|
+
|
|
4
|
+
type Props = {
|
|
5
|
+
plan: Pick<PaywallPlan, 'priceLabel' | 'billingInterval' | 'billingIntervalCount'> | null;
|
|
6
|
+
hasActiveDiscount: boolean;
|
|
7
|
+
copy: StepPaywallLocaleContent['checkout'];
|
|
8
|
+
catalogLabels: Record<string, string>;
|
|
9
|
+
};
|
|
10
|
+
|
|
11
|
+
export function PaywallRenewalDisclaimer({ plan, hasActiveDiscount, copy, catalogLabels }: Props) {
|
|
12
|
+
if (!plan) return <p className='paywall-v2-disclaimer' />;
|
|
13
|
+
const count = Math.max(1, plan.billingIntervalCount || 1);
|
|
14
|
+
const cadenceCopy = plan.billingInterval ? copy.cadence[plan.billingInterval] : null;
|
|
15
|
+
const cadence = cadenceCopy
|
|
16
|
+
? (count === 1 ? cadenceCopy.one : cadenceCopy.many).replace('{count}', String(count))
|
|
17
|
+
: copy.cadenceFallback;
|
|
18
|
+
const text = copy.renewalConsentTemplate
|
|
19
|
+
.replace('{intro}', hasActiveDiscount ? copy.discountedIntroLabel : copy.selectedIntroLabel)
|
|
20
|
+
.replace('{amount}', catalogLabels[plan.priceLabel] ?? plan.priceLabel)
|
|
21
|
+
.replace('{cadence}', cadence);
|
|
22
|
+
return <p className='paywall-v2-disclaimer'>{text}</p>;
|
|
23
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import type { PaywallDisplayPlan } from '@funnelsgrove/payments';
|
|
2
|
+
|
|
3
|
+
/** Translate recognized display labels while retaining arbitrary catalog copy and payment data. */
|
|
4
|
+
export function localizePaywallPlanLabels(plan: PaywallDisplayPlan, labels: Record<string, string>): PaywallDisplayPlan {
|
|
5
|
+
const translate = (value: string) => typeof labels[value] === 'string' ? labels[value] : value;
|
|
6
|
+
return {
|
|
7
|
+
...plan,
|
|
8
|
+
title: translate(plan.title),
|
|
9
|
+
priceLabel: translate(plan.priceLabel),
|
|
10
|
+
basePriceLabel: translate(plan.basePriceLabel),
|
|
11
|
+
oldPriceLabel: plan.oldPriceLabel ? translate(plan.oldPriceLabel) : plan.oldPriceLabel,
|
|
12
|
+
checkoutSummaryLabel: plan.checkoutSummaryLabel ? translate(plan.checkoutSummaryLabel) : plan.checkoutSummaryLabel,
|
|
13
|
+
featuredTag: plan.featuredTag ? translate(plan.featuredTag) : plan.featuredTag,
|
|
14
|
+
};
|
|
15
|
+
}
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { stepPaywallContent } from '@/steps/content/step-32-paywall.content';
|
|
1
2
|
import {
|
|
2
3
|
buildBillingDiscountCatalog,
|
|
3
4
|
createRuntimeModeBillingPlanCatalog,
|
|
@@ -19,36 +20,36 @@ const starterLivePlans = {
|
|
|
19
20
|
offerSetId: STARTER_OFFER_SET_ID,
|
|
20
21
|
offerSetKey: STARTER_OFFER_SET_KEY,
|
|
21
22
|
projectPlanId: '32b31f69-5907-4736-89c7-76d3acb38813',
|
|
22
|
-
title:
|
|
23
|
+
title: stepPaywallContent.locales.en.catalogLabels["1-Week Plan"],
|
|
23
24
|
providerPlanId: 'price_1T9WL2PPRN41Ty94g2NNOgif',
|
|
24
|
-
priceLabel:
|
|
25
|
+
priceLabel: stepPaywallContent.locales.en.catalogLabels["$7.99/week"],
|
|
25
26
|
perDayAmount: '$1.14',
|
|
26
27
|
amountCents: 799,
|
|
27
|
-
checkoutSummaryLabel:
|
|
28
|
+
checkoutSummaryLabel: stepPaywallContent.locales.en.catalogLabels["$7.99 today"],
|
|
28
29
|
},
|
|
29
30
|
secondary: {
|
|
30
31
|
offerSetId: STARTER_OFFER_SET_ID,
|
|
31
32
|
offerSetKey: STARTER_OFFER_SET_KEY,
|
|
32
33
|
projectPlanId: 'ce0a0022-7bf0-41f2-a707-edc5e6113a07',
|
|
33
|
-
title:
|
|
34
|
+
title: stepPaywallContent.locales.en.catalogLabels["1-Month Plan"],
|
|
34
35
|
providerPlanId: 'price_1TP0IIPPRN41Ty94npkzVeDR',
|
|
35
|
-
featuredTag:
|
|
36
|
-
priceLabel:
|
|
36
|
+
featuredTag: stepPaywallContent.locales.en.catalogLabels["🔥 Most Popular Choice"],
|
|
37
|
+
priceLabel: stepPaywallContent.locales.en.catalogLabels["$29.99/month"],
|
|
37
38
|
perDayAmount: '$1.00',
|
|
38
39
|
amountCents: 2999,
|
|
39
|
-
checkoutSummaryLabel:
|
|
40
|
+
checkoutSummaryLabel: stepPaywallContent.locales.en.catalogLabels["$29.99 today"],
|
|
40
41
|
isDefault: true,
|
|
41
42
|
},
|
|
42
43
|
tertiary: {
|
|
43
44
|
offerSetId: STARTER_OFFER_SET_ID,
|
|
44
45
|
offerSetKey: STARTER_OFFER_SET_KEY,
|
|
45
46
|
projectPlanId: 'fb623ebc-c2fd-4e6a-8eb6-b8ed92db1b21',
|
|
46
|
-
title:
|
|
47
|
+
title: stepPaywallContent.locales.en.catalogLabels["1-Year Plan"],
|
|
47
48
|
providerPlanId: 'price_1T06AEPPRN41Ty94zvVt5xrk',
|
|
48
|
-
priceLabel:
|
|
49
|
+
priceLabel: stepPaywallContent.locales.en.catalogLabels["$59.99/year"],
|
|
49
50
|
perDayAmount: '$0.16',
|
|
50
51
|
amountCents: 5999,
|
|
51
|
-
checkoutSummaryLabel:
|
|
52
|
+
checkoutSummaryLabel: stepPaywallContent.locales.en.catalogLabels["$59.99 today"],
|
|
52
53
|
},
|
|
53
54
|
} satisfies BillingPlanCatalog;
|
|
54
55
|
|
|
@@ -1,32 +1,33 @@
|
|
|
1
|
+
import { stepPaywallContent } from '@/steps/content/step-32-paywall.content';
|
|
1
2
|
import type { BillingPlanCatalog } from '@funnelsgrove/payments';
|
|
2
3
|
|
|
3
4
|
export const billingTestPlans = {
|
|
4
5
|
primary: {
|
|
5
6
|
projectPlanId: '32b31f69-5907-4736-89c7-76d3acb38813',
|
|
6
|
-
title:
|
|
7
|
+
title: stepPaywallContent.locales.en.catalogLabels["1-Week Plan"],
|
|
7
8
|
providerPlanId: 'price_1TOQ2fPEfkEVo5khumapOSsI',
|
|
8
|
-
priceLabel:
|
|
9
|
+
priceLabel: stepPaywallContent.locales.en.catalogLabels["$9.99/week"],
|
|
9
10
|
perDayAmount: '$1.43',
|
|
10
11
|
amountCents: 999,
|
|
11
|
-
checkoutSummaryLabel:
|
|
12
|
+
checkoutSummaryLabel: stepPaywallContent.locales.en.catalogLabels["$9.99 today"],
|
|
12
13
|
},
|
|
13
14
|
secondary: {
|
|
14
15
|
projectPlanId: 'ce0a0022-7bf0-41f2-a707-edc5e6113a07',
|
|
15
|
-
title:
|
|
16
|
+
title: stepPaywallContent.locales.en.catalogLabels["1-Month Plan"],
|
|
16
17
|
providerPlanId: 'price_1TSgPZPEfkEVo5khMbtAlqsG',
|
|
17
|
-
featuredTag:
|
|
18
|
-
priceLabel:
|
|
18
|
+
featuredTag: stepPaywallContent.locales.en.catalogLabels["🔥 Most Popular Choice"],
|
|
19
|
+
priceLabel: stepPaywallContent.locales.en.catalogLabels["$29.99/month"],
|
|
19
20
|
perDayAmount: '$1.00',
|
|
20
21
|
amountCents: 2999,
|
|
21
|
-
checkoutSummaryLabel:
|
|
22
|
+
checkoutSummaryLabel: stepPaywallContent.locales.en.catalogLabels["$29.99 today"],
|
|
22
23
|
},
|
|
23
24
|
tertiary: {
|
|
24
25
|
projectPlanId: 'fb623ebc-c2fd-4e6a-8eb6-b8ed92db1b21',
|
|
25
|
-
title:
|
|
26
|
+
title: stepPaywallContent.locales.en.catalogLabels["1-Year Plan"],
|
|
26
27
|
providerPlanId: 'price_1TBgcaPEfkEVo5khbGnGhrBA',
|
|
27
|
-
priceLabel:
|
|
28
|
+
priceLabel: stepPaywallContent.locales.en.catalogLabels["$59.99/year"],
|
|
28
29
|
perDayAmount: '$0.16',
|
|
29
30
|
amountCents: 5999,
|
|
30
|
-
checkoutSummaryLabel:
|
|
31
|
+
checkoutSummaryLabel: stepPaywallContent.locales.en.catalogLabels["$59.99 today"],
|
|
31
32
|
},
|
|
32
33
|
} satisfies BillingPlanCatalog;
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import type { LocalizedStepContent } from '@funnelsgrove/runtime';
|
|
2
|
+
|
|
3
|
+
export type RuntimeContent = {
|
|
4
|
+
loadingLabel: string;
|
|
5
|
+
unavailableTitle: string;
|
|
6
|
+
unavailableDescription: string;
|
|
7
|
+
retryLabel: string;
|
|
8
|
+
continueLabel: string;
|
|
9
|
+
};
|
|
10
|
+
|
|
11
|
+
export const runtimeContent = {
|
|
12
|
+
defaultLocale: 'en',
|
|
13
|
+
locales: {
|
|
14
|
+
en: {
|
|
15
|
+
loadingLabel: 'Loading',
|
|
16
|
+
unavailableTitle: 'This experience is temporarily unavailable',
|
|
17
|
+
unavailableDescription: 'Please check your connection and try again.',
|
|
18
|
+
retryLabel: 'Try again',
|
|
19
|
+
continueLabel: 'Continue',
|
|
20
|
+
},
|
|
21
|
+
},
|
|
22
|
+
} as const satisfies LocalizedStepContent<RuntimeContent>;
|
|
@@ -7,11 +7,15 @@ import type {
|
|
|
7
7
|
StepImage,
|
|
8
8
|
} from '@funnelsgrove/runtime';
|
|
9
9
|
|
|
10
|
-
type StepPaywallLocaleContent = {
|
|
10
|
+
export type StepPaywallLocaleContent = {
|
|
11
|
+
catalogLabels: Record<string, string>;
|
|
11
12
|
loadingLabel: string;
|
|
12
13
|
topBarDiscountLabel: string;
|
|
13
14
|
topBarCtaLabel: string;
|
|
14
15
|
countdownUnits: readonly string[];
|
|
16
|
+
countdownAriaTemplate: string;
|
|
17
|
+
selectedPlanLabel: string;
|
|
18
|
+
refundPolicyLabel: string;
|
|
15
19
|
hero: {
|
|
16
20
|
pills: readonly string[];
|
|
17
21
|
image: StepImage;
|
|
@@ -30,6 +34,7 @@ type StepPaywallLocaleContent = {
|
|
|
30
34
|
cardButtonLabel: string;
|
|
31
35
|
metaLabels: readonly string[];
|
|
32
36
|
stripeUnavailableNote: string;
|
|
37
|
+
perDayLabel: string;
|
|
33
38
|
};
|
|
34
39
|
checkout: {
|
|
35
40
|
title: string;
|
|
@@ -67,6 +72,11 @@ type StepPaywallLocaleContent = {
|
|
|
67
72
|
legalLinks: readonly LinkItem[];
|
|
68
73
|
supportText: string;
|
|
69
74
|
renewalTemplate: string;
|
|
75
|
+
renewalConsentTemplate: string;
|
|
76
|
+
discountedIntroLabel: string;
|
|
77
|
+
selectedIntroLabel: string;
|
|
78
|
+
cadenceFallback: string;
|
|
79
|
+
cadence: Record<'day' | 'week' | 'month' | 'year', { one: string; many: string }>;
|
|
70
80
|
specialOffer: {
|
|
71
81
|
image: StepImage;
|
|
72
82
|
discountLabel: string;
|
|
@@ -94,6 +104,7 @@ type StepPaywallLocaleContent = {
|
|
|
94
104
|
title: string;
|
|
95
105
|
description: string;
|
|
96
106
|
links: readonly LinkItem[];
|
|
107
|
+
detailsSuffix: string;
|
|
97
108
|
};
|
|
98
109
|
};
|
|
99
110
|
|
|
@@ -101,10 +112,27 @@ export const stepPaywallContent = {
|
|
|
101
112
|
defaultLocale: 'en',
|
|
102
113
|
locales: {
|
|
103
114
|
en: {
|
|
115
|
+
catalogLabels: {
|
|
116
|
+
"1-Week Plan": "1-Week Plan",
|
|
117
|
+
"1-Month Plan": "1-Month Plan",
|
|
118
|
+
"1-Year Plan": "1-Year Plan",
|
|
119
|
+
"🔥 Most Popular Choice": "🔥 Most Popular Choice",
|
|
120
|
+
"$7.99/week": "$7.99/week",
|
|
121
|
+
"$9.99/week": "$9.99/week",
|
|
122
|
+
"$29.99/month": "$29.99/month",
|
|
123
|
+
"$59.99/year": "$59.99/year",
|
|
124
|
+
"$7.99 today": "$7.99 today",
|
|
125
|
+
"$9.99 today": "$9.99 today",
|
|
126
|
+
"$29.99 today": "$29.99 today",
|
|
127
|
+
"$59.99 today": "$59.99 today",
|
|
128
|
+
},
|
|
104
129
|
loadingLabel: 'Loading...',
|
|
105
130
|
topBarDiscountLabel: '⏰ Discount Expires in',
|
|
106
131
|
topBarCtaLabel: 'GET MY PLAN',
|
|
107
132
|
countdownUnits: ['minutes', 'seconds'],
|
|
133
|
+
countdownAriaTemplate: 'Time left {minutes} minutes {seconds} seconds',
|
|
134
|
+
selectedPlanLabel: 'Selected plan',
|
|
135
|
+
refundPolicyLabel: 'refund policy',
|
|
108
136
|
hero: {
|
|
109
137
|
pills: ['Find hidden money and', "claim what's yours"],
|
|
110
138
|
image: {
|
|
@@ -124,6 +152,7 @@ export const stepPaywallContent = {
|
|
|
124
152
|
},
|
|
125
153
|
plans: {
|
|
126
154
|
title: 'Choose your plan',
|
|
155
|
+
perDayLabel: 'per day',
|
|
127
156
|
cardButtonLabel: 'GET MY PLAN',
|
|
128
157
|
metaLabels: ['Money-back guarantee', 'Cancel anytime'],
|
|
129
158
|
stripeUnavailableNote:
|
|
@@ -180,6 +209,16 @@ export const stepPaywallContent = {
|
|
|
180
209
|
},
|
|
181
210
|
],
|
|
182
211
|
supportText: 'Need help? Contact us at',
|
|
212
|
+
renewalConsentTemplate: 'Without cancellation, before the {intro} ends, I accept that the app will automatically charge {amount} {cadence} until I cancel. Cancel online via the profile on the website or app.',
|
|
213
|
+
discountedIntroLabel: 'discounted intro plan',
|
|
214
|
+
selectedIntroLabel: 'selected plan',
|
|
215
|
+
cadenceFallback: 'on your selected billing schedule',
|
|
216
|
+
cadence: {
|
|
217
|
+
day: { one: 'every day', many: 'every {count} days' },
|
|
218
|
+
week: { one: 'every week', many: 'every {count} weeks' },
|
|
219
|
+
month: { one: 'every month', many: 'every {count} months' },
|
|
220
|
+
year: { one: 'every year', many: 'every {count} years' },
|
|
221
|
+
},
|
|
183
222
|
renewalTemplate:
|
|
184
223
|
"You'll be charged {amount} today for your selected plan. Your subscription renews automatically unless cancelled at least 24 hours before renewal date.",
|
|
185
224
|
specialOffer: {
|
|
@@ -302,6 +341,7 @@ export const stepPaywallContent = {
|
|
|
302
341
|
],
|
|
303
342
|
},
|
|
304
343
|
guarantee: {
|
|
344
|
+
detailsSuffix: 'for details and limitations.',
|
|
305
345
|
image: {
|
|
306
346
|
src: '/paywall/money-back-badge.jpg',
|
|
307
347
|
alt: '30 day money back guarantee badge',
|