1
0
mirror of https://github.com/bitwarden/server synced 2026-03-01 02:41:33 +00:00

[PM-29604] [PM-29605] [PM-29606] Support premium subscription page redesign (#6821)

* feat(get-subscription): Add EnumMemberJsonConverter

* feat(get-subscription): Add BitwardenDiscount model

* feat(get-subscription): Add Cart model

* feat(get-subscription): Add Storage model

* feat(get-subscription): Add BitwardenSubscription model

* feat(get-subscription): Add DiscountExtensions

* feat(get-subscription): Add error code to StripeConstants

* feat(get-subscription): Add GetBitwardenSubscriptionQuery

* feat(get-subscription): Expose GET /account/billing/vnext/subscription

* feat(reinstate-subscription): Add ReinstateSubscriptionCommand

* feat(reinstate-subscription): Expose POST /account/billing/vnext/subscription/reinstate

* feat(pay-with-paypal-immediately): Add SubscriberId union

* feat(pay-with-paypal-immediately): Add BraintreeService with PayInvoice method

* feat(pay-with-paypal-immediately): Pay PayPal invoice immediately when starting premium subscription

* feat(pay-with-paypal-immediately): Pay invoice with Braintree on invoice.created for subscription cycles only

* fix(update-storage): Always invoice for premium storage update

* fix(update-storage): Move endpoint to subscription path

* docs: Note FF removal POIs

* (format): Run dotnet format
This commit is contained in:
Alex Morask
2026-01-12 10:45:41 -06:00
committed by GitHub
parent 94cd6fbff6
commit cfa8d4a165
27 changed files with 1676 additions and 67 deletions

View File

@@ -22,7 +22,7 @@ public class AccountsController(
IFeatureService featureService,
ILicensingService licensingService) : Controller
{
// TODO: Migrate to Query / AccountBillingVNextController as part of Premium -> Organization upgrade work.
// TODO: Remove with deletion of pm-29594-update-individual-subscription-page
[HttpGet("subscription")]
public async Task<SubscriptionResponseModel> GetSubscriptionAsync(
[FromServices] GlobalSettings globalSettings,
@@ -61,7 +61,7 @@ public class AccountsController(
}
}
// TODO: Migrate to Command / AccountBillingVNextController as PUT /account/billing/vnext/subscription
// TODO: Remove with deletion of pm-29594-update-individual-subscription-page
[HttpPost("storage")]
[SelfHosted(NotSelfHostedOnly = true)]
public async Task<PaymentResponseModel> PostStorageAsync([FromBody] StorageRequestModel model)
@@ -118,7 +118,7 @@ public class AccountsController(
user.IsExpired());
}
// TODO: Migrate to Command / AccountBillingVNextController as POST /account/billing/vnext/subscription/reinstate
// TODO: Remove with deletion of pm-29594-update-individual-subscription-page
[HttpPost("reinstate-premium")]
[SelfHosted(NotSelfHostedOnly = true)]
public async Task PostReinstateAsync()
@@ -131,10 +131,4 @@ public class AccountsController(
await userService.ReinstatePremiumAsync(user);
}
private async Task<IEnumerable<Guid>> GetOrganizationIdsClaimingUserAsync(Guid userId)
{
var organizationsClaimingUser = await userService.GetOrganizationsClaimingUserAsync(userId);
return organizationsClaimingUser.Select(o => o.Id);
}
}

View File

@@ -7,6 +7,8 @@ using Bit.Core.Billing.Licenses.Queries;
using Bit.Core.Billing.Payment.Commands;
using Bit.Core.Billing.Payment.Queries;
using Bit.Core.Billing.Premium.Commands;
using Bit.Core.Billing.Subscriptions.Commands;
using Bit.Core.Billing.Subscriptions.Queries;
using Bit.Core.Entities;
using Bit.Core.Utilities;
using Microsoft.AspNetCore.Authorization;
@@ -21,9 +23,11 @@ namespace Bit.Api.Billing.Controllers.VNext;
public class AccountBillingVNextController(
ICreateBitPayInvoiceForCreditCommand createBitPayInvoiceForCreditCommand,
ICreatePremiumCloudHostedSubscriptionCommand createPremiumCloudHostedSubscriptionCommand,
IGetBitwardenSubscriptionQuery getBitwardenSubscriptionQuery,
IGetCreditQuery getCreditQuery,
IGetPaymentMethodQuery getPaymentMethodQuery,
IGetUserLicenseQuery getUserLicenseQuery,
IReinstateSubscriptionCommand reinstateSubscriptionCommand,
IUpdatePaymentMethodCommand updatePaymentMethodCommand,
IUpdatePremiumStorageCommand updatePremiumStorageCommand,
IUpgradePremiumToOrganizationCommand upgradePremiumToOrganizationCommand) : BaseBillingController
@@ -91,10 +95,30 @@ public class AccountBillingVNextController(
return TypedResults.Ok(response);
}
[HttpPut("storage")]
[HttpGet("subscription")]
[RequireFeature(FeatureFlagKeys.PM29594_UpdateIndividualSubscriptionPage)]
[InjectUser]
public async Task<IResult> UpdateStorageAsync(
public async Task<IResult> GetSubscriptionAsync(
[BindNever] User user)
{
var subscription = await getBitwardenSubscriptionQuery.Run(user);
return TypedResults.Ok(subscription);
}
[HttpPost("subscription/reinstate")]
[RequireFeature(FeatureFlagKeys.PM29594_UpdateIndividualSubscriptionPage)]
[InjectUser]
public async Task<IResult> ReinstateSubscriptionAsync(
[BindNever] User user)
{
var result = await reinstateSubscriptionCommand.Run(user);
return Handle(result);
}
[HttpPut("subscription/storage")]
[RequireFeature(FeatureFlagKeys.PM29594_UpdateIndividualSubscriptionPage)]
[InjectUser]
public async Task<IResult> UpdateSubscriptionStorageAsync(
[BindNever] User user,
[FromBody] StorageUpdateRequest request)
{

View File

@@ -13,7 +13,6 @@ public class StorageUpdateRequest : IValidatableObject
/// Must be between 0 and the maximum allowed (minus base storage).
/// </summary>
[Required]
[Range(0, 99)]
public short AdditionalStorageGb { get; set; }
public IEnumerable<ValidationResult> Validate(ValidationContext validationContext)
@@ -22,14 +21,14 @@ public class StorageUpdateRequest : IValidatableObject
{
yield return new ValidationResult(
"Additional storage cannot be negative.",
new[] { nameof(AdditionalStorageGb) });
[nameof(AdditionalStorageGb)]);
}
if (AdditionalStorageGb > 99)
{
yield return new ValidationResult(
"Maximum additional storage is 99 GB.",
new[] { nameof(AdditionalStorageGb) });
[nameof(AdditionalStorageGb)]);
}
}
}

View File

@@ -10,6 +10,7 @@ using Bit.Core.Utilities;
namespace Bit.Api.Models.Response;
// TODO: Remove with deletion of pm-29594-update-individual-subscription-page
public class SubscriptionResponseModel : ResponseModel
{