Skip to main content

Account

The account popup's actions: signing out, and deleting the account in two steps (request, then confirm). After either one the SDK has already signed the player out and cleared the account's data; your page only opens the Login scene.

You useIt gives you
IProjectVAccountPresenterObserve the account state, sign out, request and confirm deletion
ProjectVAccountSnapshotWhat to draw: status, the pending deletion, the last error
ProjectVAccountDeletionChallengeA pending deletion and the time it expires
ProjectVAccountErrorWhy a command failed: Category, Code, RequestId, LocalizationKey
ProjectVAccountCommandResultThe outcome of one command

Account has no screen contract; the popup uses the presenter directly.

Quick startโ€‹

using System;
using System.Threading;
using Cysharp.Threading.Tasks;
using SilverTiger.ProjectV.Unity.Presentation.Account;
using UnityEngine;
using VContainer;

public sealed class AccountPopup : MonoBehaviour
{
private IProjectVAccountPresenter account = null!;
private IDisposable? subscription;
private CancellationTokenSource? lifetime;

[Inject]
public void Construct(IProjectVAccountPresenter presenter) => account = presenter;

private async void OnEnable()
{
lifetime = new CancellationTokenSource();
var token = lifetime.Token;
var accountSub = await account.SubscribeAsync(OnAccountChanged);
if (token.IsCancellationRequested)
{
// The popup closed while subscribing.
accountSub.Dispose();
return;
}
subscription = accountSub;
}

private void OnDisable()
{
subscription?.Dispose();
subscription = null;
lifetime?.Cancel();
lifetime?.Dispose();
lifetime = null;
}

// May be called off the Unity main thread.
private void OnAccountChanged(ProjectVAccountSnapshot snapshot) => RenderAsync(snapshot).Forget();

private async UniTaskVoid RenderAsync(ProjectVAccountSnapshot snapshot)
{
await UniTask.SwitchToMainThread();
if (this == null)
{
return;
}

// Sent once, right after logout or deletion.
if (snapshot.RequiresLoginNavigation)
{
if (snapshot.RemoteDeletionSucceeded)
{
ShowAccountDeletedNotice();
}
OpenLogin();
return;
}

SetButtonsInteractable(!IsBusy(snapshot.Status));
ShowConfirmStep(snapshot.CanConfirmDeletion, snapshot.PendingDeletion?.ExpiresAt);
ShowError(snapshot.Error?.LocalizationKey);
}

public async void OnLogoutClicked()
{
await account.LogoutAsync(lifetime!.Token);
}

private static bool IsBusy(ProjectVAccountStatus status) =>
status == ProjectVAccountStatus.RequestingDeletion ||
status == ProjectVAccountStatus.ConfirmingDeletion ||
status == ProjectVAccountStatus.ShuttingDown;

private void OpenLogin() { }
private void ShowAccountDeletedNotice() { }
private void SetButtonsInteractable(bool interactable) { }
private void ShowConfirmStep(bool visible, DateTimeOffset? expiresAt) { }
private void ShowError(string? localizationKey) { }
}

That is the whole popup: subscribe, draw, call a command. The listener opens Login; the commands need no handling of their own beyond the deletion flow.

Main thread

SubscribeAsync listeners may run off the Unity main thread. Switch to the main thread before touching Unity objects, as RenderAsync does.

What to drawโ€‹

The listener receives a ProjectVAccountSnapshot:

MemberTypeUse it for
StatusProjectVAccountStatusWhat is running (table below)
RequiresLoginNavigationboolStatus is SignedOut: open the Login scene. Sent once
CanConfirmDeletionboolA deletion is pending: enable the final confirm button
PendingDeletionProjectVAccountDeletionChallenge?The pending deletion; ExpiresAt is when it lapses
RemoteDeletionSucceededboolThe server deleted the account. Tell the player it is gone
ErrorProjectVAccountError?Why the last command failed, or null

For each ProjectVAccountStatus:

StatusWhat to show
ReadyThe account options
RequestingDeletion, ConfirmingDeletion, ShuttingDownA spinner; disable the buttons
AwaitingDeletionConfirmationThe final warning with the confirm button, until PendingDeletion.ExpiresAt
SignedOutNothing; open Login
FailedThe error; the account options again

