Handling of AI pack through mmicrosoft store.

This commit is contained in:
Burak Kaan Köse
2026-04-02 15:07:05 +02:00
parent 7b369201b0
commit 8f16f553f5
26 changed files with 765 additions and 578 deletions
@@ -27,15 +27,15 @@ public abstract partial class AccountManagementPageViewModelBase : CoreBaseViewM
[ObservableProperty]
[NotifyPropertyChangedFor(nameof(IsPurchasePanelVisible))]
private bool hasUnlimitedAccountProduct;
public partial bool HasUnlimitedAccountProduct { get; set; }
[ObservableProperty]
[NotifyPropertyChangedFor(nameof(IsAccountCreationAlmostOnLimit))]
[NotifyPropertyChangedFor(nameof(IsPurchasePanelVisible))]
private bool isAccountCreationBlocked;
public partial bool IsAccountCreationBlocked { get; set; }
[ObservableProperty]
private IAccountProviderDetailViewModel _startupAccount;
public partial IAccountProviderDetailViewModel StartupAccount { get; set; }
public int FREE_ACCOUNT_COUNT { get; } = 3;
protected IDialogServiceBase DialogService { get; }
@@ -94,7 +94,7 @@ public abstract partial class AccountManagementPageViewModelBase : CoreBaseViewM
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(() =>
{
@@ -21,30 +21,63 @@ public partial class WinoAddOnItemViewModel : ObservableObject
};
[ObservableProperty]
[NotifyPropertyChangedFor(nameof(ShowPurchaseState))]
[NotifyPropertyChangedFor(nameof(ShowUsageSummary))]
public partial bool IsPurchased { 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]
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]
public partial int UsageCount { get; set; }
[ObservableProperty]
[NotifyPropertyChangedFor(nameof(ShowUsageSummary))]
public partial int UsageLimit { get; set; } = 1;
[ObservableProperty]
[NotifyPropertyChangedFor(nameof(ShowUsageSummary))]
public partial double UsagePercentage { get; set; }
[ObservableProperty]
[NotifyPropertyChangedFor(nameof(ShowUsageSummary))]
public partial string RenewalText { get; set; } = string.Empty;
[ObservableProperty]
[NotifyPropertyChangedFor(nameof(ShowUsageSummary))]
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)
{
ProductType = productType;
@@ -1,8 +1,7 @@
#nullable enable
using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Linq;
using Wino.Core.Domain.Entities.Shared;
using System.Threading.Tasks;
using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Input;
@@ -10,7 +9,6 @@ using CommunityToolkit.Mvvm.Messaging;
using Wino.Core.Domain;
using Wino.Core.Domain.Enums;
using Wino.Core.Domain.Interfaces;
using Wino.Core.Domain.Models.Accounts;
using Wino.Core.Domain.Models.Navigation;
using Wino.Core.ViewModels.Data;
using Wino.Mail.Api.Contracts.Common;
@@ -24,9 +22,10 @@ public partial class WinoAccountManagementPageViewModel : CoreBaseViewModel,
IRecipient<WinoAccountAddOnPurchasedMessage>
{
private readonly IWinoAccountProfileService _profileService;
private readonly IWinoAddOnService _addOnService;
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; } = [];
@@ -50,14 +49,17 @@ public partial class WinoAccountManagementPageViewModel : CoreBaseViewModel,
public bool IsSignedOut => !IsSignedIn;
public WinoAccountManagementPageViewModel(IWinoAccountProfileService profileService,
IWinoAddOnService addOnService,
IMailDialogService dialogService,
INativeAppService nativeAppService)
IStoreManagementService storeManagementService)
{
_profileService = profileService;
_addOnService = addOnService;
_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)
@@ -166,15 +168,6 @@ public partial class WinoAccountManagementPageViewModel : CoreBaseViewModel,
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(() =>
{
IsCheckoutInProgress = true;
@@ -183,9 +176,9 @@ public partial class WinoAccountManagementPageViewModel : CoreBaseViewModel,
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,
Translator.WinoAccount_Management_PurchaseStartFailed,
@@ -193,13 +186,16 @@ public partial class WinoAccountManagementPageViewModel : CoreBaseViewModel,
return;
}
var isLaunched = await _nativeAppService.LaunchUriAsync(new Uri(checkoutSession.Result.Url)).ConfigureAwait(false);
if (!isLaunched)
var syncResult = await _profileService.SyncStoreEntitlementsAsync().ConfigureAwait(false);
if (!syncResult.IsSuccess && !string.Equals(syncResult.ErrorCode, "MissingAccessToken", StringComparison.Ordinal))
{
_dialogService.InfoBarMessage(Translator.GeneralTitle_Error,
Translator.WinoAccount_Management_PurchaseStartFailed,
TranslateStoreSyncError(syncResult.ErrorCode),
InfoBarMessageType.Error);
return;
}
await HandleAddOnPurchasedAsync().ConfigureAwait(false);
}
catch (OperationCanceledException)
{
@@ -221,40 +217,7 @@ public partial class WinoAccountManagementPageViewModel : CoreBaseViewModel,
}
private bool CanPurchaseAddOn(WinoAddOnItemViewModel? addOn)
=> addOn != null && !addOn.IsPurchased && !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);
}
}
=> addOn != null && !addOn.IsPurchased && !addOn.IsLoading && !IsCheckoutInProgress;
[RelayCommand]
private Task ExportSettingsAsync() => Task.CompletedTask;
@@ -296,43 +259,58 @@ public partial class WinoAccountManagementPageViewModel : CoreBaseViewModel,
private async Task LoadAsync()
{
WinoAccount? cachedAccount = null;
try
{
var cachedAccount = await _profileService.GetActiveAccountAsync().ConfigureAwait(false);
var cachedAddOns = await _addOnService.GetAvailableAddOnsAsync(true).ConfigureAwait(false);
cachedAccount = await _profileService.GetActiveAccountAsync().ConfigureAwait(false);
if (cachedAccount != null)
{
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 UpdateAddOnsAsync(addOns).ConfigureAwait(false);
await Task.WhenAll(loadAiPackTask, loadUnlimitedAccountsTask).ConfigureAwait(false);
}
catch (Exception)
{
_dialogService.InfoBarMessage(Translator.GeneralTitle_Error,
Translator.WinoAccount_Management_LoadFailed,
InfoBarMessageType.Error);
await ResetStateAsync().ConfigureAwait(false);
if (cachedAccount == null)
{
_dialogService.InfoBarMessage(Translator.GeneralTitle_Error,
Translator.WinoAccount_Management_LoadFailed,
InfoBarMessageType.Error);
await ResetStateAsync().ConfigureAwait(false);
}
}
finally
{
@@ -369,46 +347,151 @@ public partial class WinoAccountManagementPageViewModel : CoreBaseViewModel,
AccountEmail = string.Empty;
AccountStatusText = string.Empty;
IsCheckoutInProgress = false;
AddOns.Clear();
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(() =>
{
AddOns.Clear();
foreach (var item in items)
{
AddOns.Add(item);
}
ResetAddOnItem(_aiPackAddOn);
ResetAddOnItem(_unlimitedAccountsAddOn);
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,
PurchaseCommand = PurchaseAddOnCommand,
ManageCommand = ManageAiPackCommand,
UsageCount = addOn.UsageCount ?? 0,
UsageLimit = addOn.UsageLimit is > 0 ? addOn.UsageLimit.Value : 1,
UsagePercentage = addOn.UsagePercentage
_ => Translator.WinoAccount_Management_StoreSyncFailed
};
if (addOn.RenewalDateUtc is DateTimeOffset renewalDateUtc)
{
item.RenewalText = string.Format(Translator.WinoAccount_Management_AiPackRenews, renewalDateUtc.LocalDateTime);
item.UsageResetText = string.Format(Translator.WinoAccount_Management_AiPackResets, renewalDateUtc.LocalDateTime);
}
private static bool IsAccessTokenExpired(WinoAccount account)
=> string.IsNullOrWhiteSpace(account.AccessToken) || account.AccessTokenExpiresAtUtc <= DateTime.UtcNow;
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();
});
}
}
}