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 use | It gives you |
|---|---|
IProjectVAccountPresenter | Observe the account state, sign out, request and confirm deletion |
ProjectVAccountSnapshot | What to draw: status, the pending deletion, the last error |
ProjectVAccountDeletionChallenge | A pending deletion and the time it expires |
ProjectVAccountError | Why a command failed: Category, Code, RequestId, LocalizationKey |
ProjectVAccountCommandResult | The 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.
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:
| Member | Type | Use it for |
|---|---|---|
Status | ProjectVAccountStatus | What is running (table below) |
RequiresLoginNavigation | bool | Status is SignedOut: open the Login scene. Sent once |
CanConfirmDeletion | bool | A deletion is pending: enable the final confirm button |
PendingDeletion | ProjectVAccountDeletionChallenge? | The pending deletion; ExpiresAt is when it lapses |
RemoteDeletionSucceeded | bool | The server deleted the account. Tell the player it is gone |
Error | ProjectVAccountError? | Why the last command failed, or null |
For each ProjectVAccountStatus:
| Status | What to show |
|---|---|
Ready | The account options |
RequestingDeletion, ConfirmingDeletion, ShuttingDown | A spinner; disable the buttons |
AwaitingDeletionConfirmation | The final warning with the confirm button, until PendingDeletion.ExpiresAt |
SignedOut | Nothing; open Login |
Failed | The 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)
| Parameter | Type | Value |
|---|---|---|
cancellationToken | CancellationToken | Cancels 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:
IsSuccess | Snapshot.Status | What happened |
|---|---|---|
true | SignedOut | The player is signed out. Open Login (the listener already got it) |
false | Failed | Signing 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)
| Command | On success | Then |
|---|---|---|
RequestDeletionAsync | AwaitingDeletionConfirmation; PendingDeletion.ExpiresAt is set | Show the final warning |
ConfirmDeletionAsync | SignedOut with RemoteDeletionSucceeded true | Tell 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:
IsSuccess | RemoteDeletionSucceeded | Status | What happened |
|---|---|---|---|
true | true | SignedOut | Deleted and signed out. Open Login |
false | true | SignedOut | Deleted, but signing out on this device then failed (Error is set). Still open Login |
false | false | Failed | Not 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>):
| Situation | Error.Category / Code | What the player sees |
|---|---|---|
| Confirm without a pending deletion, or before the request finished | Validation, PROJECTV_ACCOUNT_DELETE_CONFIRMATION_REQUIRED | Ask the player to start deletion again |
| The deletion request expired or was refused | the server's code | The error; start deletion again |
| Your token fired | Cancelled, PROJECTV_ACCOUNT_CANCELLED | Nothing; the popup is closing |
| No connection or a timeout | Network, Timeout or Unavailable | A no-connection message; let the player try again |
| Anything unexpected | Client, PROJECTV_ACCOUNT_BACKEND_FAILED | A 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
CurrentforSignedOut. It is back toReadystraight away; use the listener or the command result. - Treating
IsSuccessfalse after confirm as "not deleted". CheckRemoteDeletionSucceededfirst. - Confirming with an old request. After any failure, call
RequestDeletionAsyncagain. - 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
OnDisableorOnDestroy.