From 3f7e20236709a267ed1e8840a148ca33926c9d69 Mon Sep 17 00:00:00 2001 From: Aditya Hegde Date: Tue, 26 Nov 2024 11:49:11 +0530 Subject: [PATCH] fix: billing email and deploy paywall copy (#6132) * Update some billing email copy * Update quota error to match plan * Fix trial org usage percent * PR comments --- runtime/pkg/email/email.go | 40 +++++++++---------- .../pkg/email/templates/call_to_action.mjml | 4 +- .../email/templates/gen/call_to_action.html | 2 +- .../email/templates/gen/informational.html | 2 +- .../email/templates/gen/welcome_to_team.html | 7 +--- .../email/templates/gen/welcome_to_trial.html | 2 +- .../pkg/email/templates/informational.mjml | 4 +- .../pkg/email/templates/welcome_to_team.mjml | 8 +--- .../pkg/email/templates/welcome_to_trial.mjml | 4 +- web-admin/src/features/billing/plans/utils.ts | 2 +- web-common/src/features/billing/issues.ts | 8 ++++ .../src/features/project/ProjectDeployer.ts | 28 +++++++++++-- .../src/features/project/deploy-errors.ts | 9 ++++- .../src/routes/(misc)/deploy/+page.svelte | 9 +++-- 14 files changed, 76 insertions(+), 53 deletions(-) diff --git a/runtime/pkg/email/email.go b/runtime/pkg/email/email.go index c08ba108f5b..35828a9381c 100644 --- a/runtime/pkg/email/email.go +++ b/runtime/pkg/email/email.go @@ -400,7 +400,7 @@ func (c *Client) SendInvoicePaymentFailed(opts *InvoicePaymentFailed) error { ToName: opts.ToName, Subject: fmt.Sprintf("Payment failed for %s. Please update your payment method", opts.OrgName), PreButton: template.HTML(fmt.Sprintf(` -We couldn’t process your payment for %s. You have until %s to update your payment details before your org is hibernating. +We couldn’t process your payment for %s. You have until %s to update your payment details before your org is hibernating. `, opts.OrgName, opts.GracePeriodEndDate.Format(dateFormat))), ButtonText: "Update Payment Info", ButtonLink: opts.PaymentURL, @@ -424,7 +424,7 @@ func (c *Client) SendInvoicePaymentSuccess(opts *InvoicePaymentSuccess) error { Body: template.HTML(fmt.Sprintf(` Thank you for your payment!

-Your payment for %s has been successfully processed. +Your payment for %s has been successfully processed.

If you believe this charge to be in error or have any questions, please email support@rilldata.com.

@@ -447,7 +447,7 @@ func (c *Client) SendInvoiceUnpaid(opts *InvoiceUnpaid) error { ToName: opts.ToName, Subject: fmt.Sprintf("Invoice for %s is now past due. Org is now hibernated", opts.OrgName), PreButton: template.HTML(fmt.Sprintf(` -%s and its projects have been hibernated due to an overdue payment. +%s and its projects have been hibernated due to an overdue payment.

Restore access by updating your payment information today! `, opts.ToName)), @@ -473,11 +473,9 @@ func (c *Client) SendSubscriptionCancelled(opts *SubscriptionCancelled) error { PreButton: template.HTML(fmt.Sprintf(` We’re sorry to see you go!

-You’ve successfully canceled the %s for %s. You’ll still have access to Rill Cloud until %s. After this date, your subscription will expire, and you will no longer have access. +You’ve successfully canceled the %s plan for %s. You’ll still have access to Rill Cloud until %s. After this date, your subscription will expire, and you will no longer have access.

If you change your mind, you can always reactivate your subscription! -

-If you found that our service did not meet your needs, please reply to this email and we’ll do our best to address your feedback and concerns. `, opts.PlanName, opts.ToName, opts.EndDate.Format(dateFormat))), ButtonText: "Billing Settings", ButtonLink: opts.BillingURL, @@ -498,7 +496,7 @@ func (c *Client) SendSubscriptionEnded(opts *SubscriptionEnded) error { ToName: opts.ToName, Subject: fmt.Sprintf("Subscription for %s has now ended. Org is hibernated", opts.OrgName), PreButton: template.HTML(fmt.Sprintf(` -Your cancelled subscription for %s has and its projects are now hibernating. We hope you enjoyed using Rill Cloud during your time with us. +Your cancelled subscription for %s has and its projects are now hibernating. We hope you enjoyed using Rill Cloud during your time with us.

If you’d like to reactive your subscription and regain access, you can easily do so at any time by renewing your subscription from here: `, opts.OrgName)), @@ -527,7 +525,7 @@ func (c *Client) SendTrialStarted(opts *TrialStarted) error { Subject: fmt.Sprintf("A 30-day free trial for %s has started", opts.OrgName), FrontendURL: opts.FrontendURL, WelcomeText: template.HTML(fmt.Sprintf(` -You now have access to Rill Cloud until %s to explore all features. Let us know if you need any help along the way! +You now have access to Rill Cloud until %s to explore all features. Let us know if you need any help along the way! `, opts.TrialEndDate.Format(dateFormat))), }) } @@ -548,7 +546,7 @@ func (c *Client) SendTrialEndingSoon(opts *TrialEndingSoon) error { ToName: opts.ToName, Subject: fmt.Sprintf("Your Rill Cloud trial for %s is expiring in %d days", opts.OrgName, days), PreButton: template.HTML(fmt.Sprintf(` -Your trial for %s ends on %s. +Your trial for %s ends on %s.

How's Rill working out for you? Have you checked out our newest features highlighted in our Release Notes?

@@ -577,7 +575,7 @@ func (c *Client) SendTrialEnded(opts *TrialEnded) error { PreButton: template.HTML(fmt.Sprintf(` Hi %s,

-Your Rill Cloud trial has now expired. %s will be hibernated on %s. We hope you’ve enjoyed using our software. If you’d like to keep using Rill Cloud, upgrade to our Team Plan! +Your Rill Cloud trial has now expired. %s will be hibernated on %s. We hope you’ve enjoyed using our software. If you’d like to keep using Rill Cloud, upgrade to our Team Plan! `, opts.ToName, opts.OrgName, opts.GracePeriodEndDate.Format(dateFormat))), ButtonText: "Upgrade to Team Plan", ButtonLink: opts.UpgradeURL, @@ -598,7 +596,7 @@ func (c *Client) SendTrialGracePeriodEnded(opts *TrialGracePeriodEnded) error { ToName: opts.ToName, Subject: fmt.Sprintf("Trial plan grace period for %s has ended. Org is now hibernated", opts.OrgName), PreButton: template.HTML(fmt.Sprintf(` -%s and its projects are now hibernating. +%s and its projects are now hibernating.

Reactivate your org by upgrading to the Team Plan today! `, opts.OrgName)), @@ -624,7 +622,7 @@ func (c *Client) SendTrialExtended(opts *TrialExtended) error { ToEmail: opts.ToEmail, ToName: opts.ToName, Subject: fmt.Sprintf("Your trial for %s has been extended", opts.OrgName), - Body: template.HTML(fmt.Sprintf("Your trial for %q has been extended until %s.", opts.OrgName, opts.TrialEndDate.Format(dateFormat))), + Body: template.HTML(fmt.Sprintf("Your trial for %q has been extended until %s.", opts.OrgName, opts.TrialEndDate.Format(dateFormat))), }) } @@ -639,8 +637,8 @@ func (c *Client) SendPlanUpdate(opts *PlanUpdate) error { return c.SendInformational(&Informational{ ToEmail: opts.ToEmail, ToName: opts.ToName, - Subject: fmt.Sprintf("Your plan for %s has been updated to %s", opts.OrgName, opts.PlanName), - Body: template.HTML(fmt.Sprintf("%q has been updated to %q.", opts.OrgName, opts.PlanName)), + Subject: fmt.Sprintf("Your plan for %s has been updated to %s plan", opts.OrgName, opts.PlanName), + Body: template.HTML(fmt.Sprintf("%q has been updated to %q plan.", opts.OrgName, opts.PlanName)), }) } @@ -655,8 +653,8 @@ func (c *Client) SendSubscriptionRenewed(opts *SubscriptionRenewed) error { return c.SendInformational(&Informational{ ToEmail: opts.ToEmail, ToName: opts.ToName, - Subject: fmt.Sprintf("Your %s subscription for %s has been renewed", opts.PlanName, opts.OrgName), - Body: template.HTML(fmt.Sprintf("Your subscription for %q has been renewed for %q.", opts.OrgName, opts.PlanName)), + Subject: fmt.Sprintf("Your %s subscription for %s plan has been renewed", opts.PlanName, opts.OrgName), + Body: template.HTML(fmt.Sprintf("Your subscription for %q has been renewed for %q plan.", opts.OrgName, opts.PlanName)), }) } @@ -674,12 +672,12 @@ func (c *Client) SendTeamPlanStarted(opts *TeamPlan) error { return c.SendWelcomeToTeam(&Welcome{ ToEmail: opts.ToEmail, ToName: opts.ToName, - Subject: fmt.Sprintf("Welcome to the %s", opts.PlanName), + Subject: fmt.Sprintf("Welcome to the %s plan", opts.PlanName), FrontendURL: opts.FrontendURL, WelcomeText: template.HTML(fmt.Sprintf(` -Thank you! You’ve successfully upgraded %s to the %s. +Thank you! You’ve successfully upgraded %s to the %s plan.

-Your next billing cycle starts on %s. +Your next billing cycle starts on %s. `, opts.OrgName, opts.PlanName, opts.BillingStartDate.Format(dateFormat))), }) } @@ -689,10 +687,10 @@ func (c *Client) SendTeamPlanRenewal(opts *TeamPlan) error { return c.SendWelcomeToTeam(&Welcome{ ToEmail: opts.ToEmail, ToName: opts.ToName, - Subject: fmt.Sprintf("Your %s subscription for %s has been renewed", opts.PlanName, opts.OrgName), + Subject: fmt.Sprintf("Your %s plan subscription for %s has been renewed", opts.PlanName, opts.OrgName), FrontendURL: opts.FrontendURL, WelcomeText: template.HTML(fmt.Sprintf(` -Thank you! You’ve successfully renewed to the %s for %s. +Thank you! You’ve successfully renewed to the %s for %s plan.

Your next billing cycle starts on %s. `, opts.OrgName, opts.PlanName, opts.BillingStartDate.Format(dateFormat))), diff --git a/runtime/pkg/email/templates/call_to_action.mjml b/runtime/pkg/email/templates/call_to_action.mjml index 19a2d633d96..31140174fe3 100644 --- a/runtime/pkg/email/templates/call_to_action.mjml +++ b/runtime/pkg/email/templates/call_to_action.mjml @@ -15,7 +15,7 @@ - + @@ -58,4 +58,4 @@ - \ No newline at end of file + diff --git a/runtime/pkg/email/templates/gen/call_to_action.html b/runtime/pkg/email/templates/gen/call_to_action.html index 2120c1bd25c..b82ca9b95d7 100644 --- a/runtime/pkg/email/templates/gen/call_to_action.html +++ b/runtime/pkg/email/templates/gen/call_to_action.html @@ -111,7 +111,7 @@ - + diff --git a/runtime/pkg/email/templates/gen/informational.html b/runtime/pkg/email/templates/gen/informational.html index 6082ef3b045..a7f3db7551b 100644 --- a/runtime/pkg/email/templates/gen/informational.html +++ b/runtime/pkg/email/templates/gen/informational.html @@ -111,7 +111,7 @@ - + diff --git a/runtime/pkg/email/templates/gen/welcome_to_team.html b/runtime/pkg/email/templates/gen/welcome_to_team.html index eca04ec0256..05f2e82bf54 100644 --- a/runtime/pkg/email/templates/gen/welcome_to_team.html +++ b/runtime/pkg/email/templates/gen/welcome_to_team.html @@ -111,18 +111,13 @@ - + - - -
Welcome to Rill
- - diff --git a/runtime/pkg/email/templates/gen/welcome_to_trial.html b/runtime/pkg/email/templates/gen/welcome_to_trial.html index 8000a17302f..74acf054ad4 100644 --- a/runtime/pkg/email/templates/gen/welcome_to_trial.html +++ b/runtime/pkg/email/templates/gen/welcome_to_trial.html @@ -111,7 +111,7 @@ - + diff --git a/runtime/pkg/email/templates/informational.mjml b/runtime/pkg/email/templates/informational.mjml index 76bc0dbb991..e897109a894 100644 --- a/runtime/pkg/email/templates/informational.mjml +++ b/runtime/pkg/email/templates/informational.mjml @@ -15,7 +15,7 @@ - + @@ -49,4 +49,4 @@ - \ No newline at end of file + diff --git a/runtime/pkg/email/templates/welcome_to_team.mjml b/runtime/pkg/email/templates/welcome_to_team.mjml index df7240be2cd..a72364cbb4f 100644 --- a/runtime/pkg/email/templates/welcome_to_team.mjml +++ b/runtime/pkg/email/templates/welcome_to_team.mjml @@ -15,11 +15,7 @@ - - - - Welcome to Rill - + @@ -62,4 +58,4 @@ - \ No newline at end of file + diff --git a/runtime/pkg/email/templates/welcome_to_trial.mjml b/runtime/pkg/email/templates/welcome_to_trial.mjml index d1ce64d7e06..712d1111760 100644 --- a/runtime/pkg/email/templates/welcome_to_trial.mjml +++ b/runtime/pkg/email/templates/welcome_to_trial.mjml @@ -15,7 +15,7 @@ - + Welcome to Rill @@ -66,4 +66,4 @@ - \ No newline at end of file + diff --git a/web-admin/src/features/billing/plans/utils.ts b/web-admin/src/features/billing/plans/utils.ts index 803b17a702c..3c7c1462495 100644 --- a/web-admin/src/features/billing/plans/utils.ts +++ b/web-admin/src/features/billing/plans/utils.ts @@ -15,7 +15,7 @@ export function formatUsageVsQuota( const formattedUsage = formatMemorySize(usageInBytes); const formattedQuota = formatMemorySize(quota); const percent = - formattedUsage > formattedQuota + usageInBytes > quota ? "100+" : Math.round((usageInBytes * 100) / quota) + ""; return `${formattedUsage} of ${formattedQuota} (${percent}%)`; diff --git a/web-common/src/features/billing/issues.ts b/web-common/src/features/billing/issues.ts index a319c7e1dde..966553e508a 100644 --- a/web-common/src/features/billing/issues.ts +++ b/web-common/src/features/billing/issues.ts @@ -6,3 +6,11 @@ import { export function getNeverSubscribedIssue(issues: BillingIssue[]) { return issues.find((i) => i.type === BillingIssueType.NEVER_SUBSCRIBED); } + +export function getTrialIssue(issues: BillingIssue[]) { + return issues.find( + (i) => + i.type === BillingIssueType.ON_TRIAL || + i.type === BillingIssueType.TRIAL_ENDED, + ); +} diff --git a/web-common/src/features/project/ProjectDeployer.ts b/web-common/src/features/project/ProjectDeployer.ts index a10461751f1..3f847fc6ad3 100644 --- a/web-common/src/features/project/ProjectDeployer.ts +++ b/web-common/src/features/project/ProjectDeployer.ts @@ -1,9 +1,10 @@ import { page } from "$app/stores"; import type { ConnectError } from "@connectrpc/connect"; +import { getTrialIssue } from "@rilldata/web-common/features/billing/issues"; import { sanitizeOrgName } from "@rilldata/web-common/features/organization/sanitizeOrgName"; import { DeployErrorType, - extractDeployError, + getPrettyDeployError, } from "@rilldata/web-common/features/project/deploy-errors"; import { queryClient } from "@rilldata/web-common/lib/svelte-query/globalQueryClient"; import { waitUntil } from "@rilldata/web-common/lib/waitUtils"; @@ -19,6 +20,7 @@ import { createLocalServiceGetCurrentProject, createLocalServiceGetCurrentUser, createLocalServiceGetMetadata, + createLocalServiceListOrganizationsAndBillingMetadataRequest, createLocalServiceRedeploy, getLocalServiceGetCurrentUserQueryKey, localServiceGetCurrentUser, @@ -27,6 +29,8 @@ import { derived, get, writable } from "svelte/store"; export class ProjectDeployer { public readonly metadata = createLocalServiceGetMetadata(); + public readonly orgsMetadata = + createLocalServiceListOrganizationsAndBillingMetadataRequest(); public readonly user = createLocalServiceGetCurrentUser(); public readonly project = createLocalServiceGetCurrentProject(); public readonly promptOrgSelection = writable(false); @@ -53,27 +57,43 @@ export class ProjectDeployer { return derived( [ this.metadata, + this.orgsMetadata, this.user, this.project, + this.org, this.deployMutation, this.redeployMutation, ], - ([metadata, user, project, deployMutation, redeployMutation]) => { + ([ + metadata, + orgsMetadata, + user, + project, + org, + deployMutation, + redeployMutation, + ]) => { if ( metadata.error || + orgsMetadata.error || user.error || project.error || deployMutation.error || redeployMutation.error ) { + const orgMetadata = orgsMetadata?.data?.orgs.find( + (om) => om.name === org, + ); + const onTrial = !!getTrialIssue(orgMetadata?.issues ?? []); return { isLoading: false, - error: extractDeployError( + error: getPrettyDeployError( (metadata.error as ConnectError) ?? (user.error as ConnectError) ?? (project.error as ConnectError) ?? (deployMutation.error as ConnectError) ?? (redeployMutation.error as ConnectError), + onTrial, ), }; } @@ -189,7 +209,7 @@ export class ProjectDeployer { ); return resp.frontendUrl; } catch (e) { - const err = extractDeployError(e); + const err = getPrettyDeployError(e, false); if (err.type === DeployErrorType.PermissionDenied && checkNextOrg) { i++; } else { diff --git a/web-common/src/features/project/deploy-errors.ts b/web-common/src/features/project/deploy-errors.ts index 078ac912033..ae1660b3099 100644 --- a/web-common/src/features/project/deploy-errors.ts +++ b/web-common/src/features/project/deploy-errors.ts @@ -41,7 +41,10 @@ export type DeployError = { message: string; }; -export function extractDeployError(error: ConnectError): DeployError { +export function getPrettyDeployError( + error: ConnectError, + orgOnTrial: boolean, +): DeployError { if (!error) { return { type: DeployErrorType.Unknown, @@ -81,10 +84,12 @@ export function extractDeployError(error: ConnectError): DeployError { const projectQuotaMatch = ProjectQuotaErrorMatcher.exec(desc); if (projectQuotaMatch?.length) { const projectQuota = Number(projectQuotaMatch[1]); + const planLabel = orgOnTrial ? "current plan" : "trial plan"; + return { type: DeployErrorType.ProjectLimitHit, title: "To deploy this project, start a Team plan", - message: `Your trial plan is limited to ${projectQuota} project${projectQuota > 1 ? "s" : ""}. To have unlimited projects, upgrade to a Team plan.`, + message: `Your ${planLabel} is limited to ${projectQuota} project${projectQuota > 1 ? "s" : ""}. To have unlimited projects, upgrade to a Team plan.`, }; } diff --git a/web-local/src/routes/(misc)/deploy/+page.svelte b/web-local/src/routes/(misc)/deploy/+page.svelte index b0af37865db..fed138a0081 100644 --- a/web-local/src/routes/(misc)/deploy/+page.svelte +++ b/web-local/src/routes/(misc)/deploy/+page.svelte @@ -15,12 +15,13 @@ import type { PageData } from "./$types"; export let data: PageData; - $: ({ orgMetadata } = data); + $: ({ orgParam } = data); - const deployer = new ProjectDeployer(data.orgParam); + const deployer = new ProjectDeployer(orgParam); const metadata = deployer.metadata; const user = deployer.user; const project = deployer.project; + const orgsMetadata = deployer.orgsMetadata; const deployerStatus = deployer.getStatus(); const promptOrgSelection = deployer.promptOrgSelection; // This org is set by the deployer. @@ -29,7 +30,7 @@ const org = deployer.org; let isEmptyOrg = false; $: { - const om = orgMetadata.orgs.find((o) => o.name === $org); + const om = $orgsMetadata?.data?.orgs.find((o) => o.name === $org); isEmptyOrg = !!om?.issues && !!getNeverSubscribedIssue(om.issues); } @@ -59,7 +60,7 @@ } function onBack() { - if (orgMetadata.orgs.length) { + if ($orgsMetadata.data?.orgs?.length) { promptOrgSelection.set(true); } else { void goto("/");