Skip to content

Commit bd059d4

Browse files
authored
All-in-one Login with Oauth Flow (#20)
* All-in-one Login with Oauth Flow * Update Thirdweb.Transactions.Tests.cs --------- Signed-off-by: Firekeeper <[email protected]>
1 parent d0d24d7 commit bd059d4

File tree

5 files changed

+261
-31
lines changed

5 files changed

+261
-31
lines changed

Thirdweb.Console/Program.cs

Lines changed: 35 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
using Newtonsoft.Json;
55
using Nethereum.RPC.Eth.DTOs;
66
using Nethereum.Hex.HexTypes;
7+
using System.Diagnostics;
78

89
DotEnv.Load();
910

@@ -22,32 +23,45 @@
2223

2324
// Create wallets (this is an advanced use case, typically one wallet is plenty)
2425
var privateKeyWallet = await PrivateKeyWallet.Create(client: client, privateKeyHex: privateKey);
25-
var inAppWallet = await InAppWallet.Create(client: client, email: "[email protected]"); // or email: null, phoneNumber: "+1234567890"
2626

27-
// // Reset InAppWallet (optional step for testing login flow)
28-
// if (await inAppWallet.IsConnected())
29-
// {
30-
// await inAppWallet.Disconnect();
31-
// }
27+
// var inAppWallet = await InAppWallet.Create(client: client, email: "[email protected]"); // or email: null, phoneNumber: "+1234567890"
28+
var inAppWallet = await InAppWallet.Create(client: client, authprovider: AuthProvider.Google); // or email: null, phoneNumber: "+1234567890"
29+
30+
// Reset InAppWallet (optional step for testing login flow)
31+
if (await inAppWallet.IsConnected())
32+
{
33+
await inAppWallet.Disconnect();
34+
}
3235

3336
// Relog if InAppWallet not logged in
3437
if (!await inAppWallet.IsConnected())
3538
{
36-
await inAppWallet.SendOTP();
37-
Console.WriteLine("Please submit the OTP.");
38-
var otp = Console.ReadLine();
39-
(var inAppWalletAddress, var canRetry) = await inAppWallet.SubmitOTP(otp);
40-
if (inAppWalletAddress == null && canRetry)
41-
{
42-
Console.WriteLine("Please submit the OTP again.");
43-
otp = Console.ReadLine();
44-
(inAppWalletAddress, _) = await inAppWallet.SubmitOTP(otp);
45-
}
46-
if (inAppWalletAddress == null)
47-
{
48-
Console.WriteLine("OTP login failed. Please try again.");
49-
return;
50-
}
39+
var address = await inAppWallet.LoginWithOauth(
40+
isMobile: false,
41+
(url) =>
42+
{
43+
var psi = new ProcessStartInfo { FileName = url, UseShellExecute = true };
44+
_ = Process.Start(psi);
45+
},
46+
"thirdweb://",
47+
new InAppWalletBrowser()
48+
);
49+
Console.WriteLine($"InAppWallet address: {address}");
50+
// await inAppWallet.SendOTP();
51+
// Console.WriteLine("Please submit the OTP.");
52+
// var otp = Console.ReadLine();
53+
// (var inAppWalletAddress, var canRetry) = await inAppWallet.SubmitOTP(otp);
54+
// if (inAppWalletAddress == null && canRetry)
55+
// {
56+
// Console.WriteLine("Please submit the OTP again.");
57+
// otp = Console.ReadLine();
58+
// (inAppWalletAddress, _) = await inAppWallet.SubmitOTP(otp);
59+
// }
60+
// if (inAppWalletAddress == null)
61+
// {
62+
// Console.WriteLine("OTP login failed. Please try again.");
63+
// return;
64+
// }
5165
}
5266

5367
// Create smart wallet with InAppWallet signer

Thirdweb.Tests/Thirdweb.Transactions.Tests.cs

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -221,10 +221,13 @@ public async Task EstimateTotalCosts_HigherThanGasCostsByValue()
221221
_ = transaction.SetValue(new BigInteger(1000000000000000000)); // 100 gwei accounting for fluctuations
222222
_ = transaction.SetGasLimit(21000);
223223

224-
var totalCosts = await ThirdwebTransaction.EstimateTotalCosts(transaction);
225-
var gasCosts = await ThirdwebTransaction.EstimateGasCosts(transaction);
224+
var totalCostsTask = ThirdwebTransaction.EstimateTotalCosts(transaction);
225+
var gasCostsTask = ThirdwebTransaction.EstimateGasCosts(transaction);
226226

227-
Assert.True(totalCosts.wei > gasCosts.wei);
227+
var costs = await Task.WhenAll(totalCostsTask, gasCostsTask);
228+
229+
Assert.True(costs[0].wei > costs[1].wei);
230+
Assert.True(costs[0].wei - costs[1].wei == transaction.Input.Value.Value);
228231
}
229232

