1
0
mirror of https://github.com/bitwarden/server.git synced 2024-11-21 12:05:42 +01: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
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
20 changed files with 19 additions and 809 deletions

View File

@ -684,17 +684,6 @@ public class AccountsController : Controller
throw new BadRequestException(ModelState);
}
[HttpPost("iap-check")]
public async Task PostIapCheck([FromBody] IapCheckRequestModel model)
{
var user = await _userService.GetUserByPrincipalAsync(User);
if (user == null)
{
throw new UnauthorizedAccessException();
}
await _userService.IapCheckAsync(user, model.PaymentMethodType.Value);
}
[HttpPost("premium")]
public async Task<PaymentResponseModel> PostPremium(PremiumRequestModel model)
{

View File

@ -1,19 +0,0 @@
using System.ComponentModel.DataAnnotations;
using Enums = Bit.Core.Enums;
namespace Bit.Api.Models.Request;
public class IapCheckRequestModel : IValidatableObject
{
[Required]
public Enums.PaymentMethodType? PaymentMethodType { get; set; }
public IEnumerable<ValidationResult> Validate(ValidationContext validationContext)
{
if (PaymentMethodType != Enums.PaymentMethodType.AppleInApp)
{
yield return new ValidationResult("Not a supported in-app purchase payment method.",
new string[] { nameof(PaymentMethodType) });
}
}
}

View File

@ -18,7 +18,6 @@ public class SubscriptionResponseModel : ResponseModel
MaxStorageGb = user.MaxStorageGb;
License = license;
Expiration = License.Expires;
UsingInAppPurchase = subscription.UsingInAppPurchase;
}
public SubscriptionResponseModel(User user, UserLicense license = null)
@ -42,7 +41,6 @@ public class SubscriptionResponseModel : ResponseModel
public BillingSubscription Subscription { get; set; }
public UserLicense License { get; set; }
public DateTime? Expiration { get; set; }
public bool UsingInAppPurchase { get; set; }
}
public class BillingCustomerDiscount

View File