SignedOut is published once and account.Current then returns to Ready. React to it in the listener or in the command result; never poll Current for it.

Sign outโ€‹

Task<ProjectVAccountCommandResult> LogoutAsync(CancellationToken cancellationToken)
ParameterTypeValue
cancellationTokenCancellationTokenCancels the request, for example when the popup closes

Signs the player out on this device. The account and its progress are kept.

Returns a ProjectVAccountCommandResult:

IsSuccessSnapshot.StatusWhat happened
trueSignedOutThe player is signed out. Open Login (the listener already got it)
falseFailedSigning out failed; show Error and stay on the page

Delete the accountโ€‹

Deletion takes two calls. Nothing is deleted by the first.

Task<ProjectVAccountCommandResult> RequestDeletionAsync(CancellationToken cancellationToken)
Task<ProjectVAccountCommandResult> ConfirmDeletionAsync(CancellationToken cancellationToken)
CommandOn successThen
RequestDeletionAsyncAwaitingDeletionConfirmation; PendingDeletion.ExpiresAt is setShow the final warning
ConfirmDeletionAsyncSignedOut with RemoteDeletionSucceeded trueTell the player the account is gone and open Login

Call ConfirmDeletionAsync only after RequestDeletionAsync has finished and while CanConfirmDeletion is true.

private async UniTask DeleteAsync(string typedConfirmation, CancellationToken token)
{
// The typed phrase is game UI; the SDK does not require one.
if (typedConfirmation != "DELETE")
{
ShowInline("Type DELETE in capital letters to confirm.");
return;
}

var request = await account.RequestDeletionAsync(token);
if (!request.Snapshot.CanConfirmDeletion)
{
ShowError(request.Snapshot.Error?.LocalizationKey);
return;
}

// Normally the player confirms a final warning here.
var confirm = await account.ConfirmDeletionAsync(token);
if (confirm.Snapshot.RemoteDeletionSucceeded)
{
// The account is gone, even if IsSuccess is false.
OpenLogin();
return;
}

ShowError(confirm.Snapshot.Error?.LocalizationKey);
}

Returns for ConfirmDeletionAsync:

IsSuccessRemoteDeletionSucceededStatusWhat happened
truetrueSignedOutDeleted and signed out. Open Login
falsetrueSignedOutDeleted, but signing out on this device then failed (Error is set). Still open Login
falsefalseFailedNot deleted. The pending deletion is cleared; request it again

When it failsโ€‹

Any failed command clears the pending deletion, so the player must request deletion again. Show the message for Error.LocalizationKey (projectv.account.<category>):

SituationError.Category / CodeWhat the player sees
Confirm without a pending deletion, or before the request finishedValidation, PROJECTV_ACCOUNT_DELETE_CONFIRMATION_REQUIREDAsk the player to start deletion again
The deletion request expired or was refusedthe server's codeThe error; start deletion again
Your token firedCancelled, PROJECTV_ACCOUNT_CANCELLEDNothing; the popup is closing
No connection or a timeoutNetwork, Timeout or UnavailableA no-connection message; let the player try again
Anything unexpectedClient, PROJECTV_ACCOUNT_BACKEND_FAILEDA generic error

Put Error.RequestId, when set, in a support panel.

Rulesโ€‹

  • Commands run one at a time. A second tap while one runs waits for it and then runs too; it is not merged. Disable the buttons while busy.
  • Open Login on RequiresLoginNavigation, after logout or deletion. The SDK has already cleared the session and all account data.
  • A Guest who signs in again after deletion gets a new account.

Common mistakesโ€‹

  • Polling Current for SignedOut. It is back to Ready straight away; use the listener or the command result.
  • Treating IsSuccess false after confirm as "not deleted". Check RemoteDeletionSucceeded first.
  • Confirming with an old request. After any failure, call RequestDeletionAsync again.
  • Deleting PlayerPrefs or saved sign-in data yourself. The SDK does it.
  • Touching Unity objects in the listener without switching threads.
  • Not disposing the subscription. Dispose it in OnDisable or OnDestroy.