230233
[Fact]
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
public interface IThirdwebBrowser
2+
{
3+
Task<BrowserResult> Login(string loginUrl, string redirectUrl, Action<string> browserOpenAction, CancellationToken cancellationToken = default);
4+
}
5+
6+
public enum BrowserStatus
7+
{
8+
Success,
9+
UserCanceled,
10+
Timeout,
11+
UnknownError,
12+
}
13+
14+
public class BrowserResult
15+
{
16+
public BrowserStatus status { get; }
17+
18+
public string callbackUrl { get; }
19+
20+
public string error { get; }
21+
22+
public BrowserResult(BrowserStatus status, string callbackUrl)
23+
{
24+
this.status = status;
25+
this.callbackUrl = callbackUrl;
26+
}
27+
28+
public BrowserResult(BrowserStatus status, string callbackUrl, string error)
29+
{
30+
this.status = status;
31+
this.callbackUrl = callbackUrl;
32+
this.error = error;
33+
}
34+
}

Thirdweb/Thirdweb.Wallets/InAppWallet/InAppWallet.cs

Lines changed: 83 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,42 +1,65 @@
1+
using System.Web;
12
using Nethereum.Signer;
23
using Thirdweb.EWS;
34

45
namespace Thirdweb
56
{
7+
public enum AuthProvider
8+
{
9+
Default,
10+
Google,
11+
Apple,
12+
Facebook,
13+
// JWT,
14+
// AuthEndpoint
15+
}
16+
617
public class InAppWallet : PrivateKeyWallet
718
{
819
internal EmbeddedWallet _embeddedWallet;
920
internal string _email;
1021
internal string _phoneNumber;
22+
internal string _authProvider;
1123

12-
internal InAppWallet(ThirdwebClient client, string email, string phoneNumber, EmbeddedWallet embeddedWallet, EthECKey ecKey)
24+
internal InAppWallet(ThirdwebClient client, string email, string phoneNumber, string authProvider, EmbeddedWallet embeddedWallet, EthECKey ecKey)
1325
: base(client, ecKey)
1426
{
1527
_email = email;
1628
_phoneNumber = phoneNumber;
1729
_embeddedWallet = embeddedWallet;
30+
_authProvider = authProvider;
1831
}
1932

20-
public static async Task<InAppWallet> Create(ThirdwebClient client, string email = null, string phoneNumber = null)
33+
public static async Task<InAppWallet> Create(ThirdwebClient client, string email = null, string phoneNumber = null, AuthProvider authprovider = AuthProvider.Default)
2134
{
22-
if (string.IsNullOrEmpty(email) && string.IsNullOrEmpty(phoneNumber))
35+
if (string.IsNullOrEmpty(email) && string.IsNullOrEmpty(phoneNumber) && authprovider == AuthProvider.Default)
2336
{
24-
throw new ArgumentException("Email or Phone Number must be provided to login.");
37+
throw new ArgumentException("Email, Phone Number, or OAuth Provider must be provided to login.");
2538
}
2639

40+
var authproviderStr = authprovider switch
41+
{
42+
AuthProvider.Google => "Google",
43+
AuthProvider.Apple => "Apple",
44+
AuthProvider.Facebook => "Facebook",
45+
AuthProvider.Default => string.IsNullOrEmpty(email) ? "PhoneOTP" : "EmailOTP",
46+
_ => throw new ArgumentException("Invalid AuthProvider"),
47+
};
48+
2749
var embeddedWallet = new EmbeddedWallet(client);
2850
EthECKey ecKey;
2951
try
3052
{
31-
var user = await embeddedWallet.GetUserAsync(email, email == null ? "PhoneOTP" : "EmailOTP");
53+
if (!string.IsNullOrEmpty(authproviderStr)) { }
54+
var user = await embeddedWallet.GetUserAsync(email, authproviderStr);
3255
ecKey = new EthECKey(user.Account.PrivateKey);
3356
}
3457
catch
3558
{
36-
Console.WriteLine("User not found. Please call InAppWallet.SendOTP() to initialize the login process.");
59+
Console.WriteLine("User not found. Please call InAppWallet.SendOTP() or InAppWallet.LoginWithOauth to initialize the login process.");
3760
ecKey = null;
3861
}
39-
return new InAppWallet(client, email, phoneNumber, embeddedWallet, ecKey);
62+
return new InAppWallet(client, email, phoneNumber, authproviderStr, embeddedWallet, ecKey);
4063
}
4164

4265
public override async Task Disconnect()
@@ -45,6 +68,59 @@ public override async Task Disconnect()
4568
await _embeddedWallet.SignOutAsync();
4669
}
4770

71+
#region OAuth2 Flow
72+
73+
public virtual async Task<string> LoginWithOauth(
74+
bool isMobile,
75+
Action<string> browserOpenAction,
76+
string mobileRedirectScheme = "thirdweb://",
77+
IThirdwebBrowser browser = null,
78+
CancellationToken cancellationToken = default
79+
)
80+
{
81+
if (isMobile && string.IsNullOrEmpty(mobileRedirectScheme))
82+
{
83+
throw new ArgumentNullException(nameof(mobileRedirectScheme), "Mobile redirect scheme cannot be null or empty on this platform.");
84+
}
85+
86+
var platform = "unity";
87+
var redirectUrl = isMobile ? mobileRedirectScheme : "http://localhost:8789/";
88+
var loginUrl = await _embeddedWallet.FetchHeadlessOauthLoginLinkAsync(_authProvider);
89+
loginUrl = $"{loginUrl}?platform={platform}&redirectUrl={redirectUrl}&developerClientId={_client.ClientId}&authOption={_authProvider}";
90+
91+
browser ??= new InAppWalletBrowser();
92+
var browserResult = await browser.Login(loginUrl, redirectUrl, browserOpenAction, cancellationToken);
93+
var callbackUrl =
94+
browserResult.status != BrowserStatus.Success
95+
? throw new Exception($"Failed to login with {_authProvider}: {browserResult.status} | {browserResult.error}")
96+
: browserResult.callbackUrl;
97+
98+
while (string.IsNullOrEmpty(callbackUrl))
99+
{
100+
if (cancellationToken.IsCancellationRequested)
101+
{
102+
throw new TaskCanceledException("LoginWithOauth was cancelled.");
103+
}
104+
await Task.Delay(100, cancellationToken);
105+
}
106+
107+
var decodedUrl = HttpUtility.UrlDecode(callbackUrl);
108+
Uri uri = new(decodedUrl);
109+
var queryString = uri.Query;
110+
var queryDict = HttpUtility.ParseQueryString(queryString);
111+
var authResultJson = queryDict["authResult"];
112+
113+
var res = await _embeddedWallet.SignInWithOauthAsync(_authProvider, authResultJson, null);
114+
if (res.User == null)
115+
{
116+
throw new Exception("Failed to login with OAuth2");
117+
}
118+
_ecKey = new EthECKey(res.User.Account.PrivateKey);
119+
return await GetAddress();
120+
}
121+
122+
#endregion
123+
48124
#region OTP Flow
49125

50126
public async Task SendOTP()
Lines changed: 103 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,103 @@
1+
using System.Net;
2+
3+
namespace Thirdweb
4+
{
5+
public class InAppWalletBrowser : IThirdwebBrowser
6+
{
7+
private TaskCompletionSource<BrowserResult> _taskCompletionSource;
8+
9+
private readonly string closePageResponse =
10+
@"
11+
<html>
12+
<head>
13+
<style>
14+
body {
15+
font-family: Arial, sans-serif;
16+
background-color: #2c2c2c;
17+
color: #ffffff;
18+
display: flex;
19+
justify-content: center;
20+
align-items: center;
21+
height: 100vh;
22+
flex-direction: column;
23+
}
24+
.container {
25+
background-color: #3c3c3c;
26+
padding: 20px;
27+
border-radius: 10px;
28+
box-shadow: 0 0 10px rgba(0,0,0,0.3);
29+
text-align: center;
30+
}
31+
.instruction {
32+
margin-top: 20px;
33+
font-size: 18px;
34+
}
35+
</style>
36+
</head>
37+
<body>
38+
<div class='container'>
39+
<b>DONE!</b>
40+
<div class='instruction'>
41+
You can close this tab/window now.
42+
</div>
43+
</div>
44+
</body>
45+
</html>";
46+
47+
public async Task<BrowserResult> Login(string loginUrl, string redirectUrl, Action<string> browserOpenAction, CancellationToken cancellationToken = default)
48+
{
49+
_taskCompletionSource = new TaskCompletionSource<BrowserResult>();
50+
51+
cancellationToken.Register(() =>
52+
{
53+
_taskCompletionSource?.TrySetCanceled();
54+
});
55+
56+
using var httpListener = new HttpListener();
57+
58+
try
59+
{
60+
redirectUrl = AddForwardSlashIfNecessary(redirectUrl);
61+
httpListener.Prefixes.Add(redirectUrl);
62+
httpListener.Start();
63+
_ = httpListener.BeginGetContext(IncomingHttpRequest, httpListener);
64+
65+
Console.WriteLine($"Opening browser with URL: {loginUrl}");
66+
browserOpenAction.Invoke(loginUrl);
67+
68+
var completedTask = await Task.WhenAny(_taskCompletionSource.Task, Task.Delay(TimeSpan.FromSeconds(30), cancellationToken));
69+
return completedTask == _taskCompletionSource.Task ? await _taskCompletionSource.Task : new BrowserResult(BrowserStatus.Timeout, null, "The operation timed out.");
70+
}
71+
finally
72+
{
73+
httpListener.Stop();
74+
}
75+
}
76+
77+
private void IncomingHttpRequest(IAsyncResult result)
78+
{
79+
var httpListener = (HttpListener)result.AsyncState;
80+
var httpContext = httpListener.EndGetContext(result);
81+
var httpRequest = httpContext.Request;
82+
var httpResponse = httpContext.Response;
83+
var buffer = System.Text.Encoding.UTF8.GetBytes(closePageResponse);
84+
85+
httpResponse.ContentLength64 = buffer.Length;
86+
var output = httpResponse.OutputStream;
87+
output.Write(buffer, 0, buffer.Length);
88+
output.Close();
89+
90+
_taskCompletionSource.SetResult(new BrowserResult(BrowserStatus.Success, httpRequest.Url.ToString()));
91+
}
92+
93+
private string AddForwardSlashIfNecessary(string url)
94+
{
95+
string forwardSlash = "/";
96+
if (!url.EndsWith(forwardSlash))
97+
{
98+
url += forwardSlash;
99+
}
100+
return url;
101+
}
102+
}
103+
}

0 commit comments

Comments
 (0)