mirror of
https://github.com/rcourtman/Pulse.git
synced 2026-04-28 03:20:11 +00:00
Make Cloud trial signup copy explicit
This commit is contained in:
parent
3ffdf785f1
commit
6cc4659e75
8 changed files with 65 additions and 33 deletions
|
|
@ -234,6 +234,11 @@ Community limit enforcement.
|
|||
surfaces must describe the real commercial flow as secure checkout ->
|
||||
Pulse Account -> open workspace, not as an immediate workspace creation or
|
||||
trial-only shortcut.
|
||||
Public hosted Cloud trial signup must state the trial duration and
|
||||
checkout economics before Stripe handoff: Stripe may collect a payment
|
||||
method, but the subscription starts with the configured trial period and no
|
||||
upfront charge, then Pulse Account opens the provisioned workspace after
|
||||
checkout completes.
|
||||
20. Add contract tests where runtime and pricing need to stay aligned
|
||||
21. Add or change hosted browser org-context bootstrap through `frontend-modern/src/App.tsx`, `frontend-modern/src/AppLayout.tsx`, `frontend-modern/src/useAppRuntimeState.ts`, and `frontend-modern/src/utils/apiClient.ts`
|
||||
That same hosted bootstrap boundary also owns the runtime-capability JSON
|
||||
|
|
|
|||
|
|
@ -69,7 +69,7 @@ function CloudTierCard(props: { tier: CloudPlanDefinition }) {
|
|||
href={`/cloud/signup?tier=${t.tier}`}
|
||||
class="w-full inline-flex items-center justify-center rounded-md bg-blue-600 px-4 py-2 text-sm font-semibold text-white transition-colors hover:bg-blue-700"
|
||||
>
|
||||
{`Choose ${t.name}`}
|
||||
{`Start ${t.name} Trial`}
|
||||
</A>
|
||||
</div>
|
||||
</Card>
|
||||
|
|
|
|||
|
|
@ -20,25 +20,27 @@ describe('CloudPricing', () => {
|
|||
</Router>
|
||||
));
|
||||
|
||||
expect(await screen.findByRole('link', { name: 'Choose Starter' })).toHaveAttribute(
|
||||
expect(await screen.findByRole('link', { name: 'Start Starter Trial' })).toHaveAttribute(
|
||||
'href',
|
||||
'/cloud/signup?tier=starter',
|
||||
);
|
||||
expect(screen.getByText('Founding rate')).toBeInTheDocument();
|
||||
expect(screen.getByText('$19')).toBeInTheDocument();
|
||||
expect(screen.getByText('$29/month')).toBeInTheDocument();
|
||||
expect(screen.getByRole('link', { name: 'Choose Power' })).toHaveAttribute(
|
||||
expect(screen.getByRole('link', { name: 'Start Power Trial' })).toHaveAttribute(
|
||||
'href',
|
||||
'/cloud/signup?tier=power',
|
||||
);
|
||||
expect(screen.getByRole('link', { name: 'Choose Max' })).toHaveAttribute(
|
||||
expect(screen.getByRole('link', { name: 'Start Max Trial' })).toHaveAttribute(
|
||||
'href',
|
||||
'/cloud/signup?tier=max',
|
||||
);
|
||||
expect(screen.queryByText('Starter founding rate')).not.toBeInTheDocument();
|
||||
expect(screen.getAllByText('All Pro features')).toHaveLength(1);
|
||||
expect(screen.getByText('Managed hosting')).toBeInTheDocument();
|
||||
expect(screen.getByText('Choose a Cloud plan and complete secure checkout.')).toBeInTheDocument();
|
||||
expect(
|
||||
screen.getByText('Choose a Cloud plan and start the 14-day trial in secure checkout.'),
|
||||
).toBeInTheDocument();
|
||||
expect(screen.getByText('How it works')).toBeInTheDocument();
|
||||
expect(screen.getByRole('link', { name: 'Open Pulse Account' })).toHaveAttribute(
|
||||
'href',
|
||||
|
|
|
|||
|
|
@ -60,10 +60,14 @@ describe('HostedSignup', () => {
|
|||
));
|
||||
|
||||
expect(await screen.findByText('Workspace')).toBeInTheDocument();
|
||||
expect(screen.getByText('Create your Pulse Cloud account and hosted workspace.')).toBeInTheDocument();
|
||||
expect(
|
||||
screen.getByText('Start your 14-day Pulse Cloud trial and hosted workspace.'),
|
||||
).toBeInTheDocument();
|
||||
expect(screen.getByText('Plan')).toBeInTheDocument();
|
||||
expect(screen.getByText('How it works')).toBeInTheDocument();
|
||||
expect(screen.getByText('Choose a Cloud plan and complete secure checkout.')).toBeInTheDocument();
|
||||
expect(
|
||||
screen.getByText('Choose a Cloud plan and start the 14-day trial in secure checkout.'),
|
||||
).toBeInTheDocument();
|
||||
expect(screen.getByText('Already signed up?')).toBeInTheDocument();
|
||||
expect(screen.getByText('Request a fresh Pulse Account sign-in link.')).toBeInTheDocument();
|
||||
expect(screen.getByRole('button', { name: 'Email Pulse Account Link' })).toBeInTheDocument();
|
||||
|
|
@ -74,7 +78,7 @@ describe('HostedSignup', () => {
|
|||
fireEvent.input(screen.getByLabelText('Organization Name'), {
|
||||
target: { value: 'Pulse Labs' },
|
||||
});
|
||||
fireEvent.submit(screen.getByRole('button', { name: 'Continue to Checkout' }).closest('form')!);
|
||||
fireEvent.submit(screen.getByRole('button', { name: 'Start Trial in Checkout' }).closest('form')!);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(signupMock).toHaveBeenCalledWith({
|
||||
|
|
|
|||
|
|
@ -32,7 +32,7 @@ describe('cloudPlans', () => {
|
|||
it('keeps shared cloud commercial copy in the common contract', () => {
|
||||
expect(CLOUD_COMMERCIAL_PRESENTATION).toEqual({
|
||||
pageTitle: 'Pulse Cloud',
|
||||
pageDescription: 'Managed Pulse hosting with Pro features included.',
|
||||
pageDescription: 'Managed Pulse hosting with Pro features included. Start with a 14-day trial.',
|
||||
includedInAllHeading: 'Included in every Cloud plan',
|
||||
includedInAllItems: [
|
||||
'All Pro features',
|
||||
|
|
@ -50,15 +50,15 @@ describe('cloudPlans', () => {
|
|||
it('keeps hosted signup commercial copy in the common contract', () => {
|
||||
expect(HOSTED_SIGNUP_PRESENTATION).toEqual({
|
||||
pageTitlePrefix: 'Pulse Cloud',
|
||||
pageDescription: 'Create your Pulse Cloud account and hosted workspace.',
|
||||
pageDescription: 'Start your 14-day Pulse Cloud trial and hosted workspace.',
|
||||
workspaceHeading: 'Workspace',
|
||||
planHeading: 'Plan',
|
||||
nextHeading: 'How it works',
|
||||
nextSteps: CLOUD_ACCOUNT_FLOW_STEPS,
|
||||
existingAccountHeading: 'Already signed up?',
|
||||
existingAccountDescription: 'Request a fresh Pulse Account sign-in link.',
|
||||
createWorkspaceLabel: 'Continue to Checkout',
|
||||
creatingWorkspaceLabel: 'Preparing Checkout...',
|
||||
createWorkspaceLabel: 'Start Trial in Checkout',
|
||||
creatingWorkspaceLabel: 'Preparing Trial Checkout...',
|
||||
emailSignInLinkLabel: 'Email Pulse Account Link',
|
||||
sendingSignInLinkLabel: 'Sending...',
|
||||
});
|
||||
|
|
|
|||
|
|
@ -46,7 +46,7 @@ export interface HostedSignupPresentation {
|
|||
}
|
||||
|
||||
export const CLOUD_ACCOUNT_FLOW_STEPS = [
|
||||
'Choose a Cloud plan and complete secure checkout.',
|
||||
'Choose a Cloud plan and start the 14-day trial in secure checkout.',
|
||||
'Use the email link to open Pulse Account.',
|
||||
'Open your workspace and connect systems.',
|
||||
] as const;
|
||||
|
|
@ -104,7 +104,7 @@ export const CLOUD_PLAN_LABELS: Record<string, string> = {
|
|||
|
||||
export const CLOUD_COMMERCIAL_PRESENTATION: CloudCommercialPresentation = {
|
||||
pageTitle: 'Pulse Cloud',
|
||||
pageDescription: 'Managed Pulse hosting with Pro features included.',
|
||||
pageDescription: 'Managed Pulse hosting with Pro features included. Start with a 14-day trial.',
|
||||
includedInAllHeading: 'Included in every Cloud plan',
|
||||
includedInAllItems: [
|
||||
'All Pro features',
|
||||
|
|
@ -120,15 +120,15 @@ export const CLOUD_COMMERCIAL_PRESENTATION: CloudCommercialPresentation = {
|
|||
|
||||
export const HOSTED_SIGNUP_PRESENTATION: HostedSignupPresentation = {
|
||||
pageTitlePrefix: 'Pulse Cloud',
|
||||
pageDescription: 'Create your Pulse Cloud account and hosted workspace.',
|
||||
pageDescription: 'Start your 14-day Pulse Cloud trial and hosted workspace.',
|
||||
workspaceHeading: 'Workspace',
|
||||
planHeading: 'Plan',
|
||||
nextHeading: 'How it works',
|
||||
nextSteps: CLOUD_ACCOUNT_FLOW_STEPS,
|
||||
existingAccountHeading: 'Already signed up?',
|
||||
existingAccountDescription: 'Request a fresh Pulse Account sign-in link.',
|
||||
createWorkspaceLabel: 'Continue to Checkout',
|
||||
creatingWorkspaceLabel: 'Preparing Checkout...',
|
||||
createWorkspaceLabel: 'Start Trial in Checkout',
|
||||
creatingWorkspaceLabel: 'Preparing Trial Checkout...',
|
||||
emailSignInLinkLabel: 'Email Pulse Account Link',
|
||||
sendingSignInLinkLabel: 'Sending...',
|
||||
} as const;
|
||||
|
|
|
|||
|
|
@ -58,7 +58,7 @@ var publicCloudSignupPageTemplate = template.Must(template.New("public-cloud-sig
|
|||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<title>Start Pulse Cloud</title>
|
||||
<title>Start Pulse Cloud Trial</title>
|
||||
<style nonce="{{.Nonce}}">
|
||||
:root { color-scheme: light; }
|
||||
body { margin: 0; font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif; background: linear-gradient(140deg, #f8fafc, #e2e8f0); color: #0f172a; }
|
||||
|
|
@ -83,8 +83,8 @@ var publicCloudSignupPageTemplate = template.Must(template.New("public-cloud-sig
|
|||
<body>
|
||||
<div class="wrap">
|
||||
<div class="card">
|
||||
<h1>Start Pulse Cloud</h1>
|
||||
<p>Create your hosted Pulse workspace. Checkout is secure and provisioning starts automatically after payment confirmation.</p>
|
||||
<h1>Start your {{.TrialDays}}-day Pulse Cloud trial</h1>
|
||||
<p>Create your hosted Pulse workspace. Stripe checkout securely collects a payment method, but the Cloud subscription starts with a {{.TrialDays}}-day trial and no upfront charge. Provisioning starts after checkout confirms the subscription.</p>
|
||||
{{if .ErrorMessage}}<div class="error">{{.ErrorMessage}}</div>{{end}}
|
||||
{{if .Cancelled}}<div class="note">Checkout was cancelled. You can start again below.</div>{{end}}
|
||||
|
||||
|
|
@ -94,9 +94,9 @@ var publicCloudSignupPageTemplate = template.Must(template.New("public-cloud-sig
|
|||
{{if or .HasPower .HasMax}}
|
||||
<label>Plan</label>
|
||||
<div class="tier-group">
|
||||
<label class="tier-option"><input type="radio" name="tier" value="starter" {{if eq .Tier "starter"}}checked{{end}}> <strong>Starter</strong> — 10 hosts, from $29/mo</label>
|
||||
{{if .HasPower}}<label class="tier-option"><input type="radio" name="tier" value="power" {{if eq .Tier "power"}}checked{{end}}> <strong>Power</strong> — 30 hosts, from $49/mo</label>{{end}}
|
||||
{{if .HasMax}}<label class="tier-option"><input type="radio" name="tier" value="max" {{if eq .Tier "max"}}checked{{end}}> <strong>Max</strong> — 75 hosts, from $79/mo</label>{{end}}
|
||||
<label class="tier-option"><input type="radio" name="tier" value="starter" {{if eq .Tier "starter"}}checked{{end}}> <strong>Starter</strong> — 10 monitored systems, $29/mo after trial</label>
|
||||
{{if .HasPower}}<label class="tier-option"><input type="radio" name="tier" value="power" {{if eq .Tier "power"}}checked{{end}}> <strong>Power</strong> — 30 monitored systems, $49/mo after trial</label>{{end}}
|
||||
{{if .HasMax}}<label class="tier-option"><input type="radio" name="tier" value="max" {{if eq .Tier "max"}}checked{{end}}> <strong>Max</strong> — 75 monitored systems, $79/mo after trial</label>{{end}}
|
||||
</div>
|
||||
{{else}}
|
||||
<input type="hidden" name="tier" value="starter">
|
||||
|
|
@ -113,7 +113,7 @@ var publicCloudSignupPageTemplate = template.Must(template.New("public-cloud-sig
|
|||
|
||||
<p class="fine">After checkout, we will email a Pulse Account sign-in link so you can open your hosted workspace.</p>
|
||||
<ol>
|
||||
<li>Your Stripe checkout completes securely.</li>
|
||||
<li>Stripe starts your {{.TrialDays}}-day Cloud trial securely.</li>
|
||||
<li>Pulse Cloud provisions your hosted workspace.</li>
|
||||
<li>The email link opens Pulse Account, where you open your workspace and continue setup.</li>
|
||||
</ol>
|
||||
|
|
@ -128,7 +128,7 @@ var publicCloudSignupCompleteTemplate = template.Must(template.New("public-cloud
|
|||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<title>Pulse Cloud Checkout Complete</title>
|
||||
<title>Pulse Cloud Trial Checkout Complete</title>
|
||||
<style nonce="{{.Nonce}}">
|
||||
:root { color-scheme: light; }
|
||||
body { margin: 0; font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif; background: #f8fafc; color: #0f172a; }
|
||||
|
|
@ -141,8 +141,8 @@ var publicCloudSignupCompleteTemplate = template.Must(template.New("public-cloud
|
|||
<body>
|
||||
<div class="wrap">
|
||||
<div class="card">
|
||||
<h1>Checkout Complete</h1>
|
||||
<p>Your checkout completed. Pulse Cloud is provisioning your hosted workspace.</p>
|
||||
<h1>Trial checkout complete</h1>
|
||||
<p>Your {{.TrialDays}}-day Pulse Cloud trial checkout completed. Pulse Cloud is provisioning your hosted workspace.</p>
|
||||
<p>Watch your inbox for a Pulse Account sign-in link. That link lands in Pulse Account, where you can open your workspace and continue setup.</p>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -172,11 +172,13 @@ type publicCloudSignupPageData struct {
|
|||
Nonce string
|
||||
HasPower bool // true if Cloud Power price is configured
|
||||
HasMax bool // true if Cloud Max price is configured
|
||||
TrialDays int
|
||||
}
|
||||
|
||||
// publicCloudSignupCompleteData carries nonce for the signup-complete page.
|
||||
type publicCloudSignupCompleteData struct {
|
||||
Nonce string
|
||||
Nonce string
|
||||
TrialDays int
|
||||
}
|
||||
|
||||
type publicCloudSignupRequest struct {
|
||||
|
|
@ -269,6 +271,7 @@ func (h *PublicCloudSignupHandlers) HandleSignupPage(w http.ResponseWriter, r *h
|
|||
Cancelled: strings.EqualFold(strings.TrimSpace(r.URL.Query().Get("cancelled")), "1"),
|
||||
HasPower: h.cfg != nil && strings.TrimSpace(h.cfg.CloudPowerPriceID) != "",
|
||||
HasMax: h.cfg != nil && strings.TrimSpace(h.cfg.CloudMaxPriceID) != "",
|
||||
TrialDays: trialSignupTrialDays,
|
||||
}
|
||||
h.renderSignupPage(w, r, http.StatusOK, data)
|
||||
case http.MethodPost:
|
||||
|
|
@ -290,6 +293,7 @@ func (h *PublicCloudSignupHandlers) HandleSignupPage(w http.ResponseWriter, r *h
|
|||
ErrorMessage: "Invalid plan tier selected.",
|
||||
HasPower: h.cfg != nil && strings.TrimSpace(h.cfg.CloudPowerPriceID) != "",
|
||||
HasMax: h.cfg != nil && strings.TrimSpace(h.cfg.CloudMaxPriceID) != "",
|
||||
TrialDays: trialSignupTrialDays,
|
||||
})
|
||||
return
|
||||
}
|
||||
|
|
@ -303,6 +307,7 @@ func (h *PublicCloudSignupHandlers) HandleSignupPage(w http.ResponseWriter, r *h
|
|||
ErrorMessage: "A valid email address is required.",
|
||||
HasPower: h.cfg != nil && strings.TrimSpace(h.cfg.CloudPowerPriceID) != "",
|
||||
HasMax: h.cfg != nil && strings.TrimSpace(h.cfg.CloudMaxPriceID) != "",
|
||||
TrialDays: trialSignupTrialDays,
|
||||
})
|
||||
return
|
||||
}
|
||||
|
|
@ -315,6 +320,7 @@ func (h *PublicCloudSignupHandlers) HandleSignupPage(w http.ResponseWriter, r *h
|
|||
ErrorMessage: "Organization name must be 3-64 characters and cannot contain slashes.",
|
||||
HasPower: h.cfg != nil && strings.TrimSpace(h.cfg.CloudPowerPriceID) != "",
|
||||
HasMax: h.cfg != nil && strings.TrimSpace(h.cfg.CloudMaxPriceID) != "",
|
||||
TrialDays: trialSignupTrialDays,
|
||||
})
|
||||
return
|
||||
}
|
||||
|
|
@ -327,6 +333,7 @@ func (h *PublicCloudSignupHandlers) HandleSignupPage(w http.ResponseWriter, r *h
|
|||
ErrorMessage: "The selected plan tier is not currently available.",
|
||||
HasPower: h.cfg != nil && strings.TrimSpace(h.cfg.CloudPowerPriceID) != "",
|
||||
HasMax: h.cfg != nil && strings.TrimSpace(h.cfg.CloudMaxPriceID) != "",
|
||||
TrialDays: trialSignupTrialDays,
|
||||
})
|
||||
return
|
||||
}
|
||||
|
|
@ -342,6 +349,7 @@ func (h *PublicCloudSignupHandlers) HandleSignupPage(w http.ResponseWriter, r *h
|
|||
ErrorMessage: "Unable to create checkout session. Please try again.",
|
||||
HasPower: h.cfg != nil && strings.TrimSpace(h.cfg.CloudPowerPriceID) != "",
|
||||
HasMax: h.cfg != nil && strings.TrimSpace(h.cfg.CloudMaxPriceID) != "",
|
||||
TrialDays: trialSignupTrialDays,
|
||||
})
|
||||
return
|
||||
}
|
||||
|
|
@ -358,7 +366,8 @@ func (h *PublicCloudSignupHandlers) HandleSignupComplete(w http.ResponseWriter,
|
|||
}
|
||||
w.Header().Set("Content-Type", "text/html; charset=utf-8")
|
||||
if err := publicCloudSignupCompleteTemplate.Execute(w, publicCloudSignupCompleteData{
|
||||
Nonce: cpsec.NonceFromContext(r.Context()),
|
||||
Nonce: cpsec.NonceFromContext(r.Context()),
|
||||
TrialDays: trialSignupTrialDays,
|
||||
}); err != nil {
|
||||
log.Error().Err(err).Msg("public cloud signup complete page render failed")
|
||||
}
|
||||
|
|
@ -407,7 +416,7 @@ func (h *PublicCloudSignupHandlers) HandlePublicSignup(w http.ResponseWriter, r
|
|||
|
||||
writePublicSignupJSON(w, http.StatusCreated, map[string]any{
|
||||
"checkout_url": checkoutURL,
|
||||
"message": "Checkout session created. Continue in Stripe to provision your Pulse Cloud workspace.",
|
||||
"message": fmt.Sprintf("Checkout session created. Continue in Stripe to start your %d-day Pulse Cloud trial and provision your workspace.", trialSignupTrialDays),
|
||||
})
|
||||
}
|
||||
|
||||
|
|
@ -553,6 +562,9 @@ func (h *PublicCloudSignupHandlers) buildCheckoutMetadata(priceID, orgName strin
|
|||
|
||||
func (h *PublicCloudSignupHandlers) renderSignupPage(w http.ResponseWriter, r *http.Request, status int, data publicCloudSignupPageData) {
|
||||
data.Nonce = cpsec.NonceFromContext(r.Context())
|
||||
if data.TrialDays <= 0 {
|
||||
data.TrialDays = trialSignupTrialDays
|
||||
}
|
||||
w.Header().Set("Content-Type", "text/html; charset=utf-8")
|
||||
w.WriteHeader(status)
|
||||
if err := publicCloudSignupPageTemplate.Execute(w, data); err != nil {
|
||||
|
|
|
|||
|
|
@ -75,9 +75,12 @@ func TestPublicCloudSignupHandleSignupPageRendersForm(t *testing.T) {
|
|||
t.Fatalf("status=%d, want %d", rec.Code, http.StatusOK)
|
||||
}
|
||||
body := rec.Body.String()
|
||||
if !strings.Contains(body, "<form") || !strings.Contains(body, "Start Pulse Cloud") {
|
||||
if !strings.Contains(body, "<form") || !strings.Contains(body, "Start your 14-day Pulse Cloud trial") {
|
||||
t.Fatalf("expected signup form markup in response body")
|
||||
}
|
||||
if !strings.Contains(body, "no upfront charge") {
|
||||
t.Fatalf("expected trial/no-upfront-charge copy in response body")
|
||||
}
|
||||
if !strings.Contains(body, `action="/cloud/signup"`) {
|
||||
t.Fatalf("expected signup form to post to the canonical cloud signup path")
|
||||
}
|
||||
|
|
@ -94,8 +97,11 @@ func TestPublicCloudSignupHandleSignupCompleteRendersPulseAccountHandoff(t *test
|
|||
t.Fatalf("status=%d, want %d", rec.Code, http.StatusOK)
|
||||
}
|
||||
body := rec.Body.String()
|
||||
if !strings.Contains(body, "Checkout Complete") {
|
||||
t.Fatalf("expected checkout completion heading")
|
||||
if !strings.Contains(body, "Trial checkout complete") {
|
||||
t.Fatalf("expected trial checkout completion heading")
|
||||
}
|
||||
if !strings.Contains(body, "14-day Pulse Cloud trial") {
|
||||
t.Fatalf("expected trial duration in completion copy")
|
||||
}
|
||||
if !strings.Contains(body, "Pulse Account sign-in link") {
|
||||
t.Fatalf("expected Pulse Account handoff copy")
|
||||
|
|
@ -204,6 +210,9 @@ func TestPublicCloudSignupHandlePublicSignupCreatesCheckout(t *testing.T) {
|
|||
if got := strings.TrimSpace(asString(payload["checkout_url"])); got != "https://checkout.stripe.com/c/pay/cs_live" {
|
||||
t.Fatalf("checkout_url=%q, want stripe URL", got)
|
||||
}
|
||||
if got := strings.TrimSpace(asString(payload["message"])); !strings.Contains(got, "14-day Pulse Cloud trial") {
|
||||
t.Fatalf("message=%q, want trial copy", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestPublicCloudSignupHandlePublicMagicLinkRequestAlwaysOpaqueWhenUnknown(t *testing.T) {
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue