1
0
mirror of https://github.com/bitwarden/server.git synced 2025-07-03 00:52:49 -05:00

[PM-5548] Eliminate in-app purchase logic (#3640)

* Eliminate in-app purchase logic

* Totally remove obsolete and unused properties / types

* Remove unused enum values

* Restore token update
This commit is contained in:
Matt Bishop
2024-01-11 15:26:32 -05:00
committed by GitHub
parent b9c6e00c2d
commit 23f9d2261d
20 changed files with 19 additions and 809 deletions

View File

@ -1,5 +1,4 @@
using Bit.Billing.Models;
using Bit.Core.AdminConsole.Entities;
using Bit.Core.AdminConsole.Entities;
using Bit.Core.Entities;
using Bit.Core.Enums;
using Bit.Core.Exceptions;
@ -16,14 +15,11 @@ namespace Bit.Core.Services;
public class StripePaymentService : IPaymentService
{
private const string PremiumPlanId = "premium-annually";
private const string PremiumPlanAppleIapId = "premium-annually-appleiap";
private const decimal PremiumPlanAppleIapPrice = 14.99M;
private const string StoragePlanId = "storage-gb-annually";
private const string ProviderDiscountId = "msp-discount-35";
private readonly ITransactionRepository _transactionRepository;
private readonly IUserRepository _userRepository;
private readonly IAppleIapService _appleIapService;
private readonly ILogger<StripePaymentService> _logger;
private readonly Braintree.IBraintreeGateway _btGateway;
private readonly ITaxRateRepository _taxRateRepository;
@ -33,7 +29,6 @@ public class StripePaymentService : IPaymentService
public StripePaymentService(
ITransactionRepository transactionRepository,
IUserRepository userRepository,
IAppleIapService appleIapService,
ILogger<StripePaymentService> logger,
ITaxRateRepository taxRateRepository,
IStripeAdapter stripeAdapter,
@ -42,7 +37,6 @@ public class StripePaymentService : IPaymentService
{
_transactionRepository = transactionRepository;
_userRepository = userRepository;
_appleIapService = appleIapService;
_logger = logger;
_taxRateRepository = taxRateRepository;
_stripeAdapter = stripeAdapter;
@ -345,21 +339,16 @@ public class StripePaymentService : IPaymentService
{
throw new BadRequestException("Your account does not have any credit available.");
}
if (paymentMethodType == PaymentMethodType.BankAccount || paymentMethodType == PaymentMethodType.GoogleInApp)
if (paymentMethodType is PaymentMethodType.BankAccount)
{
throw new GatewayException("Payment method is not supported at this time.");
}
if ((paymentMethodType == PaymentMethodType.GoogleInApp ||
paymentMethodType == PaymentMethodType.AppleInApp) && additionalStorageGb > 0)
{
throw new BadRequestException("You cannot add storage with this payment method.");
}
var createdStripeCustomer = false;
Stripe.Customer customer = null;
Braintree.Customer braintreeCustomer = null;
var stripePaymentMethod = paymentMethodType == PaymentMethodType.Card ||
paymentMethodType == PaymentMethodType.BankAccount || paymentMethodType == PaymentMethodType.Credit;
var stripePaymentMethod = paymentMethodType is PaymentMethodType.Card or PaymentMethodType.BankAccount
or PaymentMethodType.Credit;
string stipeCustomerPaymentMethodId = null;
string stipeCustomerSourceToken = null;
@ -379,19 +368,9 @@ public class StripePaymentService : IPaymentService
{
if (!string.IsNullOrWhiteSpace(paymentToken))
{
try
{
await UpdatePaymentMethodAsync(user, paymentMethodType, paymentToken, true, taxInfo);
}
catch (Exception e)
{
var message = e.Message.ToLowerInvariant();
if (message.Contains("apple") || message.Contains("in-app"))
{
throw;
}
}
await UpdatePaymentMethodAsync(user, paymentMethodType, paymentToken, taxInfo);
}
try
{
customer = await _stripeAdapter.CustomerGetAsync(user.GatewayCustomerId);
@ -425,18 +404,6 @@ public class StripePaymentService : IPaymentService
braintreeCustomer = customerResult.Target;
stripeCustomerMetadata.Add("btCustomerId", braintreeCustomer.Id);
}
else if (paymentMethodType == PaymentMethodType.AppleInApp)
{
var verifiedReceiptStatus = await _appleIapService.GetVerifiedReceiptStatusAsync(paymentToken);
if (verifiedReceiptStatus == null)
{
throw new GatewayException("Cannot verify apple in-app purchase.");
}
var receiptOriginalTransactionId = verifiedReceiptStatus.GetOriginalTransactionId();
await VerifyAppleReceiptNotInUseAsync(receiptOriginalTransactionId, user);
await _appleIapService.SaveReceiptAsync(verifiedReceiptStatus, user.Id);
stripeCustomerMetadata.Add("appleReceipt", receiptOriginalTransactionId);
}
else if (!stripePaymentMethod)
{
throw new GatewayException("Payment method is not supported at this time.");
@ -488,8 +455,8 @@ public class StripePaymentService : IPaymentService
subCreateOptions.Items.Add(new Stripe.SubscriptionItemOptions
{
Plan = paymentMethodType == PaymentMethodType.AppleInApp ? PremiumPlanAppleIapId : PremiumPlanId,
Quantity = 1,
Plan = PremiumPlanId,
Quantity = 1
});
if (!string.IsNullOrWhiteSpace(taxInfo?.BillingAddressCountry)
@ -547,7 +514,6 @@ public class StripePaymentService : IPaymentService
{
var addedCreditToStripeCustomer = false;
Braintree.Transaction braintreeTransaction = null;
Transaction appleTransaction = null;
var subInvoiceMetadata = new Dictionary<string, string>();
Stripe.Subscription subscription = null;
@ -564,39 +530,9 @@ public class StripePaymentService : IPaymentService
if (previewInvoice.AmountDue > 0)
{
var appleReceiptOrigTransactionId = customer.Metadata != null &&
customer.Metadata.ContainsKey("appleReceipt") ? customer.Metadata["appleReceipt"] : null;
var braintreeCustomerId = customer.Metadata != null &&
customer.Metadata.ContainsKey("btCustomerId") ? customer.Metadata["btCustomerId"] : null;
if (!string.IsNullOrWhiteSpace(appleReceiptOrigTransactionId))
{
if (!subscriber.IsUser())
{
throw new GatewayException("In-app purchase is only allowed for users.");
}
var appleReceipt = await _appleIapService.GetReceiptAsync(
appleReceiptOrigTransactionId);
var verifiedAppleReceipt = await _appleIapService.GetVerifiedReceiptStatusAsync(
appleReceipt.Item1);
if (verifiedAppleReceipt == null)
{
throw new GatewayException("Failed to get Apple in-app purchase receipt data.");
}
subInvoiceMetadata.Add("appleReceipt", verifiedAppleReceipt.GetOriginalTransactionId());
var lastTransactionId = verifiedAppleReceipt.GetLastTransactionId();
subInvoiceMetadata.Add("appleReceiptTransactionId", lastTransactionId);
var existingTransaction = await _transactionRepository.GetByGatewayIdAsync(
GatewayType.AppStore, lastTransactionId);
if (existingTransaction == null)
{
appleTransaction = verifiedAppleReceipt.BuildTransactionFromLastTransaction(
PremiumPlanAppleIapPrice, subscriber.Id);
appleTransaction.Type = TransactionType.Charge;
await _transactionRepository.CreateAsync(appleTransaction);
}
}
else if (!string.IsNullOrWhiteSpace(braintreeCustomerId))
if (!string.IsNullOrWhiteSpace(braintreeCustomerId))
{
var btInvoiceAmount = (previewInvoice.AmountDue / 100M);
var transactionResult = await _btGateway.Transaction.SaleAsync(
@ -712,10 +648,6 @@ public class StripePaymentService : IPaymentService
{
await _btGateway.Customer.DeleteAsync(braintreeCustomer.Id);
}
if (appleTransaction != null)
{
await _transactionRepository.DeleteAsync(appleTransaction);
}
if (e is Stripe.StripeException strEx &&
(strEx.StripeError?.Message?.Contains("cannot be used because it is not verified") ?? false))
@ -965,12 +897,6 @@ public class StripePaymentService : IPaymentService
customerOptions.AddExpand("default_source");
customerOptions.AddExpand("invoice_settings.default_payment_method");
var customer = await _stripeAdapter.CustomerGetAsync(subscriber.GatewayCustomerId, customerOptions);
var usingInAppPaymentMethod = customer.Metadata.ContainsKey("appleReceipt");
if (usingInAppPaymentMethod)
{
throw new BadRequestException("Cannot perform this action with in-app purchase payment method. " +
"Contact support.");
}
string paymentIntentClientSecret = null;
@ -1128,8 +1054,7 @@ public class StripePaymentService : IPaymentService
return paymentIntentClientSecret;
}
public async Task CancelSubscriptionAsync(ISubscriber subscriber, bool endOfPeriod = false,
bool skipInAppPurchaseCheck = false)
public async Task CancelSubscriptionAsync(ISubscriber subscriber, bool endOfPeriod = false)
{
if (subscriber == null)
{
@ -1141,15 +1066,6 @@ public class StripePaymentService : IPaymentService
throw new GatewayException("No subscription.");
}
if (!string.IsNullOrWhiteSpace(subscriber.GatewayCustomerId) && !skipInAppPurchaseCheck)
{
var customer = await _stripeAdapter.CustomerGetAsync(subscriber.GatewayCustomerId);
if (customer.Metadata.ContainsKey("appleReceipt"))
{
throw new BadRequestException("You are required to manage your subscription from the app store.");
}
}
var sub = await _stripeAdapter.SubscriptionGetAsync(subscriber.GatewaySubscriptionId);
if (sub == null)
{
@ -1216,7 +1132,7 @@ public class StripePaymentService : IPaymentService
}
public async Task<bool> UpdatePaymentMethodAsync(ISubscriber subscriber, PaymentMethodType paymentMethodType,
string paymentToken, bool allowInAppPurchases = false, TaxInfo taxInfo = null)
string paymentToken, TaxInfo taxInfo = null)
{
if (subscriber == null)
{
@ -1230,7 +1146,6 @@ public class StripePaymentService : IPaymentService
}
var createdCustomer = false;
AppleReceiptStatus appleReceiptStatus = null;
Braintree.Customer braintreeCustomer = null;
string stipeCustomerSourceToken = null;
string stipeCustomerPaymentMethodId = null;
@ -1238,23 +1153,10 @@ public class StripePaymentService : IPaymentService
{
{ "region", _globalSettings.BaseServiceUri.CloudRegion }
};
var stripePaymentMethod = paymentMethodType == PaymentMethodType.Card ||
paymentMethodType == PaymentMethodType.BankAccount;
var inAppPurchase = paymentMethodType == PaymentMethodType.AppleInApp ||
paymentMethodType == PaymentMethodType.GoogleInApp;
var stripePaymentMethod = paymentMethodType is PaymentMethodType.Card or PaymentMethodType.BankAccount;
Stripe.Customer customer = null;
if (!allowInAppPurchases && inAppPurchase)
{
throw new GatewayException("In-app purchase payment method is not allowed.");
}
if (!subscriber.IsUser() && inAppPurchase)
{
throw new GatewayException("In-app purchase payment method is only allowed for users.");
}
if (!string.IsNullOrWhiteSpace(subscriber.GatewayCustomerId))
{
var options = new Stripe.CustomerGetOptions();
@ -1266,16 +1168,6 @@ public class StripePaymentService : IPaymentService
}
}
if (inAppPurchase && customer != null && customer.Balance != 0)
{
throw new GatewayException("Customer balance cannot exist when using in-app purchases.");
}
if (!inAppPurchase && customer != null && stripeCustomerMetadata.ContainsKey("appleReceipt"))
{
throw new GatewayException("Cannot change from in-app payment method. Contact support.");
}
var hadBtCustomer = stripeCustomerMetadata.ContainsKey("btCustomerId");
if (stripePaymentMethod)
{
@ -1345,15 +1237,6 @@ public class StripePaymentService : IPaymentService
braintreeCustomer = customerResult.Target;
}
}
else if (paymentMethodType == PaymentMethodType.AppleInApp)
{
appleReceiptStatus = await _appleIapService.GetVerifiedReceiptStatusAsync(paymentToken);
if (appleReceiptStatus == null)
{
throw new GatewayException("Cannot verify Apple in-app purchase.");
}
await VerifyAppleReceiptNotInUseAsync(appleReceiptStatus.GetOriginalTransactionId(), subscriber);
}
else
{
throw new GatewayException("Payment method is not supported at this time.");
@ -1373,25 +1256,6 @@ public class StripePaymentService : IPaymentService
stripeCustomerMetadata.Add("btCustomerId", braintreeCustomer.Id);
}
if (appleReceiptStatus != null)
{
var originalTransactionId = appleReceiptStatus.GetOriginalTransactionId();
if (stripeCustomerMetadata.ContainsKey("appleReceipt"))
{
if (originalTransactionId != stripeCustomerMetadata["appleReceipt"])
{
var nowSec = Utilities.CoreHelpers.ToEpocSeconds(DateTime.UtcNow);
stripeCustomerMetadata.Add($"appleReceipt_{nowSec}", stripeCustomerMetadata["appleReceipt"]);
}
stripeCustomerMetadata["appleReceipt"] = originalTransactionId;
}
else
{
stripeCustomerMetadata.Add("appleReceipt", originalTransactionId);
}
await _appleIapService.SaveReceiptAsync(appleReceiptStatus, subscriber.Id);
}
try
{
if (customer == null)
@ -1595,11 +1459,6 @@ public class StripePaymentService : IPaymentService
{
subscriptionInfo.CustomerDiscount = new SubscriptionInfo.BillingCustomerDiscount(customer.Discount);
}
if (subscriber.IsUser())
{
subscriptionInfo.UsingInAppPurchase = customer.Metadata.ContainsKey("appleReceipt");
}
}
if (!string.IsNullOrWhiteSpace(subscriber.GatewaySubscriptionId))
@ -1762,19 +1621,6 @@ public class StripePaymentService : IPaymentService
return cardPaymentMethods.OrderByDescending(m => m.Created).FirstOrDefault();
}
private async Task VerifyAppleReceiptNotInUseAsync(string receiptOriginalTransactionId, ISubscriber subscriber)
{
var existingReceipt = await _appleIapService.GetReceiptAsync(receiptOriginalTransactionId);
if (existingReceipt != null && existingReceipt.Item2.HasValue && existingReceipt.Item2 != subscriber.Id)
{
var existingUser = await _userRepository.GetByIdAsync(existingReceipt.Item2.Value);
if (existingUser != null)
{
throw new GatewayException("Apple receipt already in use by another user.");
}
}
}
private decimal GetBillingBalance(Stripe.Customer customer)
{
return customer != null ? customer.Balance / 100M : default;
@ -1787,14 +1633,6 @@ public class StripePaymentService : IPaymentService
return null;
}
if (customer.Metadata?.ContainsKey("appleReceipt") ?? false)
{
return new BillingInfo.BillingSource
{
Type = PaymentMethodType.AppleInApp
};
}
if (customer.Metadata?.ContainsKey("btCustomerId") ?? false)
{
try