Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion samples/BSkyOauth/BSkyOAuth.Android/BSkyOAuth.csproj
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net9.0-android</TargetFramework>
<TargetFramework>net10.0-android</TargetFramework>
<SupportedOSPlatformVersion>21</SupportedOSPlatformVersion>
<OutputType>Exe</OutputType>
<Nullable>enable</Nullable>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net9.0</TargetFramework>
<TargetFramework>net10.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<PublishAOT>true</PublishAOT>
Expand Down
2 changes: 1 addition & 1 deletion samples/BSkyOauth/BSkyOAuth.MacCatalyst/BSkyOAuth.csproj
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net9.0-maccatalyst</TargetFramework>
<TargetFramework>net10.0-maccatalyst</TargetFramework>
<!-- The default runtime is maccatalyst-x64, except in Release config, in which case the default is maccatalyst-x64;maccatalyst-arm64.
When specifying both architectures, use the plural <RuntimeIdentifiers> instead of the singular <RuntimeIdentifer>.
The App Store will NOT accept apps with ONLY maccatalyst-arm64 indicated;
Expand Down
202 changes: 191 additions & 11 deletions samples/BSkyOauth/BSkyOAuth.MacCatalyst/LoginViewController.cs
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,14 @@ public sealed class LoginViewController : UIViewController

private UIButton authButton;

private UIButton refreshTokenButton;

private UIButton getTimelineButton;

private UIButton saveSessionButton;

private UIButton loadSessionButton;

private UITextField handleField;

/// <summary>
Expand All @@ -31,34 +39,77 @@ public LoginViewController()
this.oauthManager = new OAuthManager(this, "vip.drasticactions", this.OnSuccess, this.OnError);
var atProtocolBuilder = new ATProtocolBuilder();
this.atProtocol = atProtocolBuilder.Build();
this.atProtocol.SessionUpdated += (sender, args) =>
{
Console.WriteLine($"Session updated: {args.Session.ToString()}");
};

this.View!.BackgroundColor = UIColor.SystemBackground;

this.handleField = new UITextField();
this.handleField.Placeholder = "Handle";
this.handleField.TranslatesAutoresizingMaskIntoConstraints = false;
this.View!.AddSubview(this.handleField);

this.authButton = new UIButton(UIButtonType.System);
this.authButton.SetTitle("Authenticate", UIControlState.Normal);
this.authButton.TranslatesAutoresizingMaskIntoConstraints = false;
this.authButton.TouchUpInside += this.AuthButton_TouchUpInside;
this.View!.AddSubview(this.authButton);

this.handleField = new UITextField();
this.handleField.Placeholder = "Handle";
this.refreshTokenButton = new UIButton(UIButtonType.System);
this.refreshTokenButton.SetTitle("Refresh Token", UIControlState.Normal);
this.refreshTokenButton.TranslatesAutoresizingMaskIntoConstraints = false;
this.refreshTokenButton.Enabled = false;
this.refreshTokenButton.TouchUpInside += this.RefreshTokenButton_TouchUpInside;
this.View!.AddSubview(this.refreshTokenButton);

this.View!.AddSubview(this.handleField);
this.handleField.TranslatesAutoresizingMaskIntoConstraints = false;
this.getTimelineButton = new UIButton(UIButtonType.System);
this.getTimelineButton.SetTitle("Get Timeline", UIControlState.Normal);
this.getTimelineButton.TranslatesAutoresizingMaskIntoConstraints = false;
this.getTimelineButton.Enabled = false;
this.getTimelineButton.TouchUpInside += this.GetTimelineButton_TouchUpInside;
this.View!.AddSubview(this.getTimelineButton);

this.saveSessionButton = new UIButton(UIButtonType.System);
this.saveSessionButton.SetTitle("Save Session", UIControlState.Normal);
this.saveSessionButton.TranslatesAutoresizingMaskIntoConstraints = false;
this.saveSessionButton.Enabled = false;
this.saveSessionButton.TouchUpInside += this.SaveSessionButton_TouchUpInside;
this.View!.AddSubview(this.saveSessionButton);

