Handling of AI pack through mmicrosoft store.
This commit is contained in:
@@ -33,7 +33,7 @@
|
|||||||
<PackageVersion Include="Microsoft.Identity.Client.Extensions.Msal" Version="4.82.1" />
|
<PackageVersion Include="Microsoft.Identity.Client.Extensions.Msal" Version="4.82.1" />
|
||||||
<PackageVersion Include="Microsoft.NETCore.UniversalWindowsPlatform" Version="6.2.14" />
|
<PackageVersion Include="Microsoft.NETCore.UniversalWindowsPlatform" Version="6.2.14" />
|
||||||
<PackageVersion Include="Microsoft.Xaml.Behaviors.WinUI.Managed" Version="3.0.1" />
|
<PackageVersion Include="Microsoft.Xaml.Behaviors.WinUI.Managed" Version="3.0.1" />
|
||||||
<PackageVersion Include="Wino.Mail.Contracts" Version="1.0.9" />
|
<PackageVersion Include="Wino.Mail.Contracts" Version="1.0.13" />
|
||||||
<PackageVersion Include="MimeKit" Version="4.15.1" />
|
<PackageVersion Include="MimeKit" Version="4.15.1" />
|
||||||
<PackageVersion Include="morelinq" Version="4.4.0" />
|
<PackageVersion Include="morelinq" Version="4.4.0" />
|
||||||
<PackageVersion Include="Nito.AsyncEx" Version="5.1.2" />
|
<PackageVersion Include="Nito.AsyncEx" Version="5.1.2" />
|
||||||
|
|||||||
@@ -1,24 +0,0 @@
|
|||||||
using System;
|
|
||||||
using SQLite;
|
|
||||||
|
|
||||||
namespace Wino.Core.Domain.Entities.Shared;
|
|
||||||
|
|
||||||
public class WinoAccountAddOnCache
|
|
||||||
{
|
|
||||||
[PrimaryKey]
|
|
||||||
public Guid AccountId { get; set; }
|
|
||||||
|
|
||||||
public bool HasAiPack { get; set; }
|
|
||||||
|
|
||||||
public int? AiUsageCount { get; set; }
|
|
||||||
|
|
||||||
public int? AiUsageLimit { get; set; }
|
|
||||||
|
|
||||||
public DateTime? AiBillingPeriodStartUtc { get; set; }
|
|
||||||
|
|
||||||
public DateTime? AiBillingPeriodEndUtc { get; set; }
|
|
||||||
|
|
||||||
public bool HasUnlimitedAccounts { get; set; }
|
|
||||||
|
|
||||||
public DateTime LastUpdatedUtc { get; set; }
|
|
||||||
}
|
|
||||||
@@ -1,5 +1,7 @@
|
|||||||
using System.Threading.Tasks;
|
#nullable enable
|
||||||
|
using System.Threading.Tasks;
|
||||||
using Wino.Core.Domain.Enums;
|
using Wino.Core.Domain.Enums;
|
||||||
|
|
||||||
namespace Wino.Core.Domain.Interfaces;
|
namespace Wino.Core.Domain.Interfaces;
|
||||||
|
|
||||||
public interface IStoreManagementService
|
public interface IStoreManagementService
|
||||||
@@ -13,4 +15,14 @@ public interface IStoreManagementService
|
|||||||
/// Attempts to purchase the given add-on.
|
/// Attempts to purchase the given add-on.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
Task<StorePurchaseResult> PurchaseAsync(WinoAddOnProductType productType);
|
Task<StorePurchaseResult> PurchaseAsync(WinoAddOnProductType productType);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Requests a Microsoft Store collections ID key for the current customer.
|
||||||
|
/// </summary>
|
||||||
|
Task<string?> GetCustomerCollectionsIdAsync(string serviceTicket, string publisherUserId);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Requests a Microsoft Store purchase ID key for the current customer.
|
||||||
|
/// </summary>
|
||||||
|
Task<string?> GetCustomerPurchaseIdAsync(string serviceTicket, string publisherUserId);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,11 +2,9 @@
|
|||||||
using System.Text.Json;
|
using System.Text.Json;
|
||||||
using System.Threading;
|
using System.Threading;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using Wino.Core.Domain.Enums;
|
|
||||||
using Wino.Core.Domain.Models.Accounts;
|
using Wino.Core.Domain.Models.Accounts;
|
||||||
using Wino.Mail.Api.Contracts.Ai;
|
using Wino.Mail.Api.Contracts.Ai;
|
||||||
using Wino.Mail.Api.Contracts.Auth;
|
using Wino.Mail.Api.Contracts.Auth;
|
||||||
using Wino.Mail.Api.Contracts.Billing;
|
|
||||||
using Wino.Mail.Api.Contracts.Common;
|
using Wino.Mail.Api.Contracts.Common;
|
||||||
|
|
||||||
namespace Wino.Core.Domain.Interfaces;
|
namespace Wino.Core.Domain.Interfaces;
|
||||||
@@ -24,8 +22,9 @@ public interface IWinoAccountApiClient
|
|||||||
Task<ApiEnvelope<AiTextResultDto>> SummarizeAsync(string html, CancellationToken cancellationToken = default);
|
Task<ApiEnvelope<AiTextResultDto>> SummarizeAsync(string html, CancellationToken cancellationToken = default);
|
||||||
Task<ApiEnvelope<AiTextResultDto>> TranslateAsync(string html, string targetLanguage, CancellationToken cancellationToken = default);
|
Task<ApiEnvelope<AiTextResultDto>> TranslateAsync(string html, string targetLanguage, CancellationToken cancellationToken = default);
|
||||||
Task<ApiEnvelope<AiTextResultDto>> RewriteAsync(string html, string mode, CancellationToken cancellationToken = default);
|
Task<ApiEnvelope<AiTextResultDto>> RewriteAsync(string html, string mode, CancellationToken cancellationToken = default);
|
||||||
Task<ApiEnvelope<CheckoutSessionResultDto>> CreateCheckoutSessionAsync(WinoAddOnProductType productId, CancellationToken cancellationToken = default);
|
Task<ApiEnvelope<WinoStoreCollectionsIdTicketInfo>> CreateCollectionsIdTicketAsync(CancellationToken cancellationToken = default);
|
||||||
Task<ApiEnvelope<CustomerPortalResultDto>> CreateCustomerPortalSessionAsync(CancellationToken cancellationToken = default);
|
Task<ApiEnvelope<WinoStoreCollectionsIdTicketInfo>> CreatePurchaseIdTicketAsync(CancellationToken cancellationToken = default);
|
||||||
|
Task<ApiEnvelope<JsonElement>> SyncStoreEntitlementsAsync(string? storeIdKey, string? purchaseIdKey, CancellationToken cancellationToken = default);
|
||||||
Task<string?> GetSettingsAsync(CancellationToken cancellationToken = default);
|
Task<string?> GetSettingsAsync(CancellationToken cancellationToken = default);
|
||||||
Task<bool> SaveSettingsAsync(string settingsJson, CancellationToken cancellationToken = default);
|
Task<bool> SaveSettingsAsync(string settingsJson, CancellationToken cancellationToken = default);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,11 +4,9 @@ using System.Text.Json;
|
|||||||
using System.Threading;
|
using System.Threading;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using Wino.Core.Domain.Entities.Shared;
|
using Wino.Core.Domain.Entities.Shared;
|
||||||
using Wino.Core.Domain.Enums;
|
|
||||||
using Wino.Core.Domain.Models.Accounts;
|
using Wino.Core.Domain.Models.Accounts;
|
||||||
using Wino.Mail.Api.Contracts.Ai;
|
using Wino.Mail.Api.Contracts.Ai;
|
||||||
using Wino.Mail.Api.Contracts.Auth;
|
using Wino.Mail.Api.Contracts.Auth;
|
||||||
using Wino.Mail.Api.Contracts.Billing;
|
|
||||||
using Wino.Mail.Api.Contracts.Common;
|
using Wino.Mail.Api.Contracts.Common;
|
||||||
|
|
||||||
namespace Wino.Core.Domain.Interfaces;
|
namespace Wino.Core.Domain.Interfaces;
|
||||||
@@ -22,17 +20,14 @@ public interface IWinoAccountProfileService
|
|||||||
Task<ApiEnvelope<EmailConfirmationResendResultDto>> ResendEmailConfirmationAsync(string endpoint, string ticket, CancellationToken cancellationToken = default);
|
Task<ApiEnvelope<EmailConfirmationResendResultDto>> ResendEmailConfirmationAsync(string endpoint, string ticket, CancellationToken cancellationToken = default);
|
||||||
Task<ApiEnvelope<JsonElement>> ForgotPasswordAsync(string email, CancellationToken cancellationToken = default);
|
Task<ApiEnvelope<JsonElement>> ForgotPasswordAsync(string email, CancellationToken cancellationToken = default);
|
||||||
Task<WinoAccount?> GetActiveAccountAsync();
|
Task<WinoAccount?> GetActiveAccountAsync();
|
||||||
Task<WinoAccountAddOnSnapshot?> GetCachedAddOnSnapshotAsync();
|
|
||||||
Task<WinoAccount?> GetAuthenticatedAccountAsync(CancellationToken cancellationToken = default);
|
Task<WinoAccount?> GetAuthenticatedAccountAsync(CancellationToken cancellationToken = default);
|
||||||
Task<bool> HasActiveAccountAsync();
|
Task<bool> HasActiveAccountAsync();
|
||||||
Task<bool> HasAddOnAsync(WinoAddOnProductType productId, CancellationToken cancellationToken = default);
|
|
||||||
Task<ApiEnvelope<AuthUserDto>> GetCurrentUserAsync(CancellationToken cancellationToken = default);
|
Task<ApiEnvelope<AuthUserDto>> GetCurrentUserAsync(CancellationToken cancellationToken = default);
|
||||||
Task<ApiEnvelope<AiStatusResultDto>> GetAiStatusAsync(CancellationToken cancellationToken = default);
|
Task<ApiEnvelope<AiStatusResultDto>> GetAiStatusAsync(CancellationToken cancellationToken = default);
|
||||||
Task<ApiEnvelope<AiTextResultDto>> SummarizeAsync(string html, CancellationToken cancellationToken = default);
|
Task<ApiEnvelope<AiTextResultDto>> SummarizeAsync(string html, CancellationToken cancellationToken = default);
|
||||||
Task<ApiEnvelope<AiTextResultDto>> TranslateAsync(string html, string targetLanguage, CancellationToken cancellationToken = default);
|
Task<ApiEnvelope<AiTextResultDto>> TranslateAsync(string html, string targetLanguage, CancellationToken cancellationToken = default);
|
||||||
Task<ApiEnvelope<AiTextResultDto>> RewriteAsync(string html, string mode, CancellationToken cancellationToken = default);
|
Task<ApiEnvelope<AiTextResultDto>> RewriteAsync(string html, string mode, CancellationToken cancellationToken = default);
|
||||||
Task<ApiEnvelope<CheckoutSessionResultDto>> CreateCheckoutSessionAsync(WinoAddOnProductType productId, CancellationToken cancellationToken = default);
|
Task<ApiEnvelope<JsonElement>> SyncStoreEntitlementsAsync(CancellationToken cancellationToken = default);
|
||||||
Task<ApiEnvelope<CustomerPortalResultDto>> CreateCustomerPortalSessionAsync(CancellationToken cancellationToken = default);
|
|
||||||
Task<bool> ProcessBillingCallbackAsync(Uri callbackUri, CancellationToken cancellationToken = default);
|
Task<bool> ProcessBillingCallbackAsync(Uri callbackUri, CancellationToken cancellationToken = default);
|
||||||
Task SignOutAsync(CancellationToken cancellationToken = default);
|
Task SignOutAsync(CancellationToken cancellationToken = default);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,12 +0,0 @@
|
|||||||
#nullable enable
|
|
||||||
using System.Collections.Generic;
|
|
||||||
using System.Threading;
|
|
||||||
using System.Threading.Tasks;
|
|
||||||
using Wino.Core.Domain.Models.Accounts;
|
|
||||||
|
|
||||||
namespace Wino.Core.Domain.Interfaces;
|
|
||||||
|
|
||||||
public interface IWinoAddOnService
|
|
||||||
{
|
|
||||||
Task<IReadOnlyList<WinoAddOnInfo>> GetAvailableAddOnsAsync(bool useCachedDataOnly = false, CancellationToken cancellationToken = default);
|
|
||||||
}
|
|
||||||
@@ -1,12 +0,0 @@
|
|||||||
using System;
|
|
||||||
|
|
||||||
namespace Wino.Core.Domain.Models.Accounts;
|
|
||||||
|
|
||||||
public sealed record WinoAccountAddOnSnapshot(
|
|
||||||
bool HasAiPack,
|
|
||||||
int? UsageCount = null,
|
|
||||||
int? UsageLimit = null,
|
|
||||||
DateTimeOffset? BillingPeriodStartUtc = null,
|
|
||||||
DateTimeOffset? BillingPeriodEndUtc = null,
|
|
||||||
bool HasUnlimitedAccounts = false,
|
|
||||||
DateTimeOffset? LastUpdatedUtc = null);
|
|
||||||
@@ -1,12 +0,0 @@
|
|||||||
using System;
|
|
||||||
using Wino.Core.Domain.Enums;
|
|
||||||
|
|
||||||
namespace Wino.Core.Domain.Models.Accounts;
|
|
||||||
|
|
||||||
public sealed record WinoAddOnInfo(
|
|
||||||
WinoAddOnProductType ProductType,
|
|
||||||
bool IsPurchased,
|
|
||||||
int? UsageCount = null,
|
|
||||||
int? UsageLimit = null,
|
|
||||||
double UsagePercentage = 0,
|
|
||||||
DateTimeOffset? RenewalDateUtc = null);
|
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
#nullable enable
|
||||||
|
using System;
|
||||||
|
|
||||||
|
namespace Wino.Core.Domain.Models.Accounts;
|
||||||
|
|
||||||
|
public sealed record WinoStoreCollectionsIdTicketInfo(
|
||||||
|
string ServiceTicket,
|
||||||
|
string PublisherUserId,
|
||||||
|
DateTimeOffset ExpiresAtUtc);
|
||||||
@@ -1240,14 +1240,17 @@
|
|||||||
"WinoAddOn_UNLIMITED_ACCOUNTS_Description": "Remove the account limit and add as many mail accounts as you need.",
|
"WinoAddOn_UNLIMITED_ACCOUNTS_Description": "Remove the account limit and add as many mail accounts as you need.",
|
||||||
"WinoAddOn_UNLIMITED_ACCOUNTS_Keywords": "accounts, unlimited, premium, add-on",
|
"WinoAddOn_UNLIMITED_ACCOUNTS_Keywords": "accounts, unlimited, premium, add-on",
|
||||||
"WinoAccount_Management_PurchaseRequiresSignIn": "Sign in with your Wino Account to complete this purchase.",
|
"WinoAccount_Management_PurchaseRequiresSignIn": "Sign in with your Wino Account to complete this purchase.",
|
||||||
"WinoAccount_Management_PurchaseStartFailed": "Wino could not start the checkout session for this add-on.",
|
"WinoAccount_Management_PurchaseStartFailed": "Wino could not complete this Microsoft Store purchase.",
|
||||||
|
"WinoAccount_Management_StoreSyncFailed": "Your purchase finished, but Wino could not refresh your account benefits yet. Please try again in a moment.",
|
||||||
"WinoAccount_Management_AiPackSubscriptionActive": "Your subscription is active",
|
"WinoAccount_Management_AiPackSubscriptionActive": "Your subscription is active",
|
||||||
"WinoAccount_Management_AiPackRenews": "Renews {0:d}",
|
"WinoAccount_Management_AiPackRenews": "Renews {0:d}",
|
||||||
"WinoAccount_Management_AiPackRequestsUsed": "Requests used this month",
|
"WinoAccount_Management_AiPackRequestsUsed": "Requests used this month",
|
||||||
"WinoAccount_Management_AiPackResets": "Resets {0:d}",
|
"WinoAccount_Management_AiPackResets": "Resets {0:d}",
|
||||||
|
"WinoAccount_Management_AiPackUsageLoadFailed": "We had issues loading your AI usage balance.",
|
||||||
"WinoAccount_Management_AiPackFeatureTranslate": "Translate",
|
"WinoAccount_Management_AiPackFeatureTranslate": "Translate",
|
||||||
"WinoAccount_Management_AiPackFeatureRewrite": "Rewrite",
|
"WinoAccount_Management_AiPackFeatureRewrite": "Rewrite",
|
||||||
"WinoAccount_Management_AiPackFeatureSummarize": "Summarize",
|
"WinoAccount_Management_AiPackFeatureSummarize": "Summarize",
|
||||||
|
"WinoAccount_Management_AddOnLoadFailed": "We had issues loading this add-on.",
|
||||||
"WinoAccount_Management_SyncPreferencesTitle": "Synchronize Preferences",
|
"WinoAccount_Management_SyncPreferencesTitle": "Synchronize Preferences",
|
||||||
"WinoAccount_Management_SyncPreferencesDescription": "Import or export your preferences to cloud. Import them across devices.",
|
"WinoAccount_Management_SyncPreferencesDescription": "Import or export your preferences to cloud. Import them across devices.",
|
||||||
"WinoAccount_Management_SignOutTitle": "Sign out",
|
"WinoAccount_Management_SignOutTitle": "Sign out",
|
||||||
|
|||||||
@@ -27,7 +27,6 @@ public class WinoAccountProfileServiceTests : IAsyncLifetime
|
|||||||
{
|
{
|
||||||
_databaseService = new InMemoryDatabaseService();
|
_databaseService = new InMemoryDatabaseService();
|
||||||
await _databaseService.InitializeAsync();
|
await _databaseService.InitializeAsync();
|
||||||
await _databaseService.Connection.CreateTableAsync<WinoAccountAddOnCache>();
|
|
||||||
_service = new WinoAccountProfileService(_databaseService, _apiClient.Object, _storeManagementService.Object);
|
_service = new WinoAccountProfileService(_databaseService, _apiClient.Object, _storeManagementService.Object);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -96,18 +95,6 @@ public class WinoAccountProfileServiceTests : IAsyncLifetime
|
|||||||
persisted.Should().BeEmpty();
|
persisted.Should().BeEmpty();
|
||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
|
||||||
public async Task HasAddOnAsync_ShouldUseLegacyStoreForUnlimitedAccounts()
|
|
||||||
{
|
|
||||||
_storeManagementService
|
|
||||||
.Setup(x => x.HasProductAsync(WinoAddOnProductType.UNLIMITED_ACCOUNTS))
|
|
||||||
.ReturnsAsync(true);
|
|
||||||
|
|
||||||
var hasAddOn = await _service.HasAddOnAsync(WinoAddOnProductType.UNLIMITED_ACCOUNTS);
|
|
||||||
|
|
||||||
hasAddOn.Should().BeTrue();
|
|
||||||
}
|
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public async Task LoginAsync_ShouldPreserveEnvelopeErrorMessage()
|
public async Task LoginAsync_ShouldPreserveEnvelopeErrorMessage()
|
||||||
{
|
{
|
||||||
@@ -190,8 +177,7 @@ public class WinoAccountProfileServiceTests : IAsyncLifetime
|
|||||||
"Premium",
|
"Premium",
|
||||||
authResult.User.HasPassword,
|
authResult.User.HasPassword,
|
||||||
authResult.User.HasGoogleLogin,
|
authResult.User.HasGoogleLogin,
|
||||||
authResult.User.HasFacebookLogin,
|
authResult.User.HasFacebookLogin)));
|
||||||
authResult.User.HasUnlimitedAccounts)));
|
|
||||||
|
|
||||||
await _service.LoginAsync("first@example.com", "pw");
|
await _service.LoginAsync("first@example.com", "pw");
|
||||||
|
|
||||||
@@ -237,7 +223,7 @@ public class WinoAccountProfileServiceTests : IAsyncLifetime
|
|||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public async Task SummarizeAsync_ShouldPersistQuotaSnapshot()
|
public async Task SummarizeAsync_ShouldReturnQuotaBackedResponse()
|
||||||
{
|
{
|
||||||
var authResult = CreateAuthResult("first@example.com");
|
var authResult = CreateAuthResult("first@example.com");
|
||||||
|
|
||||||
@@ -265,18 +251,12 @@ public class WinoAccountProfileServiceTests : IAsyncLifetime
|
|||||||
|
|
||||||
response.IsSuccess.Should().BeTrue();
|
response.IsSuccess.Should().BeTrue();
|
||||||
response.Result?.Html.Should().Be("<p>Summary</p>");
|
response.Result?.Html.Should().Be("<p>Summary</p>");
|
||||||
|
|
||||||
var cachedSnapshot = await _service.GetCachedAddOnSnapshotAsync();
|
|
||||||
cachedSnapshot.Should().NotBeNull();
|
|
||||||
cachedSnapshot!.HasAiPack.Should().BeTrue();
|
|
||||||
cachedSnapshot.UsageCount.Should().Be(4);
|
|
||||||
cachedSnapshot.UsageLimit.Should().Be(1000);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private static AuthResultDto CreateAuthResult(string email)
|
private static AuthResultDto CreateAuthResult(string email)
|
||||||
{
|
{
|
||||||
return new AuthResultDto(
|
return new AuthResultDto(
|
||||||
new AuthUserDto(Guid.NewGuid(), email, "Active", true, false, false, false),
|
new AuthUserDto(Guid.NewGuid(), email, "Active", true, false, false),
|
||||||
"access-token",
|
"access-token",
|
||||||
DateTimeOffset.UtcNow.AddMinutes(30),
|
DateTimeOffset.UtcNow.AddMinutes(30),
|
||||||
"refresh-token",
|
"refresh-token",
|
||||||
|
|||||||
@@ -0,0 +1,187 @@
|
|||||||
|
using System.Net;
|
||||||
|
using System.Net.Http;
|
||||||
|
using System.Reflection;
|
||||||
|
using FluentAssertions;
|
||||||
|
using Google.Apis.Requests;
|
||||||
|
using Moq;
|
||||||
|
using Wino.Core.Domain.Entities.Mail;
|
||||||
|
using Wino.Core.Domain.Entities.Shared;
|
||||||
|
using Wino.Core.Domain.Interfaces;
|
||||||
|
using Wino.Core.Domain.Models.Synchronization;
|
||||||
|
using Wino.Core.Integration.Processors;
|
||||||
|
using Wino.Core.Requests.Bundles;
|
||||||
|
using Wino.Core.Requests.Mail;
|
||||||
|
using Wino.Core.Synchronizers.Mail;
|
||||||
|
using Xunit;
|
||||||
|
|
||||||
|
namespace Wino.Core.Tests.Synchronizers;
|
||||||
|
|
||||||
|
public sealed class GmailSynchronizerRequestSuccessTests
|
||||||
|
{
|
||||||
|
[Fact]
|
||||||
|
public async Task ProcessSingleNativeRequestResponseAsync_BatchMarkReadRequest_PersistsLocalReadStateForEachMail()
|
||||||
|
{
|
||||||
|
var changeProcessor = new Mock<IGmailChangeProcessor>(MockBehavior.Strict);
|
||||||
|
changeProcessor
|
||||||
|
.Setup(x => x.ChangeMailReadStatusAsync("mail-1", true))
|
||||||
|
.Returns(Task.CompletedTask);
|
||||||
|
changeProcessor
|
||||||
|
.Setup(x => x.ChangeMailReadStatusAsync("mail-2", true))
|
||||||
|
.Returns(Task.CompletedTask);
|
||||||
|
|
||||||
|
var synchronizer = CreateSynchronizer(changeProcessor.Object);
|
||||||
|
var request = new BatchMarkReadRequest(
|
||||||
|
[
|
||||||
|
new MarkReadRequest(CreateMailCopy("mail-1"), IsRead: true),
|
||||||
|
new MarkReadRequest(CreateMailCopy("mail-2"), IsRead: true)
|
||||||
|
]);
|
||||||
|
var bundle = new HttpRequestBundle<IClientServiceRequest>(Mock.Of<IClientServiceRequest>(), request);
|
||||||
|
using var response = new HttpResponseMessage(HttpStatusCode.OK)
|
||||||
|
{
|
||||||
|
Content = new StringContent(string.Empty)
|
||||||
|
};
|
||||||
|
|
||||||
|
await InvokeProcessSingleNativeRequestResponseAsync(synchronizer, bundle, response);
|
||||||
|
|
||||||
|
changeProcessor.Verify(x => x.ChangeMailReadStatusAsync("mail-1", true), Times.Once);
|
||||||
|
changeProcessor.Verify(x => x.ChangeMailReadStatusAsync("mail-2", true), Times.Once);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task ProcessSingleNativeRequestResponseAsync_BatchChangeFlagRequest_PersistsLocalFlagStateForEachMail()
|
||||||
|
{
|
||||||
|
var changeProcessor = new Mock<IGmailChangeProcessor>(MockBehavior.Strict);
|
||||||
|
changeProcessor
|
||||||
|
.Setup(x => x.ChangeFlagStatusAsync("mail-1", true))
|
||||||
|
.Returns(Task.CompletedTask);
|
||||||
|
changeProcessor
|
||||||
|
.Setup(x => x.ChangeFlagStatusAsync("mail-2", true))
|
||||||
|
.Returns(Task.CompletedTask);
|
||||||
|
|
||||||
|
var synchronizer = CreateSynchronizer(changeProcessor.Object);
|
||||||
|
var request = new BatchChangeFlagRequest(
|
||||||
|
[
|
||||||
|
new ChangeFlagRequest(CreateMailCopy("mail-1"), IsFlagged: true),
|
||||||
|
new ChangeFlagRequest(CreateMailCopy("mail-2"), IsFlagged: true)
|
||||||
|
]);
|
||||||
|
var bundle = new HttpRequestBundle<IClientServiceRequest>(Mock.Of<IClientServiceRequest>(), request);
|
||||||
|
using var response = new HttpResponseMessage(HttpStatusCode.OK)
|
||||||
|
{
|
||||||
|
Content = new StringContent(string.Empty)
|
||||||
|
};
|
||||||
|
|
||||||
|
await InvokeProcessSingleNativeRequestResponseAsync(synchronizer, bundle, response);
|
||||||
|
|
||||||
|
changeProcessor.Verify(x => x.ChangeFlagStatusAsync("mail-1", true), Times.Once);
|
||||||
|
changeProcessor.Verify(x => x.ChangeFlagStatusAsync("mail-2", true), Times.Once);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task ProcessSingleNativeRequestResponseAsync_HandledRequestError_DoesNotPersistLocalReadState()
|
||||||
|
{
|
||||||
|
var changeProcessor = new Mock<IGmailChangeProcessor>(MockBehavior.Strict);
|
||||||
|
var errorFactory = new Mock<IGmailSynchronizerErrorHandlerFactory>(MockBehavior.Strict);
|
||||||
|
errorFactory
|
||||||
|
.Setup(x => x.HandleErrorAsync(It.IsAny<SynchronizerErrorContext>()))
|
||||||
|
.ReturnsAsync(true);
|
||||||
|
|
||||||
|
var synchronizer = CreateSynchronizer(changeProcessor.Object, errorFactory.Object);
|
||||||
|
var request = new BatchMarkReadRequest(
|
||||||
|
[
|
||||||
|
new MarkReadRequest(CreateMailCopy("mail-1"), IsRead: true)
|
||||||
|
]);
|
||||||
|
var bundle = new HttpRequestBundle<IClientServiceRequest>(Mock.Of<IClientServiceRequest>(), request);
|
||||||
|
using var response = new HttpResponseMessage(HttpStatusCode.OK)
|
||||||
|
{
|
||||||
|
Content = new StringContent(string.Empty)
|
||||||
|
};
|
||||||
|
var error = new RequestError
|
||||||
|
{
|
||||||
|
Code = 429,
|
||||||
|
Message = "rate limit"
|
||||||
|
};
|
||||||
|
|
||||||
|
await InvokeProcessSingleNativeRequestResponseAsync(synchronizer, bundle, response, error);
|
||||||
|
|
||||||
|
changeProcessor.Verify(x => x.ChangeMailReadStatusAsync(It.IsAny<string>(), It.IsAny<bool>()), Times.Never);
|
||||||
|
errorFactory.Verify(x => x.HandleErrorAsync(It.IsAny<SynchronizerErrorContext>()), Times.Once);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task ProcessSingleNativeRequestResponseAsync_HandledRequestError_RevertsOptimisticReadState()
|
||||||
|
{
|
||||||
|
var changeProcessor = new Mock<IGmailChangeProcessor>(MockBehavior.Strict);
|
||||||
|
var errorFactory = new Mock<IGmailSynchronizerErrorHandlerFactory>(MockBehavior.Strict);
|
||||||
|
errorFactory
|
||||||
|
.Setup(x => x.HandleErrorAsync(It.IsAny<SynchronizerErrorContext>()))
|
||||||
|
.ReturnsAsync(true);
|
||||||
|
|
||||||
|
var mail = CreateMailCopy("mail-1");
|
||||||
|
var request = new BatchMarkReadRequest(
|
||||||
|
[
|
||||||
|
new MarkReadRequest(mail, IsRead: true)
|
||||||
|
]);
|
||||||
|
request.ApplyUIChanges();
|
||||||
|
|
||||||
|
var synchronizer = CreateSynchronizer(changeProcessor.Object, errorFactory.Object);
|
||||||
|
var bundle = new HttpRequestBundle<IClientServiceRequest>(Mock.Of<IClientServiceRequest>(), request);
|
||||||
|
using var response = new HttpResponseMessage(HttpStatusCode.OK)
|
||||||
|
{
|
||||||
|
Content = new StringContent(string.Empty)
|
||||||
|
};
|
||||||
|
var error = new RequestError
|
||||||
|
{
|
||||||
|
Code = 429,
|
||||||
|
Message = "rate limit"
|
||||||
|
};
|
||||||
|
|
||||||
|
await InvokeProcessSingleNativeRequestResponseAsync(synchronizer, bundle, response, error);
|
||||||
|
|
||||||
|
mail.IsRead.Should().BeFalse();
|
||||||
|
changeProcessor.Verify(x => x.ChangeMailReadStatusAsync(It.IsAny<string>(), It.IsAny<bool>()), Times.Never);
|
||||||
|
errorFactory.Verify(x => x.HandleErrorAsync(It.IsAny<SynchronizerErrorContext>()), Times.Once);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static GmailSynchronizer CreateSynchronizer(
|
||||||
|
IGmailChangeProcessor changeProcessor,
|
||||||
|
IGmailSynchronizerErrorHandlerFactory? errorFactory = null)
|
||||||
|
{
|
||||||
|
var account = new MailAccount
|
||||||
|
{
|
||||||
|
Id = Guid.NewGuid(),
|
||||||
|
Name = "Gmail",
|
||||||
|
Address = "user@example.com"
|
||||||
|
};
|
||||||
|
|
||||||
|
var authenticator = new Mock<IGmailAuthenticator>(MockBehavior.Loose);
|
||||||
|
|
||||||
|
return new GmailSynchronizer(account, authenticator.Object, changeProcessor, errorFactory ?? Mock.Of<IGmailSynchronizerErrorHandlerFactory>());
|
||||||
|
}
|
||||||
|
|
||||||
|
private static MailCopy CreateMailCopy(string id) =>
|
||||||
|
new()
|
||||||
|
{
|
||||||
|
UniqueId = Guid.NewGuid(),
|
||||||
|
Id = id,
|
||||||
|
FolderId = Guid.NewGuid(),
|
||||||
|
IsRead = false,
|
||||||
|
IsFlagged = false
|
||||||
|
};
|
||||||
|
|
||||||
|
private static async Task InvokeProcessSingleNativeRequestResponseAsync(
|
||||||
|
GmailSynchronizer synchronizer,
|
||||||
|
HttpRequestBundle<IClientServiceRequest> bundle,
|
||||||
|
HttpResponseMessage response,
|
||||||
|
RequestError? error = null)
|
||||||
|
{
|
||||||
|
var method = typeof(GmailSynchronizer).GetMethod(
|
||||||
|
"ProcessSingleNativeRequestResponseAsync",
|
||||||
|
BindingFlags.Instance | BindingFlags.NonPublic);
|
||||||
|
|
||||||
|
method.Should().NotBeNull();
|
||||||
|
|
||||||
|
var task = method!.Invoke(synchronizer, [bundle, error, response, CancellationToken.None]) as Task;
|
||||||
|
task.Should().NotBeNull();
|
||||||
|
await task!;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -27,15 +27,15 @@ public abstract partial class AccountManagementPageViewModelBase : CoreBaseViewM
|
|||||||
|
|
||||||
[ObservableProperty]
|
[ObservableProperty]
|
||||||
[NotifyPropertyChangedFor(nameof(IsPurchasePanelVisible))]
|
[NotifyPropertyChangedFor(nameof(IsPurchasePanelVisible))]
|
||||||
private bool hasUnlimitedAccountProduct;
|
public partial bool HasUnlimitedAccountProduct { get; set; }
|
||||||
|
|
||||||
[ObservableProperty]
|
[ObservableProperty]
|
||||||
[NotifyPropertyChangedFor(nameof(IsAccountCreationAlmostOnLimit))]
|
[NotifyPropertyChangedFor(nameof(IsAccountCreationAlmostOnLimit))]
|
||||||
[NotifyPropertyChangedFor(nameof(IsPurchasePanelVisible))]
|
[NotifyPropertyChangedFor(nameof(IsPurchasePanelVisible))]
|
||||||
private bool isAccountCreationBlocked;
|
public partial bool IsAccountCreationBlocked { get; set; }
|
||||||
|
|
||||||
[ObservableProperty]
|
[ObservableProperty]
|
||||||
private IAccountProviderDetailViewModel _startupAccount;
|
public partial IAccountProviderDetailViewModel StartupAccount { get; set; }
|
||||||
|
|
||||||
public int FREE_ACCOUNT_COUNT { get; } = 3;
|
public int FREE_ACCOUNT_COUNT { get; } = 3;
|
||||||
protected IDialogServiceBase DialogService { get; }
|
protected IDialogServiceBase DialogService { get; }
|
||||||
@@ -94,7 +94,7 @@ public abstract partial class AccountManagementPageViewModelBase : CoreBaseViewM
|
|||||||
|
|
||||||
public async Task ManageStorePurchasesAsync()
|
public async Task ManageStorePurchasesAsync()
|
||||||
{
|
{
|
||||||
var hasUnlimitedAccountProduct = await WinoAccountProfileService.HasAddOnAsync(WinoAddOnProductType.UNLIMITED_ACCOUNTS).ConfigureAwait(false);
|
var hasUnlimitedAccountProduct = await StoreManagementService.HasProductAsync(WinoAddOnProductType.UNLIMITED_ACCOUNTS).ConfigureAwait(false);
|
||||||
|
|
||||||
await ExecuteUIThread(() =>
|
await ExecuteUIThread(() =>
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -21,30 +21,63 @@ public partial class WinoAddOnItemViewModel : ObservableObject
|
|||||||
};
|
};
|
||||||
|
|
||||||
[ObservableProperty]
|
[ObservableProperty]
|
||||||
|
[NotifyPropertyChangedFor(nameof(ShowPurchaseState))]
|
||||||
|
[NotifyPropertyChangedFor(nameof(ShowUsageSummary))]
|
||||||
public partial bool IsPurchased { get; set; }
|
public partial bool IsPurchased { get; set; }
|
||||||
|
|
||||||
public ICommand? PurchaseCommand { get; set; }
|
public ICommand? PurchaseCommand { get; set; }
|
||||||
|
|
||||||
public ICommand? ManageCommand { get; set; }
|
[ObservableProperty]
|
||||||
|
[NotifyPropertyChangedFor(nameof(ShowLoadingState))]
|
||||||
|
[NotifyPropertyChangedFor(nameof(ShowPurchaseState))]
|
||||||
|
[NotifyPropertyChangedFor(nameof(ShowUsageSummary))]
|
||||||
|
[NotifyPropertyChangedFor(nameof(ShowErrorState))]
|
||||||
|
public partial bool IsLoading { get; set; }
|
||||||
|
|
||||||
[ObservableProperty]
|
[ObservableProperty]
|
||||||
public partial bool IsPurchaseInProgress { get; set; }
|
public partial bool IsPurchaseInProgress { get; set; }
|
||||||
|
|
||||||
|
[ObservableProperty]
|
||||||
|
[NotifyPropertyChangedFor(nameof(ShowErrorState))]
|
||||||
|
[NotifyPropertyChangedFor(nameof(ShowUsageSummary))]
|
||||||
|
public partial bool HasUsageData { get; set; }
|
||||||
|
|
||||||
|
[ObservableProperty]
|
||||||
|
[NotifyPropertyChangedFor(nameof(ShowErrorState))]
|
||||||
|
[NotifyPropertyChangedFor(nameof(ShowUsageSummary))]
|
||||||
|
public partial string ErrorText { get; set; } = string.Empty;
|
||||||
|
|
||||||
[ObservableProperty]
|
[ObservableProperty]
|
||||||
public partial int UsageCount { get; set; }
|
public partial int UsageCount { get; set; }
|
||||||
|
|
||||||
[ObservableProperty]
|
[ObservableProperty]
|
||||||
|
[NotifyPropertyChangedFor(nameof(ShowUsageSummary))]
|
||||||
public partial int UsageLimit { get; set; } = 1;
|
public partial int UsageLimit { get; set; } = 1;
|
||||||
|
|
||||||
[ObservableProperty]
|
[ObservableProperty]
|
||||||
|
[NotifyPropertyChangedFor(nameof(ShowUsageSummary))]
|
||||||
public partial double UsagePercentage { get; set; }
|
public partial double UsagePercentage { get; set; }
|
||||||
|
|
||||||
[ObservableProperty]
|
[ObservableProperty]
|
||||||
|
[NotifyPropertyChangedFor(nameof(ShowUsageSummary))]
|
||||||
public partial string RenewalText { get; set; } = string.Empty;
|
public partial string RenewalText { get; set; } = string.Empty;
|
||||||
|
|
||||||
[ObservableProperty]
|
[ObservableProperty]
|
||||||
|
[NotifyPropertyChangedFor(nameof(ShowUsageSummary))]
|
||||||
public partial string UsageResetText { get; set; } = string.Empty;
|
public partial string UsageResetText { get; set; } = string.Empty;
|
||||||
|
|
||||||
|
public bool ShowLoadingState => IsLoading;
|
||||||
|
|
||||||
|
public bool ShowPurchaseState => !IsLoading && string.IsNullOrWhiteSpace(ErrorText);
|
||||||
|
|
||||||
|
public bool ShowUsageSummary => ProductType == WinoAddOnProductType.AI_PACK &&
|
||||||
|
IsPurchased &&
|
||||||
|
!IsLoading &&
|
||||||
|
string.IsNullOrWhiteSpace(ErrorText) &&
|
||||||
|
HasUsageData;
|
||||||
|
|
||||||
|
public bool ShowErrorState => !IsLoading && !string.IsNullOrWhiteSpace(ErrorText);
|
||||||
|
|
||||||
public WinoAddOnItemViewModel(WinoAddOnProductType productType)
|
public WinoAddOnItemViewModel(WinoAddOnProductType productType)
|
||||||
{
|
{
|
||||||
ProductType = productType;
|
ProductType = productType;
|
||||||
|
|||||||
@@ -1,8 +1,7 @@
|
|||||||
#nullable enable
|
#nullable enable
|
||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
|
||||||
using System.Collections.ObjectModel;
|
using System.Collections.ObjectModel;
|
||||||
using System.Linq;
|
using Wino.Core.Domain.Entities.Shared;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using CommunityToolkit.Mvvm.ComponentModel;
|
using CommunityToolkit.Mvvm.ComponentModel;
|
||||||
using CommunityToolkit.Mvvm.Input;
|
using CommunityToolkit.Mvvm.Input;
|
||||||
@@ -10,7 +9,6 @@ using CommunityToolkit.Mvvm.Messaging;
|
|||||||
using Wino.Core.Domain;
|
using Wino.Core.Domain;
|
||||||
using Wino.Core.Domain.Enums;
|
using Wino.Core.Domain.Enums;
|
||||||
using Wino.Core.Domain.Interfaces;
|
using Wino.Core.Domain.Interfaces;
|
||||||
using Wino.Core.Domain.Models.Accounts;
|
|
||||||
using Wino.Core.Domain.Models.Navigation;
|
using Wino.Core.Domain.Models.Navigation;
|
||||||
using Wino.Core.ViewModels.Data;
|
using Wino.Core.ViewModels.Data;
|
||||||
using Wino.Mail.Api.Contracts.Common;
|
using Wino.Mail.Api.Contracts.Common;
|
||||||
@@ -24,9 +22,10 @@ public partial class WinoAccountManagementPageViewModel : CoreBaseViewModel,
|
|||||||
IRecipient<WinoAccountAddOnPurchasedMessage>
|
IRecipient<WinoAccountAddOnPurchasedMessage>
|
||||||
{
|
{
|
||||||
private readonly IWinoAccountProfileService _profileService;
|
private readonly IWinoAccountProfileService _profileService;
|
||||||
private readonly IWinoAddOnService _addOnService;
|
|
||||||
private readonly IMailDialogService _dialogService;
|
private readonly IMailDialogService _dialogService;
|
||||||
private readonly INativeAppService _nativeAppService;
|
private readonly IStoreManagementService _storeManagementService;
|
||||||
|
private readonly WinoAddOnItemViewModel _aiPackAddOn;
|
||||||
|
private readonly WinoAddOnItemViewModel _unlimitedAccountsAddOn;
|
||||||
|
|
||||||
public ObservableCollection<WinoAddOnItemViewModel> AddOns { get; } = [];
|
public ObservableCollection<WinoAddOnItemViewModel> AddOns { get; } = [];
|
||||||
|
|
||||||
@@ -50,14 +49,17 @@ public partial class WinoAccountManagementPageViewModel : CoreBaseViewModel,
|
|||||||
public bool IsSignedOut => !IsSignedIn;
|
public bool IsSignedOut => !IsSignedIn;
|
||||||
|
|
||||||
public WinoAccountManagementPageViewModel(IWinoAccountProfileService profileService,
|
public WinoAccountManagementPageViewModel(IWinoAccountProfileService profileService,
|
||||||
IWinoAddOnService addOnService,
|
|
||||||
IMailDialogService dialogService,
|
IMailDialogService dialogService,
|
||||||
INativeAppService nativeAppService)
|
IStoreManagementService storeManagementService)
|
||||||
{
|
{
|
||||||
_profileService = profileService;
|
_profileService = profileService;
|
||||||
_addOnService = addOnService;
|
|
||||||
_dialogService = dialogService;
|
_dialogService = dialogService;
|
||||||
_nativeAppService = nativeAppService;
|
_storeManagementService = storeManagementService;
|
||||||
|
|
||||||
|
_aiPackAddOn = CreateAddOnItem(WinoAddOnProductType.AI_PACK);
|
||||||
|
_unlimitedAccountsAddOn = CreateAddOnItem(WinoAddOnProductType.UNLIMITED_ACCOUNTS);
|
||||||
|
AddOns.Add(_aiPackAddOn);
|
||||||
|
AddOns.Add(_unlimitedAccountsAddOn);
|
||||||
}
|
}
|
||||||
|
|
||||||
public override void OnNavigatedTo(NavigationMode mode, object parameters)
|
public override void OnNavigatedTo(NavigationMode mode, object parameters)
|
||||||
@@ -166,15 +168,6 @@ public partial class WinoAccountManagementPageViewModel : CoreBaseViewModel,
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
var account = await _profileService.GetAuthenticatedAccountAsync().ConfigureAwait(false);
|
|
||||||
if (account == null)
|
|
||||||
{
|
|
||||||
_dialogService.InfoBarMessage(Translator.GeneralTitle_Warning,
|
|
||||||
Translator.WinoAccount_Management_PurchaseRequiresSignIn,
|
|
||||||
InfoBarMessageType.Warning);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
await ExecuteUIThread(() =>
|
await ExecuteUIThread(() =>
|
||||||
{
|
{
|
||||||
IsCheckoutInProgress = true;
|
IsCheckoutInProgress = true;
|
||||||
@@ -183,9 +176,9 @@ public partial class WinoAccountManagementPageViewModel : CoreBaseViewModel,
|
|||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
var checkoutSession = await _profileService.CreateCheckoutSessionAsync(addOn.ProductType).ConfigureAwait(false);
|
var purchaseResult = await _storeManagementService.PurchaseAsync(addOn.ProductType);
|
||||||
|
|
||||||
if (!checkoutSession.IsSuccess || string.IsNullOrWhiteSpace(checkoutSession.Result?.Url))
|
if (purchaseResult == StorePurchaseResult.NotPurchased)
|
||||||
{
|
{
|
||||||
_dialogService.InfoBarMessage(Translator.GeneralTitle_Error,
|
_dialogService.InfoBarMessage(Translator.GeneralTitle_Error,
|
||||||
Translator.WinoAccount_Management_PurchaseStartFailed,
|
Translator.WinoAccount_Management_PurchaseStartFailed,
|
||||||
@@ -193,13 +186,16 @@ public partial class WinoAccountManagementPageViewModel : CoreBaseViewModel,
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
var isLaunched = await _nativeAppService.LaunchUriAsync(new Uri(checkoutSession.Result.Url)).ConfigureAwait(false);
|
var syncResult = await _profileService.SyncStoreEntitlementsAsync().ConfigureAwait(false);
|
||||||
if (!isLaunched)
|
if (!syncResult.IsSuccess && !string.Equals(syncResult.ErrorCode, "MissingAccessToken", StringComparison.Ordinal))
|
||||||
{
|
{
|
||||||
_dialogService.InfoBarMessage(Translator.GeneralTitle_Error,
|
_dialogService.InfoBarMessage(Translator.GeneralTitle_Error,
|
||||||
Translator.WinoAccount_Management_PurchaseStartFailed,
|
TranslateStoreSyncError(syncResult.ErrorCode),
|
||||||
InfoBarMessageType.Error);
|
InfoBarMessageType.Error);
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
await HandleAddOnPurchasedAsync().ConfigureAwait(false);
|
||||||
}
|
}
|
||||||
catch (OperationCanceledException)
|
catch (OperationCanceledException)
|
||||||
{
|
{
|
||||||
@@ -221,40 +217,7 @@ public partial class WinoAccountManagementPageViewModel : CoreBaseViewModel,
|
|||||||
}
|
}
|
||||||
|
|
||||||
private bool CanPurchaseAddOn(WinoAddOnItemViewModel? addOn)
|
private bool CanPurchaseAddOn(WinoAddOnItemViewModel? addOn)
|
||||||
=> addOn != null && !addOn.IsPurchased && !IsCheckoutInProgress;
|
=> addOn != null && !addOn.IsPurchased && !addOn.IsLoading && !IsCheckoutInProgress;
|
||||||
|
|
||||||
[RelayCommand]
|
|
||||||
private async Task ManageAiPackAsync()
|
|
||||||
{
|
|
||||||
try
|
|
||||||
{
|
|
||||||
var portalSession = await _profileService.CreateCustomerPortalSessionAsync().ConfigureAwait(false);
|
|
||||||
if (!portalSession.IsSuccess || string.IsNullOrWhiteSpace(portalSession.Result?.Url))
|
|
||||||
{
|
|
||||||
_dialogService.InfoBarMessage(Translator.GeneralTitle_Error,
|
|
||||||
Translator.WinoAccount_Management_PurchaseStartFailed,
|
|
||||||
InfoBarMessageType.Error);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
var isLaunched = await _nativeAppService.LaunchUriAsync(new Uri(portalSession.Result.Url)).ConfigureAwait(false);
|
|
||||||
if (!isLaunched)
|
|
||||||
{
|
|
||||||
_dialogService.InfoBarMessage(Translator.GeneralTitle_Error,
|
|
||||||
Translator.WinoAccount_Management_PurchaseStartFailed,
|
|
||||||
InfoBarMessageType.Error);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
catch (OperationCanceledException)
|
|
||||||
{
|
|
||||||
}
|
|
||||||
catch (Exception)
|
|
||||||
{
|
|
||||||
_dialogService.InfoBarMessage(Translator.GeneralTitle_Error,
|
|
||||||
Translator.WinoAccount_Management_PurchaseStartFailed,
|
|
||||||
InfoBarMessageType.Error);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
[RelayCommand]
|
[RelayCommand]
|
||||||
private Task ExportSettingsAsync() => Task.CompletedTask;
|
private Task ExportSettingsAsync() => Task.CompletedTask;
|
||||||
@@ -296,43 +259,58 @@ public partial class WinoAccountManagementPageViewModel : CoreBaseViewModel,
|
|||||||
|
|
||||||
private async Task LoadAsync()
|
private async Task LoadAsync()
|
||||||
{
|
{
|
||||||
|
WinoAccount? cachedAccount = null;
|
||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
var cachedAccount = await _profileService.GetActiveAccountAsync().ConfigureAwait(false);
|
cachedAccount = await _profileService.GetActiveAccountAsync().ConfigureAwait(false);
|
||||||
var cachedAddOns = await _addOnService.GetAvailableAddOnsAsync(true).ConfigureAwait(false);
|
|
||||||
|
|
||||||
if (cachedAccount != null)
|
if (cachedAccount != null)
|
||||||
{
|
{
|
||||||
await ApplyAccountStateAsync(cachedAccount).ConfigureAwait(false);
|
await ApplyAccountStateAsync(cachedAccount).ConfigureAwait(false);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (cachedAddOns.Count > 0)
|
await ExecuteUIThread(() => IsBusy = true);
|
||||||
|
await ResetAddOnStatesAsync().ConfigureAwait(false);
|
||||||
|
var loadAiPackTask = LoadAiPackAddOnAsync();
|
||||||
|
var loadUnlimitedAccountsTask = LoadUnlimitedAccountsAddOnAsync();
|
||||||
|
|
||||||
|
var resolvedAccount = cachedAccount;
|
||||||
|
|
||||||
|
if (cachedAccount == null || IsAccessTokenExpired(cachedAccount))
|
||||||
{
|
{
|
||||||
await UpdateAddOnsAsync(cachedAddOns).ConfigureAwait(false);
|
try
|
||||||
|
{
|
||||||
|
var account = await _profileService.GetAuthenticatedAccountAsync().ConfigureAwait(false);
|
||||||
|
if (account != null)
|
||||||
|
{
|
||||||
|
resolvedAccount = account;
|
||||||
|
|
||||||
|
var refreshedProfileResult = await _profileService.RefreshProfileAsync().ConfigureAwait(false);
|
||||||
|
if (refreshedProfileResult.IsSuccess && refreshedProfileResult.Account != null)
|
||||||
|
{
|
||||||
|
resolvedAccount = refreshedProfileResult.Account;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (Exception)
|
||||||
|
{
|
||||||
|
resolvedAccount ??= cachedAccount;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
await ExecuteUIThread(() => IsBusy = cachedAccount == null && cachedAddOns.Count == 0);
|
|
||||||
|
|
||||||
var account = await _profileService.GetAuthenticatedAccountAsync().ConfigureAwait(false);
|
|
||||||
var refreshedProfileResult = account == null
|
|
||||||
? null
|
|
||||||
: await _profileService.RefreshProfileAsync().ConfigureAwait(false);
|
|
||||||
var addOns = await _addOnService.GetAvailableAddOnsAsync().ConfigureAwait(false);
|
|
||||||
|
|
||||||
var resolvedAccount = refreshedProfileResult?.IsSuccess == true && refreshedProfileResult.Account != null
|
|
||||||
? refreshedProfileResult.Account
|
|
||||||
: account;
|
|
||||||
|
|
||||||
await ApplyAccountStateAsync(resolvedAccount).ConfigureAwait(false);
|
await ApplyAccountStateAsync(resolvedAccount).ConfigureAwait(false);
|
||||||
|
await Task.WhenAll(loadAiPackTask, loadUnlimitedAccountsTask).ConfigureAwait(false);
|
||||||
await UpdateAddOnsAsync(addOns).ConfigureAwait(false);
|
|
||||||
}
|
}
|
||||||
catch (Exception)
|
catch (Exception)
|
||||||
{
|
{
|
||||||
_dialogService.InfoBarMessage(Translator.GeneralTitle_Error,
|
if (cachedAccount == null)
|
||||||
Translator.WinoAccount_Management_LoadFailed,
|
{
|
||||||
InfoBarMessageType.Error);
|
_dialogService.InfoBarMessage(Translator.GeneralTitle_Error,
|
||||||
await ResetStateAsync().ConfigureAwait(false);
|
Translator.WinoAccount_Management_LoadFailed,
|
||||||
|
InfoBarMessageType.Error);
|
||||||
|
await ResetStateAsync().ConfigureAwait(false);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
finally
|
finally
|
||||||
{
|
{
|
||||||
@@ -369,46 +347,151 @@ public partial class WinoAccountManagementPageViewModel : CoreBaseViewModel,
|
|||||||
AccountEmail = string.Empty;
|
AccountEmail = string.Empty;
|
||||||
AccountStatusText = string.Empty;
|
AccountStatusText = string.Empty;
|
||||||
IsCheckoutInProgress = false;
|
IsCheckoutInProgress = false;
|
||||||
AddOns.Clear();
|
|
||||||
PurchaseAddOnCommand.NotifyCanExecuteChanged();
|
PurchaseAddOnCommand.NotifyCanExecuteChanged();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
await ResetAddOnStatesAsync().ConfigureAwait(false);
|
||||||
}
|
}
|
||||||
|
|
||||||
private async Task UpdateAddOnsAsync(IReadOnlyList<WinoAddOnInfo> addOns)
|
private WinoAddOnItemViewModel CreateAddOnItem(WinoAddOnProductType productType)
|
||||||
{
|
{
|
||||||
var items = addOns.Select(CreateAddOnItem).ToList();
|
return new WinoAddOnItemViewModel(productType)
|
||||||
|
{
|
||||||
|
PurchaseCommand = PurchaseAddOnCommand,
|
||||||
|
UsageLimit = 1
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task ResetAddOnStatesAsync()
|
||||||
|
{
|
||||||
await ExecuteUIThread(() =>
|
await ExecuteUIThread(() =>
|
||||||
{
|
{
|
||||||
AddOns.Clear();
|
ResetAddOnItem(_aiPackAddOn);
|
||||||
|
ResetAddOnItem(_unlimitedAccountsAddOn);
|
||||||
foreach (var item in items)
|
|
||||||
{
|
|
||||||
AddOns.Add(item);
|
|
||||||
}
|
|
||||||
|
|
||||||
PurchaseAddOnCommand.NotifyCanExecuteChanged();
|
PurchaseAddOnCommand.NotifyCanExecuteChanged();
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
private WinoAddOnItemViewModel CreateAddOnItem(WinoAddOnInfo addOn)
|
private static void ResetAddOnItem(WinoAddOnItemViewModel addOn)
|
||||||
{
|
{
|
||||||
var item = new WinoAddOnItemViewModel(addOn.ProductType)
|
addOn.IsLoading = true;
|
||||||
|
addOn.IsPurchased = false;
|
||||||
|
addOn.IsPurchaseInProgress = false;
|
||||||
|
addOn.HasUsageData = false;
|
||||||
|
addOn.ErrorText = string.Empty;
|
||||||
|
addOn.UsageCount = 0;
|
||||||
|
addOn.UsageLimit = 1;
|
||||||
|
addOn.UsagePercentage = 0;
|
||||||
|
addOn.RenewalText = string.Empty;
|
||||||
|
addOn.UsageResetText = string.Empty;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static string TranslateStoreSyncError(string? errorCode)
|
||||||
|
=> errorCode switch
|
||||||
{
|
{
|
||||||
IsPurchased = addOn.IsPurchased,
|
_ => Translator.WinoAccount_Management_StoreSyncFailed
|
||||||
PurchaseCommand = PurchaseAddOnCommand,
|
|
||||||
ManageCommand = ManageAiPackCommand,
|
|
||||||
UsageCount = addOn.UsageCount ?? 0,
|
|
||||||
UsageLimit = addOn.UsageLimit is > 0 ? addOn.UsageLimit.Value : 1,
|
|
||||||
UsagePercentage = addOn.UsagePercentage
|
|
||||||
};
|
};
|
||||||
|
|
||||||
if (addOn.RenewalDateUtc is DateTimeOffset renewalDateUtc)
|
private static bool IsAccessTokenExpired(WinoAccount account)
|
||||||
{
|
=> string.IsNullOrWhiteSpace(account.AccessToken) || account.AccessTokenExpiresAtUtc <= DateTime.UtcNow;
|
||||||
item.RenewalText = string.Format(Translator.WinoAccount_Management_AiPackRenews, renewalDateUtc.LocalDateTime);
|
|
||||||
item.UsageResetText = string.Format(Translator.WinoAccount_Management_AiPackResets, renewalDateUtc.LocalDateTime);
|
|
||||||
}
|
|
||||||
|
|
||||||
return item;
|
private async Task LoadUnlimitedAccountsAddOnAsync()
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var hasUnlimitedAccounts = await _storeManagementService.HasProductAsync(WinoAddOnProductType.UNLIMITED_ACCOUNTS).ConfigureAwait(false);
|
||||||
|
await ExecuteUIThread(() =>
|
||||||
|
{
|
||||||
|
_unlimitedAccountsAddOn.IsPurchased = hasUnlimitedAccounts;
|
||||||
|
_unlimitedAccountsAddOn.ErrorText = string.Empty;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
catch (Exception)
|
||||||
|
{
|
||||||
|
await ExecuteUIThread(() =>
|
||||||
|
{
|
||||||
|
_unlimitedAccountsAddOn.ErrorText = Translator.WinoAccount_Management_AddOnLoadFailed;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
await ExecuteUIThread(() =>
|
||||||
|
{
|
||||||
|
_unlimitedAccountsAddOn.IsLoading = false;
|
||||||
|
PurchaseAddOnCommand.NotifyCanExecuteChanged();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task LoadAiPackAddOnAsync()
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var hasAiPack = await _storeManagementService.HasProductAsync(WinoAddOnProductType.AI_PACK).ConfigureAwait(false);
|
||||||
|
|
||||||
|
await ExecuteUIThread(() =>
|
||||||
|
{
|
||||||
|
_aiPackAddOn.IsPurchased = hasAiPack;
|
||||||
|
_aiPackAddOn.ErrorText = string.Empty;
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!hasAiPack)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var aiStatusResponse = await _profileService.GetAiStatusAsync().ConfigureAwait(false);
|
||||||
|
if (!aiStatusResponse.IsSuccess || aiStatusResponse.Result == null)
|
||||||
|
{
|
||||||
|
await ExecuteUIThread(() =>
|
||||||
|
{
|
||||||
|
_aiPackAddOn.HasUsageData = false;
|
||||||
|
_aiPackAddOn.ErrorText = Translator.WinoAccount_Management_AiPackUsageLoadFailed;
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var aiStatus = aiStatusResponse.Result;
|
||||||
|
if (aiStatus.MonthlyLimit is not int usageLimit || usageLimit <= 0 || aiStatus.Used is not int usageCount)
|
||||||
|
{
|
||||||
|
await ExecuteUIThread(() =>
|
||||||
|
{
|
||||||
|
_aiPackAddOn.HasUsageData = false;
|
||||||
|
_aiPackAddOn.ErrorText = Translator.WinoAccount_Management_AiPackUsageLoadFailed;
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
await ExecuteUIThread(() =>
|
||||||
|
{
|
||||||
|
_aiPackAddOn.HasUsageData = true;
|
||||||
|
_aiPackAddOn.ErrorText = string.Empty;
|
||||||
|
_aiPackAddOn.UsageCount = usageCount;
|
||||||
|
_aiPackAddOn.UsageLimit = usageLimit;
|
||||||
|
_aiPackAddOn.UsagePercentage = usageLimit > 0 ? (double)usageCount / usageLimit * 100 : 0;
|
||||||
|
_aiPackAddOn.RenewalText = aiStatus.CurrentPeriodEndUtc is DateTimeOffset renewalDateUtc
|
||||||
|
? string.Format(Translator.WinoAccount_Management_AiPackRenews, renewalDateUtc.LocalDateTime)
|
||||||
|
: string.Empty;
|
||||||
|
_aiPackAddOn.UsageResetText = aiStatus.CurrentPeriodEndUtc is DateTimeOffset resetDateUtc
|
||||||
|
? string.Format(Translator.WinoAccount_Management_AiPackResets, resetDateUtc.LocalDateTime)
|
||||||
|
: string.Empty;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
catch (Exception)
|
||||||
|
{
|
||||||
|
await ExecuteUIThread(() =>
|
||||||
|
{
|
||||||
|
_aiPackAddOn.HasUsageData = false;
|
||||||
|
_aiPackAddOn.ErrorText = Translator.WinoAccount_Management_AddOnLoadFailed;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
await ExecuteUIThread(() =>
|
||||||
|
{
|
||||||
|
_aiPackAddOn.IsLoading = false;
|
||||||
|
PurchaseAddOnCommand.NotifyCanExecuteChanged();
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1622,6 +1622,16 @@ public class GmailSynchronizer : WinoSynchronizer<IClientServiceRequest, Message
|
|||||||
// Try to handle the error with registered handlers
|
// Try to handle the error with registered handlers
|
||||||
var handled = await _gmailSynchronizerErrorHandlerFactory.HandleErrorAsync(errorContext);
|
var handled = await _gmailSynchronizerErrorHandlerFactory.HandleErrorAsync(errorContext);
|
||||||
|
|
||||||
|
if (handled)
|
||||||
|
{
|
||||||
|
if (ShouldRevertOptimisticMailStateChange(bundle?.UIChangeRequest))
|
||||||
|
{
|
||||||
|
bundle?.UIChangeRequest?.RevertUIChanges();
|
||||||
|
}
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
// If not handled by any specific handler, apply default error handling
|
// If not handled by any specific handler, apply default error handling
|
||||||
if (!handled)
|
if (!handled)
|
||||||
{
|
{
|
||||||
@@ -1649,6 +1659,12 @@ public class GmailSynchronizer : WinoSynchronizer<IClientServiceRequest, Message
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static bool ShouldRevertOptimisticMailStateChange(IUIChangeRequest request)
|
||||||
|
=> request is BatchMarkReadRequest
|
||||||
|
|| request is MarkReadRequest
|
||||||
|
|| request is BatchChangeFlagRequest
|
||||||
|
|| request is ChangeFlagRequest;
|
||||||
|
|
||||||
private bool ShouldUpdateSyncIdentifier(ulong? historyId)
|
private bool ShouldUpdateSyncIdentifier(ulong? historyId)
|
||||||
{
|
{
|
||||||
if (historyId == null) return false;
|
if (historyId == null) return false;
|
||||||
@@ -1672,7 +1688,13 @@ public class GmailSynchronizer : WinoSynchronizer<IClientServiceRequest, Message
|
|||||||
HttpResponseMessage httpResponseMessage,
|
HttpResponseMessage httpResponseMessage,
|
||||||
CancellationToken cancellationToken = default)
|
CancellationToken cancellationToken = default)
|
||||||
{
|
{
|
||||||
await ProcessGmailRequestErrorAsync(error, bundle);
|
if (error != null)
|
||||||
|
{
|
||||||
|
await ProcessGmailRequestErrorAsync(error, bundle).ConfigureAwait(false);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
await PersistSuccessfulMailStateChangesAsync(bundle).ConfigureAwait(false);
|
||||||
|
|
||||||
if (bundle is HttpRequestBundle<IClientServiceRequest, Message> messageBundle)
|
if (bundle is HttpRequestBundle<IClientServiceRequest, Message> messageBundle)
|
||||||
{
|
{
|
||||||
@@ -1735,6 +1757,34 @@ public class GmailSynchronizer : WinoSynchronizer<IClientServiceRequest, Message
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private async Task PersistSuccessfulMailStateChangesAsync(IRequestBundle<IClientServiceRequest> bundle)
|
||||||
|
{
|
||||||
|
switch (bundle.UIChangeRequest)
|
||||||
|
{
|
||||||
|
case BatchMarkReadRequest batchMarkReadRequest:
|
||||||
|
foreach (var request in batchMarkReadRequest)
|
||||||
|
{
|
||||||
|
await _gmailChangeProcessor.ChangeMailReadStatusAsync(request.Item.Id, request.IsRead).ConfigureAwait(false);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
|
||||||
|
case MarkReadRequest markReadRequest:
|
||||||
|
await _gmailChangeProcessor.ChangeMailReadStatusAsync(markReadRequest.Item.Id, markReadRequest.IsRead).ConfigureAwait(false);
|
||||||
|
break;
|
||||||
|
|
||||||
|
case BatchChangeFlagRequest batchChangeFlagRequest:
|
||||||
|
foreach (var request in batchChangeFlagRequest)
|
||||||
|
{
|
||||||
|
await _gmailChangeProcessor.ChangeFlagStatusAsync(request.Item.Id, request.IsFlagged).ConfigureAwait(false);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
|
||||||
|
case ChangeFlagRequest changeFlagRequest:
|
||||||
|
await _gmailChangeProcessor.ChangeFlagStatusAsync(changeFlagRequest.Item.Id, changeFlagRequest.IsFlagged).ConfigureAwait(false);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Gmail Archive is a special folder that is not visible in the Gmail web interface.
|
/// Gmail Archive is a special folder that is not visible in the Gmail web interface.
|
||||||
/// We need to handle it separately.
|
/// We need to handle it separately.
|
||||||
|
|||||||
@@ -145,7 +145,7 @@ public class ImapSynchronizer : WinoSynchronizer<ImapRequest, ImapMessageCreatio
|
|||||||
var remoteFolder = await client.GetFolderAsync(folder.RemoteFolderId);
|
var remoteFolder = await client.GetFolderAsync(folder.RemoteFolderId);
|
||||||
|
|
||||||
await remoteFolder.OpenAsync(FolderAccess.ReadWrite).ConfigureAwait(false);
|
await remoteFolder.OpenAsync(FolderAccess.ReadWrite).ConfigureAwait(false);
|
||||||
await remoteFolder.StoreAsync(GetUniqueId(item.Item.Id), new StoreFlagsRequest(item.Item.IsFlagged ? StoreAction.Add : StoreAction.Remove, MessageFlags.Flagged) { Silent = true }).ConfigureAwait(false);
|
await remoteFolder.StoreAsync(GetUniqueId(item.Item.Id), new StoreFlagsRequest(item.IsFlagged ? StoreAction.Add : StoreAction.Remove, MessageFlags.Flagged) { Silent = true }).ConfigureAwait(false);
|
||||||
await remoteFolder.CloseAsync().ConfigureAwait(false);
|
await remoteFolder.CloseAsync().ConfigureAwait(false);
|
||||||
}, requests);
|
}, requests);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -23,7 +23,7 @@
|
|||||||
<Identity
|
<Identity
|
||||||
Name="58272BurakKSE.WinoMailPreview"
|
Name="58272BurakKSE.WinoMailPreview"
|
||||||
Publisher="CN=51FBDAF3-E212-4149-89A2-A2636B3BC911"
|
Publisher="CN=51FBDAF3-E212-4149-89A2-A2636B3BC911"
|
||||||
Version="2.0.14.0" />
|
Version="2.0.15.0" />
|
||||||
|
|
||||||
<mp:PhoneIdentity PhoneProductId="3879fcfb-a561-4599-9103-e0c9b35a271f" PhonePublisherId="00000000-0000-0000-0000-000000000000"/>
|
<mp:PhoneIdentity PhoneProductId="3879fcfb-a561-4599-9103-e0c9b35a271f" PhonePublisherId="00000000-0000-0000-0000-000000000000"/>
|
||||||
|
|
||||||
|
|||||||
@@ -3,8 +3,9 @@ using System.Collections.Generic;
|
|||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using Windows.Services.Store;
|
using Windows.Services.Store;
|
||||||
using Wino.Core.Domain.Interfaces;
|
using Wino.Core.Domain.Interfaces;
|
||||||
using WinoStorePurchaseResult = Wino.Core.Domain.Enums.StorePurchaseResult;
|
using WinRT.Interop;
|
||||||
using WinoAddOnProductType = Wino.Core.Domain.Enums.WinoAddOnProductType;
|
using WinoAddOnProductType = Wino.Core.Domain.Enums.WinoAddOnProductType;
|
||||||
|
using WinoStorePurchaseResult = Wino.Core.Domain.Enums.StorePurchaseResult;
|
||||||
|
|
||||||
namespace Wino.Mail.WinUI.Services;
|
namespace Wino.Mail.WinUI.Services;
|
||||||
|
|
||||||
@@ -14,11 +15,13 @@ public class StoreManagementService : IStoreManagementService
|
|||||||
|
|
||||||
private readonly Dictionary<WinoAddOnProductType, string> productIds = new Dictionary<WinoAddOnProductType, string>()
|
private readonly Dictionary<WinoAddOnProductType, string> productIds = new Dictionary<WinoAddOnProductType, string>()
|
||||||
{
|
{
|
||||||
{ WinoAddOnProductType.UNLIMITED_ACCOUNTS, "UnlimitedAccounts" }
|
{ WinoAddOnProductType.UNLIMITED_ACCOUNTS, "UnlimitedAccounts" },
|
||||||
|
{ WinoAddOnProductType.AI_PACK, "AI_PACK" },
|
||||||
};
|
};
|
||||||
|
|
||||||
private readonly Dictionary<WinoAddOnProductType, string> skuIds = new Dictionary<WinoAddOnProductType, string>()
|
private readonly Dictionary<WinoAddOnProductType, string> skuIds = new Dictionary<WinoAddOnProductType, string>()
|
||||||
{
|
{
|
||||||
|
{ WinoAddOnProductType.AI_PACK, "9N2FH734RBVS" },
|
||||||
{ WinoAddOnProductType.UNLIMITED_ACCOUNTS, "9P02MXZ42GSM" }
|
{ WinoAddOnProductType.UNLIMITED_ACCOUNTS, "9P02MXZ42GSM" }
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -60,6 +63,7 @@ public class StoreManagementService : IStoreManagementService
|
|||||||
return WinoStorePurchaseResult.AlreadyPurchased;
|
return WinoStorePurchaseResult.AlreadyPurchased;
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
|
InitializeStoreContextWithWindow();
|
||||||
var result = await CurrentContext.RequestPurchaseAsync(productKey);
|
var result = await CurrentContext.RequestPurchaseAsync(productKey);
|
||||||
|
|
||||||
switch (result.Status)
|
switch (result.Status)
|
||||||
@@ -73,4 +77,39 @@ public class StoreManagementService : IStoreManagementService
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public async Task<string?> GetCustomerCollectionsIdAsync(string serviceTicket, string publisherUserId)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrWhiteSpace(serviceTicket) || string.IsNullOrWhiteSpace(publisherUserId))
|
||||||
|
{
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
InitializeStoreContextWithWindow();
|
||||||
|
var collectionsId = await CurrentContext.GetCustomerCollectionsIdAsync(serviceTicket, publisherUserId);
|
||||||
|
return string.IsNullOrWhiteSpace(collectionsId) ? null : collectionsId;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<string?> GetCustomerPurchaseIdAsync(string serviceTicket, string publisherUserId)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrWhiteSpace(serviceTicket) || string.IsNullOrWhiteSpace(publisherUserId))
|
||||||
|
{
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
InitializeStoreContextWithWindow();
|
||||||
|
var purchaseId = await CurrentContext.GetCustomerPurchaseIdAsync(serviceTicket, publisherUserId);
|
||||||
|
return string.IsNullOrWhiteSpace(purchaseId) ? null : purchaseId;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void InitializeStoreContextWithWindow()
|
||||||
|
{
|
||||||
|
var mainWindow = WinoApplication.MainWindow;
|
||||||
|
if (mainWindow == null)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
InitializeWithWindow.Initialize(CurrentContext, WindowNative.GetWindowHandle(mainWindow));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -67,7 +67,7 @@
|
|||||||
<DataTemplate x:Key="SettingsShellWinoAccountItemTemplate" x:DataType="menu:SettingsShellPageMenuItem">
|
<DataTemplate x:Key="SettingsShellWinoAccountItemTemplate" x:DataType="menu:SettingsShellPageMenuItem">
|
||||||
<coreControls:WinoNavigationViewItem Content="{x:Bind Title}" DataContext="{x:Bind}">
|
<coreControls:WinoNavigationViewItem Content="{x:Bind Title}" DataContext="{x:Bind}">
|
||||||
<muxc:NavigationViewItem.Icon>
|
<muxc:NavigationViewItem.Icon>
|
||||||
<BitmapIcon UriSource="/Assets/Wino_Icon.ico" />
|
<BitmapIcon ShowAsMonochrome="False" UriSource="/Assets/Wino_Icon.ico" />
|
||||||
<!--<FontIcon FontFamily="{StaticResource SymbolThemeFontFamily}" Glyph="{x:Bind Glyph}" />-->
|
<!--<FontIcon FontFamily="{StaticResource SymbolThemeFontFamily}" Glyph="{x:Bind Glyph}" />-->
|
||||||
</muxc:NavigationViewItem.Icon>
|
</muxc:NavigationViewItem.Icon>
|
||||||
</coreControls:WinoNavigationViewItem>
|
</coreControls:WinoNavigationViewItem>
|
||||||
|
|||||||
@@ -32,6 +32,26 @@
|
|||||||
Style="{StaticResource CaptionTextBlockStyle}"
|
Style="{StaticResource CaptionTextBlockStyle}"
|
||||||
Text="{x:Bind domain:Translator.GetTranslatedString(KeywordsKey)}"
|
Text="{x:Bind domain:Translator.GetTranslatedString(KeywordsKey)}"
|
||||||
TextWrapping="WrapWholeWords" />
|
TextWrapping="WrapWholeWords" />
|
||||||
|
<StackPanel
|
||||||
|
Orientation="Horizontal"
|
||||||
|
Spacing="8"
|
||||||
|
Visibility="{x:Bind ShowLoadingState, Mode=OneWay}">
|
||||||
|
<ProgressRing
|
||||||
|
Width="18"
|
||||||
|
Height="18"
|
||||||
|
IsActive="{x:Bind ShowLoadingState, Mode=OneWay}" />
|
||||||
|
<TextBlock
|
||||||
|
VerticalAlignment="Center"
|
||||||
|
Foreground="{ThemeResource TextFillColorSecondaryBrush}"
|
||||||
|
Style="{StaticResource CaptionTextBlockStyle}"
|
||||||
|
Text="{x:Bind domain:Translator.Busy}" />
|
||||||
|
</StackPanel>
|
||||||
|
<TextBlock
|
||||||
|
Foreground="{ThemeResource SystemFillColorCautionBrush}"
|
||||||
|
Style="{StaticResource CaptionTextBlockStyle}"
|
||||||
|
Text="{x:Bind ErrorText, Mode=OneWay}"
|
||||||
|
TextWrapping="WrapWholeWords"
|
||||||
|
Visibility="{x:Bind ShowErrorState, Mode=OneWay}" />
|
||||||
<StackPanel
|
<StackPanel
|
||||||
Margin="0,4,0,0"
|
Margin="0,4,0,0"
|
||||||
Orientation="Horizontal"
|
Orientation="Horizontal"
|
||||||
@@ -40,7 +60,8 @@
|
|||||||
Command="{x:Bind PurchaseCommand}"
|
Command="{x:Bind PurchaseCommand}"
|
||||||
CommandParameter="{x:Bind}"
|
CommandParameter="{x:Bind}"
|
||||||
Content="{x:Bind domain:Translator.Buttons_Purchase}"
|
Content="{x:Bind domain:Translator.Buttons_Purchase}"
|
||||||
Style="{StaticResource AccentButtonStyle}" />
|
Style="{StaticResource AccentButtonStyle}"
|
||||||
|
Visibility="{x:Bind ShowPurchaseState, Mode=OneWay}" />
|
||||||
<ProgressRing
|
<ProgressRing
|
||||||
Width="18"
|
Width="18"
|
||||||
Height="18"
|
Height="18"
|
||||||
@@ -88,41 +109,65 @@
|
|||||||
Text="{x:Bind RenewalText, Mode=OneWay}" />
|
Text="{x:Bind RenewalText, Mode=OneWay}" />
|
||||||
</StackPanel>
|
</StackPanel>
|
||||||
</controls:SettingsExpander.Description>
|
</controls:SettingsExpander.Description>
|
||||||
<HyperlinkButton Command="{x:Bind ManageCommand}" Content="{x:Bind domain:Translator.Buttons_Manage}" />
|
|
||||||
<controls:SettingsExpander.Items>
|
<controls:SettingsExpander.Items>
|
||||||
<controls:SettingsCard HorizontalContentAlignment="Stretch">
|
<controls:SettingsCard HorizontalContentAlignment="Stretch">
|
||||||
<controls:SettingsCard.Header>
|
<controls:SettingsCard.Header>
|
||||||
<StackPanel MinWidth="400" Spacing="8">
|
<StackPanel Spacing="8">
|
||||||
<StackPanel Orientation="Horizontal" Spacing="4">
|
<StackPanel
|
||||||
<TextBlock
|
MinWidth="400"
|
||||||
FontSize="24"
|
Spacing="8"
|
||||||
FontWeight="Bold"
|
Visibility="{x:Bind ShowUsageSummary, Mode=OneWay}">
|
||||||
Text="{x:Bind UsageCount, Mode=OneWay}" />
|
<StackPanel Orientation="Horizontal" Spacing="4">
|
||||||
<TextBlock
|
<TextBlock
|
||||||
Margin="0,0,0,2"
|
FontSize="24"
|
||||||
VerticalAlignment="Bottom"
|
FontWeight="Bold"
|
||||||
Foreground="{ThemeResource TextFillColorSecondaryBrush}"
|
Text="{x:Bind UsageCount, Mode=OneWay}" />
|
||||||
Style="{StaticResource CaptionTextBlockStyle}">
|
<TextBlock
|
||||||
<Run Text="/ " />
|
Margin="0,0,0,2"
|
||||||
<Run Text="{x:Bind UsageLimit, Mode=OneWay}" />
|
VerticalAlignment="Bottom"
|
||||||
</TextBlock>
|
Foreground="{ThemeResource TextFillColorSecondaryBrush}"
|
||||||
|
Style="{StaticResource CaptionTextBlockStyle}">
|
||||||
|
<Run Text="/ " />
|
||||||
|
<Run Text="{x:Bind UsageLimit, Mode=OneWay}" />
|
||||||
|
</TextBlock>
|
||||||
|
</StackPanel>
|
||||||
|
<ProgressBar
|
||||||
|
Height="8"
|
||||||
|
Maximum="100"
|
||||||
|
Value="{x:Bind UsagePercentage, Mode=OneWay}" />
|
||||||
|
<Grid>
|
||||||
|
<TextBlock
|
||||||
|
HorizontalAlignment="Left"
|
||||||
|
Foreground="{ThemeResource TextFillColorSecondaryBrush}"
|
||||||
|
Style="{StaticResource CaptionTextBlockStyle}"
|
||||||
|
Text="{x:Bind domain:Translator.WinoAccount_Management_AiPackRequestsUsed}" />
|
||||||
|
<TextBlock
|
||||||
|
HorizontalAlignment="Right"
|
||||||
|
Foreground="{ThemeResource TextFillColorSecondaryBrush}"
|
||||||
|
Style="{StaticResource CaptionTextBlockStyle}"
|
||||||
|
Text="{x:Bind UsageResetText, Mode=OneWay}" />
|
||||||
|
</Grid>
|
||||||
</StackPanel>
|
</StackPanel>
|
||||||
<ProgressBar
|
<StackPanel
|
||||||
Height="8"
|
Orientation="Horizontal"
|
||||||
Maximum="100"
|
Spacing="8"
|
||||||
Value="{x:Bind UsagePercentage, Mode=OneWay}" />
|
Visibility="{x:Bind ShowLoadingState, Mode=OneWay}">
|
||||||
<Grid>
|
<ProgressRing
|
||||||
|
Width="18"
|
||||||
|
Height="18"
|
||||||
|
IsActive="{x:Bind ShowLoadingState, Mode=OneWay}" />
|
||||||
<TextBlock
|
<TextBlock
|
||||||
HorizontalAlignment="Left"
|
VerticalAlignment="Center"
|
||||||
Foreground="{ThemeResource TextFillColorSecondaryBrush}"
|
Foreground="{ThemeResource TextFillColorSecondaryBrush}"
|
||||||
Style="{StaticResource CaptionTextBlockStyle}"
|
Style="{StaticResource CaptionTextBlockStyle}"
|
||||||
Text="{x:Bind domain:Translator.WinoAccount_Management_AiPackRequestsUsed}" />
|
Text="{x:Bind domain:Translator.Busy}" />
|
||||||
<TextBlock
|
</StackPanel>
|
||||||
HorizontalAlignment="Right"
|
<TextBlock
|
||||||
Foreground="{ThemeResource TextFillColorSecondaryBrush}"
|
Foreground="{ThemeResource SystemFillColorCautionBrush}"
|
||||||
Style="{StaticResource CaptionTextBlockStyle}"
|
Style="{StaticResource CaptionTextBlockStyle}"
|
||||||
Text="{x:Bind UsageResetText, Mode=OneWay}" />
|
Text="{x:Bind ErrorText, Mode=OneWay}"
|
||||||
</Grid>
|
TextWrapping="WrapWholeWords"
|
||||||
|
Visibility="{x:Bind ShowErrorState, Mode=OneWay}" />
|
||||||
</StackPanel>
|
</StackPanel>
|
||||||
</controls:SettingsCard.Header>
|
</controls:SettingsCard.Header>
|
||||||
</controls:SettingsCard>
|
</controls:SettingsCard>
|
||||||
@@ -148,11 +193,32 @@
|
|||||||
Style="{StaticResource CaptionTextBlockStyle}"
|
Style="{StaticResource CaptionTextBlockStyle}"
|
||||||
Text="{x:Bind domain:Translator.GetTranslatedString(KeywordsKey)}"
|
Text="{x:Bind domain:Translator.GetTranslatedString(KeywordsKey)}"
|
||||||
TextWrapping="WrapWholeWords" />
|
TextWrapping="WrapWholeWords" />
|
||||||
|
<StackPanel
|
||||||
|
Orientation="Horizontal"
|
||||||
|
Spacing="8"
|
||||||
|
Visibility="{x:Bind ShowLoadingState, Mode=OneWay}">
|
||||||
|
<ProgressRing
|
||||||
|
Width="18"
|
||||||
|
Height="18"
|
||||||
|
IsActive="{x:Bind ShowLoadingState, Mode=OneWay}" />
|
||||||
|
<TextBlock
|
||||||
|
VerticalAlignment="Center"
|
||||||
|
Foreground="{ThemeResource TextFillColorSecondaryBrush}"
|
||||||
|
Style="{StaticResource CaptionTextBlockStyle}"
|
||||||
|
Text="{x:Bind domain:Translator.Busy}" />
|
||||||
|
</StackPanel>
|
||||||
|
<TextBlock
|
||||||
|
Foreground="{ThemeResource SystemFillColorCautionBrush}"
|
||||||
|
Style="{StaticResource CaptionTextBlockStyle}"
|
||||||
|
Text="{x:Bind ErrorText, Mode=OneWay}"
|
||||||
|
TextWrapping="WrapWholeWords"
|
||||||
|
Visibility="{x:Bind ShowErrorState, Mode=OneWay}" />
|
||||||
<Border
|
<Border
|
||||||
Padding="12,4"
|
Padding="12,4"
|
||||||
HorizontalAlignment="Left"
|
HorizontalAlignment="Left"
|
||||||
Background="{ThemeResource SystemFillColorSuccessBackgroundBrush}"
|
Background="{ThemeResource SystemFillColorSuccessBackgroundBrush}"
|
||||||
CornerRadius="12">
|
CornerRadius="12"
|
||||||
|
Visibility="{x:Bind ShowPurchaseState, Mode=OneWay}">
|
||||||
<TextBlock
|
<TextBlock
|
||||||
Foreground="{ThemeResource SystemFillColorSuccessBrush}"
|
Foreground="{ThemeResource SystemFillColorSuccessBrush}"
|
||||||
Style="{StaticResource CaptionTextBlockStyle}"
|
Style="{StaticResource CaptionTextBlockStyle}"
|
||||||
@@ -173,18 +239,6 @@
|
|||||||
<ScrollViewer>
|
<ScrollViewer>
|
||||||
<StackPanel Spacing="{StaticResource SettingsCardSpacing}">
|
<StackPanel Spacing="{StaticResource SettingsCardSpacing}">
|
||||||
|
|
||||||
<StackPanel
|
|
||||||
x:Name="BusyPanel"
|
|
||||||
HorizontalAlignment="Center"
|
|
||||||
x:Load="{x:Bind ViewModel.IsBusy, Mode=OneWay}"
|
|
||||||
Spacing="8">
|
|
||||||
<ProgressRing
|
|
||||||
Width="32"
|
|
||||||
Height="32"
|
|
||||||
IsActive="True" />
|
|
||||||
<TextBlock HorizontalAlignment="Center" Text="{x:Bind domain:Translator.Busy}" />
|
|
||||||
</StackPanel>
|
|
||||||
|
|
||||||
<StackPanel
|
<StackPanel
|
||||||
x:Name="SignedOutPanel"
|
x:Name="SignedOutPanel"
|
||||||
HorizontalAlignment="Stretch"
|
HorizontalAlignment="Stretch"
|
||||||
@@ -260,15 +314,52 @@
|
|||||||
VerticalAlignment="Center"
|
VerticalAlignment="Center"
|
||||||
Data="F1 M 3.75 5 L 3.75 4.902344 C 3.75 4.225262 3.885091 3.588867 4.155273 2.993164 C 4.425456 2.397461 4.790039 1.878256 5.249023 1.435547 C 5.708008 0.99284 6.238606 0.642904 6.84082 0.385742 C 7.443034 0.128582 8.079427 0 8.75 0 C 9.440104 0 10.089518 0.130209 10.698242 0.390625 C 11.306966 0.651043 11.837564 1.007488 12.290039 1.459961 C 12.742513 1.912436 13.098958 2.443035 13.359375 3.051758 C 13.619791 3.660482 13.75 4.309896 13.75 5 C 13.75 5.690104 13.619791 6.339519 13.359375 6.948242 C 13.098958 7.556967 12.742513 8.087565 12.290039 8.540039 C 11.837564 8.992514 11.306966 9.348959 10.698242 9.609375 C 10.089518 9.869792 9.440104 10 8.75 10 C 8.059896 10 7.410481 9.869792 6.801758 9.609375 C 6.193034 9.348959 5.662435 8.992514 5.209961 8.540039 C 4.757487 8.087565 4.401042 7.556967 4.140625 6.948242 C 3.880208 6.339519 3.75 5.690104 3.75 5 Z M 5 5 L 5 5.078125 C 5 5.585938 5.100911 6.062826 5.302734 6.508789 C 5.504557 6.954753 5.777995 7.34375 6.123047 7.675781 C 6.468099 8.007812 6.866862 8.269857 7.319336 8.461914 C 7.77181 8.653972 8.248697 8.75 8.75 8.75 C 9.270833 8.75 9.759114 8.652344 10.214844 8.457031 C 10.670572 8.261719 11.067708 7.994792 11.40625 7.65625 C 11.744791 7.317709 12.011719 6.920573 12.207031 6.464844 C 12.402344 6.009115 12.5 5.520834 12.5 5 C 12.5 4.479167 12.402344 3.990887 12.207031 3.535156 C 12.011719 3.079428 11.744791 2.682293 11.40625 2.34375 C 11.067708 2.005209 10.670572 1.738281 10.214844 1.542969 C 9.759114 1.347656 9.270833 1.25 8.75 1.25 C 8.229166 1.25 7.740885 1.347656 7.285156 1.542969 C 6.829427 1.738281 6.432292 2.005209 6.09375 2.34375 C 5.755208 2.682293 5.488281 3.079428 5.292969 3.535156 C 5.097656 3.990887 5 4.479167 5 5 Z M 20 14.375 L 20 18.125 C 20 18.385416 19.951172 18.629557 19.853516 18.857422 C 19.755859 19.085287 19.622395 19.283854 19.453125 19.453125 C 19.283854 19.622396 19.085285 19.755859 18.857422 19.853516 C 18.629557 19.951172 18.385416 20 18.125 20 L 11.875 20 C 11.614583 20 11.370442 19.951172 11.142578 19.853516 C 10.914713 19.755859 10.716146 19.622396 10.546875 19.453125 C 10.377604 19.283854 10.244141 19.085287 10.146484 18.857422 C 10.048828 18.629557 10 18.385416 10 18.125 L 10 14.375 C 10 14.114584 10.048828 13.870443 10.146484 13.642578 C 10.244141 13.414714 10.377604 13.216146 10.546875 13.046875 C 10.716146 12.877604 10.914713 12.744141 11.142578 12.646484 C 11.370442 12.548828 11.614583 12.5 11.875 12.5 L 12.5 12.5 L 12.5 11.25 C 12.5 11.080729 12.532552 10.921225 12.597656 10.771484 C 12.66276 10.621745 12.753906 10.488281 12.871094 10.371094 C 13.118488 10.123698 13.411457 10 13.75 10 L 16.25 10 C 16.41927 10 16.578775 10.032553 16.728516 10.097656 C 16.878254 10.162761 17.011719 10.253906 17.128906 10.371094 C 17.376301 10.61849 17.5 10.911459 17.5 11.25 L 17.5 12.5 L 18.125 12.5 C 18.385416 12.5 18.629557 12.548828 18.857422 12.646484 C 19.085285 12.744141 19.283854 12.877604 19.453125 13.046875 C 19.622395 13.216146 19.755859 13.414714 19.853516 13.642578 C 19.951172 13.870443 20 14.114584 20 14.375 Z M 11.25 11.25 C 10.800781 11.25 10.382486 11.360678 9.995117 11.582031 C 9.607747 11.803386 9.303385 12.109375 9.082031 12.5 L 2.5 12.5 C 2.324219 12.5 2.159831 12.532553 2.006836 12.597656 C 1.853841 12.662761 1.722005 12.750651 1.611328 12.861328 C 1.500651 12.972006 1.41276 13.103842 1.347656 13.256836 C 1.282552 13.409831 1.25 13.574219 1.25 13.75 C 1.25 14.420573 1.360677 15.008139 1.582031 15.512695 C 1.803385 16.017252 2.102865 16.455078 2.480469 16.826172 C 2.858073 17.197266 3.297526 17.50651 3.798828 17.753906 C 4.30013 18.001303 4.827474 18.198242 5.380859 18.344727 C 5.934244 18.491211 6.50065 18.595377 7.080078 18.657227 C 7.659505 18.719076 8.216146 18.75 8.75 18.75 C 8.75 19.205729 8.860677 19.622396 9.082031 20 L 8.75 20 C 8.190104 20 7.618814 19.973959 7.036133 19.921875 C 6.45345 19.869791 5.878906 19.775391 5.3125 19.638672 C 4.746094 19.501953 4.197591 19.319662 3.666992 19.091797 C 3.136393 18.863932 2.646484 18.574219 2.197266 18.222656 C 1.474609 17.66276 0.927734 17.005209 0.556641 16.25 C 0.185547 15.494792 0 14.661458 0 13.75 C 0 13.404948 0.065104 13.081055 0.195312 12.77832 C 0.325521 12.475586 0.504557 12.210287 0.732422 11.982422 C 0.960286 11.754558 1.225586 11.575521 1.52832 11.445312 C 1.831055 11.315104 2.154948 11.25 2.5 11.25 Z M 16.25 11.25 L 13.75 11.25 L 13.75 12.5 L 16.25 12.5 Z " />
|
Data="F1 M 3.75 5 L 3.75 4.902344 C 3.75 4.225262 3.885091 3.588867 4.155273 2.993164 C 4.425456 2.397461 4.790039 1.878256 5.249023 1.435547 C 5.708008 0.99284 6.238606 0.642904 6.84082 0.385742 C 7.443034 0.128582 8.079427 0 8.75 0 C 9.440104 0 10.089518 0.130209 10.698242 0.390625 C 11.306966 0.651043 11.837564 1.007488 12.290039 1.459961 C 12.742513 1.912436 13.098958 2.443035 13.359375 3.051758 C 13.619791 3.660482 13.75 4.309896 13.75 5 C 13.75 5.690104 13.619791 6.339519 13.359375 6.948242 C 13.098958 7.556967 12.742513 8.087565 12.290039 8.540039 C 11.837564 8.992514 11.306966 9.348959 10.698242 9.609375 C 10.089518 9.869792 9.440104 10 8.75 10 C 8.059896 10 7.410481 9.869792 6.801758 9.609375 C 6.193034 9.348959 5.662435 8.992514 5.209961 8.540039 C 4.757487 8.087565 4.401042 7.556967 4.140625 6.948242 C 3.880208 6.339519 3.75 5.690104 3.75 5 Z M 5 5 L 5 5.078125 C 5 5.585938 5.100911 6.062826 5.302734 6.508789 C 5.504557 6.954753 5.777995 7.34375 6.123047 7.675781 C 6.468099 8.007812 6.866862 8.269857 7.319336 8.461914 C 7.77181 8.653972 8.248697 8.75 8.75 8.75 C 9.270833 8.75 9.759114 8.652344 10.214844 8.457031 C 10.670572 8.261719 11.067708 7.994792 11.40625 7.65625 C 11.744791 7.317709 12.011719 6.920573 12.207031 6.464844 C 12.402344 6.009115 12.5 5.520834 12.5 5 C 12.5 4.479167 12.402344 3.990887 12.207031 3.535156 C 12.011719 3.079428 11.744791 2.682293 11.40625 2.34375 C 11.067708 2.005209 10.670572 1.738281 10.214844 1.542969 C 9.759114 1.347656 9.270833 1.25 8.75 1.25 C 8.229166 1.25 7.740885 1.347656 7.285156 1.542969 C 6.829427 1.738281 6.432292 2.005209 6.09375 2.34375 C 5.755208 2.682293 5.488281 3.079428 5.292969 3.535156 C 5.097656 3.990887 5 4.479167 5 5 Z M 20 14.375 L 20 18.125 C 20 18.385416 19.951172 18.629557 19.853516 18.857422 C 19.755859 19.085287 19.622395 19.283854 19.453125 19.453125 C 19.283854 19.622396 19.085285 19.755859 18.857422 19.853516 C 18.629557 19.951172 18.385416 20 18.125 20 L 11.875 20 C 11.614583 20 11.370442 19.951172 11.142578 19.853516 C 10.914713 19.755859 10.716146 19.622396 10.546875 19.453125 C 10.377604 19.283854 10.244141 19.085287 10.146484 18.857422 C 10.048828 18.629557 10 18.385416 10 18.125 L 10 14.375 C 10 14.114584 10.048828 13.870443 10.146484 13.642578 C 10.244141 13.414714 10.377604 13.216146 10.546875 13.046875 C 10.716146 12.877604 10.914713 12.744141 11.142578 12.646484 C 11.370442 12.548828 11.614583 12.5 11.875 12.5 L 12.5 12.5 L 12.5 11.25 C 12.5 11.080729 12.532552 10.921225 12.597656 10.771484 C 12.66276 10.621745 12.753906 10.488281 12.871094 10.371094 C 13.118488 10.123698 13.411457 10 13.75 10 L 16.25 10 C 16.41927 10 16.578775 10.032553 16.728516 10.097656 C 16.878254 10.162761 17.011719 10.253906 17.128906 10.371094 C 17.376301 10.61849 17.5 10.911459 17.5 11.25 L 17.5 12.5 L 18.125 12.5 C 18.385416 12.5 18.629557 12.548828 18.857422 12.646484 C 19.085285 12.744141 19.283854 12.877604 19.453125 13.046875 C 19.622395 13.216146 19.755859 13.414714 19.853516 13.642578 C 19.951172 13.870443 20 14.114584 20 14.375 Z M 11.25 11.25 C 10.800781 11.25 10.382486 11.360678 9.995117 11.582031 C 9.607747 11.803386 9.303385 12.109375 9.082031 12.5 L 2.5 12.5 C 2.324219 12.5 2.159831 12.532553 2.006836 12.597656 C 1.853841 12.662761 1.722005 12.750651 1.611328 12.861328 C 1.500651 12.972006 1.41276 13.103842 1.347656 13.256836 C 1.282552 13.409831 1.25 13.574219 1.25 13.75 C 1.25 14.420573 1.360677 15.008139 1.582031 15.512695 C 1.803385 16.017252 2.102865 16.455078 2.480469 16.826172 C 2.858073 17.197266 3.297526 17.50651 3.798828 17.753906 C 4.30013 18.001303 4.827474 18.198242 5.380859 18.344727 C 5.934244 18.491211 6.50065 18.595377 7.080078 18.657227 C 7.659505 18.719076 8.216146 18.75 8.75 18.75 C 8.75 19.205729 8.860677 19.622396 9.082031 20 L 8.75 20 C 8.190104 20 7.618814 19.973959 7.036133 19.921875 C 6.45345 19.869791 5.878906 19.775391 5.3125 19.638672 C 4.746094 19.501953 4.197591 19.319662 3.666992 19.091797 C 3.136393 18.863932 2.646484 18.574219 2.197266 18.222656 C 1.474609 17.66276 0.927734 17.005209 0.556641 16.25 C 0.185547 15.494792 0 14.661458 0 13.75 C 0 13.404948 0.065104 13.081055 0.195312 12.77832 C 0.325521 12.475586 0.504557 12.210287 0.732422 11.982422 C 0.960286 11.754558 1.225586 11.575521 1.52832 11.445312 C 1.831055 11.315104 2.154948 11.25 2.5 11.25 Z M 16.25 11.25 L 13.75 11.25 L 13.75 12.5 L 16.25 12.5 Z " />
|
||||||
</controls:SettingsCard.HeaderIcon>
|
</controls:SettingsCard.HeaderIcon>
|
||||||
<Border
|
<StackPanel Orientation="Horizontal" Spacing="8">
|
||||||
Padding="12,4"
|
<Border
|
||||||
Background="{ThemeResource SystemFillColorSuccessBackgroundBrush}"
|
Padding="12,4"
|
||||||
CornerRadius="12">
|
Background="{ThemeResource SystemFillColorSuccessBackgroundBrush}"
|
||||||
<TextBlock
|
CornerRadius="12">
|
||||||
Foreground="{ThemeResource SystemFillColorSuccessBrush}"
|
<TextBlock
|
||||||
Style="{StaticResource CaptionTextBlockStyle}"
|
Foreground="{ThemeResource SystemFillColorSuccessBrush}"
|
||||||
Text="{x:Bind ViewModel.AccountStatusText, Mode=OneWay}" />
|
Style="{StaticResource CaptionTextBlockStyle}"
|
||||||
</Border>
|
Text="{x:Bind ViewModel.AccountStatusText, Mode=OneWay}" />
|
||||||
|
</Border>
|
||||||
|
<StackPanel
|
||||||
|
Orientation="Horizontal"
|
||||||
|
Spacing="6"
|
||||||
|
Visibility="{x:Bind ViewModel.IsBusy, Mode=OneWay}">
|
||||||
|
<ProgressRing
|
||||||
|
Width="16"
|
||||||
|
Height="16"
|
||||||
|
VerticalAlignment="Center"
|
||||||
|
IsActive="{x:Bind ViewModel.IsBusy, Mode=OneWay}" />
|
||||||
|
<TextBlock
|
||||||
|
VerticalAlignment="Center"
|
||||||
|
Foreground="{ThemeResource TextFillColorSecondaryBrush}"
|
||||||
|
Style="{StaticResource CaptionTextBlockStyle}"
|
||||||
|
Text="{x:Bind domain:Translator.Busy}" />
|
||||||
|
</StackPanel>
|
||||||
|
</StackPanel>
|
||||||
|
</controls:SettingsCard>
|
||||||
|
|
||||||
|
<controls:SettingsCard
|
||||||
|
Command="{x:Bind ViewModel.ChangePasswordCommand}"
|
||||||
|
Description="{x:Bind domain:Translator.WinoAccount_ChangePassword_Description}"
|
||||||
|
Header="{x:Bind domain:Translator.WinoAccount_ChangePassword_Title}"
|
||||||
|
IsClickEnabled="True">
|
||||||
|
<controls:SettingsCard.HeaderIcon>
|
||||||
|
<FontIcon FontFamily="{StaticResource SymbolThemeFontFamily}" Glyph="" />
|
||||||
|
</controls:SettingsCard.HeaderIcon>
|
||||||
|
</controls:SettingsCard>
|
||||||
|
|
||||||
|
<controls:SettingsCard
|
||||||
|
Command="{x:Bind ViewModel.SignOutCommand}"
|
||||||
|
Description="{x:Bind domain:Translator.WinoAccount_Management_SignOutDescription}"
|
||||||
|
Header="{x:Bind domain:Translator.WinoAccount_Management_SignOutTitle}"
|
||||||
|
IsClickEnabled="True">
|
||||||
|
<controls:SettingsCard.HeaderIcon>
|
||||||
|
<FontIcon FontFamily="{StaticResource SymbolThemeFontFamily}" Glyph="" />
|
||||||
|
</controls:SettingsCard.HeaderIcon>
|
||||||
</controls:SettingsCard>
|
</controls:SettingsCard>
|
||||||
|
|
||||||
<TextBlock
|
<TextBlock
|
||||||
@@ -301,30 +392,6 @@
|
|||||||
</StackPanel>
|
</StackPanel>
|
||||||
</controls:SettingsCard>
|
</controls:SettingsCard>
|
||||||
|
|
||||||
<TextBlock
|
|
||||||
Margin="0,12,0,4"
|
|
||||||
Style="{StaticResource BodyStrongTextBlockStyle}"
|
|
||||||
Text="{x:Bind domain:Translator.WinoAccount_Management_AccountActionsSectionHeader}" />
|
|
||||||
|
|
||||||
<controls:SettingsCard
|
|
||||||
Command="{x:Bind ViewModel.SignOutCommand}"
|
|
||||||
Description="{x:Bind domain:Translator.WinoAccount_Management_SignOutDescription}"
|
|
||||||
Header="{x:Bind domain:Translator.WinoAccount_Management_SignOutTitle}"
|
|
||||||
IsClickEnabled="True">
|
|
||||||
<controls:SettingsCard.HeaderIcon>
|
|
||||||
<FontIcon FontFamily="{StaticResource SymbolThemeFontFamily}" Glyph="" />
|
|
||||||
</controls:SettingsCard.HeaderIcon>
|
|
||||||
</controls:SettingsCard>
|
|
||||||
|
|
||||||
<controls:SettingsCard
|
|
||||||
Command="{x:Bind ViewModel.ChangePasswordCommand}"
|
|
||||||
Description="{x:Bind domain:Translator.WinoAccount_ChangePassword_Description}"
|
|
||||||
Header="{x:Bind domain:Translator.WinoAccount_ChangePassword_Title}"
|
|
||||||
IsClickEnabled="True">
|
|
||||||
<!--<controls:SettingsCard.HeaderIcon>
|
|
||||||
<SymbolIcon Symbol="Permission" />
|
|
||||||
</controls:SettingsCard.HeaderIcon>-->
|
|
||||||
</controls:SettingsCard>
|
|
||||||
</StackPanel>
|
</StackPanel>
|
||||||
</StackPanel>
|
</StackPanel>
|
||||||
</ScrollViewer>
|
</ScrollViewer>
|
||||||
|
|||||||
@@ -68,8 +68,7 @@ public class DatabaseService : IDatabaseService
|
|||||||
Connection.CreateTableAsync<CalendarAttachment>(),
|
Connection.CreateTableAsync<CalendarAttachment>(),
|
||||||
Connection.CreateTableAsync<Reminder>(),
|
Connection.CreateTableAsync<Reminder>(),
|
||||||
Connection.CreateTableAsync<MailInvitationCalendarMapping>(),
|
Connection.CreateTableAsync<MailInvitationCalendarMapping>(),
|
||||||
Connection.CreateTableAsync<WinoAccount>(),
|
Connection.CreateTableAsync<WinoAccount>());
|
||||||
Connection.CreateTableAsync<WinoAccountAddOnCache>());
|
|
||||||
|
|
||||||
await EnsureSchemaUpgradesAsync().ConfigureAwait(false);
|
await EnsureSchemaUpgradesAsync().ConfigureAwait(false);
|
||||||
await EnsureIndexesAsync().ConfigureAwait(false);
|
await EnsureIndexesAsync().ConfigureAwait(false);
|
||||||
@@ -142,6 +141,8 @@ public class DatabaseService : IDatabaseService
|
|||||||
.ExecuteAsync($"ALTER TABLE {nameof(AccountContact)} ADD COLUMN {nameof(AccountContact.ContactPictureFileId)} TEXT NULL")
|
.ExecuteAsync($"ALTER TABLE {nameof(AccountContact)} ADD COLUMN {nameof(AccountContact.ContactPictureFileId)} TEXT NULL")
|
||||||
.ConfigureAwait(false);
|
.ConfigureAwait(false);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
await Connection.ExecuteAsync("DROP TABLE IF EXISTS WinoAccountAddOnCache").ConfigureAwait(false);
|
||||||
}
|
}
|
||||||
|
|
||||||
private async Task EnsureKeyboardShortcutSchemaAsync()
|
private async Task EnsureKeyboardShortcutSchemaAsync()
|
||||||
@@ -208,7 +209,6 @@ SET {nameof(KeyboardShortcut.Action)} =
|
|||||||
await Connection.ExecuteAsync("CREATE UNIQUE INDEX IF NOT EXISTS IX_MailAccountPreferences_AccountId ON MailAccountPreferences(AccountId)").ConfigureAwait(false);
|
await Connection.ExecuteAsync("CREATE UNIQUE INDEX IF NOT EXISTS IX_MailAccountPreferences_AccountId ON MailAccountPreferences(AccountId)").ConfigureAwait(false);
|
||||||
await Connection.ExecuteAsync("CREATE UNIQUE INDEX IF NOT EXISTS IX_CustomServerInformation_AccountId ON CustomServerInformation(AccountId)").ConfigureAwait(false);
|
await Connection.ExecuteAsync("CREATE UNIQUE INDEX IF NOT EXISTS IX_CustomServerInformation_AccountId ON CustomServerInformation(AccountId)").ConfigureAwait(false);
|
||||||
await Connection.ExecuteAsync("CREATE UNIQUE INDEX IF NOT EXISTS IX_WinoAccount_Email ON WinoAccount(Email)").ConfigureAwait(false);
|
await Connection.ExecuteAsync("CREATE UNIQUE INDEX IF NOT EXISTS IX_WinoAccount_Email ON WinoAccount(Email)").ConfigureAwait(false);
|
||||||
await Connection.ExecuteAsync("CREATE UNIQUE INDEX IF NOT EXISTS IX_WinoAccountAddOnCache_AccountId ON WinoAccountAddOnCache(AccountId)").ConfigureAwait(false);
|
|
||||||
|
|
||||||
// Calendar indexes
|
// Calendar indexes
|
||||||
await Connection.ExecuteAsync("CREATE INDEX IF NOT EXISTS IX_AccountCalendar_AccountId ON AccountCalendar(AccountId)").ConfigureAwait(false);
|
await Connection.ExecuteAsync("CREATE INDEX IF NOT EXISTS IX_AccountCalendar_AccountId ON AccountCalendar(AccountId)").ConfigureAwait(false);
|
||||||
|
|||||||
@@ -29,7 +29,6 @@ public static class ServicesContainerSetup
|
|||||||
services.AddTransient<IKeyboardShortcutService, KeyboardShortcutService>();
|
services.AddTransient<IKeyboardShortcutService, KeyboardShortcutService>();
|
||||||
services.AddSingleton<IWinoAccountApiClient, WinoAccountApiClient>();
|
services.AddSingleton<IWinoAccountApiClient, WinoAccountApiClient>();
|
||||||
services.AddSingleton<IWinoAccountProfileService, WinoAccountProfileService>();
|
services.AddSingleton<IWinoAccountProfileService, WinoAccountProfileService>();
|
||||||
services.AddTransient<IWinoAddOnService, WinoAddOnService>();
|
|
||||||
services.AddSingleton<IContactPictureFileService, ContactPictureFileService>();
|
services.AddSingleton<IContactPictureFileService, ContactPictureFileService>();
|
||||||
|
|
||||||
services.AddTransient<ICalDavClient, CalDavClient>();
|
services.AddTransient<ICalDavClient, CalDavClient>();
|
||||||
|
|||||||
@@ -11,12 +11,10 @@ using System.Text.Json.Serialization.Metadata;
|
|||||||
using System.Threading;
|
using System.Threading;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using Wino.Core.Domain.Entities.Shared;
|
using Wino.Core.Domain.Entities.Shared;
|
||||||
using Wino.Core.Domain.Enums;
|
|
||||||
using Wino.Core.Domain.Interfaces;
|
using Wino.Core.Domain.Interfaces;
|
||||||
using Wino.Core.Domain.Models.Accounts;
|
using Wino.Core.Domain.Models.Accounts;
|
||||||
using Wino.Mail.Api.Contracts.Ai;
|
using Wino.Mail.Api.Contracts.Ai;
|
||||||
using Wino.Mail.Api.Contracts.Auth;
|
using Wino.Mail.Api.Contracts.Auth;
|
||||||
using Wino.Mail.Api.Contracts.Billing;
|
|
||||||
using Wino.Mail.Api.Contracts.Common;
|
using Wino.Mail.Api.Contracts.Common;
|
||||||
|
|
||||||
namespace Wino.Services;
|
namespace Wino.Services;
|
||||||
@@ -137,25 +135,27 @@ public sealed class WinoAccountApiClient : IWinoAccountApiClient, IDisposable
|
|||||||
WinoAccountApiJsonContext.Default.ApiEnvelopeAiTextResultDto,
|
WinoAccountApiJsonContext.Default.ApiEnvelopeAiTextResultDto,
|
||||||
cancellationToken);
|
cancellationToken);
|
||||||
|
|
||||||
public Task<ApiEnvelope<CheckoutSessionResultDto>> CreateCheckoutSessionAsync(WinoAddOnProductType productId, CancellationToken cancellationToken = default)
|
public Task<ApiEnvelope<WinoStoreCollectionsIdTicketInfo>> CreateCollectionsIdTicketAsync(CancellationToken cancellationToken = default)
|
||||||
{
|
|
||||||
var endpoint = productId switch
|
|
||||||
{
|
|
||||||
WinoAddOnProductType.AI_PACK => "api/v1/billing/ai-pack/checkout-session",
|
|
||||||
WinoAddOnProductType.UNLIMITED_ACCOUNTS => "api/v1/billing/unlimited-accounts/checkout-session",
|
|
||||||
_ => string.Empty
|
|
||||||
};
|
|
||||||
|
|
||||||
return string.IsNullOrWhiteSpace(endpoint)
|
|
||||||
? Task.FromResult(ApiEnvelope<CheckoutSessionResultDto>.Failure("UnknownProduct"))
|
|
||||||
: SendAuthorizedRequestAsync(HttpMethod.Post, endpoint, WinoAccountApiJsonContext.Default.ApiEnvelopeCheckoutSessionResultDto, cancellationToken);
|
|
||||||
}
|
|
||||||
|
|
||||||
public Task<ApiEnvelope<CustomerPortalResultDto>> CreateCustomerPortalSessionAsync(CancellationToken cancellationToken = default)
|
|
||||||
=> SendAuthorizedRequestAsync(
|
=> SendAuthorizedRequestAsync(
|
||||||
HttpMethod.Post,
|
HttpMethod.Post,
|
||||||
"api/v1/billing/ai-pack/customer-portal-session",
|
"api/v1/store/collections-id-ticket",
|
||||||
WinoAccountApiJsonContext.Default.ApiEnvelopeCustomerPortalResultDto,
|
WinoAccountApiJsonContext.Default.ApiEnvelopeWinoStoreCollectionsIdTicketInfo,
|
||||||
|
cancellationToken);
|
||||||
|
|
||||||
|
public Task<ApiEnvelope<WinoStoreCollectionsIdTicketInfo>> CreatePurchaseIdTicketAsync(CancellationToken cancellationToken = default)
|
||||||
|
=> SendAuthorizedRequestAsync(
|
||||||
|
HttpMethod.Post,
|
||||||
|
"api/v1/store/purchase-id-ticket",
|
||||||
|
WinoAccountApiJsonContext.Default.ApiEnvelopeWinoStoreCollectionsIdTicketInfo,
|
||||||
|
cancellationToken);
|
||||||
|
|
||||||
|
public Task<ApiEnvelope<JsonElement>> SyncStoreEntitlementsAsync(string? storeIdKey, string? purchaseIdKey, CancellationToken cancellationToken = default)
|
||||||
|
=> SendAuthorizedRequestAsync(
|
||||||
|
HttpMethod.Post,
|
||||||
|
"api/v1/store/entitlements/sync",
|
||||||
|
new SyncStoreEntitlementsRequest(storeIdKey, purchaseIdKey),
|
||||||
|
WinoAccountApiJsonContext.Default.SyncStoreEntitlementsRequest,
|
||||||
|
WinoAccountApiJsonContext.Default.ApiEnvelopeJsonElement,
|
||||||
cancellationToken);
|
cancellationToken);
|
||||||
|
|
||||||
public async Task<string?> GetSettingsAsync(CancellationToken cancellationToken = default)
|
public async Task<string?> GetSettingsAsync(CancellationToken cancellationToken = default)
|
||||||
@@ -471,7 +471,7 @@ public sealed class WinoAccountApiClient : IWinoAccountApiClient, IDisposable
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!string.IsNullOrWhiteSpace(account.AccessToken) && account.AccessTokenExpiresAtUtc > DateTime.UtcNow.AddMinutes(1))
|
if (!string.IsNullOrWhiteSpace(account.AccessToken) && account.AccessTokenExpiresAtUtc > DateTime.UtcNow)
|
||||||
{
|
{
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
@@ -542,12 +542,14 @@ public sealed class WinoAccountApiClient : IWinoAccountApiClient, IDisposable
|
|||||||
[JsonSerializable(typeof(SummarizeRequest))]
|
[JsonSerializable(typeof(SummarizeRequest))]
|
||||||
[JsonSerializable(typeof(TranslateRequest))]
|
[JsonSerializable(typeof(TranslateRequest))]
|
||||||
[JsonSerializable(typeof(RewriteRequest))]
|
[JsonSerializable(typeof(RewriteRequest))]
|
||||||
|
[JsonSerializable(typeof(SyncStoreEntitlementsRequest))]
|
||||||
[JsonSerializable(typeof(ApiEnvelope<AuthResultDto>))]
|
[JsonSerializable(typeof(ApiEnvelope<AuthResultDto>))]
|
||||||
[JsonSerializable(typeof(ApiEnvelope<EmailConfirmationResendResultDto>))]
|
[JsonSerializable(typeof(ApiEnvelope<EmailConfirmationResendResultDto>))]
|
||||||
[JsonSerializable(typeof(ApiEnvelope<AuthUserDto>))]
|
[JsonSerializable(typeof(ApiEnvelope<AuthUserDto>))]
|
||||||
[JsonSerializable(typeof(ApiEnvelope<AiStatusResultDto>))]
|
[JsonSerializable(typeof(ApiEnvelope<AiStatusResultDto>))]
|
||||||
[JsonSerializable(typeof(ApiEnvelope<AiTextResultDto>))]
|
[JsonSerializable(typeof(ApiEnvelope<AiTextResultDto>))]
|
||||||
[JsonSerializable(typeof(ApiEnvelope<CheckoutSessionResultDto>))]
|
[JsonSerializable(typeof(ApiEnvelope<WinoStoreCollectionsIdTicketInfo>))]
|
||||||
[JsonSerializable(typeof(ApiEnvelope<CustomerPortalResultDto>))]
|
|
||||||
[JsonSerializable(typeof(ApiEnvelope<JsonElement>))]
|
[JsonSerializable(typeof(ApiEnvelope<JsonElement>))]
|
||||||
internal sealed partial class WinoAccountApiJsonContext : JsonSerializerContext;
|
internal sealed partial class WinoAccountApiJsonContext : JsonSerializerContext;
|
||||||
|
|
||||||
|
internal sealed record SyncStoreEntitlementsRequest(string? StoreIdKey, string? PurchaseIdKey);
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
#nullable enable
|
#nullable enable
|
||||||
using System;
|
using System;
|
||||||
using System.Reflection;
|
|
||||||
using System.Text.Json;
|
using System.Text.Json;
|
||||||
using System.Threading;
|
using System.Threading;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
@@ -11,7 +10,6 @@ using Wino.Core.Domain.Interfaces;
|
|||||||
using Wino.Core.Domain.Models.Accounts;
|
using Wino.Core.Domain.Models.Accounts;
|
||||||
using Wino.Mail.Api.Contracts.Ai;
|
using Wino.Mail.Api.Contracts.Ai;
|
||||||
using Wino.Mail.Api.Contracts.Auth;
|
using Wino.Mail.Api.Contracts.Auth;
|
||||||
using Wino.Mail.Api.Contracts.Billing;
|
|
||||||
using Wino.Mail.Api.Contracts.Common;
|
using Wino.Mail.Api.Contracts.Common;
|
||||||
using Wino.Messaging.UI;
|
using Wino.Messaging.UI;
|
||||||
|
|
||||||
@@ -79,7 +77,7 @@ public sealed class WinoAccountProfileService : BaseDatabaseService, IWinoAccoun
|
|||||||
return WinoAccountOperationResult.Failure(ApiErrorCodes.RefreshTokenInvalid);
|
return WinoAccountOperationResult.Failure(ApiErrorCodes.RefreshTokenInvalid);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!string.IsNullOrWhiteSpace(account.AccessToken) && account.AccessTokenExpiresAtUtc > DateTime.UtcNow.AddMinutes(1))
|
if (!string.IsNullOrWhiteSpace(account.AccessToken) && account.AccessTokenExpiresAtUtc > DateTime.UtcNow)
|
||||||
{
|
{
|
||||||
return WinoAccountOperationResult.Success(account);
|
return WinoAccountOperationResult.Success(account);
|
||||||
}
|
}
|
||||||
@@ -123,14 +121,6 @@ public sealed class WinoAccountProfileService : BaseDatabaseService, IWinoAccoun
|
|||||||
}
|
}
|
||||||
|
|
||||||
var refreshedAccount = MergeAccountProfile(account, response.Result);
|
var refreshedAccount = MergeAccountProfile(account, response.Result);
|
||||||
var hasUnlimitedAccounts = TryGetBooleanProperty(response.Result, "HasUnlimitedAccounts", out var cachedHasUnlimitedAccounts)
|
|
||||||
? cachedHasUnlimitedAccounts
|
|
||||||
: (bool?)null;
|
|
||||||
|
|
||||||
if (hasUnlimitedAccounts.HasValue)
|
|
||||||
{
|
|
||||||
await PersistAddOnCacheAsync(refreshedAccount.Id, null, hasUnlimitedAccounts.Value).ConfigureAwait(false);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (AreEquivalentProfiles(account, refreshedAccount))
|
if (AreEquivalentProfiles(account, refreshedAccount))
|
||||||
{
|
{
|
||||||
@@ -149,18 +139,6 @@ public sealed class WinoAccountProfileService : BaseDatabaseService, IWinoAccoun
|
|||||||
return account;
|
return account;
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task<WinoAccountAddOnSnapshot?> GetCachedAddOnSnapshotAsync()
|
|
||||||
{
|
|
||||||
var account = await GetActiveAccountAsync().ConfigureAwait(false);
|
|
||||||
if (account == null)
|
|
||||||
{
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
var cache = await GetAddOnCacheAsync(account.Id).ConfigureAwait(false);
|
|
||||||
return cache == null ? null : Map(cache);
|
|
||||||
}
|
|
||||||
|
|
||||||
public async Task<WinoAccount?> GetAuthenticatedAccountAsync(CancellationToken cancellationToken = default)
|
public async Task<WinoAccount?> GetAuthenticatedAccountAsync(CancellationToken cancellationToken = default)
|
||||||
{
|
{
|
||||||
var account = await GetActiveAccountAsync().ConfigureAwait(false);
|
var account = await GetActiveAccountAsync().ConfigureAwait(false);
|
||||||
@@ -176,7 +154,7 @@ public sealed class WinoAccountProfileService : BaseDatabaseService, IWinoAccoun
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (account.AccessTokenExpiresAtUtc > DateTime.UtcNow.AddMinutes(1))
|
if (account.AccessTokenExpiresAtUtc > DateTime.UtcNow)
|
||||||
{
|
{
|
||||||
return account;
|
return account;
|
||||||
}
|
}
|
||||||
@@ -193,16 +171,6 @@ public sealed class WinoAccountProfileService : BaseDatabaseService, IWinoAccoun
|
|||||||
public async Task<bool> HasActiveAccountAsync()
|
public async Task<bool> HasActiveAccountAsync()
|
||||||
=> await Connection.Table<WinoAccount>().CountAsync().ConfigureAwait(false) > 0;
|
=> await Connection.Table<WinoAccount>().CountAsync().ConfigureAwait(false) > 0;
|
||||||
|
|
||||||
public async Task<bool> HasAddOnAsync(WinoAddOnProductType productId, CancellationToken cancellationToken = default)
|
|
||||||
{
|
|
||||||
return productId switch
|
|
||||||
{
|
|
||||||
WinoAddOnProductType.AI_PACK => await HasAiPackAsync(cancellationToken).ConfigureAwait(false),
|
|
||||||
WinoAddOnProductType.UNLIMITED_ACCOUNTS => await HasUnlimitedAccountsAsync(cancellationToken).ConfigureAwait(false),
|
|
||||||
_ => false
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
public async Task<ApiEnvelope<AuthUserDto>> GetCurrentUserAsync(CancellationToken cancellationToken = default)
|
public async Task<ApiEnvelope<AuthUserDto>> GetCurrentUserAsync(CancellationToken cancellationToken = default)
|
||||||
{
|
{
|
||||||
var account = await GetAuthenticatedAccountAsync(cancellationToken).ConfigureAwait(false);
|
var account = await GetAuthenticatedAccountAsync(cancellationToken).ConfigureAwait(false);
|
||||||
@@ -221,11 +189,7 @@ public sealed class WinoAccountProfileService : BaseDatabaseService, IWinoAccoun
|
|||||||
if (response.Result != null)
|
if (response.Result != null)
|
||||||
{
|
{
|
||||||
var refreshedAccount = MergeAccountProfile(account, response.Result);
|
var refreshedAccount = MergeAccountProfile(account, response.Result);
|
||||||
var hasUnlimitedAccounts = TryGetBooleanProperty(response.Result, "HasUnlimitedAccounts", out var cachedHasUnlimitedAccounts)
|
await PersistProfileDataAsync(account, refreshedAccount).ConfigureAwait(false);
|
||||||
? cachedHasUnlimitedAccounts
|
|
||||||
: (bool?)null;
|
|
||||||
|
|
||||||
await PersistProfileDataAsync(account, refreshedAccount, hasUnlimitedAccounts).ConfigureAwait(false);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return response;
|
return response;
|
||||||
@@ -246,11 +210,6 @@ public sealed class WinoAccountProfileService : BaseDatabaseService, IWinoAccoun
|
|||||||
return response;
|
return response;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (response.Result != null)
|
|
||||||
{
|
|
||||||
await PersistAddOnCacheAsync(account.Id, response.Result).ConfigureAwait(false);
|
|
||||||
}
|
|
||||||
|
|
||||||
return response;
|
return response;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -263,36 +222,65 @@ public sealed class WinoAccountProfileService : BaseDatabaseService, IWinoAccoun
|
|||||||
public async Task<ApiEnvelope<AiTextResultDto>> RewriteAsync(string html, string mode, CancellationToken cancellationToken = default)
|
public async Task<ApiEnvelope<AiTextResultDto>> RewriteAsync(string html, string mode, CancellationToken cancellationToken = default)
|
||||||
=> await ExecuteAiOperationAsync(account => _apiClient.RewriteAsync(html, mode, cancellationToken), "rewrite", cancellationToken).ConfigureAwait(false);
|
=> await ExecuteAiOperationAsync(account => _apiClient.RewriteAsync(html, mode, cancellationToken), "rewrite", cancellationToken).ConfigureAwait(false);
|
||||||
|
|
||||||
public async Task<ApiEnvelope<CheckoutSessionResultDto>> CreateCheckoutSessionAsync(WinoAddOnProductType productId, CancellationToken cancellationToken = default)
|
public async Task<ApiEnvelope<JsonElement>> SyncStoreEntitlementsAsync(CancellationToken cancellationToken = default)
|
||||||
{
|
{
|
||||||
var account = await GetAuthenticatedAccountAsync(cancellationToken).ConfigureAwait(false);
|
var account = await GetActiveAccountAsync().ConfigureAwait(false);
|
||||||
if (account == null)
|
if (account == null || string.IsNullOrWhiteSpace(account.AccessToken))
|
||||||
{
|
{
|
||||||
return ApiEnvelope<CheckoutSessionResultDto>.Failure("MissingAccessToken");
|
return ApiEnvelope<JsonElement>.Failure("MissingAccessToken");
|
||||||
}
|
}
|
||||||
|
|
||||||
var response = await _apiClient.CreateCheckoutSessionAsync(productId, cancellationToken).ConfigureAwait(false);
|
string? storeIdKey = null;
|
||||||
|
string? purchaseIdKey = null;
|
||||||
|
|
||||||
|
var collectionsTicketResponse = await _apiClient.CreateCollectionsIdTicketAsync(cancellationToken).ConfigureAwait(false);
|
||||||
|
if (collectionsTicketResponse.IsSuccess && collectionsTicketResponse.Result != null)
|
||||||
|
{
|
||||||
|
storeIdKey = await _storeManagementService.GetCustomerCollectionsIdAsync(
|
||||||
|
collectionsTicketResponse.Result.ServiceTicket,
|
||||||
|
collectionsTicketResponse.Result.PublisherUserId).ConfigureAwait(false);
|
||||||
|
|
||||||
|
if (string.IsNullOrWhiteSpace(storeIdKey))
|
||||||
|
{
|
||||||
|
_logger.Warning("Failed to obtain Microsoft Store collections ID key for Wino account {Email}.", account.Email);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
_logger.Warning("Failed to create Microsoft Store collections ticket for Wino account {Email}. Error code: {ErrorCode}", account.Email, collectionsTicketResponse.ErrorCode);
|
||||||
|
}
|
||||||
|
|
||||||
|
var purchaseTicketResponse = await _apiClient.CreatePurchaseIdTicketAsync(cancellationToken).ConfigureAwait(false);
|
||||||
|
if (purchaseTicketResponse.IsSuccess && purchaseTicketResponse.Result != null)
|
||||||
|
{
|
||||||
|
purchaseIdKey = await _storeManagementService.GetCustomerPurchaseIdAsync(
|
||||||
|
purchaseTicketResponse.Result.ServiceTicket,
|
||||||
|
purchaseTicketResponse.Result.PublisherUserId).ConfigureAwait(false);
|
||||||
|
|
||||||
|
if (string.IsNullOrWhiteSpace(purchaseIdKey))
|
||||||
|
{
|
||||||
|
_logger.Warning("Failed to obtain Microsoft Store purchase ID key for Wino account {Email}.", account.Email);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
_logger.Warning("Failed to create Microsoft Store purchase ticket for Wino account {Email}. Error code: {ErrorCode}", account.Email, purchaseTicketResponse.ErrorCode);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (string.IsNullOrWhiteSpace(storeIdKey) && string.IsNullOrWhiteSpace(purchaseIdKey))
|
||||||
|
{
|
||||||
|
return ApiEnvelope<JsonElement>.Failure("StoreEntitlementKeysMissing");
|
||||||
|
}
|
||||||
|
|
||||||
|
var response = await _apiClient.SyncStoreEntitlementsAsync(storeIdKey, purchaseIdKey, cancellationToken).ConfigureAwait(false);
|
||||||
if (!response.IsSuccess)
|
if (!response.IsSuccess)
|
||||||
{
|
{
|
||||||
_logger.Warning("Failed to create checkout session for product {ProductId} and Wino account {Email}. Error code: {ErrorCode}", productId, account.Email, response.ErrorCode);
|
_logger.Warning("Failed to sync Microsoft Store entitlements for Wino account {Email}. Error code: {ErrorCode}", account.Email, response.ErrorCode);
|
||||||
|
return response;
|
||||||
}
|
}
|
||||||
|
|
||||||
return response;
|
await RefreshProfileAsync(cancellationToken).ConfigureAwait(false);
|
||||||
}
|
await GetAiStatusAsync(cancellationToken).ConfigureAwait(false);
|
||||||
|
|
||||||
public async Task<ApiEnvelope<CustomerPortalResultDto>> CreateCustomerPortalSessionAsync(CancellationToken cancellationToken = default)
|
|
||||||
{
|
|
||||||
var account = await GetAuthenticatedAccountAsync(cancellationToken).ConfigureAwait(false);
|
|
||||||
if (account == null)
|
|
||||||
{
|
|
||||||
return ApiEnvelope<CustomerPortalResultDto>.Failure("MissingAccessToken");
|
|
||||||
}
|
|
||||||
|
|
||||||
var response = await _apiClient.CreateCustomerPortalSessionAsync(cancellationToken).ConfigureAwait(false);
|
|
||||||
if (!response.IsSuccess)
|
|
||||||
{
|
|
||||||
_logger.Warning("Failed to create customer portal session for Wino account {Email}. Error code: {ErrorCode}", account.Email, response.ErrorCode);
|
|
||||||
}
|
|
||||||
|
|
||||||
return response;
|
return response;
|
||||||
}
|
}
|
||||||
@@ -324,7 +312,7 @@ public sealed class WinoAccountProfileService : BaseDatabaseService, IWinoAccoun
|
|||||||
|
|
||||||
var refreshResult = await RefreshProfileAsync(cancellationToken).ConfigureAwait(false);
|
var refreshResult = await RefreshProfileAsync(cancellationToken).ConfigureAwait(false);
|
||||||
|
|
||||||
if (refreshResult.IsSuccess && await HasAddOnAsync(targetProductType.Value, cancellationToken).ConfigureAwait(false))
|
if (refreshResult.IsSuccess && await _storeManagementService.HasProductAsync(targetProductType.Value).ConfigureAwait(false))
|
||||||
{
|
{
|
||||||
ReportUIChange(new WinoAccountAddOnPurchasedMessage(targetProductType.Value));
|
ReportUIChange(new WinoAccountAddOnPurchasedMessage(targetProductType.Value));
|
||||||
return true;
|
return true;
|
||||||
@@ -365,8 +353,6 @@ public sealed class WinoAccountProfileService : BaseDatabaseService, IWinoAccoun
|
|||||||
}
|
}
|
||||||
|
|
||||||
await Connection.DeleteAllAsync<WinoAccount>().ConfigureAwait(false);
|
await Connection.DeleteAllAsync<WinoAccount>().ConfigureAwait(false);
|
||||||
await Connection.DeleteAllAsync<WinoAccountAddOnCache>().ConfigureAwait(false);
|
|
||||||
|
|
||||||
if (account != null)
|
if (account != null)
|
||||||
{
|
{
|
||||||
ReportUIChange(new WinoAccountProfileDeletedMessage(account));
|
ReportUIChange(new WinoAccountProfileDeletedMessage(account));
|
||||||
@@ -395,29 +381,18 @@ public sealed class WinoAccountProfileService : BaseDatabaseService, IWinoAccoun
|
|||||||
await Connection.InsertOrReplaceAsync(account, typeof(WinoAccount)).ConfigureAwait(false);
|
await Connection.InsertOrReplaceAsync(account, typeof(WinoAccount)).ConfigureAwait(false);
|
||||||
}
|
}
|
||||||
|
|
||||||
private async Task PersistProfileDataAsync(WinoAccount originalAccount, WinoAccount refreshedAccount, bool? hasUnlimitedAccounts)
|
private async Task PersistProfileDataAsync(WinoAccount originalAccount, WinoAccount refreshedAccount)
|
||||||
{
|
{
|
||||||
if (!AreEquivalentProfiles(originalAccount, refreshedAccount))
|
if (!AreEquivalentProfiles(originalAccount, refreshedAccount))
|
||||||
{
|
{
|
||||||
await PersistAccountAsync(refreshedAccount).ConfigureAwait(false);
|
await PersistAccountAsync(refreshedAccount).ConfigureAwait(false);
|
||||||
PublishProfileUpdated(refreshedAccount);
|
PublishProfileUpdated(refreshedAccount);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (hasUnlimitedAccounts.HasValue)
|
|
||||||
{
|
|
||||||
await PersistAddOnCacheAsync(refreshedAccount.Id, null, hasUnlimitedAccounts.Value).ConfigureAwait(false);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private void PublishProfileUpdated(WinoAccount account)
|
private void PublishProfileUpdated(WinoAccount account)
|
||||||
=> ReportUIChange(new WinoAccountProfileUpdatedMessage(account));
|
=> ReportUIChange(new WinoAccountProfileUpdatedMessage(account));
|
||||||
|
|
||||||
private async Task<bool> HasAiPackAsync(CancellationToken cancellationToken)
|
|
||||||
{
|
|
||||||
var response = await GetAiStatusAsync(cancellationToken).ConfigureAwait(false);
|
|
||||||
return response.IsSuccess && response.Result?.HasAiPack == true;
|
|
||||||
}
|
|
||||||
|
|
||||||
private async Task<ApiEnvelope<AiTextResultDto>> ExecuteAiOperationAsync(Func<WinoAccount, Task<ApiEnvelope<AiTextResultDto>>> executeAsync,
|
private async Task<ApiEnvelope<AiTextResultDto>> ExecuteAiOperationAsync(Func<WinoAccount, Task<ApiEnvelope<AiTextResultDto>>> executeAsync,
|
||||||
string operationName,
|
string operationName,
|
||||||
CancellationToken cancellationToken)
|
CancellationToken cancellationToken)
|
||||||
@@ -434,94 +409,9 @@ public sealed class WinoAccountProfileService : BaseDatabaseService, IWinoAccoun
|
|||||||
_logger.Warning("Failed to {Operation} HTML with AI for Wino account {Email}. Error code: {ErrorCode}", operationName, account.Email, response.ErrorCode);
|
_logger.Warning("Failed to {Operation} HTML with AI for Wino account {Email}. Error code: {ErrorCode}", operationName, account.Email, response.ErrorCode);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (response.Quota != null)
|
|
||||||
{
|
|
||||||
await PersistAddOnCacheAsync(account.Id, Map(response.Quota)).ConfigureAwait(false);
|
|
||||||
}
|
|
||||||
|
|
||||||
return response;
|
return response;
|
||||||
}
|
}
|
||||||
|
|
||||||
private async Task<bool> HasUnlimitedAccountsAsync(CancellationToken cancellationToken)
|
|
||||||
{
|
|
||||||
var cachedSnapshot = await GetCachedAddOnSnapshotAsync().ConfigureAwait(false);
|
|
||||||
if (cachedSnapshot?.HasUnlimitedAccounts == true)
|
|
||||||
{
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (await HasRemoteUnlimitedAccountsAsync(cancellationToken).ConfigureAwait(false))
|
|
||||||
{
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
return await _storeManagementService.HasProductAsync(WinoAddOnProductType.UNLIMITED_ACCOUNTS).ConfigureAwait(false);
|
|
||||||
}
|
|
||||||
|
|
||||||
private async Task<bool> HasRemoteUnlimitedAccountsAsync(CancellationToken cancellationToken)
|
|
||||||
{
|
|
||||||
var response = await GetCurrentUserAsync(cancellationToken).ConfigureAwait(false);
|
|
||||||
if (!response.IsSuccess || response.Result == null)
|
|
||||||
{
|
|
||||||
return (await GetCachedAddOnSnapshotAsync().ConfigureAwait(false))?.HasUnlimitedAccounts == true;
|
|
||||||
}
|
|
||||||
|
|
||||||
return TryGetBooleanProperty(response.Result, "HasUnlimitedAccounts", out var hasUnlimitedAccounts) && hasUnlimitedAccounts;
|
|
||||||
}
|
|
||||||
|
|
||||||
private async Task<WinoAccountAddOnCache?> GetAddOnCacheAsync(Guid accountId)
|
|
||||||
=> await Connection.Table<WinoAccountAddOnCache>()
|
|
||||||
.Where(cache => cache.AccountId == accountId)
|
|
||||||
.FirstOrDefaultAsync()
|
|
||||||
.ConfigureAwait(false);
|
|
||||||
|
|
||||||
private async Task PersistAddOnCacheAsync(Guid accountId, AiStatusResultDto? aiStatus = null, bool? hasUnlimitedAccounts = null)
|
|
||||||
{
|
|
||||||
var cache = await GetAddOnCacheAsync(accountId).ConfigureAwait(false) ?? new WinoAccountAddOnCache
|
|
||||||
{
|
|
||||||
AccountId = accountId
|
|
||||||
};
|
|
||||||
|
|
||||||
if (aiStatus != null)
|
|
||||||
{
|
|
||||||
cache.HasAiPack = aiStatus.HasAiPack;
|
|
||||||
cache.AiUsageCount = aiStatus.HasAiPack ? aiStatus.Used : null;
|
|
||||||
cache.AiUsageLimit = aiStatus.HasAiPack ? aiStatus.MonthlyLimit : null;
|
|
||||||
cache.AiBillingPeriodStartUtc = aiStatus.CurrentPeriodStartUtc?.UtcDateTime;
|
|
||||||
cache.AiBillingPeriodEndUtc = aiStatus.CurrentPeriodEndUtc?.UtcDateTime;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (hasUnlimitedAccounts.HasValue)
|
|
||||||
{
|
|
||||||
cache.HasUnlimitedAccounts = hasUnlimitedAccounts.Value;
|
|
||||||
}
|
|
||||||
|
|
||||||
cache.LastUpdatedUtc = DateTime.UtcNow;
|
|
||||||
|
|
||||||
await Connection.InsertOrReplaceAsync(cache, typeof(WinoAccountAddOnCache)).ConfigureAwait(false);
|
|
||||||
}
|
|
||||||
|
|
||||||
private static WinoAccountAddOnSnapshot Map(WinoAccountAddOnCache cache)
|
|
||||||
=> new(
|
|
||||||
cache.HasAiPack,
|
|
||||||
cache.AiUsageCount,
|
|
||||||
cache.AiUsageLimit,
|
|
||||||
cache.AiBillingPeriodStartUtc is DateTime periodStartUtc ? new DateTimeOffset(DateTime.SpecifyKind(periodStartUtc, DateTimeKind.Utc)) : null,
|
|
||||||
cache.AiBillingPeriodEndUtc is DateTime periodEndUtc ? new DateTimeOffset(DateTime.SpecifyKind(periodEndUtc, DateTimeKind.Utc)) : null,
|
|
||||||
cache.HasUnlimitedAccounts,
|
|
||||||
new DateTimeOffset(DateTime.SpecifyKind(cache.LastUpdatedUtc, DateTimeKind.Utc)));
|
|
||||||
|
|
||||||
private static AiStatusResultDto Map(QuotaInfoDto quota)
|
|
||||||
=> new(
|
|
||||||
quota.HasAiPack,
|
|
||||||
quota.EntitlementStatus,
|
|
||||||
quota.CurrentPeriodStartUtc,
|
|
||||||
quota.CurrentPeriodEndUtc,
|
|
||||||
quota.MonthlyLimit,
|
|
||||||
quota.Used,
|
|
||||||
quota.Remaining,
|
|
||||||
quota.Product);
|
|
||||||
|
|
||||||
private static bool AreEquivalentProfiles(WinoAccount left, WinoAccount right)
|
private static bool AreEquivalentProfiles(WinoAccount left, WinoAccount right)
|
||||||
=> left.Id == right.Id &&
|
=> left.Id == right.Id &&
|
||||||
string.Equals(left.Email, right.Email, StringComparison.Ordinal) &&
|
string.Equals(left.Email, right.Email, StringComparison.Ordinal) &&
|
||||||
@@ -546,21 +436,6 @@ public sealed class WinoAccountProfileService : BaseDatabaseService, IWinoAccoun
|
|||||||
LastAuthenticatedUtc = existingAccount.LastAuthenticatedUtc
|
LastAuthenticatedUtc = existingAccount.LastAuthenticatedUtc
|
||||||
};
|
};
|
||||||
|
|
||||||
[System.Diagnostics.CodeAnalysis.UnconditionalSuppressMessage("Trimming", "IL2075", Justification = "The reflected contract property is a stable API field read from a concrete DTO instance.")]
|
|
||||||
private static bool TryGetBooleanProperty(object instance, string propertyName, out bool value)
|
|
||||||
{
|
|
||||||
value = false;
|
|
||||||
|
|
||||||
var property = instance.GetType().GetProperty(propertyName, BindingFlags.Instance | BindingFlags.Public);
|
|
||||||
if (property?.PropertyType != typeof(bool))
|
|
||||||
{
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
value = (bool)(property.GetValue(instance) ?? false);
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
private static WinoAddOnProductType? ResolveProductType(Uri callbackUri)
|
private static WinoAddOnProductType? ResolveProductType(Uri callbackUri)
|
||||||
{
|
{
|
||||||
var productCode = GetQueryParameter(callbackUri, "productCode");
|
var productCode = GetQueryParameter(callbackUri, "productCode");
|
||||||
|
|||||||
@@ -1,86 +0,0 @@
|
|||||||
#nullable enable
|
|
||||||
using System.Collections.Generic;
|
|
||||||
using System.Threading;
|
|
||||||
using System.Threading.Tasks;
|
|
||||||
using Wino.Core.Domain.Enums;
|
|
||||||
using Wino.Core.Domain.Interfaces;
|
|
||||||
using Wino.Core.Domain.Models.Accounts;
|
|
||||||
|
|
||||||
namespace Wino.Services;
|
|
||||||
|
|
||||||
public sealed class WinoAddOnService : IWinoAddOnService
|
|
||||||
{
|
|
||||||
private readonly IWinoAccountProfileService _profileService;
|
|
||||||
|
|
||||||
public WinoAddOnService(IWinoAccountProfileService profileService)
|
|
||||||
{
|
|
||||||
_profileService = profileService;
|
|
||||||
}
|
|
||||||
|
|
||||||
public async Task<IReadOnlyList<WinoAddOnInfo>> GetAvailableAddOnsAsync(bool useCachedDataOnly = false, CancellationToken cancellationToken = default)
|
|
||||||
{
|
|
||||||
var cachedSnapshot = await _profileService.GetCachedAddOnSnapshotAsync().ConfigureAwait(false);
|
|
||||||
|
|
||||||
if (useCachedDataOnly)
|
|
||||||
{
|
|
||||||
return BuildAddOnInfos(cachedSnapshot);
|
|
||||||
}
|
|
||||||
|
|
||||||
var aiStatusTask = _profileService.GetAiStatusAsync(cancellationToken);
|
|
||||||
var hasUnlimitedAccountsTask = _profileService.HasAddOnAsync(WinoAddOnProductType.UNLIMITED_ACCOUNTS, cancellationToken);
|
|
||||||
|
|
||||||
await Task.WhenAll(aiStatusTask, hasUnlimitedAccountsTask).ConfigureAwait(false);
|
|
||||||
|
|
||||||
var aiStatusResponse = await aiStatusTask.ConfigureAwait(false);
|
|
||||||
var aiStatus = aiStatusResponse.IsSuccess ? aiStatusResponse.Result : null;
|
|
||||||
var hasUnlimitedAccounts = await hasUnlimitedAccountsTask.ConfigureAwait(false);
|
|
||||||
|
|
||||||
if (aiStatus == null && cachedSnapshot != null)
|
|
||||||
{
|
|
||||||
return BuildAddOnInfos(cachedSnapshot with
|
|
||||||
{
|
|
||||||
HasUnlimitedAccounts = hasUnlimitedAccounts || cachedSnapshot.HasUnlimitedAccounts
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
return
|
|
||||||
[
|
|
||||||
new WinoAddOnInfo(
|
|
||||||
WinoAddOnProductType.AI_PACK,
|
|
||||||
aiStatus?.HasAiPack == true,
|
|
||||||
aiStatus?.Used,
|
|
||||||
aiStatus?.MonthlyLimit,
|
|
||||||
aiStatus?.HasAiPack == true && aiStatus.MonthlyLimit is int limit && limit > 0 && aiStatus.Used is int used
|
|
||||||
? (double)used / limit * 100
|
|
||||||
: 0,
|
|
||||||
aiStatus?.CurrentPeriodEndUtc),
|
|
||||||
new WinoAddOnInfo(
|
|
||||||
WinoAddOnProductType.UNLIMITED_ACCOUNTS,
|
|
||||||
hasUnlimitedAccounts || cachedSnapshot?.HasUnlimitedAccounts == true)
|
|
||||||
];
|
|
||||||
}
|
|
||||||
|
|
||||||
private static IReadOnlyList<WinoAddOnInfo> BuildAddOnInfos(WinoAccountAddOnSnapshot? snapshot)
|
|
||||||
{
|
|
||||||
if (snapshot == null)
|
|
||||||
{
|
|
||||||
return [];
|
|
||||||
}
|
|
||||||
|
|
||||||
return
|
|
||||||
[
|
|
||||||
new WinoAddOnInfo(
|
|
||||||
WinoAddOnProductType.AI_PACK,
|
|
||||||
snapshot.HasAiPack,
|
|
||||||
snapshot.UsageCount,
|
|
||||||
snapshot.UsageLimit,
|
|
||||||
snapshot.HasAiPack && snapshot.UsageLimit is int limit && limit > 0 && snapshot.UsageCount is int used
|
|
||||||
? (double)used / limit * 100
|
|
||||||
: 0,
|
|
||||||
snapshot.BillingPeriodEndUtc),
|
|
||||||
new WinoAddOnInfo(
|
|
||||||
WinoAddOnProductType.UNLIMITED_ACCOUNTS,
|
|
||||||
snapshot.HasUnlimitedAccounts)
|
|
||||||
];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Reference in New Issue
Block a user