Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions src/Orbit.Api/Extensions/ServiceCollectionExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -226,6 +226,7 @@ public static WebApplicationBuilder AddOrbitDatabase(this WebApplicationBuilder
builder.Services.AddScoped<Orbit.Application.Gamification.Services.GamificationNotifiers>();
builder.Services.AddScoped<IGoogleTokenService, GoogleTokenService>();
builder.Services.AddScoped<GoogleSignInFlow>();
builder.Services.AddScoped<AccountReactivation>();
builder.Services.AddScoped<IAuthUserStore, AuthUserStore>();
builder.Services.AddScoped<ISignupJobDispatcher, SignupJobDispatcher>();
builder.Services.AddScoped<IGoogleAuthorizationCodeService, GoogleAuthorizationCodeService>();
Expand Down
25 changes: 25 additions & 0 deletions src/Orbit.Application/Auth/Commands/AccountReactivation.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
using Orbit.Domain.Entities;
using Orbit.Domain.Enums;
using Orbit.Domain.Interfaces;

namespace Orbit.Application.Auth.Commands;

public sealed class AccountReactivation(
IGenericRepository<OwedSubscriptionCancellation> owedCancellations)
{
public async Task<string?> ReactivateAsync(User user, CancellationToken cancellationToken)
{
if (!user.IsDeactivated)
return null;

var playToken = user.PlayRenewalStoppedByDeletion ? user.PlayPurchaseToken : null;
if (playToken is not null)
{
await owedCancellations.DeleteAsync(record => record.Provider == SubscriptionSource.GooglePlay
&& record.ProviderIdentifier == playToken, cancellationToken);
}

user.CancelDeactivation();
return playToken;
}
}
9 changes: 7 additions & 2 deletions src/Orbit.Application/Auth/Commands/GoogleSignInFlow.cs
Original file line number Diff line number Diff line change
Expand Up @@ -13,14 +13,16 @@ public class GoogleSignInFlow(
IUnitOfWork unitOfWork,
IAuthSessionService authSessionService,
ISignupJobDispatcher signupJobDispatcher,
IBackgroundJobClient backgroundJobClient)
IBackgroundJobClient backgroundJobClient,
AccountReactivation accountReactivation)
{
public async Task<Result<LoginResponse>> CompleteAsync(
string rawEmail, string name, string language, string? referralCode,
string? googleAccessToken, string? googleRefreshToken, CancellationToken cancellationToken)
{
var email = rawEmail.Trim().ToLowerInvariant();
User? reactivatedUser = null;
string? reactivatedPlayToken = null;
var signupPending = false;
var signupLanguage = language;
var signupReferralCode = referralCode;
Expand All @@ -38,7 +40,7 @@ public async Task<Result<LoginResponse>> CompleteAsync(
var wasReactivated = user.IsDeactivated;
if (wasReactivated)
{
user.CancelDeactivation();
reactivatedPlayToken = await accountReactivation.ReactivateAsync(user, transactionToken);
reactivatedUser = user;
}

Expand All @@ -60,7 +62,10 @@ public async Task<Result<LoginResponse>> CompleteAsync(
return result;

if (result.Value.WasReactivated && reactivatedUser is not null)
{
CancelSubscriptionOnDeletionJob.EnqueueRestoration(backgroundJobClient, reactivatedUser);
NotifyPlayRenewalOnReactivationJob.Enqueue(backgroundJobClient, reactivatedUser.Id, reactivatedPlayToken);
}

signupJobDispatcher.Dispatch(result.Value.UserId, signupPending, signupLanguage, signupReferralCode);

Expand Down
9 changes: 7 additions & 2 deletions src/Orbit.Application/Auth/Commands/VerifyCodeCommand.cs
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,8 @@ public class VerifyCodeCommandHandler(
IUnitOfWork unitOfWork,
IAuthSessionService authSessionService,
ISignupJobDispatcher signupJobDispatcher,
IBackgroundJobClient backgroundJobClient) : IRequestHandler<VerifyCodeCommand, Result<LoginResponse>>
IBackgroundJobClient backgroundJobClient,
AccountReactivation accountReactivation) : IRequestHandler<VerifyCodeCommand, Result<LoginResponse>>
{
public async Task<Result<LoginResponse>> Handle(VerifyCodeCommand request, CancellationToken cancellationToken)
{
Expand All @@ -31,6 +32,7 @@ public async Task<Result<LoginResponse>> Handle(VerifyCodeCommand request, Cance
return codeValidation.PropagateError<LoginResponse>();

User? reactivatedUser = null;
string? reactivatedPlayToken = null;
var signupPending = false;
var signupLanguage = request.Language;
var signupReferralCode = request.ReferralCode;
Expand All @@ -48,7 +50,7 @@ public async Task<Result<LoginResponse>> Handle(VerifyCodeCommand request, Cance
var wasReactivated = user.IsDeactivated;
if (wasReactivated)
{
user.CancelDeactivation();
reactivatedPlayToken = await accountReactivation.ReactivateAsync(user, transactionToken);
reactivatedUser = user;
}
await unitOfWork.SaveChangesAsync(transactionToken);
Expand All @@ -70,7 +72,10 @@ public async Task<Result<LoginResponse>> Handle(VerifyCodeCommand request, Cance
return result;

if (result.Value.WasReactivated && reactivatedUser is not null)
{
CancelSubscriptionOnDeletionJob.EnqueueRestoration(backgroundJobClient, reactivatedUser);
NotifyPlayRenewalOnReactivationJob.Enqueue(backgroundJobClient, reactivatedUser.Id, reactivatedPlayToken);
}

signupJobDispatcher.Dispatch(result.Value.UserId, signupPending, signupLanguage, signupReferralCode);

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
using Hangfire;
using Microsoft.Extensions.Options;
using Orbit.Application.Common;
using Orbit.Domain.Entities;
using Orbit.Domain.Interfaces;

namespace Orbit.Application.Auth.Jobs;

public sealed class NotifyPlayRenewalOnReactivationJob(
IGenericRepository<User> users,
IGenericRepository<Notification> notifications,
IUnitOfWork unitOfWork,
IPlayBillingService play,
IOptions<GooglePlaySettings> playSettings)
{
public static void Enqueue(IBackgroundJobClient jobs, Guid userId, string? purchaseToken)
{
if (string.IsNullOrWhiteSpace(purchaseToken))
return;

var reactivationId = Guid.NewGuid();
jobs.Enqueue<NotifyPlayRenewalOnReactivationJob>(job =>
job.ExecuteAsync(userId, purchaseToken, reactivationId, CancellationToken.None));
}

[AutomaticRetry(Attempts = 26, DelaysInSeconds = new[] { 60, 300, 3600 }, OnAttemptsExceeded = AttemptsExceededAction.Fail)]
public async Task ExecuteAsync(Guid userId, string purchaseToken, Guid reactivationId, CancellationToken cancellationToken)
{
if (unitOfWork.HasActiveTransaction)
throw new InvalidOperationException("Play renewal verification requires no active transaction.");

var purchase = await play.VerifyAsync(playSettings.Value.ProductId, purchaseToken, cancellationToken);
if (purchase is not { CanRenew: false })
return;

unitOfWork.ResetTracking();
await unitOfWork.ExecuteInTransactionAsync(async ct =>
{
await unitOfWork.AcquireUserRowLockAsync(userId, ct);
var user = await users.FindOneTrackedIgnoringFiltersAsync(user => user.Id == userId, ct);
if (user is null || user.IsDeactivated || user.PlayPurchaseToken != purchaseToken)
return;

var dedupeKey = $"play-reactivation:{userId}:{reactivationId}";
if (await notifications.FindOneTrackedIgnoringFiltersAsync(notice => notice.DedupeKey == dedupeKey, ct) is not null)
return;

var isPortuguese = LocaleHelper.IsPortuguese(user.Language);
var title = isPortuguese
? "Sua renovação no Google Play está desativada"
: "Your Google Play renewal is off";
var body = isPortuguese
? "Sua conta do Orbit está ativa de novo. A renovação no Google Play foi desativada quando você pediu a exclusão da conta. Para voltar a renovar, abra o Google Play, vá em Pagamentos e assinaturas > Assinaturas, escolha Orbit e toque em Assinar novamente. Se essa opção não aparecer, assine o Pro de novo pelo Orbit."
: "Your Orbit account is active again. Deleting your account turned off your Google Play renewal. To renew again, open Google Play, go to Payments & subscriptions > Subscriptions, select Orbit, and tap Resubscribe. If that option is unavailable, subscribe to Pro again in Orbit.";
await notifications.AddAsync(Notification.Create(user.Id, title, body, dedupeKey: dedupeKey), ct);
await unitOfWork.SaveChangesAsync(ct);
}, cancellationToken);
}
}
1 change: 1 addition & 0 deletions src/Orbit.Domain/Entities/User.cs
Original file line number Diff line number Diff line change
Expand Up @@ -549,6 +549,7 @@

public void CancelDeactivation()
{
PlayRenewalStoppedByDeletion = false;
if (!IsLifetimePro && PlanExpiresAt.HasValue && PlanExpiresAt.Value <= DateTime.UtcNow)
Plan = UserPlan.Free;

Expand Down Expand Up @@ -707,7 +708,7 @@
? PreGapFreezeAwardStreak.Value
: (int?)null;
var awardCursor = savedCursor.HasValue
? (preGapStreak.HasValue ? Math.Min(savedCursor.Value, earnedBeforeTheGap) : savedCursor.Value)

Check warning on line 711 in src/Orbit.Domain/Entities/User.cs

View workflow job for this annotation

GitHub Actions / SonarCloud Analysis

Extract this nested ternary operation into an independent statement.
: earnedBeforeTheGap;
SetStreakState(currentStreak, longestStreak, lastActiveDate);
LastFreezeAwardStreak = awardCursor;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -84,7 +84,7 @@ public async Task VerifyCode_Valid_ReturnsLoginResponse()
_authSessionService.CreateSessionAsync(Arg.Any<Guid>(), Arg.Any<string>(), Arg.Any<CancellationToken>())
.Returns(Result.Success(new SessionTokens("jwt-token", "refresh-token")));

var handler = new VerifyCodeCommandHandler(_cache, _userRepo, _unitOfWork, _authSessionService, new SignupJobDispatcher(_backgroundJobClient, new HttpContextAccessor(), NullLogger<SignupJobDispatcher>.Instance), _backgroundJobClient);
var handler = new VerifyCodeCommandHandler(_cache, _userRepo, _unitOfWork, _authSessionService, new SignupJobDispatcher(_backgroundJobClient, new HttpContextAccessor(), NullLogger<SignupJobDispatcher>.Instance), _backgroundJobClient, new AccountReactivation(Substitute.For<IGenericRepository<OwedSubscriptionCancellation>>()));
var command = new VerifyCodeCommand(TestEmail, "123456");

var result = await handler.Handle(command, CancellationToken.None);
Expand All @@ -100,7 +100,7 @@ public async Task VerifyCode_WrongCode_ReturnsFailure()
{
SetupCacheWithCode("123456");

var handler = new VerifyCodeCommandHandler(_cache, _userRepo, _unitOfWork, _authSessionService, new SignupJobDispatcher(_backgroundJobClient, new HttpContextAccessor(), NullLogger<SignupJobDispatcher>.Instance), _backgroundJobClient);
var handler = new VerifyCodeCommandHandler(_cache, _userRepo, _unitOfWork, _authSessionService, new SignupJobDispatcher(_backgroundJobClient, new HttpContextAccessor(), NullLogger<SignupJobDispatcher>.Instance), _backgroundJobClient, new AccountReactivation(Substitute.For<IGenericRepository<OwedSubscriptionCancellation>>()));
var command = new VerifyCodeCommand(TestEmail, "999999");

var result = await handler.Handle(command, CancellationToken.None);
Expand All @@ -118,7 +118,7 @@ public async Task VerifyCode_MaxAttempts_ReturnsFailure()
new MemoryCacheEntryOptions { AbsoluteExpirationRelativeToNow = TimeSpan.FromMinutes(15) });
SetupCacheWithCode("123456");

var handler = new VerifyCodeCommandHandler(_cache, _userRepo, _unitOfWork, _authSessionService, new SignupJobDispatcher(_backgroundJobClient, new HttpContextAccessor(), NullLogger<SignupJobDispatcher>.Instance), _backgroundJobClient);
var handler = new VerifyCodeCommandHandler(_cache, _userRepo, _unitOfWork, _authSessionService, new SignupJobDispatcher(_backgroundJobClient, new HttpContextAccessor(), NullLogger<SignupJobDispatcher>.Instance), _backgroundJobClient, new AccountReactivation(Substitute.For<IGenericRepository<OwedSubscriptionCancellation>>()));
var command = new VerifyCodeCommand(TestEmail, "123456");

var result = await handler.Handle(command, CancellationToken.None);
Expand All @@ -134,7 +134,7 @@ public async Task VerifyCode_NewUser_CreatesAccount()
_authSessionService.CreateSessionAsync(Arg.Any<Guid>(), Arg.Any<string>(), Arg.Any<CancellationToken>())
.Returns(Result.Success(new SessionTokens("jwt-token", "refresh-token")));

var handler = new VerifyCodeCommandHandler(_cache, _userRepo, _unitOfWork, _authSessionService, new SignupJobDispatcher(_backgroundJobClient, new HttpContextAccessor(), NullLogger<SignupJobDispatcher>.Instance), _backgroundJobClient);
var handler = new VerifyCodeCommandHandler(_cache, _userRepo, _unitOfWork, _authSessionService, new SignupJobDispatcher(_backgroundJobClient, new HttpContextAccessor(), NullLogger<SignupJobDispatcher>.Instance), _backgroundJobClient, new AccountReactivation(Substitute.For<IGenericRepository<OwedSubscriptionCancellation>>()));
var command = new VerifyCodeCommand(TestEmail, "123456");

var result = await handler.Handle(command, CancellationToken.None);
Expand All @@ -153,7 +153,7 @@ public async Task VerifyCode_ExistingUser_ReturnsToken()
_authSessionService.CreateSessionAsync(Arg.Any<Guid>(), Arg.Any<string>(), Arg.Any<CancellationToken>())
.Returns(Result.Success(new SessionTokens("jwt-token", "refresh-token")));

var handler = new VerifyCodeCommandHandler(_cache, _userRepo, _unitOfWork, _authSessionService, new SignupJobDispatcher(_backgroundJobClient, new HttpContextAccessor(), NullLogger<SignupJobDispatcher>.Instance), _backgroundJobClient);
var handler = new VerifyCodeCommandHandler(_cache, _userRepo, _unitOfWork, _authSessionService, new SignupJobDispatcher(_backgroundJobClient, new HttpContextAccessor(), NullLogger<SignupJobDispatcher>.Instance), _backgroundJobClient, new AccountReactivation(Substitute.For<IGenericRepository<OwedSubscriptionCancellation>>()));
var command = new VerifyCodeCommand(TestEmail, "123456");

var result = await handler.Handle(command, CancellationToken.None);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -63,15 +63,15 @@ private static async Task<List<string>> SignInAsync(bool google, bool exists, bo
});
if (google)
{
var flow = new GoogleSignInFlow(users, work, sessions, new SignupJobDispatcher(jobs, new HttpContextAccessor(), NullLogger<SignupJobDispatcher>.Instance), jobs);
var flow = new GoogleSignInFlow(users, work, sessions, new SignupJobDispatcher(jobs, new HttpContextAccessor(), NullLogger<SignupJobDispatcher>.Instance), jobs, new AccountReactivation(Substitute.For<IGenericRepository<OwedSubscriptionCancellation>>()));
(await flow.CompleteAsync("person@example.com", "Person", "en", withExtras ? "friend" : null, withExtras ? "access" : null, withExtras ? "refresh" : null,
CancellationToken.None)).IsSuccess.Should().BeTrue();
}
else
{
using var cache = new MemoryCache(new MemoryCacheOptions());
cache.Set("verify:person@example.com", new VerificationEntry("123456", 0, DateTime.UtcNow));
var handler = new VerifyCodeCommandHandler(cache, users, work, sessions, new SignupJobDispatcher(jobs, new HttpContextAccessor(), NullLogger<SignupJobDispatcher>.Instance), jobs);
var handler = new VerifyCodeCommandHandler(cache, users, work, sessions, new SignupJobDispatcher(jobs, new HttpContextAccessor(), NullLogger<SignupJobDispatcher>.Instance), jobs, new AccountReactivation(Substitute.For<IGenericRepository<OwedSubscriptionCancellation>>()));
(await handler.Handle(new VerifyCodeCommand("person@example.com", "123456", ReferralCode: withExtras ? "friend" : null),
CancellationToken.None)).IsSuccess.Should().BeTrue();
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -108,11 +108,11 @@ public Scenario()
public Task<Result<LoginResponse>> SignInAsync(bool google, string language, string referral)
{
if (google)
return new GoogleSignInFlow(_users, _work, _sessions, new SignupJobDispatcher(_jobs, new HttpContextAccessor(), NullLogger<SignupJobDispatcher>.Instance), _jobs).CompleteAsync(
return new GoogleSignInFlow(_users, _work, _sessions, new SignupJobDispatcher(_jobs, new HttpContextAccessor(), NullLogger<SignupJobDispatcher>.Instance), _jobs, new AccountReactivation(Substitute.For<IGenericRepository<OwedSubscriptionCancellation>>())).CompleteAsync(
"person@example.com", "Person", language, referral, null, null, CancellationToken.None);

Cache.Set("verify:person@example.com", new VerificationEntry("123456", 0, DateTime.UtcNow));
return new VerifyCodeCommandHandler(Cache, _users, _work, _sessions, new SignupJobDispatcher(_jobs, new HttpContextAccessor(), NullLogger<SignupJobDispatcher>.Instance), _jobs).Handle(
return new VerifyCodeCommandHandler(Cache, _users, _work, _sessions, new SignupJobDispatcher(_jobs, new HttpContextAccessor(), NullLogger<SignupJobDispatcher>.Instance), _jobs, new AccountReactivation(Substitute.For<IGenericRepository<OwedSubscriptionCancellation>>())).Handle(
new VerifyCodeCommand("person@example.com", "123456", language, referral), CancellationToken.None);
}

Expand Down
Loading
Loading