this.loadSessionButton = new UIButton(UIButtonType.System);
this.loadSessionButton.SetTitle("Load Session", UIControlState.Normal);
this.loadSessionButton.TranslatesAutoresizingMaskIntoConstraints = false;
this.loadSessionButton.TouchUpInside += this.LoadSessionButton_TouchUpInside;
this.View!.AddSubview(this.loadSessionButton);

this.View!.AddConstraints(new[]
{
NSLayoutConstraint.Create(this.handleField, NSLayoutAttribute.Top, NSLayoutRelation.Equal, this.View!, NSLayoutAttribute.Top, 1, 100),
NSLayoutConstraint.Create(this.handleField, NSLayoutAttribute.Leading, NSLayoutRelation.Equal, this.View!, NSLayoutAttribute.Leading, 1, 20),
NSLayoutConstraint.Create(this.handleField, NSLayoutAttribute.Trailing, NSLayoutRelation.Equal, this.View!, NSLayoutAttribute.Trailing, 1, -20),
NSLayoutConstraint.Create(this.handleField, NSLayoutAttribute.Height, NSLayoutRelation.Equal, 1, 40),
});

this.authButton.TranslatesAutoresizingMaskIntoConstraints = false;
this.View!.AddConstraints(new[]
{
NSLayoutConstraint.Create(this.authButton, NSLayoutAttribute.Top, NSLayoutRelation.Equal, this.handleField, NSLayoutAttribute.Bottom, 1, 20),
NSLayoutConstraint.Create(this.authButton, NSLayoutAttribute.Leading, NSLayoutRelation.Equal, this.View!, NSLayoutAttribute.Leading, 1, 20),
NSLayoutConstraint.Create(this.authButton, NSLayoutAttribute.Trailing, NSLayoutRelation.Equal, this.View!, NSLayoutAttribute.Trailing, 1, -20),
NSLayoutConstraint.Create(this.authButton, NSLayoutAttribute.Height, NSLayoutRelation.Equal, 1, 40),
NSLayoutConstraint.Create(this.refreshTokenButton, NSLayoutAttribute.Top, NSLayoutRelation.Equal, this.authButton, NSLayoutAttribute.Bottom, 1, 20),
NSLayoutConstraint.Create(this.refreshTokenButton, NSLayoutAttribute.Leading, NSLayoutRelation.Equal, this.View!, NSLayoutAttribute.Leading, 1, 20),
NSLayoutConstraint.Create(this.refreshTokenButton, NSLayoutAttribute.Trailing, NSLayoutRelation.Equal, this.View!, NSLayoutAttribute.Trailing, 1, -20),
NSLayoutConstraint.Create(this.refreshTokenButton, NSLayoutAttribute.Height, NSLayoutRelation.Equal, 1, 40),
NSLayoutConstraint.Create(this.getTimelineButton, NSLayoutAttribute.Top, NSLayoutRelation.Equal, this.refreshTokenButton, NSLayoutAttribute.Bottom, 1, 20),
NSLayoutConstraint.Create(this.getTimelineButton, NSLayoutAttribute.Leading, NSLayoutRelation.Equal, this.View!, NSLayoutAttribute.Leading, 1, 20),
NSLayoutConstraint.Create(this.getTimelineButton, NSLayoutAttribute.Trailing, NSLayoutRelation.Equal, this.View!, NSLayoutAttribute.Trailing, 1, -20),
NSLayoutConstraint.Create(this.getTimelineButton, NSLayoutAttribute.Height, NSLayoutRelation.Equal, 1, 40),
NSLayoutConstraint.Create(this.saveSessionButton, NSLayoutAttribute.Top, NSLayoutRelation.Equal, this.getTimelineButton, NSLayoutAttribute.Bottom, 1, 20),
NSLayoutConstraint.Create(this.saveSessionButton, NSLayoutAttribute.Leading, NSLayoutRelation.Equal, this.View!, NSLayoutAttribute.Leading, 1, 20),
NSLayoutConstraint.Create(this.saveSessionButton, NSLayoutAttribute.Trailing, NSLayoutRelation.Equal, this.View!, NSLayoutAttribute.Trailing, 1, -20),
NSLayoutConstraint.Create(this.saveSessionButton, NSLayoutAttribute.Height, NSLayoutRelation.Equal, 1, 40),
NSLayoutConstraint.Create(this.loadSessionButton, NSLayoutAttribute.Top, NSLayoutRelation.Equal, this.saveSessionButton, NSLayoutAttribute.Bottom, 1, 20),
NSLayoutConstraint.Create(this.loadSessionButton, NSLayoutAttribute.Leading, NSLayoutRelation.Equal, this.View!, NSLayoutAttribute.Leading, 1, 20),
NSLayoutConstraint.Create(this.loadSessionButton, NSLayoutAttribute.Trailing, NSLayoutRelation.Equal, this.View!, NSLayoutAttribute.Trailing, 1, -20),
NSLayoutConstraint.Create(this.loadSessionButton, NSLayoutAttribute.Height, NSLayoutRelation.Equal, 1, 40),
});
}

