1
0
mirror of https://github.com/bitwarden/server.git synced 2025-06-30 15:42:48 -05:00

[PM-20084] [PM-20086] Add TrialLength parameter to trial initiation endpoint and email (#5770)

* Add trial length parameter to trial initiation endpoint and email

* Add feature flag that pegs trial length to 7 when disabled

* Add optionality to Identity

* Move feature service injection to identity accounts controller
This commit is contained in:
Alex Morask
2025-05-08 10:43:19 -04:00
committed by GitHub
parent e4a93b24f1
commit c9b6e5de86
13 changed files with 67 additions and 13 deletions

View File

@ -7,4 +7,5 @@ public class TrialSendVerificationEmailRequestModel : RegisterSendVerificationEm
{
public ProductTierType ProductTier { get; set; }
public IEnumerable<ProductType> Products { get; set; }
public int? TrialLength { get; set; }
}

View File

@ -1,5 +1,6 @@
using Bit.Core.Auth.Models.Mail;
using Bit.Core.Billing.Enums;
using Bit.Core.Enums;
namespace Bit.Core.Billing.Models.Mail;
@ -16,13 +17,26 @@ public class TrialInitiationVerifyEmail : RegisterVerifyEmail
$"&email={Email}" +
$"&fromEmail=true" +
$"&productTier={(int)ProductTier}" +
$"&product={string.Join(",", Product.Select(p => (int)p))}";
$"&product={string.Join(",", Product.Select(p => (int)p))}" +
$"&trialLength={TrialLength}";
}
public string VerifyYourEmailHTMLCopy =>
TrialLength == 7
? "Verify your email address below to finish signing up for your free trial."
: $"Verify your email address below to finish signing up for your {ProductTier.GetDisplayName()} plan.";
public string VerifyYourEmailTextCopy =>
TrialLength == 7
? "Verify your email address using the link below and start your free trial of Bitwarden."
: $"Verify your email address using the link below and start your {ProductTier.GetDisplayName()} Bitwarden plan.";
public ProductTierType ProductTier { get; set; }
public IEnumerable<ProductType> Product { get; set; }
public int TrialLength { get; set; }
/// <summary>
/// Currently we only support one product type at a time, despite Product being a collection.
/// If we receive both PasswordManager and SecretsManager, we'll send the user to the PM trial route

View File

@ -10,5 +10,6 @@ public interface ISendTrialInitiationEmailForRegistrationCommand
string? name,
bool receiveMarketingEmails,
ProductTierType productTier,
IEnumerable<ProductType> products);
IEnumerable<ProductType> products,
int trialLength);
}

View File

