1
0
mirror of https://github.com/bitwarden/server synced 2025-12-26 05:03:18 +00:00

Merge branch 'SM-1571-DisableSMAdsForUsers' of https://github.com/bitwarden/server into SM-1571-DisableSMAdsForUsers

This commit is contained in:
cd-bitwarden
2025-10-22 17:23:17 -04:00
50 changed files with 1311 additions and 289 deletions

View File

@@ -3,6 +3,7 @@ using Bit.Core.Billing.Commands;
using Bit.Core.Billing.Constants;
using Bit.Core.Billing.Extensions;
using Bit.Core.Billing.Payment.Models;
using Bit.Core.Billing.Pricing;
using Bit.Core.Billing.Services;
using Bit.Core.Entities;
using Bit.Core.Enums;
@@ -50,7 +51,8 @@ public class CreatePremiumCloudHostedSubscriptionCommand(
ISubscriberService subscriberService,
IUserService userService,
IPushNotificationService pushNotificationService,
ILogger<CreatePremiumCloudHostedSubscriptionCommand> logger)
ILogger<CreatePremiumCloudHostedSubscriptionCommand> logger,
IPricingClient pricingClient)
: BaseBillingCommand<CreatePremiumCloudHostedSubscriptionCommand>(logger), ICreatePremiumCloudHostedSubscriptionCommand
{
private static readonly List<string> _expand = ["tax"];
@@ -255,11 +257,13 @@ public class CreatePremiumCloudHostedSubscriptionCommand(
Customer customer,
int? storage)
{
var premiumPlan = await pricingClient.GetAvailablePremiumPlan();
var subscriptionItemOptionsList = new List<SubscriptionItemOptions>
{
new ()
{
Price = StripeConstants.Prices.PremiumAnnually,
Price = premiumPlan.Seat.StripePriceId,
Quantity = 1
}
};
@@ -268,7 +272,7 @@ public class CreatePremiumCloudHostedSubscriptionCommand(
{
subscriptionItemOptionsList.Add(new SubscriptionItemOptions
{
Price = StripeConstants.Prices.StoragePlanPersonal,
Price = premiumPlan.Storage.StripePriceId,
Quantity = storage
});
}

View File

@@ -1,14 +1,12 @@
using Bit.Core.Billing.Commands;
using Bit.Core.Billing.Constants;
using Bit.Core.Billing.Payment.Models;
using Bit.Core.Billing.Pricing;
using Bit.Core.Services;
using Microsoft.Extensions.Logging;
using Stripe;
namespace Bit.Core.Billing.Premium.Commands;
using static StripeConstants;
public interface IPreviewPremiumTaxCommand
{
Task<BillingCommandResult<(decimal Tax, decimal Total)>> Run(
@@ -18,6 +16,7 @@ public interface IPreviewPremiumTaxCommand
public class PreviewPremiumTaxCommand(
ILogger<PreviewPremiumTaxCommand> logger,
IPricingClient pricingClient,
IStripeAdapter stripeAdapter) : BaseBillingCommand<PreviewPremiumTaxCommand>(logger), IPreviewPremiumTaxCommand
{
public Task<BillingCommandResult<(decimal Tax, decimal Total)>> Run(
@@ -25,6 +24,8 @@ public class PreviewPremiumTaxCommand(
BillingAddress billingAddress)
=> HandleAsync<(decimal, decimal)>(async () =>
{
var premiumPlan = await pricingClient.GetAvailablePremiumPlan();
var options = new InvoiceCreatePreviewOptions
{
AutomaticTax = new InvoiceAutomaticTaxOptions { Enabled = true },
@@ -41,7 +42,7 @@ public class PreviewPremiumTaxCommand(
{
Items =
[
new InvoiceSubscriptionDetailsItemOptions { Price = Prices.PremiumAnnually, Quantity = 1 }
new InvoiceSubscriptionDetailsItemOptions { Price = premiumPlan.Seat.StripePriceId, Quantity = 1 }
]
}
};
@@ -50,7 +51,7 @@ public class PreviewPremiumTaxCommand(
{
options.SubscriptionDetails.Items.Add(new InvoiceSubscriptionDetailsItemOptions
{
Price = Prices.StoragePlanPersonal,
Price = premiumPlan.Storage.StripePriceId,
Quantity = additionalStorage
});
}

View File

@@ -3,12 +3,14 @@ using Bit.Core.Exceptions;
using Bit.Core.Models.StaticStore;
using Bit.Core.Utilities;
#nullable enable
namespace Bit.Core.Billing.Pricing;
using OrganizationPlan = Plan;
using PremiumPlan = Premium.Plan;
public interface IPricingClient
{
// TODO: Rename with Organization focus.
/// <summary>
/// Retrieve a Bitwarden plan by its <paramref name="planType"/>. If the feature flag 'use-pricing-service' is enabled,
/// this will trigger a request to the Bitwarden Pricing Service. Otherwise, it will use the existing <see cref="StaticStore"/>.
@@ -16,8 +18,9 @@ public interface IPricingClient
/// <param name="planType">The type of plan to retrieve.</param>
/// <returns>A Bitwarden <see cref="Plan"/> record or null in the case the plan could not be found or the method was executed from a self-hosted instance.</returns>
/// <exception cref="BillingException">Thrown when the request to the Pricing Service fails unexpectedly.</exception>
Task<Plan?> GetPlan(PlanType planType);
Task<OrganizationPlan?> GetPlan(PlanType planType);
// TODO: Rename with Organization focus.
/// <summary>
/// Retrieve a Bitwarden plan by its <paramref name="planType"/>. If the feature flag 'use-pricing-service' is enabled,
/// this will trigger a request to the Bitwarden Pricing Service. Otherwise, it will use the existing <see cref="StaticStore"/>.
@@ -26,13 +29,17 @@ public interface IPricingClient
/// <returns>A Bitwarden <see cref="Plan"/> record.</returns>
/// <exception cref="NotFoundException">Thrown when the <see cref="Plan"/> for the provided <paramref name="planType"/> could not be found or the method was executed from a self-hosted instance.</exception>
/// <exception cref="BillingException">Thrown when the request to the Pricing Service fails unexpectedly.</exception>
Task<Plan> GetPlanOrThrow(PlanType planType);
Task<OrganizationPlan> GetPlanOrThrow(PlanType planType);
// TODO: Rename with Organization focus.
/// <summary>
/// Retrieve all the Bitwarden plans. If the feature flag 'use-pricing-service' is enabled,
/// this will trigger a request to the Bitwarden Pricing Service. Otherwise, it will use the existing <see cref="StaticStore"/>.
/// </summary>
/// <returns>A list of Bitwarden <see cref="Plan"/> records or an empty list in the case the method is executed from a self-hosted instance.</returns>
/// <exception cref="BillingException">Thrown when the request to the Pricing Service fails unexpectedly.</exception>
Task<List<Plan>> ListPlans();
Task<List<OrganizationPlan>> ListPlans();
Task<PremiumPlan> GetAvailablePremiumPlan();
Task<List<PremiumPlan>> ListPremiumPlans();
}

View File

@@ -1,4 +1,4 @@
namespace Bit.Core.Billing.Pricing.Models;
namespace Bit.Core.Billing.Pricing.Organizations;
public class Feature
{

View File

@@ -1,4 +1,4 @@
namespace Bit.Core.Billing.Pricing.Models;
namespace Bit.Core.Billing.Pricing.Organizations;
public class Plan
{

View File

@@ -1,8 +1,6 @@
using Bit.Core.Billing.Enums;
using Bit.Core.Billing.Pricing.Models;
using Plan = Bit.Core.Billing.Pricing.Models.Plan;
namespace Bit.Core.Billing.Pricing;
namespace Bit.Core.Billing.Pricing.Organizations;
public record PlanAdapter : Core.Models.StaticStore.Plan
{

View File

@@ -2,7 +2,7 @@
using System.Text.Json.Serialization;
using OneOf;
namespace Bit.Core.Billing.Pricing.Models;
namespace Bit.Core.Billing.Pricing.Organizations;
[JsonConverter(typeof(PurchasableJsonConverter))]
public class Purchasable(OneOf<Free, Packaged, Scalable> input) : OneOfBase<Free, Packaged, Scalable>(input)

View File

@@ -0,0 +1,10 @@
namespace Bit.Core.Billing.Pricing.Premium;
public class Plan
{
public string Name { get; init; } = null!;
public int? LegacyYear { get; init; }
public bool Available { get; init; }
public Purchasable Seat { get; init; } = null!;
public Purchasable Storage { get; init; } = null!;
}

View File

@@ -0,0 +1,7 @@
namespace Bit.Core.Billing.Pricing.Premium;
public class Purchasable
{
public string StripePriceId { get; init; } = null!;
public decimal Price { get; init; }
}

View File

@@ -1,24 +1,27 @@
using System.Net;
using System.Net.Http.Json;
using Bit.Core.Billing.Constants;
using Bit.Core.Billing.Enums;
using Bit.Core.Billing.Pricing.Organizations;
using Bit.Core.Exceptions;
using Bit.Core.Services;
using Bit.Core.Settings;
using Bit.Core.Utilities;
using Microsoft.Extensions.Logging;
using Plan = Bit.Core.Models.StaticStore.Plan;
#nullable enable
namespace Bit.Core.Billing.Pricing;
using OrganizationPlan = Bit.Core.Models.StaticStore.Plan;
using PremiumPlan = Premium.Plan;
using Purchasable = Premium.Purchasable;
public class PricingClient(
IFeatureService featureService,
GlobalSettings globalSettings,
HttpClient httpClient,
ILogger<PricingClient> logger) : IPricingClient
{
public async Task<Plan?> GetPlan(PlanType planType)
public async Task<OrganizationPlan?> GetPlan(PlanType planType)
{
if (globalSettings.SelfHosted)
{
@@ -40,16 +43,14 @@ public class PricingClient(
return null;
}
var response = await httpClient.GetAsync($"plans/lookup/{lookupKey}");
var response = await httpClient.GetAsync($"plans/organization/{lookupKey}");
if (response.IsSuccessStatusCode)
{
var plan = await response.Content.ReadFromJsonAsync<Models.Plan>();
if (plan == null)
{
throw new BillingException(message: "Deserialization of Pricing Service response resulted in null");
}
return new PlanAdapter(plan);
var plan = await response.Content.ReadFromJsonAsync<Plan>();
return plan == null
? throw new BillingException(message: "Deserialization of Pricing Service response resulted in null")
: new PlanAdapter(plan);
}
if (response.StatusCode == HttpStatusCode.NotFound)
@@ -62,19 +63,14 @@ public class PricingClient(
message: $"Request to the Pricing Service failed with status code {response.StatusCode}");
}
public async Task<Plan> GetPlanOrThrow(PlanType planType)
public async Task<OrganizationPlan> GetPlanOrThrow(PlanType planType)
{
var plan = await GetPlan(planType);
if (plan == null)
{
throw new NotFoundException();
}
return plan;
return plan ?? throw new NotFoundException($"Could not find plan for type {planType}");
}
public async Task<List<Plan>> ListPlans()
public async Task<List<OrganizationPlan>> ListPlans()
{
if (globalSettings.SelfHosted)
{
@@ -88,16 +84,51 @@ public class PricingClient(
return StaticStore.Plans.ToList();
}
var response = await httpClient.GetAsync("plans");
var response = await httpClient.GetAsync("plans/organization");
if (response.IsSuccessStatusCode)
{
var plans = await response.Content.ReadFromJsonAsync<List<Models.Plan>>();
if (plans == null)
{
throw new BillingException(message: "Deserialization of Pricing Service response resulted in null");
}
return plans.Select(Plan (plan) => new PlanAdapter(plan)).ToList();
var plans = await response.Content.ReadFromJsonAsync<List<Plan>>();
return plans == null
? throw new BillingException(message: "Deserialization of Pricing Service response resulted in null")
: plans.Select(OrganizationPlan (plan) => new PlanAdapter(plan)).ToList();
}
throw new BillingException(
message: $"Request to the Pricing Service failed with status {response.StatusCode}");
}
public async Task<PremiumPlan> GetAvailablePremiumPlan()
{
var premiumPlans = await ListPremiumPlans();
var availablePlan = premiumPlans.FirstOrDefault(premiumPlan => premiumPlan.Available);
return availablePlan ?? throw new NotFoundException("Could not find available premium plan");
}
public async Task<List<PremiumPlan>> ListPremiumPlans()
{
if (globalSettings.SelfHosted)
{
return [];
}
var usePricingService = featureService.IsEnabled(FeatureFlagKeys.UsePricingService);
var fetchPremiumPriceFromPricingService =
featureService.IsEnabled(FeatureFlagKeys.PM26793_FetchPremiumPriceFromPricingService);
if (!usePricingService || !fetchPremiumPriceFromPricingService)
{
return [CurrentPremiumPlan];
}
var response = await httpClient.GetAsync("plans/premium");
if (response.IsSuccessStatusCode)
{
var plans = await response.Content.ReadFromJsonAsync<List<PremiumPlan>>();
return plans ?? throw new BillingException(message: "Deserialization of Pricing Service response resulted in null");
}
throw new BillingException(
@@ -130,4 +161,13 @@ public class PricingClient(
PlanType.TeamsStarter2023 => "teams-starter-2023",
_ => null
};
private static PremiumPlan CurrentPremiumPlan => new()
{
Name = "Premium",
Available = true,
LegacyYear = null,
Seat = new Purchasable { Price = 10M, StripePriceId = StripeConstants.Prices.PremiumAnnually },
Storage = new Purchasable { Price = 4M, StripePriceId = StripeConstants.Prices.StoragePlanPersonal }
};
}

View File

@@ -6,6 +6,7 @@ using Bit.Core.Billing.Constants;
using Bit.Core.Billing.Extensions;
using Bit.Core.Billing.Models;
using Bit.Core.Billing.Models.Sales;
using Bit.Core.Billing.Pricing;
using Bit.Core.Billing.Tax.Models;
using Bit.Core.Entities;
using Bit.Core.Enums;
@@ -30,7 +31,8 @@ public class PremiumUserBillingService(
ISetupIntentCache setupIntentCache,
IStripeAdapter stripeAdapter,
ISubscriberService subscriberService,
IUserRepository userRepository) : IPremiumUserBillingService
IUserRepository userRepository,
IPricingClient pricingClient) : IPremiumUserBillingService
{
public async Task Credit(User user, decimal amount)
{
@@ -301,11 +303,13 @@ public class PremiumUserBillingService(
Customer customer,
int? storage)
{
var premiumPlan = await pricingClient.GetAvailablePremiumPlan();
var subscriptionItemOptionsList = new List<SubscriptionItemOptions>
{
new ()
{
Price = StripeConstants.Prices.PremiumAnnually,
Price = premiumPlan.Seat.StripePriceId,
Quantity = 1
}
};
@@ -314,7 +318,7 @@ public class PremiumUserBillingService(
{
subscriptionItemOptionsList.Add(new SubscriptionItemOptions
{
Price = StripeConstants.Prices.StoragePlanPersonal,
Price = premiumPlan.Storage.StripePriceId,
Quantity = storage
});
}

View File

@@ -154,6 +154,7 @@ public static class FeatureFlagKeys
public const string DisableAlternateLoginMethods = "pm-22110-disable-alternate-login-methods";
public const string PM23174ManageAccountRecoveryPermissionDrivesTheNeedToSetMasterPassword =
"pm-23174-manage-account-recovery-permission-drives-the-need-to-set-master-password";
public const string MJMLBasedEmailTemplates = "mjml-based-email-templates";
/* Autofill Team */
public const string IdpAutoSubmitLogin = "idp-auto-submit-login";
@@ -185,6 +186,7 @@ public static class FeatureFlagKeys
public const string PM24032_NewNavigationPremiumUpgradeButton = "pm-24032-new-navigation-premium-upgrade-button";
public const string PM23713_PremiumBadgeOpensNewPremiumUpgradeDialog = "pm-23713-premium-badge-opens-new-premium-upgrade-dialog";
public const string PremiumUpgradeNewDesign = "pm-24033-updat-premium-subscription-page";
public const string PM26793_FetchPremiumPriceFromPricingService = "pm-26793-fetch-premium-price-from-pricing-service";
/* Key Management Team */
public const string ReturnErrorOnExistingKeypair = "return-error-on-existing-keypair";

View File

@@ -0,0 +1,675 @@
<!doctype html>
<html lang="und" dir="auto" xmlns="http://www.w3.org/1999/xhtml" xmlns:v="urn:schemas-microsoft-com:vml" xmlns:o="urn:schemas-microsoft-com:office:office">
<head>
<title></title>
<!--[if !mso]><!-->
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<!--<![endif]-->
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<style type="text/css">
#outlook a { padding:0; }
body { margin:0;padding:0;-webkit-text-size-adjust:100%;-ms-text-size-adjust:100%; }
table, td { border-collapse:collapse;mso-table-lspace:0pt;mso-table-rspace:0pt; }
img { border:0;height:auto;line-height:100%; outline:none;text-decoration:none;-ms-interpolation-mode:bicubic; }
p { display:block;margin:13px 0; }
</style>
<!--[if mso]>
<noscript>
<xml>
<o:OfficeDocumentSettings>
<o:AllowPNG/>
<o:PixelsPerInch>96</o:PixelsPerInch>
</o:OfficeDocumentSettings>
</xml>
</noscript>
<![endif]-->
<!--[if lte mso 11]>
<style type="text/css">
.mj-outlook-group-fix { width:100% !important; }
</style>
<![endif]-->
<style type="text/css">
@media only screen and (min-width:480px) {
.mj-column-per-70 { width:70% !important; max-width: 70%; }
.mj-column-per-30 { width:30% !important; max-width: 30%; }
.mj-column-per-100 { width:100% !important; max-width: 100%; }
.mj-column-per-90 { width:90% !important; max-width: 90%; }
}
</style>
<style media="screen and (min-width:480px)">
.moz-text-html .mj-column-per-70 { width:70% !important; max-width: 70%; }
.moz-text-html .mj-column-per-30 { width:30% !important; max-width: 30%; }
.moz-text-html .mj-column-per-100 { width:100% !important; max-width: 100%; }
.moz-text-html .mj-column-per-90 { width:90% !important; max-width: 90%; }
</style>
<style type="text/css">
@media only screen and (max-width:479px) {
table.mj-full-width-mobile { width: 100% !important; }
td.mj-full-width-mobile { width: auto !important; }
}
</style>
<style type="text/css">
.border-fix > table {
border-collapse: separate !important;
}
.border-fix > table > tbody > tr > td {
border-radius: 3px;
}
@media only screen and
(max-width: 480px) { .hide-small-img { display: none !important; } .send-bubble { padding-left: 20px; padding-right: 20px; width: 90% !important; } }
</style>
<!-- Responsive icon visibility -->
</head>
<body style="word-spacing:normal;background-color:#e6e9ef;">
<div class="border-fix" style="background-color:#e6e9ef;" lang="und" dir="auto">
<!-- Blue Header Section -->
<!--[if mso | IE]><table align="center" border="0" cellpadding="0" cellspacing="0" class="border-fix-outlook" role="presentation" style="width:660px;" width="660" ><tr><td style="line-height:0px;font-size:0px;mso-line-height-rule:exactly;"><![endif]-->
<div class="border-fix" style="margin:0px auto;max-width:660px;">
<table align="center" border="0" cellpadding="0" cellspacing="0" role="presentation" style="width:100%;">
<tbody>
<tr>
<td style="direction:ltr;font-size:0px;padding:20px 20px 10px 20px;text-align:center;">
<!--[if mso | IE]><table role="presentation" border="0" cellpadding="0" cellspacing="0"><tr><td class="" width="660px" ><![endif]-->
<table align="center" border="0" cellpadding="0" cellspacing="0" role="presentation" style="background:#175ddc;background-color:#175ddc;width:100%;border-radius:4px 4px 0px 0px;">
<tbody>
<tr>
<td>
<!--[if mso | IE]><table align="center" border="0" cellpadding="0" cellspacing="0" class="" role="presentation" style="width:620px;" width="620" bgcolor="#175ddc" ><tr><td style="line-height:0px;font-size:0px;mso-line-height-rule:exactly;"><![endif]-->
<div style="margin:0px auto;border-radius:4px 4px 0px 0px;max-width:620px;">
<table align="center" border="0" cellpadding="0" cellspacing="0" role="presentation" style="width:100%;border-radius:4px 4px 0px 0px;">
<tbody>
<tr>
<td style="direction:ltr;font-size:0px;padding:20px 0;text-align:center;">
<!--[if mso | IE]><table role="presentation" border="0" cellpadding="0" cellspacing="0"><tr><td class="" style="vertical-align:top;width:434px;" ><![endif]-->
<div class="mj-column-per-70 mj-outlook-group-fix" style="font-size:0px;text-align:left;direction:ltr;display:inline-block;vertical-align:top;width:100%;">
<table border="0" cellpadding="0" cellspacing="0" role="presentation" style="vertical-align:top;" width="100%">
<tbody>
<tr>
<td align="left" style="font-size:0px;padding:10px 25px;word-break:break-word;">
<table border="0" cellpadding="0" cellspacing="0" role="presentation" style="border-collapse:collapse;border-spacing:0px;">
<tbody>
<tr>
<td style="width:150px;">
<img alt src="https://bitwarden.com/images/logo-horizontal-white.png" style="border:0;display:block;outline:none;text-decoration:none;height:30px;width:100%;font-size:16px;" width="150" height="30">
</td>
</tr>
</tbody>
</table>
</td>
</tr>
<tr>
<td align="left" style="font-size:0px;padding:10px 25px;padding-top:0;padding-bottom:0;word-break:break-word;">
<div style="font-family:'Helvetica Neue', Helvetica, Arial, sans-serif;font-size:16px;line-height:1;text-align:left;color:#ffffff;"><h1 style="font-weight: normal; font-size: 24px; line-height: 32px">
Verify your email to access this Bitwarden Send
</h1></div>
</td>
</tr>
</tbody>
</table>
</div>
<!--[if mso | IE]></td><td class="" style="vertical-align:bottom;width:186px;" ><![endif]-->
<div class="mj-column-per-30 mj-outlook-group-fix" style="font-size:0px;text-align:left;direction:ltr;display:inline-block;vertical-align:bottom;width:100%;">
<table border="0" cellpadding="0" cellspacing="0" role="presentation" style="vertical-align:bottom;" width="100%">
<tbody>
<tr>
<td align="center" class="hide-small-img" style="font-size:0px;padding:0px;word-break:break-word;">
<table border="0" cellpadding="0" cellspacing="0" role="presentation" style="border-collapse:collapse;border-spacing:0px;">
<tbody>
<tr>
<td style="width:140px;">
<img alt src="https://assets.bitwarden.com/email/v1/spot-secure-send-round.png" style="border:0;display:block;outline:none;text-decoration:none;height:140px;width:100%;font-size:16px;" width="140" height="140">
</td>
</tr>
</tbody>
</table>
</td>
</tr>
</tbody>
</table>
</div>
<!--[if mso | IE]></td></tr></table><![endif]-->
</td>
</tr>
</tbody>
</table>
</div>
<!--[if mso | IE]></td></tr></table><![endif]-->
</td>
</tr>
</tbody>
</table>
<!--[if mso | IE]></td></tr></table><![endif]-->
</td>
</tr>
</tbody>
</table>
</div>
<!--[if mso | IE]></td></tr></table><![endif]-->
<!-- Main Content -->
<!--[if mso | IE]><table align="center" border="0" cellpadding="0" cellspacing="0" class="" role="presentation" style="width:660px;" width="660" ><tr><td style="line-height:0px;font-size:0px;mso-line-height-rule:exactly;"><![endif]-->
<div style="margin:0px auto;max-width:660px;">
<table align="center" border="0" cellpadding="0" cellspacing="0" role="presentation" style="width:100%;">
<tbody>
<tr>
<td style="direction:ltr;font-size:0px;padding:5px 20px 10px 20px;text-align:center;">
<!--[if mso | IE]><table role="presentation" border="0" cellpadding="0" cellspacing="0"><tr><td class="" width="660px" ><table align="center" border="0" cellpadding="0" cellspacing="0" class="" role="presentation" style="width:620px;" width="620" bgcolor="#ffffff" ><tr><td style="line-height:0px;font-size:0px;mso-line-height-rule:exactly;"><![endif]-->
<div style="background:#ffffff;background-color:#ffffff;margin:0px auto;max-width:620px;">
<table align="center" border="0" cellpadding="0" cellspacing="0" role="presentation" style="background:#ffffff;background-color:#ffffff;width:100%;">
<tbody>
<tr>
<td style="direction:ltr;font-size:0px;padding:20px 0;text-align:center;">
<!--[if mso | IE]><table role="presentation" border="0" cellpadding="0" cellspacing="0"><tr><td class="" style="vertical-align:top;width:620px;" ><![endif]-->
<div class="mj-column-per-100 mj-outlook-group-fix" style="font-size:0px;text-align:left;direction:ltr;display:inline-block;vertical-align:top;width:100%;">
<table border="0" cellpadding="0" cellspacing="0" role="presentation" width="100%">
<tbody>
<tr>
<td style="vertical-align:top;padding:0px;">
<table border="0" cellpadding="0" cellspacing="0" role="presentation" style width="100%">
<tbody>
<tr>
<td align="left" style="font-size:0px;padding:10px 25px;word-break:break-word;">
<div style="font-family:'Helvetica Neue', Helvetica, Arial, sans-serif;font-size:16px;line-height:1;text-align:left;color:#1B2029;">Your verification code is:</div>
</td>
</tr>
<tr>
<td align="left" style="font-size:0px;padding:10px 25px;word-break:break-word;">
<div style="font-family:'Helvetica Neue', Helvetica, Arial, sans-serif;font-size:32px;line-height:1;text-align:left;color:#1B2029;"><b>{{Token}}</b></div>
</td>
</tr>
<tr>
<td style="font-size:0px;word-break:break-word;">
<div style="height:20px;line-height:20px;">&#8202;</div>
</td>
</tr>
<tr>
<td align="left" style="font-size:0px;padding:10px 25px;word-break:break-word;">
<div style="font-family:'Helvetica Neue', Helvetica, Arial, sans-serif;font-size:16px;line-height:1;text-align:left;color:#1B2029;">This code expires in {{Expiry}} minutes. After that, you'll need to
verify your email again.</div>
</td>
</tr>
</tbody>
</table>
</td>
</tr>
</tbody>
</table>
</div>
<!--[if mso | IE]></td></tr></table><![endif]-->
</td>
</tr>
</tbody>
</table>
</div>
<!--[if mso | IE]></td></tr></table></td></tr><tr><td class="" width="660px" ><table align="center" border="0" cellpadding="0" cellspacing="0" class="" role="presentation" style="width:620px;" width="620" bgcolor="#ffffff" ><tr><td style="line-height:0px;font-size:0px;mso-line-height-rule:exactly;"><![endif]-->
<div style="background:#ffffff;background-color:#ffffff;margin:0px auto;max-width:620px;">
<table align="center" border="0" cellpadding="0" cellspacing="0" role="presentation" style="background:#ffffff;background-color:#ffffff;width:100%;">
<tbody>
<tr>
<td style="direction:ltr;font-size:0px;padding:0px 0px 20px 0px;text-align:center;">
<!--[if mso | IE]><table role="presentation" border="0" cellpadding="0" cellspacing="0"><tr><td class="send-bubble-outlook" style="vertical-align:top;width:558px;" ><![endif]-->
<div class="mj-column-per-90 mj-outlook-group-fix send-bubble" style="font-size:0px;text-align:left;direction:ltr;display:inline-block;vertical-align:top;width:100%;">
<table border="0" cellpadding="0" cellspacing="0" role="presentation" width="100%">
<tbody>
<tr>
<td style="vertical-align:top;padding:0px;">
<table border="0" cellpadding="0" cellspacing="0" role="presentation" style="background-color:#DBE5F6;border-radius:16px;" width="100%">
<tbody>
<tr>
<td align="left" style="font-size:0px;padding:10px 25px;word-break:break-word;">
<div style="font-family:'Helvetica Neue', Helvetica, Arial, sans-serif;font-size:16px;line-height:1;text-align:left;color:#1B2029;"><p>
Bitwarden Send transmits sensitive, temporary information to
others easily and securely. Learn more about
<a href="https://bitwarden.com/help/send" class="link" style="text-decoration: none; color: #175ddc; font-weight: 600;">Bitwarden Send</a>
or
<a href="https://bitwarden.com/signup" class="link" style="text-decoration: none; color: #175ddc; font-weight: 600;">sign up</a>
to try it today.
</p></div>
</td>
</tr>
</tbody>
</table>
</td>
</tr>
</tbody>
</table>
</div>
<!--[if mso | IE]></td></tr></table><![endif]-->
</td>
</tr>
</tbody>
</table>
</div>
<!--[if mso | IE]></td></tr></table></td></tr></table><![endif]-->
</td>
</tr>
</tbody>
</table>
</div>
<!--[if mso | IE]></td></tr></table><![endif]-->
<!-- Learn More Section -->
<!--[if mso | IE]><table align="center" border="0" cellpadding="0" cellspacing="0" class="" role="presentation" style="width:660px;" width="660" ><tr><td style="line-height:0px;font-size:0px;mso-line-height-rule:exactly;"><![endif]-->
<div style="margin:0px auto;max-width:660px;">
<table align="center" border="0" cellpadding="0" cellspacing="0" role="presentation" style="width:100%;">
<tbody>
<tr>
<td style="direction:ltr;font-size:0px;padding:5px 20px 10px 20px;text-align:center;">
<!--[if mso | IE]><table role="presentation" border="0" cellpadding="0" cellspacing="0"><tr><td class="" width="660px" ><table align="center" border="0" cellpadding="0" cellspacing="0" class="" role="presentation" style="width:620px;" width="620" bgcolor="#f6f6f6" ><tr><td style="line-height:0px;font-size:0px;mso-line-height-rule:exactly;"><![endif]-->
<div style="background:#f6f6f6;background-color:#f6f6f6;margin:0px auto;border-radius:0px 0px 4px 4px;max-width:620px;">
<table align="center" border="0" cellpadding="0" cellspacing="0" role="presentation" style="background:#f6f6f6;background-color:#f6f6f6;width:100%;border-radius:0px 0px 4px 4px;">
<tbody>
<tr>
<td style="direction:ltr;font-size:0px;padding:5px 20px 10px 20px;text-align:center;">
<!--[if mso | IE]><table role="presentation" border="0" cellpadding="0" cellspacing="0"><tr><td class="" style="vertical-align:top;width:406px;" ><![endif]-->
<div class="mj-column-per-70 mj-outlook-group-fix" style="font-size:0px;text-align:left;direction:ltr;display:inline-block;vertical-align:top;width:100%;">
<table border="0" cellpadding="0" cellspacing="0" role="presentation" style="vertical-align:top;" width="100%">
<tbody>
<tr>
<td align="left" style="font-size:0px;padding:10px 25px;word-break:break-word;">
<div style="font-family:'Helvetica Neue', Helvetica, Arial, sans-serif;font-size:16px;line-height:24px;text-align:left;color:#1B2029;"><h3 style="font-size: 20px; margin: 0; line-height: 28px">
Learn more about Bitwarden
</h3>
Find user guides, product documentation, and videos on the
<a href="https://bitwarden.com/help/" class="link" style="text-decoration: none; color: #175ddc; font-weight: 600;"> Bitwarden Help Center</a>.</div>
</td>
</tr>
</tbody>
</table>
</div>
<!--[if mso | IE]></td><td class="" style="vertical-align:top;width:174px;" ><![endif]-->
<div class="mj-column-per-30 mj-outlook-group-fix" style="font-size:0px;text-align:left;direction:ltr;display:inline-block;vertical-align:top;width:100%;">
<table border="0" cellpadding="0" cellspacing="0" role="presentation" style="vertical-align:top;" width="100%">
<tbody>
<tr>
<td align="center" class="hide-small-img" style="font-size:0px;padding:10px 25px;word-break:break-word;">
<table border="0" cellpadding="0" cellspacing="0" role="presentation" style="border-collapse:collapse;border-spacing:0px;">
<tbody>
<tr>
<td style="width:94px;">
<img alt src="https://assets.bitwarden.com/email/v1/spot-community.png" style="border:0;display:block;outline:none;text-decoration:none;height:auto;width:100%;font-size:16px;" width="94" height="auto">
</td>
</tr>
</tbody>
</table>
</td>
</tr>
</tbody>
</table>
</div>
<!--[if mso | IE]></td></tr></table><![endif]-->
</td>
</tr>
</tbody>
</table>
</div>
<!--[if mso | IE]></td></tr></table></td></tr></table><![endif]-->
</td>
</tr>
</tbody>
</table>
</div>
<!--[if mso | IE]></td></tr></table><![endif]-->
<!-- Footer -->
<!--[if mso | IE]><table align="center" border="0" cellpadding="0" cellspacing="0" class="" role="presentation" style="width:660px;" width="660" ><tr><td style="line-height:0px;font-size:0px;mso-line-height-rule:exactly;"><![endif]-->
<div style="margin:0px auto;max-width:660px;">
<table align="center" border="0" cellpadding="0" cellspacing="0" role="presentation" style="width:100%;">
<tbody>
<tr>
<td style="direction:ltr;font-size:0px;padding:20px 0;text-align:center;">
<!--[if mso | IE]><table role="presentation" border="0" cellpadding="0" cellspacing="0"><tr><td class="" style="vertical-align:top;width:660px;" ><![endif]-->
<div class="mj-column-per-100 mj-outlook-group-fix" style="font-size:0px;text-align:left;direction:ltr;display:inline-block;vertical-align:top;width:100%;">
<table border="0" cellpadding="0" cellspacing="0" role="presentation" style="vertical-align:top;" width="100%">
<tbody>
<tr>
<td align="center" style="font-size:0px;padding:0;word-break:break-word;">
<!--[if mso | IE]><table align="center" border="0" cellpadding="0" cellspacing="0" role="presentation" ><tr><td><![endif]-->
<table align="center" border="0" cellpadding="0" cellspacing="0" role="presentation" style="float:none;display:inline-table;">
<tbody>
<tr>
<td style="padding:10px;vertical-align:middle;">
<table border="0" cellpadding="0" cellspacing="0" role="presentation" style="border-radius:3px;width:30px;">
<tbody>
<tr>
<td style="font-size:0;height:30px;vertical-align:middle;width:30px;">
<a href="https://x.com/bitwarden" target="_blank">
<img alt height="30" src="https://assets.bitwarden.com/email/v1/mail-x.png" style="border-radius:3px;display:block;" width="30">
</a>
</td>
</tr>
</tbody>
</table>
</td>
</tr>
</tbody>
</table>
<!--[if mso | IE]></td><td><![endif]-->
<table align="center" border="0" cellpadding="0" cellspacing="0" role="presentation" style="float:none;display:inline-table;">
<tbody>
<tr>
<td style="padding:10px;vertical-align:middle;">
<table border="0" cellpadding="0" cellspacing="0" role="presentation" style="border-radius:3px;width:30px;">
<tbody>
<tr>
<td style="font-size:0;height:30px;vertical-align:middle;width:30px;">
<a href="https://www.reddit.com/r/Bitwarden/" target="_blank">
<img alt height="30" src="https://assets.bitwarden.com/email/v1/mail-reddit.png" style="border-radius:3px;display:block;" width="30">
</a>
</td>
</tr>
</tbody>
</table>
</td>
</tr>
</tbody>
</table>
<!--[if mso | IE]></td><td><![endif]-->
<table align="center" border="0" cellpadding="0" cellspacing="0" role="presentation" style="float:none;display:inline-table;">
<tbody>
<tr>
<td style="padding:10px;vertical-align:middle;">
<table border="0" cellpadding="0" cellspacing="0" role="presentation" style="border-radius:3px;width:30px;">
<tbody>
<tr>
<td style="font-size:0;height:30px;vertical-align:middle;width:30px;">
<a href="https://community.bitwarden.com/" target="_blank">
<img alt height="30" src="https://assets.bitwarden.com/email/v1/mail-discourse.png" style="border-radius:3px;display:block;" width="30">
</a>
</td>
</tr>
</tbody>
</table>
</td>
</tr>
</tbody>
</table>
<!--[if mso | IE]></td><td><![endif]-->
<table align="center" border="0" cellpadding="0" cellspacing="0" role="presentation" style="float:none;display:inline-table;">
<tbody>
<tr>
<td style="padding:10px;vertical-align:middle;">
<table border="0" cellpadding="0" cellspacing="0" role="presentation" style="border-radius:3px;width:30px;">
<tbody>
<tr>
<td style="font-size:0;height:30px;vertical-align:middle;width:30px;">
<a href="https://github.com/bitwarden" target="_blank">
<img alt height="30" src="https://assets.bitwarden.com/email/v1/mail-github.png" style="border-radius:3px;display:block;" width="30">
</a>
</td>
</tr>
</tbody>
</table>
</td>
</tr>
</tbody>
</table>
<!--[if mso | IE]></td><td><![endif]-->
<table align="center" border="0" cellpadding="0" cellspacing="0" role="presentation" style="float:none;display:inline-table;">
<tbody>
<tr>
<td style="padding:10px;vertical-align:middle;">
<table border="0" cellpadding="0" cellspacing="0" role="presentation" style="border-radius:3px;width:30px;">
<tbody>
<tr>
<td style="font-size:0;height:30px;vertical-align:middle;width:30px;">
<a href="https://www.youtube.com/channel/UCId9a_jQqvJre0_dE2lE_Rw" target="_blank">
<img alt height="30" src="https://assets.bitwarden.com/email/v1/mail-youtube.png" style="border-radius:3px;display:block;" width="30">
</a>
</td>
</tr>
</tbody>
</table>
</td>
</tr>
</tbody>
</table>
<!--[if mso | IE]></td><td><![endif]-->
<table align="center" border="0" cellpadding="0" cellspacing="0" role="presentation" style="float:none;display:inline-table;">
<tbody>
<tr>
<td style="padding:10px;vertical-align:middle;">
<table border="0" cellpadding="0" cellspacing="0" role="presentation" style="border-radius:3px;width:30px;">
<tbody>
<tr>
<td style="font-size:0;height:30px;vertical-align:middle;width:30px;">
<a href="https://www.linkedin.com/company/bitwarden1/" target="_blank">
<img alt height="30" src="https://assets.bitwarden.com/email/v1/mail-linkedin.png" style="border-radius:3px;display:block;" width="30">
</a>
</td>
</tr>
</tbody>
</table>
</td>
</tr>
</tbody>
</table>
<!--[if mso | IE]></td><td><![endif]-->
<table align="center" border="0" cellpadding="0" cellspacing="0" role="presentation" style="float:none;display:inline-table;">
<tbody>
<tr>
<td style="padding:10px;vertical-align:middle;">
<table border="0" cellpadding="0" cellspacing="0" role="presentation" style="border-radius:3px;width:30px;">
<tbody>
<tr>
<td style="font-size:0;height:30px;vertical-align:middle;width:30px;">
<a href="https://www.facebook.com/bitwarden/" target="_blank">
<img alt height="30" src="https://assets.bitwarden.com/email/v1/mail-facebook.png" style="border-radius:3px;display:block;" width="30">
</a>
</td>
</tr>
</tbody>
</table>
</td>
</tr>
</tbody>
</table>
<!--[if mso | IE]></td></tr></table><![endif]-->
</td>
</tr>
<tr>
<td align="center" style="font-size:0px;padding:10px 25px;word-break:break-word;">
<div style="font-family:'Helvetica Neue', Helvetica, Arial, sans-serif;font-size:12px;line-height:16px;text-align:center;color:#5A6D91;"><p style="margin-bottom: 5px">
© 2025 Bitwarden Inc. 1 N. Calle Cesar Chavez, Suite 102, Santa
Barbara, CA, USA
</p>
<p style="margin-top: 5px">
Always confirm you are on a trusted Bitwarden domain before logging
in:<br>
<a href="https://bitwarden.com/">bitwarden.com</a> |
<a href="https://bitwarden.com/help/emails-from-bitwarden/">Learn why we include this</a>
</p></div>
</td>
</tr>
</tbody>
</table>
</div>
<!--[if mso | IE]></td></tr></table><![endif]-->
</td>
</tr>
</tbody>
</table>
</div>
<!--[if mso | IE]></td></tr></table><![endif]-->
</div>
</body>
</html>

View File

@@ -0,0 +1,9 @@
{{#>BasicTextLayout}}
Verify your email to access this Bitwarden Send.
Your verification code is: {{Token}}
This code can only be used once and expires in {{Expiry}} minutes. After that you'll need to verify your email again.
Bitwarden Send transmits sensitive, temporary information to others easily and securely. Learn more about Bitwarden Send or sign up to try it today.
{{/BasicTextLayout}}

View File

@@ -1,5 +1,5 @@
{
"packages": [
"components/hero"
"components/mj-bw-hero"
]
}

View File

@@ -2,38 +2,38 @@
<mj-column>
<mj-social icon-size="30px" inner-padding="10px" padding="0">
<mj-social-element
href="https://twitter.com/bitwarden"
src="https://bitwarden.com/images/mail-twitter.png"
href="https://x.com/bitwarden"
src="https://assets.bitwarden.com/email/v1/mail-x.png"
></mj-social-element>
<mj-social-element
href="https://www.reddit.com/r/Bitwarden/"
src="https://bitwarden.com/images/mail-reddit.png"
src="https://assets.bitwarden.com/email/v1/mail-reddit.png"
></mj-social-element>
<mj-social-element
href="https://community.bitwarden.com/"
src="https://bitwarden.com/images/mail-discourse.png"
src="https://assets.bitwarden.com/email/v1/mail-discourse.png"
></mj-social-element>
<mj-social-element
href="https://github.com/bitwarden"
src="https://bitwarden.com/images/mail-github.png"
src="https://assets.bitwarden.com/email/v1/mail-github.png"
></mj-social-element>
<mj-social-element
href="https://www.youtube.com/channel/UCId9a_jQqvJre0_dE2lE_Rw"
src="https://bitwarden.com/images/mail-youtube.png"
src="https://assets.bitwarden.com/email/v1/mail-youtube.png"
></mj-social-element>
<mj-social-element
href="https://www.linkedin.com/company/bitwarden1/"
src="https://bitwarden.com/images/mail-linkedin.png"
src="https://assets.bitwarden.com/email/v1/mail-linkedin.png"
></mj-social-element>
<mj-social-element
href="https://www.facebook.com/bitwarden/"
src="https://bitwarden.com/images/mail-facebook.png"
src="https://assets.bitwarden.com/email/v1/mail-facebook.png"
></mj-social-element>
</mj-social>
@@ -45,8 +45,8 @@
<p style="margin-top: 5px">
Always confirm you are on a trusted Bitwarden domain before logging
in:<br />
<a href="#">bitwarden.com</a> |
<a href="#">Learn why we include this</a>
<a href="https://bitwarden.com/">bitwarden.com</a> |
<a href="https://bitwarden.com/help/emails-from-bitwarden/">Learn why we include this</a>
</p>
</mj-text>
</mj-column>

View File

@@ -4,7 +4,7 @@
font-size="16px"
/>
<mj-button background-color="#175ddc" />
<mj-text color="#333" />
<mj-text color="#1B2029" />
<mj-body background-color="#e6e9ef" width="660px" />
</mj-attributes>
<mj-style inline="inline">
@@ -22,3 +22,9 @@
border-radius: 3px;
}
</mj-style>
<!-- Responsive icon visibility -->
<mj-style>
@media only screen and
(max-width: 480px) { .hide-small-img { display: none !important; } .send-bubble { padding-left: 20px; padding-right: 20px; width: 90% !important; } }
</mj-style>

View File

@@ -1,64 +0,0 @@
const { BodyComponent } = require("mjml-core");
class MjBwHero extends BodyComponent {
static dependencies = {
// Tell the validator which tags are allowed as our component's parent
"mj-column": ["mj-bw-hero"],
"mj-wrapper": ["mj-bw-hero"],
// Tell the validator which tags are allowed as our component's children
"mj-bw-hero": [],
};
static allowedAttributes = {
"img-src": "string",
title: "string",
"button-text": "string",
"button-url": "string",
};
static defaultAttributes = {};
render() {
return this.renderMJML(`
<mj-section
full-width="full-width"
background-color="#175ddc"
border-radius="4px 4px 0 0"
>
<mj-column width="70%">
<mj-image
align="left"
src="https://bitwarden.com/images/logo-horizontal-white.png"
width="150px"
height="30px"
></mj-image>
<mj-text color="#fff" padding-top="0" padding-bottom="0">
<h1 style="font-weight: normal; font-size: 24px; line-height: 32px">
${this.getAttribute("title")}
</h1>
</mj-text>
<mj-button
href="${this.getAttribute("button-url")}"
background-color="#fff"
color="#1A41AC"
border-radius="20px"
align="left"
>
${this.getAttribute("button-text")}
</mj-button
>
</mj-column>
<mj-column width="30%" vertical-align="bottom">
<mj-image
src="${this.getAttribute("img-src")}"
alt=""
width="140px"
height="140px"
padding="0"
/>
</mj-column>
</mj-section>
`);
}
}
module.exports = MjBwHero;

View File

@@ -0,0 +1,18 @@
<mj-section border-radius="0px 0px 4px 4px" background-color="#f6f6f6" padding="5px 20px 10px 20px">
<mj-column width="70%">
<mj-text line-height="24px">
<h3 style="font-size: 20px; margin: 0; line-height: 28px">
Learn more about Bitwarden
</h3>
Find user guides, product documentation, and videos on the
<a href="https://bitwarden.com/help/" class="link"> Bitwarden Help Center</a>.
</mj-text>
</mj-column>
<mj-column width="30%">
<mj-image
src="https://assets.bitwarden.com/email/v1/spot-community.png"
css-class="hide-small-img"
width="94px"
/>
</mj-column>
</mj-section>

View File

@@ -0,0 +1,100 @@
const { BodyComponent } = require("mjml-core");
class MjBwHero extends BodyComponent {
static dependencies = {
// Tell the validator which tags are allowed as our component's parent
"mj-column": ["mj-bw-hero"],
"mj-wrapper": ["mj-bw-hero"],
// Tell the validator which tags are allowed as our component's children
"mj-bw-hero": [],
};
static allowedAttributes = {
"img-src": "string", // REQUIRED: Source for the image displayed in the right-hand side of the blue header area
title: "string", // REQUIRED: large text stating primary purpose of the email
"button-text": "string", // OPTIONAL: text to display in the button
"button-url": "string", // OPTIONAL: URL to navigate to when the button is clicked
"sub-title": "string", // OPTIONAL: smaller text providing additional context for the title
};
static defaultAttributes = {};
render() {
if (this.getAttribute("button-text") && this.getAttribute("button-url")) {
return this.renderMJML(`
<mj-section
full-width="full-width"
background-color="#175ddc"
border-radius="4px 4px 0px 0px"
>
<mj-column width="70%">
<mj-image
align="left"
src="https://bitwarden.com/images/logo-horizontal-white.png"
width="150px"
height="30px"
></mj-image>
<mj-text color="#fff" padding-top="0" padding-bottom="0">
<h1 style="font-weight: normal; font-size: 24px; line-height: 32px">
${this.getAttribute("title")}
</h1>
</mj-text>
<mj-button
href="${this.getAttribute("button-url")}"
background-color="#fff"
color="#1A41AC"
border-radius="20px"
align="left"
>
${this.getAttribute("button-text")}
</mj-button
>
</mj-column>
<mj-column width="30%" vertical-align="bottom">
<mj-image
src="${this.getAttribute("img-src")}"
alt=""
width="140px"
height="140px"
padding="0px"
css-class="hide-small-img"
/>
</mj-column>
</mj-section>
`);
} else {
return this.renderMJML(`
<mj-section
full-width="full-width"
background-color="#175ddc"
border-radius="4px 4px 0px 0px"
>
<mj-column width="70%">
<mj-image
align="left"
src="https://bitwarden.com/images/logo-horizontal-white.png"
width="150px"
height="30px"
></mj-image>
<mj-text color="#fff" padding-top="0" padding-bottom="0">
<h1 style="font-weight: normal; font-size: 24px; line-height: 32px">
${this.getAttribute("title")}
</h1>
</mj-text>
</mj-column>
<mj-column width="30%" vertical-align="bottom">
<mj-image
src="${this.getAttribute("img-src")}"
alt=""
width="140px"
height="140px"
padding="0px"
css-class="hide-small-img"
/>
</mj-column>
</mj-section>
`);
}
}
}
module.exports = MjBwHero;

View File

@@ -0,0 +1,64 @@
<mjml>
<mj-head>
<mj-include path="../../components/head.mjml" />
<mj-style> </mj-style>
</mj-head>
<mj-body css-class="border-fix">
<!-- Blue Header Section -->
<mj-wrapper css-class="border-fix" padding="20px 20px 10px 20px">
<mj-bw-hero
img-src="https://assets.bitwarden.com/email/v1/spot-secure-send-round.png"
title="Verify your email to access this Bitwarden Send"
/>
</mj-wrapper>
<!-- Main Content -->
<mj-wrapper padding="5px 20px 10px 20px">
<mj-section background-color="#fff">
<mj-column padding="0px">
<mj-text> Your verification code is: </mj-text>
<mj-text font-size="32px"> <b>{{Token}}</b> </mj-text>
<mj-spacer height="20px" />
<mj-text>
This code expires in {{Expiry}} minutes. After that, you'll need to
verify your email again.
</mj-text>
</mj-column>
</mj-section>
<mj-section
background-color="#fff"
padding="0px 0px 20px 0px"
>
<mj-column
css-class="send-bubble"
width="90%"
inner-background-color="#DBE5F6"
inner-border-radius="16px"
padding="0px"
>
<mj-text>
<p>
Bitwarden Send transmits sensitive, temporary information to
others easily and securely. Learn more about
<a href="https://bitwarden.com/help/send" class="link"
>Bitwarden Send</a
>
or
<a href="https://bitwarden.com/signup" class="link">sign up</a>
to try it today.
</p>
</mj-text>
</mj-column>
</mj-section>
</mj-wrapper>
<!-- Learn More Section -->
<mj-wrapper padding="5px 20px 10px 20px">
<mj-include path="../../components/learn-more-footer.mjml" />
</mj-wrapper>
<!-- Footer -->
<mj-include path="../../components/footer.mjml" />
</mj-body>
</mjml>

View File

@@ -1,10 +1,10 @@
<mjml>
<mj-head>
<mj-include path="../components/head.mjml" />
<mj-include path="../../components/head.mjml" />
</mj-head>
<mj-body background-color="#f6f6f6">
<mj-include path="../components/logo.mjml" />
<mj-include path="../../components/logo.mjml" />
<mj-wrapper
background-color="#fff"
@@ -24,6 +24,6 @@
</mj-section>
</mj-wrapper>
<mj-include path="../components/footer.mjml" />
<mj-include path="../../components/footer.mjml" />
</mj-body>
</mjml>

View File

@@ -22,26 +22,7 @@
</mj-text>
</mj-column>
</mj-section>
<mj-section background-color="#fbfbfb">
<mj-column width="70%">
<mj-text line-height="24px">
<h3 style="font-size: 20px; margin: 0; line-height: 28px">
Were here for you!
</h3>
If you have any questions, search the Bitwarden
<a href="#" class="link">Help</a>
site or
<a href="#" class="link">contact us</a>.
</mj-text>
</mj-column>
<mj-column width="30%">
<mj-image
src="https://assets.bitwarden.com/email/v1/chat.png"
height="77px"
width="94px"
/>
</mj-column>
</mj-section>
<mj-include path="../components/learn-more-footer.mjml" />
</mj-wrapper>
<mj-include path="../components/footer.mjml" />

View File

@@ -9,4 +9,5 @@ public class DefaultEmailOtpViewModel : BaseMailModel
public string? TheDate { get; set; }
public string? TheTime { get; set; }
public string? TimeZone { get; set; }
public string? Expiry { get; set; }
}

View File

@@ -226,7 +226,11 @@ public class UpdateSecretsManagerSubscriptionCommand : IUpdateSecretsManagerSubs
// Check minimum seats currently in use by the organization
if (organization.SmSeats.Value > update.SmSeats.Value)
{
// Retrieve the number of currently occupied Secrets Manager seats for the organization.
var occupiedSeats = await _organizationUserRepository.GetOccupiedSmSeatCountByOrganizationIdAsync(organization.Id);
// Check if the occupied number of seats exceeds the updated seat count.
// If so, throw an exception indicating that the subscription cannot be decreased below the current usage.
if (occupiedSeats > update.SmSeats.Value)
{
throw new BadRequestException($"{occupiedSeats} users are currently occupying Secrets Manager seats. " +
@@ -412,7 +416,7 @@ public class UpdateSecretsManagerSubscriptionCommand : IUpdateSecretsManagerSubs
}
/// <summary>
/// Requests the number of Secret Manager seats and service accounts are currently used by the organization
/// Requests the number of Secret Manager seats and service accounts currently used by the organization
/// </summary>
/// <param name="organizationId"> The id of the organization</param>
/// <returns > A tuple containing the occupied seats and the occupied service account counts</returns>

View File

@@ -4,6 +4,7 @@ using Bit.Core.AdminConsole.Entities;
using Bit.Core.AdminConsole.Entities.Provider;
using Bit.Core.Auth.Entities;
using Bit.Core.Auth.Enums;
using Bit.Core.Auth.Identity.TokenProviders;
using Bit.Core.Billing.Enums;
using Bit.Core.Entities;
using Bit.Core.Models.Data.Organizations;
@@ -31,6 +32,16 @@ public interface IMailService
Task SendChangeEmailEmailAsync(string newEmailAddress, string token);
Task SendTwoFactorEmailAsync(string email, string accountEmail, string token, string deviceIp, string deviceType, TwoFactorEmailPurpose purpose);
Task SendSendEmailOtpEmailAsync(string email, string token, string subject);
/// <summary>
/// <see cref="DefaultOtpTokenProviderOptions"/> has a default expiry of 5 minutes so we set the expiry to that value int he view model.
/// Sends OTP code token to the specified email address.
/// will replace <see cref="SendSendEmailOtpEmailAsync"/> when MJML templates are fully accepted.
/// </summary>
/// <param name="email">Email address to send the OTP to</param>
/// <param name="token">Otp code token</param>
/// <param name="subject">subject line of the email</param>
/// <returns>Task</returns>
Task SendSendEmailOtpEmailv2Async(string email, string token, string subject);
Task SendFailedTwoFactorAttemptEmailAsync(string email, TwoFactorProviderType type, DateTime utcNow, string ip);
Task SendNoMasterPasswordHintEmailAsync(string email);
Task SendMasterPasswordHintEmailAsync(string email, string hint);

View File

@@ -224,6 +224,27 @@ public class HandlebarsMailService : IMailService
await _mailDeliveryService.SendEmailAsync(message);
}
public async Task SendSendEmailOtpEmailv2Async(string email, string token, string subject)
{
var message = CreateDefaultMessage(subject, email);
var requestDateTime = DateTime.UtcNow;
var model = new DefaultEmailOtpViewModel
{
Token = token,
Expiry = "5", // This should be configured through the OTPDefaultTokenProviderOptions but for now we will hardcode it to 5 minutes.
TheDate = requestDateTime.ToLongDateString(),
TheTime = requestDateTime.ToShortTimeString(),
TimeZone = _utcTimeZoneDisplay,
WebVaultUrl = _globalSettings.BaseServiceUri.VaultWithHash,
SiteName = _globalSettings.SiteName,
};
await AddMessageContentAsync(message, "Auth.SendAccessEmailOtpEmailv2", model);
message.MetaData.Add("SendGridBypassListManagement", true);
// TODO - PM-25380 change to string constant
message.Category = "SendEmailOtp";
await _mailDeliveryService.SendEmailAsync(message);
}
public async Task SendFailedTwoFactorAttemptEmailAsync(string email, TwoFactorProviderType failedType, DateTime utcNow, string ip)
{
// Check if we've sent this email within the last hour

View File

@@ -8,6 +8,7 @@ using Bit.Core.Billing.Constants;
using Bit.Core.Billing.Extensions;
using Bit.Core.Billing.Models;
using Bit.Core.Billing.Organizations.Models;
using Bit.Core.Billing.Premium.Commands;
using Bit.Core.Billing.Pricing;
using Bit.Core.Billing.Tax.Requests;
using Bit.Core.Billing.Tax.Responses;
@@ -896,11 +897,14 @@ public class StripePaymentService : IPaymentService
}
}
[Obsolete($"Use {nameof(PreviewPremiumTaxCommand)} instead.")]
public async Task<PreviewInvoiceResponseModel> PreviewInvoiceAsync(
PreviewIndividualInvoiceRequestBody parameters,
string gatewayCustomerId,
string gatewaySubscriptionId)
{
var premiumPlan = await _pricingClient.GetAvailablePremiumPlan();
var options = new InvoiceCreatePreviewOptions
{
AutomaticTax = new InvoiceAutomaticTaxOptions { Enabled = true, },
@@ -909,8 +913,17 @@ public class StripePaymentService : IPaymentService
{
Items =
[
new InvoiceSubscriptionDetailsItemOptions { Quantity = 1, Plan = StripeConstants.Prices.PremiumAnnually },
new InvoiceSubscriptionDetailsItemOptions { Quantity = parameters.PasswordManager.AdditionalStorage, Plan = StripeConstants.Prices.StoragePlanPersonal }
new InvoiceSubscriptionDetailsItemOptions
{
Quantity = 1,
Plan = premiumPlan.Seat.StripePriceId
},
new InvoiceSubscriptionDetailsItemOptions
{
Quantity = parameters.PasswordManager.AdditionalStorage,
Plan = premiumPlan.Storage.StripePriceId
}
]
},
CustomerDetails = new InvoiceCustomerDetailsOptions
@@ -1028,7 +1041,7 @@ public class StripePaymentService : IPaymentService
{
Items =
[
new()
new InvoiceSubscriptionDetailsItemOptions
{
Quantity = parameters.PasswordManager.AdditionalStorage,
Plan = plan.PasswordManager.StripeStoragePlanId
@@ -1049,7 +1062,7 @@ public class StripePaymentService : IPaymentService
{
var sponsoredPlan = Utilities.StaticStore.GetSponsoredPlan(parameters.PasswordManager.SponsoredPlan.Value);
options.SubscriptionDetails.Items.Add(
new() { Quantity = 1, Plan = sponsoredPlan.StripePlanId }
new InvoiceSubscriptionDetailsItemOptions { Quantity = 1, Plan = sponsoredPlan.StripePlanId }
);
}
else
@@ -1057,13 +1070,13 @@ public class StripePaymentService : IPaymentService
if (plan.PasswordManager.HasAdditionalSeatsOption)
{
options.SubscriptionDetails.Items.Add(
new() { Quantity = parameters.PasswordManager.Seats, Plan = plan.PasswordManager.StripeSeatPlanId }
new InvoiceSubscriptionDetailsItemOptions { Quantity = parameters.PasswordManager.Seats, Plan = plan.PasswordManager.StripeSeatPlanId }
);
}
else
{
options.SubscriptionDetails.Items.Add(
new() { Quantity = 1, Plan = plan.PasswordManager.StripePlanId }
new InvoiceSubscriptionDetailsItemOptions { Quantity = 1, Plan = plan.PasswordManager.StripePlanId }
);
}
@@ -1071,7 +1084,7 @@ public class StripePaymentService : IPaymentService
{
if (plan.SecretsManager.HasAdditionalSeatsOption)
{
options.SubscriptionDetails.Items.Add(new()
options.SubscriptionDetails.Items.Add(new InvoiceSubscriptionDetailsItemOptions
{
Quantity = parameters.SecretsManager?.Seats ?? 0,
Plan = plan.SecretsManager.StripeSeatPlanId
@@ -1080,7 +1093,7 @@ public class StripePaymentService : IPaymentService
if (plan.SecretsManager.HasAdditionalServiceAccountOption)
{
options.SubscriptionDetails.Items.Add(new()
options.SubscriptionDetails.Items.Add(new InvoiceSubscriptionDetailsItemOptions
{
Quantity = parameters.SecretsManager?.AdditionalMachineAccounts ?? 0,
Plan = plan.SecretsManager.StripeServiceAccountPlanId

View File

@@ -14,10 +14,10 @@ using Bit.Core.AdminConsole.Services;
using Bit.Core.Auth.Enums;
using Bit.Core.Auth.Models;
using Bit.Core.Auth.UserFeatures.TwoFactorAuth.Interfaces;
using Bit.Core.Billing.Constants;
using Bit.Core.Billing.Models;
using Bit.Core.Billing.Models.Business;
using Bit.Core.Billing.Models.Sales;
using Bit.Core.Billing.Pricing;
using Bit.Core.Billing.Services;
using Bit.Core.Billing.Tax.Models;
using Bit.Core.Context;
@@ -72,6 +72,7 @@ public class UserService : UserManager<User>, IUserService
private readonly ITwoFactorIsEnabledQuery _twoFactorIsEnabledQuery;
private readonly IDistributedCache _distributedCache;
private readonly IPolicyRequirementQuery _policyRequirementQuery;
private readonly IPricingClient _pricingClient;
public UserService(
IUserRepository userRepository,
@@ -106,7 +107,8 @@ public class UserService : UserManager<User>, IUserService
IRevokeNonCompliantOrganizationUserCommand revokeNonCompliantOrganizationUserCommand,
ITwoFactorIsEnabledQuery twoFactorIsEnabledQuery,
IDistributedCache distributedCache,
IPolicyRequirementQuery policyRequirementQuery)
IPolicyRequirementQuery policyRequirementQuery,
IPricingClient pricingClient)
: base(
store,
optionsAccessor,
@@ -146,6 +148,7 @@ public class UserService : UserManager<User>, IUserService
_twoFactorIsEnabledQuery = twoFactorIsEnabledQuery;
_distributedCache = distributedCache;
_policyRequirementQuery = policyRequirementQuery;
_pricingClient = pricingClient;
}
public Guid? GetProperUserId(ClaimsPrincipal principal)
@@ -972,8 +975,9 @@ public class UserService : UserManager<User>, IUserService
throw new BadRequestException("Not a premium user.");
}
var secret = await BillingHelpers.AdjustStorageAsync(_paymentService, user, storageAdjustmentGb,
StripeConstants.Prices.StoragePlanPersonal);
var premiumPlan = await _pricingClient.GetAvailablePremiumPlan();
var secret = await BillingHelpers.AdjustStorageAsync(_paymentService, user, storageAdjustmentGb, premiumPlan.Storage.StripePriceId);
await SaveUserAsync(user);
return secret;
}

View File

@@ -98,6 +98,11 @@ public class NoopMailService : IMailService
return Task.FromResult(0);
}
public Task SendSendEmailOtpEmailv2Async(string email, string token, string subject)
{
return Task.FromResult(0);
}
public Task SendFailedTwoFactorAttemptEmailAsync(string email, TwoFactorProviderType failedType, DateTime utcNow, string ip)
{
return Task.FromResult(0);