Expand All @@ -79,7 +130,7 @@ private async void AuthButton_TouchUpInside(object? sender, EventArgs e)
var (uri, error) = await this.atProtocol.GenerateOAuth2AuthenticationUrlResultAsync(
ClientMetadataUrl,
RedirectUri,
new[] { "atproto" },
new[] { "atproto", "transition:generic" },
atIdentifier!);

if (error != null)
Expand Down Expand Up @@ -114,9 +165,11 @@ private async void OnSuccess(NSUrl? callbackUrl)
var (session, error) = await this.atProtocol.AuthenticateWithOAuth2CallbackResultAsync(callbackUrl.ToString());
if (session != null)
{
// We have a session!
this.InvokeOnMainThread(() =>
{
this.refreshTokenButton.Enabled = true;
this.getTimelineButton.Enabled = true;
this.saveSessionButton.Enabled = true;
var alert = UIAlertController.Create("Success", $"Authenticated as {session.Handle}", UIAlertControllerStyle.Alert);
alert.AddAction(UIAlertAction.Create("OK", UIAlertActionStyle.Default, null));
this.PresentViewController(alert, true, null);
Expand Down Expand Up @@ -144,6 +197,133 @@ private async void OnSuccess(NSUrl? callbackUrl)
}
}

private async void RefreshTokenButton_TouchUpInside(object? sender, EventArgs e)
{
var (refreshedSession, refreshError) = await this.atProtocol.RefreshAuthSessionResultAsync();
if (refreshError != null)
{
this.InvokeOnMainThread(() =>
{
var alert = UIAlertController.Create("Error", $"Token refresh failed: {refreshError}", UIAlertControllerStyle.Alert);
alert.AddAction(UIAlertAction.Create("OK", UIAlertActionStyle.Default, null));
this.PresentViewController(alert, true, null);
});

return;
}

this.InvokeOnMainThread(() =>
{
var alert = UIAlertController.Create("Success", "OAuth token refreshed.", UIAlertControllerStyle.Alert);
alert.AddAction(UIAlertAction.Create("OK", UIAlertActionStyle.Default, null));
this.PresentViewController(alert, true, null);
});
}

private async void GetTimelineButton_TouchUpInside(object? sender, EventArgs e)
{
var (timeline, timelineError) = await this.atProtocol.Feed.GetTimelineAsync(limit: 1);
if (timelineError != null)
{
this.InvokeOnMainThread(() =>
{
var alert = UIAlertController.Create("Error", $"GetTimeline failed: {timelineError}", UIAlertControllerStyle.Alert);
alert.AddAction(UIAlertAction.Create("OK", UIAlertActionStyle.Default, null));
this.PresentViewController(alert, true, null);
});

return;
}

var postCount = timeline?.Feed?.Count ?? 0;
this.InvokeOnMainThread(() =>
{
var alert = UIAlertController.Create("Success", $"Timeline returned {postCount} post(s).", UIAlertControllerStyle.Alert);
alert.AddAction(UIAlertAction.Create("OK", UIAlertActionStyle.Default, null));
this.PresentViewController(alert, true, null);
});
}

private void SaveSessionButton_TouchUpInside(object? sender, EventArgs e)
{
var oauthSession = this.atProtocol.OAuthSession;
if (oauthSession == null)
{
this.InvokeOnMainThread(() =>
{
var alert = UIAlertController.Create("Error", "No active OAuth session to save.", UIAlertControllerStyle.Alert);
alert.AddAction(UIAlertAction.Create("OK", UIAlertActionStyle.Default, null));
this.PresentViewController(alert, true, null);
});

return;
}

var json = oauthSession.ToString();
var path = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData), "session.json");
File.WriteAllText(path, json);

