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:
parent
b9c6e00c2d
commit
23f9d2261d
@ -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)
|
||||
{
|
||||
|
@ -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) });
|
||||
}
|
||||
}
|
||||
}
|
@ -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
|
||||
|
@ -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");
|
||||
|
@ -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")]
|
||||
|
@ -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; }
|
||||
}
|
||||
}
|
@ -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
|
||||
{
|
||||
|
@ -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);
|
||||
}
|
@ -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);
|
||||
}
|
||||
}
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
@ -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);
|
||||
}
|
@ -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);
|
||||
|
@ -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);
|
||||
|
@ -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; }
|
||||
}
|
@ -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
|
||||
|
@ -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
|
||||
});
|
||||
}
|
||||
|
||||
|
@ -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>();
|
||||
|
@ -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");
|
||||
}
|
||||
}
|
||||
}
|
@ -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)
|
||||
{
|
||||
|
@ -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);
|
||||
|
Loading…
Reference in New Issue
Block a user