Skip to content

Commit 61bf717

Browse files
authored
Apple + Facebook + User Managed Recovery Flow Prep + Async Decryption (#126)
1 parent e4cb157 commit 61bf717

File tree

11 files changed

+1412
-344225
lines changed

11 files changed

+1412
-344225
lines changed

Assets/Thirdweb/Core/Scripts/Wallet.cs

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -680,17 +680,20 @@ public class AuthOptions
680680
{
681681
public AuthProvider authProvider;
682682
public string authToken;
683+
public string recoveryCode;
683684

684685
/// <summary>
685686
/// Initializes a new instance of the <see cref="AuthOptions"/> class with the specified parameters.
686687
/// </summary>
687688
/// <param name="authProvider">The authentication provider to use.</param>
688689
/// <param name="authToken">The auth token to use for validation e.g. jwt</param>
690+
/// <param name="recoveryCode">The recovery code used for CustomAuth when recovery is User Managed</param>
689691
/// <returns>A new instance of the <see cref="AuthOptions"/> class.</returns>
690-
public AuthOptions(AuthProvider authProvider, string authToken = null)
692+
public AuthOptions(AuthProvider authProvider, string authToken = null, string recoveryCode = null)
691693
{
692694
this.authProvider = authProvider;
693695
this.authToken = authToken;
696+
this.recoveryCode = recoveryCode;
694697
}
695698
}
696699

@@ -726,6 +729,16 @@ public enum AuthProvider
726729
/// </summary>
727730
Google,
728731

732+
/// <summary>
733+
/// Apple OAuth2 Flow.
734+
/// </summary>
735+
Apple,
736+
737+
/// <summary>
738+
/// Facebook OAuth2 Flow.
739+
/// </summary>
740+
Facebook,
741+
729742
/// <summary>
730743
/// Bring your own auth.
731744
/// </summary>

Assets/Thirdweb/Core/Scripts/WalletsUI/EmbeddedWalletUI.cs

Lines changed: 135 additions & 82 deletions
Original file line numberDiff line numberDiff line change
@@ -71,20 +71,61 @@ public async Task<User> Connect(EmbeddedWallet embeddedWallet, string email, Aut
7171
RecoveryInput.text = "";
7272
RecoveryInput.gameObject.SetActive(false);
7373
SubmitButton.onClick.RemoveAllListeners();
74+
RecoveryCodesCopy.onClick.RemoveAllListeners();
7475
EmbeddedWalletCanvas.SetActive(false);
7576
RecoveryCodesCanvas.SetActive(false);
7677

77-
switch (authOptions.authProvider)
78+
try
79+
{
80+
string authProvider = authOptions.authProvider switch
81+
{
82+
AuthProvider.EmailOTP => "EmailOTP",
83+
AuthProvider.Google => "Google",
84+
AuthProvider.Apple => "Apple",
85+
AuthProvider.Facebook => "Facebook",
86+
AuthProvider.CustomAuth => "CustomAuth",
87+
_ => throw new UnityException($"Unsupported auth provider: {authOptions.authProvider}"),
88+
};
89+
return await _embeddedWallet.GetUserAsync(_email, authProvider);
90+
}
91+
catch (Exception e)
92+
{
93+
ThirdwebDebug.Log($"Could not recreate user automatically, proceeding with auth: {e.Message}");
94+
}
95+
96+
try
7897
{
79-
case AuthProvider.EmailOTP:
80-
return await LoginWithOTP();
81-
case AuthProvider.Google:
82-
return await LoginWithGoogle();
83-
case AuthProvider.CustomAuth:
84-
return await LoginWithCustomJwt(authOptions.authToken);
85-
default:
86-
throw new UnityException($"Unsupported auth provider: {authOptions.authProvider}");
98+
switch (authOptions.authProvider)
99+
{
100+
case AuthProvider.EmailOTP:
101+
await LoginWithOTP();
102+
break;
103+
case AuthProvider.Google:
104+
await LoginWithOauth("Google");
105+
break;
106+
case AuthProvider.Apple:
107+
await LoginWithOauth("Apple");
108+
break;
109+
case AuthProvider.Facebook:
110+
await LoginWithOauth("Facebook");
111+
break;
112+
case AuthProvider.CustomAuth:
113+
await LoginWithCustomJwt(authOptions.authToken, authOptions.recoveryCode);
114+
break;
115+
default:
116+
throw new UnityException($"Unsupported auth provider: {authOptions.authProvider}");
117+
}
87118
}
119+
catch (Exception e)
120+
{
121+
_exception = e;
122+
}
123+
124+
await new WaitUntil(() => _user != null || _exception != null);
125+
EmbeddedWalletCanvas.SetActive(false);
126+
if (_exception != null)
127+
throw _exception;
128+
return _user;
88129
}
89130

90131
public void Cancel()
@@ -96,45 +137,26 @@ public void Cancel()
96137

97138
#region Email OTP Flow
98139

99-
private async Task<User> LoginWithOTP()
140+
private async Task LoginWithOTP()
100141
{
101142
if (_email == null)
102143
throw new UnityException("Email is required for OTP login");
103144

104-
try
105-
{
106-
_user = await _embeddedWallet.GetUserAsync(_email);
107-
}
108-
catch (Exception e)
109-
{
110-
ThirdwebDebug.Log($"Could not recreate user automatically, proceeding with auth: {e}");
111-
}
112-
113-
if (_user != null)
114-
{
115-
ThirdwebDebug.Log($"Logged In Existing User - Email: {_user.EmailAddress}, User Address: {_user.Account.Address}");
116-
return _user;
117-
}
118-
119145
SubmitButton.onClick.AddListener(OnSubmitOTP);
120146
await OnSendOTP();
121147
EmbeddedWalletCanvas.SetActive(true);
122-
await new WaitUntil(() => _user != null || _exception != null);
123-
EmbeddedWalletCanvas.SetActive(false);
124-
if (_exception != null)
125-
throw _exception;
126-
return _user;
127148
}
128149

129150
private async Task OnSendOTP()
130151
{
131152
try
132153
{
133154
(bool isNewUser, bool isNewDevice, bool needsRecoveryCode) = await _embeddedWallet.SendOtpEmailAsync(_email);
134-
RecoveryInput.gameObject.SetActive(needsRecoveryCode && !isNewUser && isNewDevice);
155+
if (needsRecoveryCode && !isNewUser && isNewDevice)
156+
DisplayRecoveryInput(false);
135157
ThirdwebDebug.Log($"finished sending OTP: isNewUser {isNewUser}, isNewDevice {isNewDevice}");
136158
}
137-
catch (System.Exception e)
159+
catch (Exception e)
138160
{
139161
_exception = e;
140162
}
@@ -148,15 +170,9 @@ private async void OnSubmitOTP()
148170
try
149171
{
150172
string otp = OTPInput.text;
151-
(var res, bool? wasEmailed) = await _embeddedWallet.VerifyOtpAsync(_email, otp, string.IsNullOrEmpty(RecoveryInput.text) ? null : RecoveryInput.text);
173+
var res = await _embeddedWallet.VerifyOtpAsync(_email, otp, string.IsNullOrEmpty(RecoveryInput.text) ? null : RecoveryInput.text);
152174
_user = res.User;
153-
if (res.MainRecoveryCode != null && wasEmailed.HasValue && wasEmailed.Value == false)
154-
{
155-
List<string> recoveryCodes = new() { res.MainRecoveryCode };
156-
if (res.BackupRecoveryCodes != null)
157-
recoveryCodes.AddRange(res.BackupRecoveryCodes);
158-
ShowRecoveryCodes(recoveryCodes);
159-
}
175+
ShowRecoveryCodes(res);
160176
}
161177
catch (Exception e)
162178
{
@@ -170,78 +186,115 @@ private async void OnSubmitOTP()
170186
}
171187
}
172188

173-
private void ShowRecoveryCodes(List<string> recoveryCodes)
174-
{
175-
string recoveryCodesString = string.Join("\n", recoveryCodes.Select((code, i) => $"{i + 1}. {code}"));
176-
string message = $"Please save the following recovery codes in a safe place:\n\n{recoveryCodesString}";
177-
ThirdwebDebug.Log(message);
178-
RecoveryCodesText.text = message;
179-
string messageToSave = JsonConvert.SerializeObject(recoveryCodes);
180-
RecoveryCodesCopy.onClick.RemoveAllListeners();
181-
RecoveryCodesCopy.onClick.AddListener(() => GUIUtility.systemCopyBuffer = messageToSave);
182-
RecoveryCodesCanvas.SetActive(true);
183-
}
184-
185189
#endregion
186190

187191
#region OAuth2 Flow
188192

189-
private async Task<User> LoginWithGoogle()
193+
private async Task LoginWithOauth(string authProviderStr)
190194
{
191195
if (Application.isMobilePlatform && string.IsNullOrEmpty(_customScheme))
192196
throw new UnityException("No custom scheme provided for mobile deeplinks, please set one in your ThirdwebConfig (found in ThirdwebManager)");
193197

194-
try
195-
{
196-
string loginUrl = await GetLoginLink();
197-
string redirectUrl = Application.isMobilePlatform ? _customScheme : "http://localhost:8789/";
198-
CrossPlatformBrowser browser = new();
199-
var browserResult = await browser.Login(loginUrl, redirectUrl);
200-
if (browserResult.status != BrowserStatus.Success)
201-
_exception = new UnityException($"Failed to login with Google: {browserResult.status} | {browserResult.error}");
202-
else
203-
_callbackUrl = browserResult.callbackUrl;
204-
}
205-
catch (Exception e)
206-
{
207-
_exception = e;
208-
}
198+
string loginUrl = await GetLoginLink(authProviderStr);
209199

210-
await new WaitUntil(() => _callbackUrl != null || _exception != null);
211-
if (_exception != null)
212-
throw _exception;
200+
string redirectUrl = Application.isMobilePlatform ? _customScheme : "http://localhost:8789/";
201+
CrossPlatformBrowser browser = new();
202+
var browserResult = await browser.Login(loginUrl, redirectUrl);
203+
if (browserResult.status != BrowserStatus.Success)
204+
_exception = new UnityException($"Failed to login with {authProviderStr}: {browserResult.status} | {browserResult.error}");
205+
else
206+
_callbackUrl = browserResult.callbackUrl;
207+
208+
await new WaitUntil(() => _callbackUrl != null);
213209

214210
string decodedUrl = HttpUtility.UrlDecode(_callbackUrl);
215211
Uri uri = new(decodedUrl);
216212
string queryString = uri.Query;
217213
var queryDict = HttpUtility.ParseQueryString(queryString);
218214
string authResultJson = queryDict["authResult"];
219-
var user = await _embeddedWallet.SignInWithGoogleAsync(authResultJson);
220-
return user;
215+
216+
bool needsRecoveryCode = await _embeddedWallet.IsRecoveryCodeNeededAsync(authResultJson);
217+
218+
if (needsRecoveryCode)
219+
{
220+
SubmitButton.onClick.AddListener(() => OnSubmitRecoveryOauth(authProviderStr, authResultJson));
221+
DisplayRecoveryInput(true);
222+
}
223+
else
224+
{
225+
try
226+
{
227+
var res = await _embeddedWallet.SignInWithOauth(authProviderStr, authResultJson, null);
228+
_user = res.User;
229+
}
230+
catch (Exception e)
231+
{
232+
_exception = e;
233+
}
234+
}
221235
}
222236

223-
private async Task<string> GetLoginLink(string authProvider = "Google")
237+
private async void OnSubmitRecoveryOauth(string authProviderStr, string authResult)
238+
{
239+
try
240+
{
241+
string recoveryCode = RecoveryInput.text;
242+
var res = await _embeddedWallet.SignInWithOauth(authProviderStr, authResult, recoveryCode);
243+
_user = res.User;
244+
ShowRecoveryCodes(res);
245+
}
246+
catch (Exception e)
247+
{
248+
_exception = e;
249+
}
250+
}
251+
252+
private async Task<string> GetLoginLink(string authProvider)
224253
{
225254
string loginUrl = await _embeddedWallet.FetchHeadlessOauthLoginLinkAsync(authProvider);
226255
string platform = "unity";
227256
string redirectUrl = UnityWebRequest.EscapeURL(Application.isMobilePlatform ? _customScheme : "http://localhost:8789/");
228257
string developerClientId = UnityWebRequest.EscapeURL(ThirdwebManager.Instance.SDK.session.Options.clientId);
229-
return $"{loginUrl}?platform={platform}&redirectUrl={redirectUrl}&developerClientId={developerClientId}";
258+
return $"{loginUrl}?platform={platform}&redirectUrl={redirectUrl}&developerClientId={developerClientId}&authOption={authProvider}";
230259
}
231260

232261
#endregion
233262

234263
#region Custom JWT Flow
235264

236-
private async Task<User> LoginWithCustomJwt(string jwtToken)
265+
private async Task LoginWithCustomJwt(string jwtToken, string recoveryCode)
237266
{
238-
try
239-
{
240-
return await _embeddedWallet.SignInWithJwtAuthAsync(jwtToken);
241-
}
242-
catch
267+
var res = await _embeddedWallet.SignInWithJwtAuthAsync(jwtToken, recoveryCode);
268+
_user = res.User;
269+
ShowRecoveryCodes(res);
270+
}
271+
272+
#endregion
273+
274+
#region Common
275+
276+
private void DisplayRecoveryInput(bool hideOtpInput)
277+
{
278+
if (hideOtpInput)
279+
OTPInput.gameObject.SetActive(false);
280+
RecoveryInput.gameObject.SetActive(true);
281+
EmbeddedWalletCanvas.SetActive(true);
282+
}
283+
284+
private void ShowRecoveryCodes(EmbeddedWallet.VerifyResult res)
285+
{
286+
if (res.MainRecoveryCode != null && res.WasEmailed.HasValue && res.WasEmailed.Value == false)
243287
{
244-
throw;
288+
List<string> recoveryCodes = new() { res.MainRecoveryCode };
289+
if (res.BackupRecoveryCodes != null)
290+
recoveryCodes.AddRange(res.BackupRecoveryCodes);
291+
string recoveryCodesString = string.Join("\n", recoveryCodes.Select((code, i) => $"{i + 1}. {code}"));
292+
string message = $"Please save the following recovery codes in a safe place:\n\n{recoveryCodesString}";
293+
ThirdwebDebug.Log(message);
294+
RecoveryCodesText.text = message;
295+
string messageToSave = JsonConvert.SerializeObject(recoveryCodes);
296+
RecoveryCodesCopy.onClick.AddListener(() => GUIUtility.systemCopyBuffer = messageToSave);
297+
RecoveryCodesCanvas.SetActive(true);
245298
}
246299
}
247300

0 commit comments

Comments
 (0)