this.InvokeOnMainThread(() =>
{
var alert = UIAlertController.Create("Success", $"Session saved to {path}", UIAlertControllerStyle.Alert);
alert.AddAction(UIAlertAction.Create("OK", UIAlertActionStyle.Default, null));
this.PresentViewController(alert, true, null);
});
}

private async void LoadSessionButton_TouchUpInside(object? sender, EventArgs e)
{
var path = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData), "session.json");
if (!File.Exists(path))
{
this.InvokeOnMainThread(() =>
{
var alert = UIAlertController.Create("Error", "No saved session file found.", UIAlertControllerStyle.Alert);
alert.AddAction(UIAlertAction.Create("OK", UIAlertActionStyle.Default, null));
this.PresentViewController(alert, true, null);
});

return;
}

var json = File.ReadAllText(path);
var authSession = AuthSession.FromString(json);
if (authSession == null)
{
this.InvokeOnMainThread(() =>
{
var alert = UIAlertController.Create("Error", "Failed to deserialize session.", UIAlertControllerStyle.Alert);
alert.AddAction(UIAlertAction.Create("OK", UIAlertActionStyle.Default, null));
this.PresentViewController(alert, true, null);
});

return;
}

var (session, error) = await this.atProtocol.AuthenticateWithOAuth2SessionResultAsync(authSession, ClientMetadataUrl);
if (error != null)
{
this.InvokeOnMainThread(() =>
{
var alert = UIAlertController.Create("Error", $"Failed to restore session: {error}", UIAlertControllerStyle.Alert);
alert.AddAction(UIAlertAction.Create("OK", UIAlertActionStyle.Default, null));
this.PresentViewController(alert, true, null);
});

return;
}

this.InvokeOnMainThread(() =>
{
this.refreshTokenButton.Enabled = true;
this.getTimelineButton.Enabled = true;
this.saveSessionButton.Enabled = true;
var alert = UIAlertController.Create("Success", $"Session loaded. Authenticated as {session?.Handle}", UIAlertControllerStyle.Alert);
alert.AddAction(UIAlertAction.Create("OK", UIAlertActionStyle.Default, null));
this.PresentViewController(alert, true, null);
});
}

