diff --git a/src/Core/GlobalSettings.cs b/src/Core/GlobalSettings.cs index 4c933b894e..239915e2eb 100644 --- a/src/Core/GlobalSettings.cs +++ b/src/Core/GlobalSettings.cs @@ -18,6 +18,7 @@ namespace Bit.Core public virtual bool DisableUserRegistration { get; set; } public virtual bool DisableEmailNewDevice { get; set; } public virtual int OrganizationInviteExpirationHours { get; set; } = 120; // 5 days + public virtual string AppleIapPassword { get; set; } public virtual InstallationSettings Installation { get; set; } = new InstallationSettings(); public virtual BaseServiceUriSettings BaseServiceUri { get; set; } = new BaseServiceUriSettings(); public virtual SqlSettings SqlServer { get; set; } = new SqlSettings(); diff --git a/src/Core/Models/Business/AppleReceiptStatus.cs b/src/Core/Models/Business/AppleReceiptStatus.cs new file mode 100644 index 0000000000..25b980437b --- /dev/null +++ b/src/Core/Models/Business/AppleReceiptStatus.cs @@ -0,0 +1,57 @@ +using System; +using Newtonsoft.Json; + +namespace Bit.Billing.Models +{ + public class AppleReceiptStatus + { + [JsonProperty("status")] + public int? Status { get; set; } + [JsonProperty("latest_receipt")] + public string LatestReceipt { get; set; } + [JsonProperty("receipt")] + public AppleReceipt Receipt { get; set; } + [JsonProperty("latest_receipt_info")] + public AppleReceipt LatestReceiptInfo { get; set; } + [JsonProperty("latest_expired_receipt_info")] + public AppleReceipt LatestExpiredReceiptInfo { get; set; } + [JsonProperty("environment")] + public string Environment { get; set; } + [JsonProperty("auto_renew_status")] + public string AutoRenewStatus { get; set; } + [JsonProperty("auto_renew_product_id")] + public string AutoRenewProductId { get; set; } + [JsonProperty("notification_type")] + public string NotificationType { get; set; } + [JsonProperty("expiration_intent")] + public string ExpirationIntent { get; set; } + [JsonProperty("is_in_billing_retry_period")] + public string IsInBillingRetryPeriod { get; set; } + + public class AppleReceipt + { + [JsonProperty("purchase_date")] + public DateTime PurchaseDate { get; set; } + [JsonProperty("original_purchase_date")] + public DateTime OriginalPurchaseDate { get; set; } + [JsonProperty("expires_date_formatted")] + public DateTime ExpiresDate { get; set; } + [JsonProperty("bid")] + public string Bid { get; set; } + [JsonProperty("bvrs")] + public string Bvrs { get; set; } + [JsonProperty("product_id")] + public string ProductId { get; set; } + [JsonProperty("item_id")] + public string ItemId { get; set; } + [JsonProperty("web_order_line_item_id")] + public string WebOrderLineItemId { get; set; } + [JsonProperty("quantity")] + public string Quantity { get; set; } + [JsonProperty("transaction_id")] + public string TransactionId { get; set; } + [JsonProperty("unique_identifier")] + public string UniqueIdentifier { get; set; } + } + } +} diff --git a/src/Core/Services/IAppleIapService.cs b/src/Core/Services/IAppleIapService.cs new file mode 100644 index 0000000000..6d716e842b --- /dev/null +++ b/src/Core/Services/IAppleIapService.cs @@ -0,0 +1,9 @@ +using System.Threading.Tasks; + +namespace Bit.Core.Services +{ + public interface IAppleIapService + { + Task VerifyReceiptAsync(string receiptData); + } +} diff --git a/src/Core/Services/Implementations/AppleIapService.cs b/src/Core/Services/Implementations/AppleIapService.cs new file mode 100644 index 0000000000..d89fe8bbba --- /dev/null +++ b/src/Core/Services/Implementations/AppleIapService.cs @@ -0,0 +1,71 @@ +using System; +using System.Net.Http; +using System.Threading.Tasks; +using Bit.Billing.Models; +using Microsoft.Extensions.Logging; +using Newtonsoft.Json; +using Newtonsoft.Json.Linq; + +namespace Bit.Core.Services.Implementations +{ + public class AppleIapService : IAppleIapService + { + private readonly HttpClient _httpClient = new HttpClient(); + + private readonly GlobalSettings _globalSettings; + private readonly ILogger _logger; + + public AppleIapService( + GlobalSettings globalSettings, + ILogger logger) + { + _globalSettings = globalSettings; + _logger = logger; + } + + public async Task VerifyReceiptAsync(string receiptData) + { + var receiptStatus = await GetReceiptStatusAsync(receiptData); + return receiptStatus?.Status == 0; + } + + private async Task 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 ?? "null"); + } + + var url = string.Format("https://{0}.itunes.apple.com/verifyReceipt", prod ? "buy" : "sandbox"); + var json = new JObject(new JProperty("receipt-data", receiptData), + new JProperty("password", _globalSettings.AppleIapPassword)).ToString(); + + var response = await _httpClient.PostAsync(url, new StringContent(json)); + if(response.IsSuccessStatusCode) + { + var responseJson = await response.Content.ReadAsStringAsync(); + var receiptStatus = JsonConvert.DeserializeObject(responseJson); + 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; + } + } +}