@ -22,7 +22,8 @@ public class SendTrialInitiationEmailForRegistrationCommand(
string? name,
bool receiveMarketingEmails,
ProductTierType productTier,
IEnumerable<ProductType> products)
IEnumerable<ProductType> products,
int trialLength)
{
ArgumentException.ThrowIfNullOrWhiteSpace(email, nameof(email));
@ -43,7 +44,12 @@ public class SendTrialInitiationEmailForRegistrationCommand(
await PerformConstantTimeOperationsAsync();
await mailService.SendTrialInitiationSignupEmailAsync(userExists, email, token, productTier, products);
if (trialLength != 0 && trialLength != 7)
{
trialLength = 7;
}
await mailService.SendTrialInitiationSignupEmailAsync(userExists, email, token, productTier, products, trialLength);
return null;
}

View File

@ -150,6 +150,7 @@ public static class FeatureFlagKeys
public const string PM199566_UpdateMSPToChargeAutomatically = "pm-199566-update-msp-to-charge-automatically";
public const string PM19956_RequireProviderPaymentMethodDuringSetup = "pm-19956-require-provider-payment-method-during-setup";
public const string UseOrganizationWarningsService = "use-organization-warnings-service";
public const string PM20322_AllowTrialLength0 = "pm-20322-allow-trial-length-0";
/* Data Insights and Reporting Team */
public const string RiskInsightsCriticalApplication = "pm-14466-risk-insights-critical-application";

View File

@ -0,0 +1,18 @@
using System.ComponentModel.DataAnnotations;
using System.Reflection;
namespace Bit.Core.Enums;
public static class EnumExtensions
{
public static string GetDisplayName(this Enum value)
{
var field = value.GetType().GetField(value.ToString());
if (field?.GetCustomAttribute<DisplayAttribute>() is { } attribute)
{
return attribute.Name ?? value.ToString();
}
return value.ToString();
}
}

View File

@ -2,7 +2,7 @@
<table width="100%" cellpadding="0" cellspacing="0" style="margin: 0; box-sizing: border-box; color: #333; line-height: 25px; -webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none;">
<tr style="margin: 0; box-sizing: border-box; color: #333; line-height: 25px; -webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none;">
<td class="content-block" style="font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; margin: 0; -webkit-font-smoothing: antialiased; padding: 0 0 10px; -webkit-text-size-adjust: none; text-align: left;" valign="top" align="center">
Verify your email address below to finish signing up for your free trial.
{{VerifyYourEmailHTMLCopy}}
</td>
</tr>
<tr style="margin: 0; box-sizing: border-box; color: #333; line-height: 25px; -webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none;">

View File

@ -1,5 +1,5 @@
{{#>BasicTextLayout}}
Verify your email address using the link below and start your free trial of Bitwarden.
{{VerifyYourEmailTextCopy}}
If you did not request this email from Bitwarden, you can safely ignore it.

View File

@ -21,7 +21,8 @@ public interface IMailService
string email,
string token,
ProductTierType productTier,
IEnumerable<ProductType> products);
IEnumerable<ProductType> products,
int trialLength);
Task SendVerifyDeleteEmailAsync(string email, Guid userId, string token);
Task SendCannotDeleteClaimedAccountEmailAsync(string email);
Task SendChangeEmailAlreadyExistsEmailAsync(string fromEmail, string toEmail);

View File

@ -84,7 +84,8 @@ public class HandlebarsMailService : IMailService
string email,
string token,
ProductTierType productTier,
IEnumerable<ProductType> products)
IEnumerable<ProductType> products,
int trialLength)
{
var message = CreateDefaultMessage("Verify your email", email);
var model = new TrialInitiationVerifyEmail
@ -95,7 +96,8 @@ public class HandlebarsMailService : IMailService
WebVaultUrl = _globalSettings.BaseServiceUri.VaultWithHash,
SiteName = _globalSettings.SiteName,
ProductTier = productTier,
Product = products
Product = products,
TrialLength = trialLength
};
await AddMessageContentAsync(message, "Billing.TrialInitiationVerifyEmail", model);
message.MetaData.Add("SendGridBypassListManagement", true);

View File

@ -33,7 +33,8 @@ public class NoopMailService : IMailService
string email,
string token,
ProductTierType productTier,
IEnumerable<ProductType> products)
IEnumerable<ProductType> products,
int trailLength)
{
return Task.FromResult(0);
}

View File

@ -1,6 +1,8 @@
using Bit.Core.Billing.Models.Api.Requests.Accounts;
using Bit.Core;
using Bit.Core.Billing.Models.Api.Requests.Accounts;
using Bit.Core.Billing.TrialInitiation.Registration;
using Bit.Core.Context;
using Bit.Core.Services;
using Bit.Core.Tools.Enums;
using Bit.Core.Tools.Models.Business;
using Bit.Core.Tools.Services;
@ -15,18 +17,24 @@ namespace Bit.Identity.Billing.Controller;
public class AccountsController(
ICurrentContext currentContext,
ISendTrialInitiationEmailForRegistrationCommand sendTrialInitiationEmailForRegistrationCommand,
IReferenceEventService referenceEventService) : Microsoft.AspNetCore.Mvc.Controller
IReferenceEventService referenceEventService,
IFeatureService featureService) : Microsoft.AspNetCore.Mvc.Controller
{
[HttpPost("trial/send-verification-email")]
[SelfHosted(NotSelfHostedOnly = true)]
public async Task<IActionResult> PostTrialInitiationSendVerificationEmailAsync([FromBody] TrialSendVerificationEmailRequestModel model)
{
var allowTrialLength0 = featureService.IsEnabled(FeatureFlagKeys.PM20322_AllowTrialLength0);
var trialLength = allowTrialLength0 ? model.TrialLength ?? 7 : 7;
var token = await sendTrialInitiationEmailForRegistrationCommand.Handle(
model.Email,
model.Name,
model.ReceiveMarketingEmails,
model.ProductTier,
model.Products);
model.Products,
trialLength);
var refEvent = new ReferenceEvent
{

View File

@ -145,6 +145,7 @@ public class Startup
// Services
services.AddBaseServices(globalSettings);
services.AddDefaultServices(globalSettings);
services.AddOptionality();
services.AddCoreLocalizationServices();
services.AddBillingOperations();