private void OnError(NSError? error)
{
this.InvokeOnMainThread(() =>
Expand Down
2 changes: 1 addition & 1 deletion samples/BSkyOauth/BSkyOAuth.MacOS/BSkyOAuth.csproj
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net9.0-macos</TargetFramework>
<TargetFramework>net10.0-macos</TargetFramework>
<OutputType>Exe</OutputType>
<Nullable>enable</Nullable>
<ImplicitUsings>true</ImplicitUsings>
Expand Down
2 changes: 1 addition & 1 deletion samples/BSkyOauth/BSkyOAuth.iOS/BSkyOAuth.csproj
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net9.0-ios</TargetFramework>
<TargetFramework>net10.0-ios</TargetFramework>
<OutputType>Exe</OutputType>
<Nullable>enable</Nullable>
<ImplicitUsings>true</ImplicitUsings>
Expand Down
2 changes: 1 addition & 1 deletion samples/BSkyOauth/BSkyOauth.Windows/BSkyOAuth.csproj
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>WinExe</OutputType>
<TargetFramework>net9.0-windows10.0.19041.0</TargetFramework>
<TargetFramework>net10.0-windows10.0.19041.0</TargetFramework>
<TargetPlatformMinVersion>10.0.17763.0</TargetPlatformMinVersion>
<RootNamespace>BSkyOAuth</RootNamespace>
<ApplicationManifest>app.manifest</ApplicationManifest>
Expand Down
2 changes: 1 addition & 1 deletion samples/OAuth/OAuth.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net9.0</TargetFramework>
<TargetFramework>net10.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<PublishAot>true</PublishAot>
Expand Down
25 changes: 20 additions & 5 deletions src/FishyFlip/ATProtocol.cs
Original file line number Diff line number Diff line change
Expand Up @@ -265,17 +265,31 @@ public async Task<string> GenerateOAuth2AuthenticationUrlAsync(string clientId,
/// <param name="session">The OAuth session.</param>
/// <param name="clientId">The client ID.</param>
/// <param name="instanceUrl">Optional. The instance URL. If null, uses https://bsky.social.</param>
/// <param name="cancellationToken">Optional. A CancellationToken that can be used to cancel the operation.</param>
/// <returns>A task that represents the asynchronous operation. The task result contains a Result object with the session details, or null if the session could not be created.</returns>
public async Task<Result<Session?>> AuthenticateWithOAuth2SessionResultAsync(AuthSession session, string clientId, string? instanceUrl = default)
public async Task<Result<Session?>> AuthenticateWithOAuth2SessionResultAsync(AuthSession session, string clientId, string? instanceUrl = default, CancellationToken cancellationToken = default)
{
if (string.IsNullOrEmpty(instanceUrl))
{
var identifier = session.Session.Did;
var (hostUrl, error) = await this.ResolveATIdentifierToHostAddressAsync(identifier, cancellationToken);

if (error is not null)
{
return error;
}

instanceUrl = hostUrl ?? throw new OAuth2Exception("Failed to resolve instance URL from ATIdentifier.");
}

var oAuth2SessionManager = new OAuth2SessionManager(this);
this.SessionManager = oAuth2SessionManager;
if (string.IsNullOrEmpty(session.ProofKey))
{
return new ATError(new OAuth2Exception("Proof key is required for OAuth2 sessions."));
}

var (session2, error2) = await oAuth2SessionManager.StartSessionAsync(session, clientId, instanceUrl);
var (session2, error2) = await oAuth2SessionManager.StartSessionAsync(session, clientId, instanceUrl, cancellationToken);
if (error2 is not null)
{
return error2;
Expand Down Expand Up @@ -308,14 +322,15 @@ public async Task<string> GenerateOAuth2AuthenticationUrlAsync(string clientId,
/// <summary>
/// Refreshes the current session asynchronously.
/// </summary>
/// <param name="token">Cancellation Token.</param>
/// <returns><see cref="AuthSession"/>.</returns>
public async Task<Result<AuthSession?>> RefreshAuthSessionResultAsync()
public async Task<Result<AuthSession?>> RefreshAuthSessionResultAsync(CancellationToken? token = default)
{
switch (this.sessionManager)
{
case OAuth2SessionManager oAuth2SessionManager:
// Refresh the token to make sure it's the most up to date.
var (resultOauth, errorOauth) = await oAuth2SessionManager.RefreshSessionAsync();
var (resultOauth, errorOauth) = await oAuth2SessionManager.RefreshSessionAsync(token ?? CancellationToken.None);
if (errorOauth is not null)
{
return errorOauth;
Expand All @@ -326,7 +341,7 @@ public async Task<string> GenerateOAuth2AuthenticationUrlAsync(string clientId,
// The information from RefreshSessionOutput is set in passwordManager.Session,
// so we can return the session from password manager, and only worry about
// checking for the error.
var (_, error) = await passwordManager.RefreshSessionAsync();
var (_, error) = await passwordManager.RefreshSessionAsync(token ?? CancellationToken.None);
if (error is not null)
{
return error;
Expand Down
1 change: 1 addition & 0 deletions src/FishyFlip/OAuth2SessionManager.cs
Original file line number Diff line number Diff line change
Expand Up @@ -247,6 +247,7 @@ public OAuth2SessionManager(ATProtocol protocol)
refreshSessionOutput.RefreshJwt = result.RefreshToken;
refreshSessionOutput.Did = this.session.Did;
refreshSessionOutput.DidDoc = this.session.DidDoc;

return refreshSessionOutput;
}

Expand Down