@ -30,7 +30,6 @@ namespace Bit.Billing.Controllers;
[Route("stripe")]
public class StripeController : Controller
{
private const decimal PremiumPlanAppleIapPrice = 14.99M;
private const string PremiumPlanId = "premium-annually";
private const string PremiumPlanIdAppStore = "premium-annually-app";
@ -42,7 +41,6 @@ public class StripeController : Controller
private readonly IOrganizationRepository _organizationRepository;
private readonly ITransactionRepository _transactionRepository;
private readonly IUserService _userService;
private readonly IAppleIapService _appleIapService;
private readonly IMailService _mailService;
private readonly ILogger<StripeController> _logger;
private readonly BraintreeGateway _btGateway;
@ -64,7 +62,6 @@ public class StripeController : Controller
IOrganizationRepository organizationRepository,
ITransactionRepository transactionRepository,
IUserService userService,
IAppleIapService appleIapService,
IMailService mailService,
IReferenceEventService referenceEventService,
ILogger<StripeController> logger,
@ -82,7 +79,6 @@ public class StripeController : Controller
_organizationRepository = organizationRepository;
_transactionRepository = transactionRepository;
_userService = userService;
_appleIapService = appleIapService;
_mailService = mailService;
_referenceEventService = referenceEventService;
_taxRateRepository = taxRateRepository;
@ -681,10 +677,6 @@ public class StripeController : Controller
{
var customerService = new CustomerService();
var customer = await customerService.GetAsync(invoice.CustomerId);
if (customer?.Metadata?.ContainsKey("appleReceipt") ?? false)
{
return await AttemptToPayInvoiceWithAppleReceiptAsync(invoice, customer);
}
if (customer?.Metadata?.ContainsKey("btCustomerId") ?? false)
{
@ -699,99 +691,6 @@ public class StripeController : Controller
return false;
}
private async Task<bool> AttemptToPayInvoiceWithAppleReceiptAsync(Invoice invoice, Customer customer)
{
if (!customer?.Metadata?.ContainsKey("appleReceipt") ?? true)
{
return false;
}
var originalAppleReceiptTransactionId = customer.Metadata["appleReceipt"];
var appleReceiptRecord = await _appleIapService.GetReceiptAsync(originalAppleReceiptTransactionId);
if (string.IsNullOrWhiteSpace(appleReceiptRecord?.Item1) || !appleReceiptRecord.Item2.HasValue)
{
return false;
}
var subscriptionService = new SubscriptionService();
var subscription = await subscriptionService.GetAsync(invoice.SubscriptionId);
var ids = GetIdsFromMetaData(subscription?.Metadata);
if (!ids.Item2.HasValue)
{
// Apple receipt is only for user subscriptions
return false;
}
if (appleReceiptRecord.Item2.Value != ids.Item2.Value)
{
_logger.LogError("User Ids for Apple Receipt and subscription do not match: {0} != {1}.",
appleReceiptRecord.Item2.Value, ids.Item2.Value);
return false;
}
var appleReceiptStatus = await _appleIapService.GetVerifiedReceiptStatusAsync(appleReceiptRecord.Item1);
if (appleReceiptStatus == null)
{
// TODO: cancel sub if receipt is cancelled?
return false;
}
var receiptExpiration = appleReceiptStatus.GetLastExpiresDate().GetValueOrDefault(DateTime.MinValue);
var invoiceDue = invoice.DueDate.GetValueOrDefault(DateTime.MinValue);
if (receiptExpiration <= invoiceDue)
{
_logger.LogWarning("Apple receipt expiration is before invoice due date. {0} <= {1}",
receiptExpiration, invoiceDue);
return false;
}
var receiptLastTransactionId = appleReceiptStatus.GetLastTransactionId();
var existingTransaction = await _transactionRepository.GetByGatewayIdAsync(
GatewayType.AppStore, receiptLastTransactionId);
if (existingTransaction != null)
{
_logger.LogWarning("There is already an existing transaction for this Apple receipt.",
receiptLastTransactionId);
return false;
}
var appleTransaction = appleReceiptStatus.BuildTransactionFromLastTransaction(
PremiumPlanAppleIapPrice, ids.Item2.Value);
appleTransaction.Type = TransactionType.Charge;
var invoiceService = new InvoiceService();
try
{
await invoiceService.UpdateAsync(invoice.Id, new InvoiceUpdateOptions
{
Metadata = new Dictionary<string, string>
{
["appleReceipt"] = appleReceiptStatus.GetOriginalTransactionId(),
["appleReceiptTransactionId"] = receiptLastTransactionId
}
});
await _transactionRepository.CreateAsync(appleTransaction);
await invoiceService.PayAsync(invoice.Id, new InvoicePayOptions { PaidOutOfBand = true });
}
catch (Exception e)
{
if (e.Message.Contains("Invoice is already paid"))
{
await invoiceService.UpdateAsync(invoice.Id, new InvoiceUpdateOptions
{
Metadata = invoice.Metadata
});
}
else
{
throw;
}
}
return true;
}
private async Task<bool> AttemptToPayInvoiceWithBraintreeAsync(Invoice invoice, Customer customer)
{
_logger.LogDebug("Attempting to pay invoice with Braintree");

View File

@ -16,10 +16,6 @@ public enum PaymentMethodType : byte
Credit = 4,
[Display(Name = "Wire Transfer")]
WireTransfer = 5,
[Display(Name = "Apple In-App Purchase")]
AppleInApp = 6,
[Display(Name = "Google In-App Purchase")]
GoogleInApp = 7,
[Display(Name = "Check")]
Check = 8,
[Display(Name = "None")]

View File

@ -1,134 +0,0 @@
using System.Text.Json.Serialization;
using Bit.Core.Entities;
using Bit.Core.Enums;
using Bit.Core.Utilities;
namespace Bit.Billing.Models;
public class AppleReceiptStatus
{
[JsonPropertyName("status")]
public int? Status { get; set; }
[JsonPropertyName("environment")]
public string Environment { get; set; }
[JsonPropertyName("latest_receipt")]
public string LatestReceipt { get; set; }
[JsonPropertyName("receipt")]
public AppleReceipt Receipt { get; set; }
[JsonPropertyName("latest_receipt_info")]
public List<AppleTransaction> LatestReceiptInfo { get; set; }
[JsonPropertyName("pending_renewal_info")]
public List<AppleRenewalInfo> PendingRenewalInfo { get; set; }
public string GetOriginalTransactionId()
{
return LatestReceiptInfo?.LastOrDefault()?.OriginalTransactionId;
}
public string GetLastTransactionId()
{
return LatestReceiptInfo?.LastOrDefault()?.TransactionId;
}
public AppleTransaction GetLastTransaction()
{
return LatestReceiptInfo?.LastOrDefault();
}
public DateTime? GetLastExpiresDate()
{
return LatestReceiptInfo?.LastOrDefault()?.ExpiresDate;
}
public string GetReceiptData()
{
return LatestReceipt;
}
public DateTime? GetLastCancellationDate()
{
return LatestReceiptInfo?.LastOrDefault()?.CancellationDate;
}
public bool IsRefunded()
{
var cancellationDate = GetLastCancellationDate();
var expiresDate = GetLastCancellationDate();
if (cancellationDate.HasValue && expiresDate.HasValue)
{
return cancellationDate.Value <= expiresDate.Value;
}
return false;
}
public Transaction BuildTransactionFromLastTransaction(decimal amount, Guid userId)
{
return new Transaction
{
Amount = amount,
CreationDate = GetLastTransaction().PurchaseDate,
Gateway = GatewayType.AppStore,
GatewayId = GetLastTransactionId(),
UserId = userId,
PaymentMethodType = PaymentMethodType.AppleInApp,
Details = GetLastTransactionId()
};
}
public class AppleReceipt
{
[JsonPropertyName("receipt_type")]
public string ReceiptType { get; set; }
[JsonPropertyName("bundle_id")]
public string BundleId { get; set; }
[JsonPropertyName("receipt_creation_date_ms")]
[JsonConverter(typeof(MsEpochConverter))]
public DateTime ReceiptCreationDate { get; set; }
[JsonPropertyName("in_app")]
public List<AppleTransaction> InApp { get; set; }
}
public class AppleRenewalInfo
{
[JsonPropertyName("expiration_intent")]
public string ExpirationIntent { get; set; }
[JsonPropertyName("auto_renew_product_id")]
public string AutoRenewProductId { get; set; }
[JsonPropertyName("original_transaction_id")]
public string OriginalTransactionId { get; set; }
[JsonPropertyName("is_in_billing_retry_period")]
public string IsInBillingRetryPeriod { get; set; }
[JsonPropertyName("product_id")]
public string ProductId { get; set; }
[JsonPropertyName("auto_renew_status")]
public string AutoRenewStatus { get; set; }
}
public class AppleTransaction
{
[JsonPropertyName("quantity")]
public string Quantity { get; set; }
[JsonPropertyName("product_id")]
public string ProductId { get; set; }
[JsonPropertyName("transaction_id")]
public string TransactionId { get; set; }
[JsonPropertyName("original_transaction_id")]
public string OriginalTransactionId { get; set; }
[JsonPropertyName("purchase_date_ms")]
[JsonConverter(typeof(MsEpochConverter))]
public DateTime PurchaseDate { get; set; }
[JsonPropertyName("original_purchase_date_ms")]
[JsonConverter(typeof(MsEpochConverter))]
public DateTime OriginalPurchaseDate { get; set; }
[JsonPropertyName("expires_date_ms")]
[JsonConverter(typeof(MsEpochConverter))]
public DateTime ExpiresDate { get; set; }
[JsonPropertyName("cancellation_date_ms")]
[JsonConverter(typeof(MsEpochConverter))]
public DateTime? CancellationDate { get; set; }
[JsonPropertyName("web_order_line_item_id")]
public string WebOrderLineItemId { get; set; }
[JsonPropertyName("cancellation_reason")]
public string CancellationReason { get; set; }
}
}

View File

@ -7,7 +7,6 @@ public class SubscriptionInfo
public BillingCustomerDiscount CustomerDiscount { get; set; }
public BillingSubscription Subscription { get; set; }
public BillingUpcomingInvoice UpcomingInvoice { get; set; }
public bool UsingInAppPurchase { get; set; }
public class BillingCustomerDiscount
{

View File

@ -1,10 +0,0 @@
namespace Bit.Core.Repositories;
public interface IMetaDataRepository
{
Task DeleteAsync(string objectName, string id);
Task<IDictionary<string, string>> GetAsync(string objectName, string id);
Task<string> GetAsync(string objectName, string id, string prop);
Task UpsertAsync(string objectName, string id, IDictionary<string, string> dict);
Task UpsertAsync(string objectName, string id, KeyValuePair<string, string> keyValuePair);
}

View File

@ -1,29 +0,0 @@
namespace Bit.Core.Repositories.Noop;
public class MetaDataRepository : IMetaDataRepository
{
public Task DeleteAsync(string objectName, string id)
{
return Task.FromResult(0);
}
public Task<IDictionary<string, string>> GetAsync(string objectName, string id)
{
return Task.FromResult(null as IDictionary<string, string>);
}
public Task<string> GetAsync(string objectName, string id, string prop)
{
return Task.FromResult(null as string);
}
public Task UpsertAsync(string objectName, string id, IDictionary<string, string> dict)
{
return Task.FromResult(0);
}
public Task UpsertAsync(string objectName, string id, KeyValuePair<string, string> keyValuePair)
{
return Task.FromResult(0);
}
}

View File

@ -1,93 +0,0 @@
using System.Net;
using Bit.Core.Models.Data;
using Bit.Core.Settings;
using Microsoft.Azure.Cosmos.Table;
namespace Bit.Core.Repositories.TableStorage;
public class MetaDataRepository : IMetaDataRepository
{
private readonly CloudTable _table;
public MetaDataRepository(GlobalSettings globalSettings)
: this(globalSettings.Events.ConnectionString)
{ }
public MetaDataRepository(string storageConnectionString)
{
var storageAccount = CloudStorageAccount.Parse(storageConnectionString);
var tableClient = storageAccount.CreateCloudTableClient();
_table = tableClient.GetTableReference("metadata");
}
public async Task<IDictionary<string, string>> GetAsync(string objectName, string id)
{
var query = new TableQuery<DictionaryEntity>().Where(
TableQuery.GenerateFilterCondition("PartitionKey", QueryComparisons.Equal, $"{objectName}_{id}"));
var queryResults = await _table.ExecuteQuerySegmentedAsync(query, null);
return queryResults.Results.FirstOrDefault()?.ToDictionary(d => d.Key, d => d.Value.StringValue);
}
public async Task<string> GetAsync(string objectName, string id, string prop)
{
var dict = await GetAsync(objectName, id);
if (dict != null && dict.ContainsKey(prop))
{
return dict[prop];
}
return null;
}
public async Task UpsertAsync(string objectName, string id, KeyValuePair<string, string> keyValuePair)
{
var query = new TableQuery<DictionaryEntity>().Where(
TableQuery.GenerateFilterCondition("PartitionKey", QueryComparisons.Equal, $"{objectName}_{id}"));
var queryResults = await _table.ExecuteQuerySegmentedAsync(query, null);
var entity = queryResults.Results.FirstOrDefault();
if (entity == null)
{
entity = new DictionaryEntity
{
PartitionKey = $"{objectName}_{id}",
RowKey = string.Empty
};
}
if (entity.ContainsKey(keyValuePair.Key))
{
entity.Remove(keyValuePair.Key);
}
entity.Add(keyValuePair.Key, keyValuePair.Value);
await _table.ExecuteAsync(TableOperation.InsertOrReplace(entity));
}
public async Task UpsertAsync(string objectName, string id, IDictionary<string, string> dict)
{
var entity = new DictionaryEntity
{
PartitionKey = $"{objectName}_{id}",
RowKey = string.Empty
};
foreach (var item in dict)
{
entity.Add(item.Key, item.Value);
}
await _table.ExecuteAsync(TableOperation.InsertOrReplace(entity));
}
public async Task DeleteAsync(string objectName, string id)
{
try
{
await _table.ExecuteAsync(TableOperation.Delete(new DictionaryEntity
{
PartitionKey = $"{objectName}_{id}",
RowKey = string.Empty,
ETag = "*"
}));
}
catch (StorageException e) when (e.RequestInformation.HttpStatusCode != (int)HttpStatusCode.NotFound)
{
throw;
}
}
}

View File

@ -1,10 +0,0 @@
using Bit.Billing.Models;
namespace Bit.Core.Services;
public interface IAppleIapService
{
Task<AppleReceiptStatus> GetVerifiedReceiptStatusAsync(string receiptData);
Task SaveReceiptAsync(AppleReceiptStatus receiptStatus, Guid userId);
Task<Tuple<string, Guid?>> GetReceiptAsync(string originalTransactionId);
}

View File

@ -33,11 +33,10 @@ public interface IPaymentService
Task<string> AdjustServiceAccountsAsync(Organization organization, Plan plan, int additionalServiceAccounts,
DateTime? prorationDate = null);
Task CancelSubscriptionAsync(ISubscriber subscriber, bool endOfPeriod = false,
bool skipInAppPurchaseCheck = false);
Task CancelSubscriptionAsync(ISubscriber subscriber, bool endOfPeriod = false);
Task ReinstateSubscriptionAsync(ISubscriber subscriber);
Task<bool> UpdatePaymentMethodAsync(ISubscriber subscriber, PaymentMethodType paymentMethodType,
string paymentToken, bool allowInAppPurchases = false, TaxInfo taxInfo = null);
string paymentToken, TaxInfo taxInfo = null);
Task<bool> CreditAccountAsync(ISubscriber subscriber, decimal creditAmount);
Task<BillingInfo> GetBillingAsync(ISubscriber subscriber);
Task<BillingInfo> GetBillingHistoryAsync(ISubscriber subscriber);

View File

@ -54,11 +54,10 @@ public interface IUserService
Task<Tuple<bool, string>> SignUpPremiumAsync(User user, string paymentToken,
PaymentMethodType paymentMethodType, short additionalStorageGb, UserLicense license,
TaxInfo taxInfo);
Task IapCheckAsync(User user, PaymentMethodType paymentMethodType);
Task UpdateLicenseAsync(User user, UserLicense license);
Task<string> AdjustStorageAsync(User user, short storageAdjustmentGb);
Task ReplacePaymentMethodAsync(User user, string paymentToken, PaymentMethodType paymentMethodType, TaxInfo taxInfo);
Task CancelPremiumAsync(User user, bool? endOfPeriod = null, bool accountDelete = false);
Task CancelPremiumAsync(User user, bool? endOfPeriod = null);
Task ReinstatePremiumAsync(User user);
Task EnablePremiumAsync(Guid userId, DateTime? expirationDate);
Task EnablePremiumAsync(User user, DateTime? expirationDate);

View File

@ -1,132 +0,0 @@
using System.Net.Http.Json;
using System.Text.Json.Serialization;
using Bit.Billing.Models;
using Bit.Core.Repositories;
using Bit.Core.Settings;
using Microsoft.AspNetCore.Hosting;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;
namespace Bit.Core.Services;
public class AppleIapService : IAppleIapService
{
private readonly HttpClient _httpClient = new HttpClient();
private readonly GlobalSettings _globalSettings;
private readonly IWebHostEnvironment _hostingEnvironment;
private readonly IMetaDataRepository _metaDataRepository;
private readonly ILogger<AppleIapService> _logger;
public AppleIapService(
GlobalSettings globalSettings,
IWebHostEnvironment hostingEnvironment,
IMetaDataRepository metaDataRepository,
ILogger<AppleIapService> logger)
{
_globalSettings = globalSettings;
_hostingEnvironment = hostingEnvironment;
_metaDataRepository = metaDataRepository;
_logger = logger;
}
public async Task<AppleReceiptStatus> GetVerifiedReceiptStatusAsync(string receiptData)
{
var receiptStatus = await GetReceiptStatusAsync(receiptData);
if (receiptStatus?.Status != 0)
{
return null;
}
var validEnvironment = _globalSettings.AppleIap.AppInReview ||
(!(_hostingEnvironment.IsProduction() || _hostingEnvironment.IsEnvironment("QA")) && receiptStatus.Environment == "Sandbox") ||
((_hostingEnvironment.IsProduction() || _hostingEnvironment.IsEnvironment("QA")) && receiptStatus.Environment != "Sandbox");
var validProductBundle = receiptStatus.Receipt.BundleId == "com.bitwarden.desktop" ||
receiptStatus.Receipt.BundleId == "com.8bit.bitwarden";
var validProduct = receiptStatus.LatestReceiptInfo.LastOrDefault()?.ProductId == "premium_annually";
var validIds = receiptStatus.GetOriginalTransactionId() != null &&
receiptStatus.GetLastTransactionId() != null;
var validTransaction = receiptStatus.GetLastExpiresDate()
.GetValueOrDefault(DateTime.MinValue) > DateTime.UtcNow;
if (validEnvironment && validProductBundle && validProduct && validIds && validTransaction)
{
return receiptStatus;
}
return null;
}
public async Task SaveReceiptAsync(AppleReceiptStatus receiptStatus, Guid userId)
{
var originalTransactionId = receiptStatus.GetOriginalTransactionId();
if (string.IsNullOrWhiteSpace(originalTransactionId))
{
throw new Exception("OriginalTransactionId is null");
}
await _metaDataRepository.UpsertAsync("AppleReceipt", originalTransactionId,
new Dictionary<string, string>
{
["Data"] = receiptStatus.GetReceiptData(),
["UserId"] = userId.ToString()
});
}
public async Task<Tuple<string, Guid?>> GetReceiptAsync(string originalTransactionId)
{
var receipt = await _metaDataRepository.GetAsync("AppleReceipt", originalTransactionId);
if (receipt == null)
{
return null;
}
return new Tuple<string, Guid?>(receipt.ContainsKey("Data") ? receipt["Data"] : null,
receipt.ContainsKey("UserId") ? new Guid(receipt["UserId"]) : (Guid?)null);
}
// Internal for testing
internal async Task<AppleReceiptStatus> GetReceiptStatusAsync(string receiptData, bool prod = true,
int attempt = 0, AppleReceiptStatus lastReceiptStatus = null)
{
try
{
if (attempt > 4)
{
throw new Exception(
$"Failed verifying Apple IAP after too many attempts. Last attempt status: {lastReceiptStatus?.Status.ToString() ?? "null"}");
}
var url = string.Format("https://{0}.itunes.apple.com/verifyReceipt", prod ? "buy" : "sandbox");
var response = await _httpClient.PostAsJsonAsync(url, new AppleVerifyReceiptRequestModel
{
ReceiptData = receiptData,
Password = _globalSettings.AppleIap.Password
});
if (response.IsSuccessStatusCode)
{
var receiptStatus = await response.Content.ReadFromJsonAsync<AppleReceiptStatus>();
if (receiptStatus.Status == 21007)
{
return await GetReceiptStatusAsync(receiptData, false, attempt + 1, receiptStatus);
}
else if (receiptStatus.Status == 21005)
{
await Task.Delay(2000);
return await GetReceiptStatusAsync(receiptData, prod, attempt + 1, receiptStatus);
}
return receiptStatus;
}
}
catch (Exception e)
{
_logger.LogWarning(e, "Error verifying Apple IAP receipt.");
}
return null;
}
}
public class AppleVerifyReceiptRequestModel
{
[JsonPropertyName("receipt-data")]
public string ReceiptData { get; set; }
[JsonPropertyName("password")]
public string Password { get; set; }
}

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

View File

@ -255,7 +255,7 @@ public class UserService : UserManager<User>, IUserService, IDisposable
{
try
{
await CancelPremiumAsync(user, null, true);
await CancelPremiumAsync(user);
}
catch (GatewayException) { }
}
@ -973,12 +973,6 @@ public class UserService : UserManager<User>, IUserService, IDisposable
throw new BadRequestException("You can't subtract storage!");
}
if ((paymentMethodType == PaymentMethodType.GoogleInApp ||
paymentMethodType == PaymentMethodType.AppleInApp) && additionalStorageGb > 0)
{
throw new BadRequestException("You cannot add storage with this payment method.");
}
string paymentIntentClientSecret = null;
IPaymentService paymentService = null;
if (_globalSettings.SelfHosted)
@ -1039,29 +1033,6 @@ public class UserService : UserManager<User>, IUserService, IDisposable
paymentIntentClientSecret);
}
public async Task IapCheckAsync(User user, PaymentMethodType paymentMethodType)
{
if (paymentMethodType != PaymentMethodType.AppleInApp)
{
throw new BadRequestException("Payment method not supported for in-app purchases.");
}
if (user.Premium)
{
throw new BadRequestException("Already a premium user.");
}
if (!string.IsNullOrWhiteSpace(user.GatewayCustomerId))
{
var customerService = new Stripe.CustomerService();
var customer = await customerService.GetAsync(user.GatewayCustomerId);
if (customer != null && customer.Balance != 0)
{
throw new BadRequestException("Customer balance cannot exist when using in-app purchases.");
}
}
}
public async Task UpdateLicenseAsync(User user, UserLicense license)
{
if (!_globalSettings.SelfHosted)
@ -1136,7 +1107,7 @@ public class UserService : UserManager<User>, IUserService, IDisposable
}
}
public async Task CancelPremiumAsync(User user, bool? endOfPeriod = null, bool accountDelete = false)
public async Task CancelPremiumAsync(User user, bool? endOfPeriod = null)
{
var eop = endOfPeriod.GetValueOrDefault(true);
if (!endOfPeriod.HasValue && user.PremiumExpirationDate.HasValue &&
@ -1144,11 +1115,11 @@ public class UserService : UserManager<User>, IUserService, IDisposable
{
eop = false;
}
await _paymentService.CancelSubscriptionAsync(user, eop, accountDelete);
await _paymentService.CancelSubscriptionAsync(user, eop);
await _referenceEventService.RaiseEventAsync(
new ReferenceEvent(ReferenceEventType.CancelSubscription, user, _currentContext)
{
EndOfPeriod = eop,
EndOfPeriod = eop
});
}

View File

@ -113,13 +113,11 @@ public static class ServiceCollectionExtensions
if (globalSettings.SelfHosted)
{
services.AddSingleton<IInstallationDeviceRepository, NoopRepos.InstallationDeviceRepository>();
services.AddSingleton<IMetaDataRepository, NoopRepos.MetaDataRepository>();
}
else
{
services.AddSingleton<IEventRepository, TableStorageRepos.EventRepository>();
services.AddSingleton<IInstallationDeviceRepository, TableStorageRepos.InstallationDeviceRepository>();
services.AddSingleton<IMetaDataRepository, TableStorageRepos.MetaDataRepository>();
}
return provider;
@ -136,7 +134,6 @@ public static class ServiceCollectionExtensions
services.AddScoped<IEventService, EventService>();
services.AddScoped<IEmergencyAccessService, EmergencyAccessService>();
services.AddSingleton<IDeviceService, DeviceService>();
services.AddSingleton<IAppleIapService, AppleIapService>();
services.AddScoped<ISsoConfigService, SsoConfigService>();
services.AddScoped<IAuthRequestService, AuthRequestService>();
services.AddScoped<ISendService, SendService>();

View File

@ -1,40 +0,0 @@
using Bit.Core.Services;
using Bit.Test.Common.AutoFixture;
using Bit.Test.Common.AutoFixture.Attributes;
using Microsoft.Extensions.Logging;
using NSubstitute;
using NSubstitute.Core;
using Xunit;
namespace Bit.Core.Test.Services;
[SutProviderCustomize]
public class AppleIapServiceTests
{
[Theory, BitAutoData]
public async Task GetReceiptStatusAsync_MoreThanFourAttempts_Throws(SutProvider<AppleIapService> sutProvider)
{
var result = await sutProvider.Sut.GetReceiptStatusAsync("test", false, 5, null);
Assert.Null(result);
var errorLog = sutProvider.GetDependency<ILogger<AppleIapService>>()
.ReceivedCalls()
.SingleOrDefault(LogOneWarning);
Assert.True(errorLog != null, "Must contain one error log of warning level containing 'null'");
static bool LogOneWarning(ICall call)
{
if (call.GetMethodInfo().Name != "Log")
{
return false;
}
var args = call.GetArguments();
var logLevel = (LogLevel)args[0];
var exception = (Exception)args[3];
return logLevel == LogLevel.Warning && exception.Message.Contains("null");
}
}
}

View File

@ -26,8 +26,6 @@ public class StripePaymentServiceTests
[BitAutoData(PaymentMethodType.BitPay)]
[BitAutoData(PaymentMethodType.Credit)]
[BitAutoData(PaymentMethodType.WireTransfer)]
[BitAutoData(PaymentMethodType.AppleInApp)]
[BitAutoData(PaymentMethodType.GoogleInApp)]
[BitAutoData(PaymentMethodType.Check)]
public async void PurchaseOrganizationAsync_Invalid(PaymentMethodType paymentMethodType, SutProvider<StripePaymentService> sutProvider)
{

View File

@ -150,12 +150,6 @@ public abstract class WebApplicationFactoryBase<T> : WebApplicationFactory<T>
services.Remove(installationDeviceRepository);
services.AddSingleton<IInstallationDeviceRepository, NoopRepos.InstallationDeviceRepository>();
// TODO: Install and use azurite in CI pipeline
var metaDataRepository =
services.First(sd => sd.ServiceType == typeof(IMetaDataRepository));
services.Remove(metaDataRepository);
services.AddSingleton<IMetaDataRepository, NoopRepos.MetaDataRepository>();
// TODO: Install and use azurite in CI pipeline
var referenceEventService = services.First(sd => sd.ServiceType == typeof(IReferenceEventService));
services.Remove(referenceEventService);