-
Notifications
You must be signed in to change notification settings - Fork 17
Expand file tree
/
Copy pathAuthenticationScreenController.cs
More file actions
315 lines (267 loc) · 12.6 KB
/
Copy pathAuthenticationScreenController.cs
File metadata and controls
315 lines (267 loc) · 12.6 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
using Arch.Core;
using Cysharp.Threading.Tasks;
using DCL.Audio;
using DCL.AvatarRendering.Wearables;
using DCL.Browser;
using DCL.CharacterPreview;
using DCL.Diagnostics;
using DCL.FeatureFlags;
using DCL.Input;
using DCL.Input.Component;
using DCL.Multiplayer.Connections.DecentralandUrls;
using DCL.Profiles;
using DCL.Profiles.Self;
using DCL.SceneLoadingScreens.SplashScreen;
using DCL.UI;
using DCL.Utilities;
using DCL.Utility;
using DCL.Web3.Authenticators;
using DCL.Web3.Identities;
using DCL.WebRequests;
using MVC;
using System;
using System.Threading;
using Utility;
namespace DCL.AuthenticationScreenFlow
{
public class AuthenticationScreenController : ControllerBase<AuthenticationScreenView>
{
public enum AuthStatus
{
None = 0,
LoginSelectionScreen = 1,
LoginRequested = 2,
VerificationRequested = 3,
ProfileFetching = 4,
LoggedIn = 5,
ProfileFetchingCached = 6,
LoggedInCached = 7,
}
internal const int ANIMATION_DELAY = 300;
internal const string LOADING_TRANSACTION_NAME = "loading_process";
private const string EPIC_STORE_INSTALL_SOURCE = "epic";
private readonly ICompositeWeb3Provider web3Authenticator;
private readonly ISelfProfile selfProfile;
private readonly UnityAppWebBrowser webBrowser;
private readonly IWeb3IdentityCache storedIdentityProvider;
private readonly ICharacterPreviewFactory characterPreviewFactory;
private readonly SplashScreen splashScreen;
private readonly CharacterPreviewEventBus characterPreviewEventBus;
private readonly string installSource;
private readonly AudioMixerVolumesController audioMixerVolumesController;
private readonly World world;
private readonly AuthScreenEmotesSettings emotesSettings;
private readonly AudioClipConfig backgroundMusic;
private readonly IWearablesProvider wearablesProvider;
private readonly IWebRequestController webRequestController;
private readonly IDecentralandUrlsSource decentralandUrlsSource;
private readonly ProfileChangesBus profileChangesBus;
private readonly string? referrer;
private AuthenticationScreenCharacterPreviewController? characterPreviewController;
private readonly IInputBlock inputBlock;
private UniTaskCompletionSource? lifeCycleTask;
private CancellationTokenSource? loginCancellationTokenSource;
public override CanvasOrdering.SortingLayer Layer => CanvasOrdering.SortingLayer.Fullscreen;
public ReactiveProperty<AuthStatus> CurrentState { get; } = new (AuthStatus.None);
public string CurrentRequestID { get; internal set; } = string.Empty;
public LoginMethod CurrentLoginMethod { get; internal set; }
// Set by the Lobby states on Enter so analytics can tag LOGGED_IN / LOGGED_IN_CACHED with
// whether the session created a new account or restored an existing one. Without this flag
// the LOGGED_IN vs LOGGED_IN_CACHED split conflates "fresh auth" with "new account",
// misclassifying returning users whose cached identity expired.
public bool IsCurrentlyNewAccount { get; internal set; }
public event Action? DiscordButtonClicked;
public event Action<string, bool>? OTPVerified;
public event Action? OTPResend;
public event Action? ProfileFinalized;
internal void RaiseProfileFinalized() =>
ProfileFinalized?.Invoke();
// Null until OnViewInstantiated: the view is created lazily on first Show and may never be instantiated.
private MVCStateMachine<AuthStateBase>? fsm;
private AuthenticationScreenAudio? audio;
public AuthenticationScreenController(
ViewFactoryMethod viewFactory,
ICompositeWeb3Provider web3Authenticator,
ISelfProfile selfProfile,
UnityAppWebBrowser webBrowser,
IWeb3IdentityCache storedIdentityProvider,
ICharacterPreviewFactory characterPreviewFactory,
SplashScreen splashScreen,
CharacterPreviewEventBus characterPreviewEventBus,
AudioMixerVolumesController audioMixerVolumesController,
string installSource,
World world,
AuthScreenEmotesSettings emotesSettings,
IInputBlock inputBlock,
AudioClipConfig backgroundMusic,
IWearablesProvider wearablesProvider,
IWebRequestController webRequestController,
IDecentralandUrlsSource decentralandUrlsSource,
ProfileChangesBus profileChangesBus,
string? referrer = null)
: base(viewFactory)
{
this.web3Authenticator = web3Authenticator;
this.selfProfile = selfProfile;
this.webBrowser = webBrowser;
this.storedIdentityProvider = storedIdentityProvider;
this.characterPreviewFactory = characterPreviewFactory;
this.splashScreen = splashScreen;
this.characterPreviewEventBus = characterPreviewEventBus;
this.audioMixerVolumesController = audioMixerVolumesController;
this.installSource = installSource;
this.world = world;
this.emotesSettings = emotesSettings;
this.inputBlock = inputBlock;
this.backgroundMusic = backgroundMusic;
this.wearablesProvider = wearablesProvider;
this.webRequestController = webRequestController;
this.decentralandUrlsSource = decentralandUrlsSource;
this.profileChangesBus = profileChangesBus;
this.referrer = referrer;
}
public override void Dispose()
{
base.Dispose();
characterPreviewController?.Dispose();
CancelLoginProcess();
audio?.Dispose();
fsm?.Dispose();
}
protected override void OnViewInstantiated()
{
base.OnViewInstantiated();
audio = new AuthenticationScreenAudio(viewInstance, audioMixerVolumesController, backgroundMusic);
characterPreviewController = new AuthenticationScreenCharacterPreviewController(viewInstance!.CharacterPreviewView, emotesSettings, characterPreviewFactory, world, characterPreviewEventBus);
bool isEpicBuild = string.Equals(installSource, EPIC_STORE_INSTALL_SOURCE, StringComparison.OrdinalIgnoreCase);
// Epic builds only support emailOTP due to deeplink limitations
// See: https://github.com/decentraland/unity-explorer/issues/9554
bool enableEmailOTP = FeaturesRegistry.Instance.IsEnabled(FeatureId.EmailOTPAuth) || isEpicBuild;
bool otherLoginMethodsEnabled = !isEpicBuild;
viewInstance.LoginSelectionAuthView.EmailOTPContainer.SetActive(enableEmailOTP);
viewInstance.DiscordButton.onClick.AddListener(OpenSupportUrl);
viewInstance.ExitButton.onClick.AddListener(ExitApplication);
// States
fsm = new MVCStateMachine<AuthStateBase>();
fsm.AddStates(
new InitAuthState(viewInstance, installSource),
new LoginSelectionAuthState(fsm, viewInstance, this, CurrentState, splashScreen, web3Authenticator, webBrowser,
enableEmailOTP, otherLoginMethodsEnabled, isEpicBuild),
new ProfileFetchingAuthState(fsm, viewInstance, this, CurrentState, selfProfile, storedIdentityProvider),
new IdentityVerificationDappDeepLinkAuthState(fsm, viewInstance, this, CurrentState, web3Authenticator),
new LobbyForExistingAccountAuthState(fsm, viewInstance, this, splashScreen, CurrentState, characterPreviewController),
new LobbyForNewAccountAuthState(fsm, viewInstance, this, CurrentState, characterPreviewController, selfProfile, wearablesProvider, webBrowser, webRequestController, decentralandUrlsSource, profileChangesBus, referrer)
);
if (enableEmailOTP)
{
var otpVerificationState = new IdentityVerificationOTPAuthState(fsm, viewInstance, this, CurrentState, web3Authenticator);
otpVerificationState.OTPVerified += (email, success) => OTPVerified?.Invoke(email, success);
otpVerificationState.OTPResend += () => OTPResend?.Invoke();
fsm.AddStates(otpVerificationState);
}
fsm.Enter<InitAuthState>();
}
/// <summary>
/// First time view is shown from bootstrap and could have storedIdentity.
/// In all other cases view is shown after logout.
/// </summary>
protected override void OnBeforeViewShow()
{
base.OnBeforeViewShow();
// Force to re-login if the identity will expire in 24hs or less, so we mitigate the chances on
// getting the identity expired while in-world, provoking signed-fetch requests to fail
IWeb3Identity? storedIdentity = storedIdentityProvider.Identity;
if (storedIdentity is { IsExpired: false } && storedIdentity.Expiration - DateTime.UtcNow > TimeSpan.FromDays(1))
{
CancelLoginProcess();
loginCancellationTokenSource = new CancellationTokenSource();
TryAutoLoginAndProceedAsync(storedIdentity, loginCancellationTokenSource.Token).Forget();
}
else
{
fsm!.Enter<LoginSelectionAuthState, int>(UIAnimationHashes.IN, true);
}
}
private async UniTaskVoid TryAutoLoginAndProceedAsync(IWeb3Identity storedIdentity, CancellationToken ct)
{
try
{
bool autoLoginSuccess = await web3Authenticator.TryAutoLoginAsync(ct);
if (autoLoginSuccess)
fsm!.Enter<ProfileFetchingAuthState, ProfileFetchingPayload>(new (storedIdentity, storedIdentity.Source != IWeb3Identity.Web3IdentitySource.TokenFile, ct));
else
{
fsm!.Enter<LoginSelectionAuthState, int>(UIAnimationHashes.IN, true);
}
}
catch (OperationCanceledException)
{ /* Expected on cancellation */
}
catch (Exception e)
{
ReportHub.LogException(e, new ReportData(ReportCategory.AUTHENTICATION));
fsm!.Enter<LoginSelectionAuthState, int>(UIAnimationHashes.IN, true);
}
}
protected override void OnViewShow()
{
base.OnViewShow();
BlockUnwantedInputs();
audio!.OnShow();
}
protected override void OnViewClose()
{
base.OnViewClose();
fsm!.CurrentState?.Exit();
CancelLoginProcess();
UnblockUnwantedInputs();
audio!.OnHide();
}
protected override async UniTask WaitForCloseIntentAsync(CancellationToken ct)
{
lifeCycleTask?.TrySetCanceled(ct);
lifeCycleTask = new UniTaskCompletionSource();
await lifeCycleTask.Task;
}
internal void TrySetLifeCycle()
{
lifeCycleTask?.TrySetResult();
lifeCycleTask = null;
}
internal void CancelLoginProcess()
{
loginCancellationTokenSource?.SafeCancelAndDispose();
loginCancellationTokenSource = null;
}
internal CancellationToken GetRestartedLoginToken()
{
loginCancellationTokenSource = loginCancellationTokenSource.SafeRestart();
return loginCancellationTokenSource.Token;
}
public void ChangeAccount()
{
ChangeAccountAsync(GetRestartedLoginToken()).Forget();
return;
async UniTaskVoid ChangeAccountAsync(CancellationToken ct)
{
await UniTask.Delay(ANIMATION_DELAY, cancellationToken: ct);
await web3Authenticator.LogoutAsync(ct);
fsm!.Enter<LoginSelectionAuthState, int>(UIAnimationHashes.SLIDE, true);
}
}
private void OpenSupportUrl()
{
webBrowser.OpenUrlMainThreadOnly(DecentralandUrl.SupportLink);
DiscordButtonClicked?.Invoke();
}
private void ExitApplication()
{
CancelLoginProcess();
ExitUtils.Exit();
}
private void BlockUnwantedInputs() =>
inputBlock.Disable(InputMapComponent.BLOCK_USER_INPUT);
private void UnblockUnwantedInputs() =>
inputBlock.Enable(InputMapComponent.BLOCK_USER_INPUT);
}
}