From e7c01caa742e586eaf30c14c644a2c06de60f0e4 Mon Sep 17 00:00:00 2001 From: Diego Santo Date: Mon, 7 Sep 2026 22:23:42 -0300 Subject: [PATCH 1/8] fix(gotrue): change following review feat(gotrue): add missing param to public doc feat(gotrue): fix resend param and add to public api doc fix(gotrue): add missing public param to public doc api feat(gotrue): add resend api to request confirmation code --- .../Authentication/ResendParametersTests.cs | 54 +++ packages/Gotrue/Gotrue/Api.cs | 12 + packages/Gotrue/Gotrue/Client.cs | 5 + packages/Gotrue/Gotrue/Constants.cs | 350 ++++++++++-------- .../Gotrue/Gotrue/Interfaces/IGotrueApi.cs | 8 + .../Gotrue/Gotrue/Interfaces/IGotrueClient.cs | 9 + .../Gotrue/Gotrue/PublicAPI.Unshipped.txt | 25 ++ .../Gotrue/Gotrue/Resend/ResendOptions.cs | 23 ++ .../Gotrue/Gotrue/Resend/ResendParameters.cs | 42 +++ packages/Gotrue/Gotrue/Resend/ResendType.cs | 30 ++ 10 files changed, 398 insertions(+), 160 deletions(-) create mode 100644 packages/Gotrue/Gotrue.Tests/Authentication/ResendParametersTests.cs create mode 100644 packages/Gotrue/Gotrue/Resend/ResendOptions.cs create mode 100644 packages/Gotrue/Gotrue/Resend/ResendParameters.cs create mode 100644 packages/Gotrue/Gotrue/Resend/ResendType.cs diff --git a/packages/Gotrue/Gotrue.Tests/Authentication/ResendParametersTests.cs b/packages/Gotrue/Gotrue.Tests/Authentication/ResendParametersTests.cs new file mode 100644 index 00000000..d315c3cd --- /dev/null +++ b/packages/Gotrue/Gotrue.Tests/Authentication/ResendParametersTests.cs @@ -0,0 +1,54 @@ +using System.Net; +using System.Threading.Tasks; +using Gotrue.Tests.Support; +using Microsoft.VisualStudio.TestTools.UnitTesting; +using Supabase.Gotrue.Resend; +using static Gotrue.Tests.TestUtils; + +namespace Gotrue.Tests.Authentication; + +[TestClass] +[TestCategory("E2E")] +public class ResendParametersTests : AuthClientFixture +{ + [TestMethod] + public async Task Resend_ShouldRequestConfirmationCode_GivenSignUpType() + { + var email = RandomEmail(); + var resend = new ResendParameters(ResendType.SignUp) { Email = email }; + var response = await this.Client.Resend(resend); + + Assert.IsNotNull(response); + Assert.AreEqual(HttpStatusCode.OK, response?.ResponseMessage?.StatusCode); + } + + [TestMethod] + public async Task Resend_ShouldRequestConfirmationCode_GivenSmsType() + { + var resend = new ResendParameters(ResendType.Sms) { Phone = "5544989899898" }; + var response = await this.Client.Resend(resend); + + Assert.IsNotNull(response); + Assert.AreEqual(HttpStatusCode.OK, response?.ResponseMessage?.StatusCode); + } + + [TestMethod] + public async Task Resend_ShouldRequestConfirmationCode_GivenPhoneChangeType() + { + var resend = new ResendParameters(ResendType.PhoneChange) { Phone = "5544989899898" }; + var response = await this.Client.Resend(resend); + + Assert.IsNotNull(response); + Assert.AreEqual(HttpStatusCode.OK, response?.ResponseMessage?.StatusCode); + } + + [TestMethod] + public async Task Resend_ShouldRequestConfirmationCode_GivenEmailChangeType() + { + var resend = new ResendParameters(ResendType.EmailChange) { Email = RandomEmail() }; + var response = await this.Client.Resend(resend); + + Assert.IsNotNull(response); + Assert.AreEqual(HttpStatusCode.OK, response?.ResponseMessage?.StatusCode); + } +} diff --git a/packages/Gotrue/Gotrue/Api.cs b/packages/Gotrue/Gotrue/Api.cs index 9c2a0958..844eb5b4 100644 --- a/packages/Gotrue/Gotrue/Api.cs +++ b/packages/Gotrue/Gotrue/Api.cs @@ -832,4 +832,16 @@ public Task GenerateLink(string jwt, GenerateLinkOptions options) return this.MakeRequestAsync(HttpMethod.Post, $"{this.Url}/token?grant_type=refresh_token", data, this.Headers.MergeLeft(headers)); } + + /// + /// Resends a confirmation code to a user's email or phone. + /// + /// + /// BaseResponse + public Task Resend(Resend.ResendParameters resendParameters) => this.MakeRequestAsync( + HttpMethod.Post, + $"{this.Url}/resend", + resendParameters, + this.Headers + ); } diff --git a/packages/Gotrue/Gotrue/Client.cs b/packages/Gotrue/Gotrue/Client.cs index 39269d58..a93ea20a 100644 --- a/packages/Gotrue/Gotrue/Client.cs +++ b/packages/Gotrue/Gotrue/Client.cs @@ -13,6 +13,8 @@ using Supabase.Gotrue.Exceptions; using Supabase.Gotrue.Interfaces; using Supabase.Gotrue.Mfa; +using Supabase.Gotrue.Resend; +using Supabase.Gotrue.Responses; using static Supabase.Gotrue.Constants; using static Supabase.Gotrue.Constants.AuthState; using static Supabase.Gotrue.Exceptions.FailureHint.Reason; @@ -947,6 +949,9 @@ private void ApplyLoadedSession(Session? before, Session? loaded) /// public void Shutdown() => this.NotifyAuthStateChange(AuthState.Shutdown); + /// + public Task Resend(ResendParameters resendParameters) => this.api.Resend(resendParameters); + /// public async Task Enroll(MfaEnrollParams mfaEnrollParams) { diff --git a/packages/Gotrue/Gotrue/Constants.cs b/packages/Gotrue/Gotrue/Constants.cs index fac3e04b..5b534b36 100644 --- a/packages/Gotrue/Gotrue/Constants.cs +++ b/packages/Gotrue/Gotrue/Constants.cs @@ -1,167 +1,197 @@ -using System.Collections.Generic; +using System.Collections.Generic; using Supabase.Core.Attributes; #pragma warning disable CS1591 -namespace Supabase.Gotrue +namespace Supabase.Gotrue; + +/// +/// Constants used throughout the Gotrue Client +/// +public static class Constants { - /// - /// Constants used throughout the Gotrue Client - /// - public static class Constants - { - /// - /// URL for the Gotrue server. Defaults to localhost:9999. - /// - public const string GOTRUE_URL = "http://localhost:9999"; - public const string AUDIENCE = ""; - public const int EXPIRY_MARGIN = 60 * 1000; - public const string STORAGE_KEY = "supabase.auth.token"; - public static readonly Dictionary CookieOptions = new Dictionary - { - { "name", "sb:token" }, - { "lifetime", 60 * 60 * 8 }, - { "domain", "" }, - { "path", "/" }, - { "sameSite", "lax" } - }; - - public enum SortOrder - { - [MapTo("asc")] - Ascending, - [MapTo("desc")] - Descending - } - - public enum MobileOtpType - { - [MapTo("sms")] - SMS, - [MapTo("phone_change")] - PhoneChange - } - - public enum EmailOtpType - { - [MapTo("signup")] - Signup, - [MapTo("invite")] - Invite, - [MapTo("magiclink")] - MagicLink, - [MapTo("recovery")] - Recovery, - [MapTo("email_change")] - EmailChange, - [MapTo("email")] - Email - } - - /// - /// Providers available to Supabase - /// Ref: https://supabase.github.io/gotrue-js/modules.html#Provider - /// - public enum Provider - { - [MapTo("anonymous_users")] - AnonymousUsers, - [MapTo("apple")] - Apple, - [MapTo("azure")] - Azure, - [MapTo("bitbucket")] - Bitbucket, - [MapTo("discord")] - Discord, - [MapTo("facebook")] - Facebook, - [MapTo("figma")] - Figma, - [MapTo("fly")] - Fly, - [MapTo("github")] - Github, - [MapTo("gitlab")] - Gitlab, - [MapTo("google")] - Google, - [MapTo("kakao")] - Kakao, - [MapTo("keycloak")] - KeyCloak, - [MapTo("linkedin")] - LinkedIn, - [MapTo("linkedin_oidc")] - LinkedInOIDC, - [MapTo("notion")] - Notion, - [MapTo("slack")] - Slack, - [MapTo("spotify")] - Spotify, - [MapTo("twitch")] - Twitch, - [MapTo("twitter")] - Twitter, - [MapTo("workos")] - WorkOS, - [MapTo("zoom")] - Zoom - } - - /// - /// States that the Auth Client will raise events for. - /// - public enum AuthState - { - SignedIn, - SignedOut, - UserUpdated, - PasswordRecovery, - TokenRefreshed, - Shutdown, - MfaChallengeVerified - } - - /// - /// Specifies the functionality expected from the `SignIn` method - /// - public enum SignInType - { - Email, - Phone, - RefreshToken - } - - /// - /// Represents an OAuth Flow type - /// - public enum OAuthFlowType - { - [MapTo("implicit")] - Implicit, - [MapTo("pkce")] - PKCE - } - - /// - /// Specifies the functionality expected from the `SignUp` method - /// - public enum SignUpType - { - Email, - Phone - } - - public enum SignOutScope - { - [MapTo("global")] - Global, - [MapTo("local")] - Local, - [MapTo("others")] - Others - } + /// + /// URL for the Gotrue server. Defaults to localhost:9999. + /// + public const string GOTRUE_URL = "http://localhost:9999"; + public const string AUDIENCE = ""; + public const int EXPIRY_MARGIN = 60 * 1000; + public const string STORAGE_KEY = "supabase.auth.token"; + public static readonly Dictionary CookieOptions = new Dictionary + { + { "name", "sb:token" }, + { "lifetime", 60 * 60 * 8 }, + { "domain", "" }, + { "path", "/" }, + { "sameSite", "lax" }, + }; + + public enum SortOrder + { + [MapTo("asc")] + Ascending, + + [MapTo("desc")] + Descending, + } + + public enum MobileOtpType + { + [MapTo("sms")] + SMS, + + [MapTo("phone_change")] + PhoneChange, + } + + public enum EmailOtpType + { + [MapTo("signup")] + Signup, + + [MapTo("invite")] + Invite, + + [MapTo("magiclink")] + MagicLink, + + [MapTo("recovery")] + Recovery, + + [MapTo("email_change")] + EmailChange, + + [MapTo("email")] + Email, + } + + /// + /// Providers available to Supabase + /// Ref: https://supabase.github.io/gotrue-js/modules.html#Provider + /// + public enum Provider + { + [MapTo("anonymous_users")] + AnonymousUsers, + + [MapTo("apple")] + Apple, + + [MapTo("azure")] + Azure, + + [MapTo("bitbucket")] + Bitbucket, + + [MapTo("discord")] + Discord, + + [MapTo("facebook")] + Facebook, + + [MapTo("figma")] + Figma, + + [MapTo("fly")] + Fly, + + [MapTo("github")] + Github, + + [MapTo("gitlab")] + Gitlab, + + [MapTo("google")] + Google, + + [MapTo("kakao")] + Kakao, + + [MapTo("keycloak")] + KeyCloak, + + [MapTo("linkedin")] + LinkedIn, + + [MapTo("linkedin_oidc")] + LinkedInOIDC, + + [MapTo("notion")] + Notion, + + [MapTo("slack")] + Slack, + + [MapTo("spotify")] + Spotify, + + [MapTo("twitch")] + Twitch, + + [MapTo("twitter")] + Twitter, + + [MapTo("workos")] + WorkOS, + + [MapTo("zoom")] + Zoom, + } + + /// + /// States that the Auth Client will raise events for. + /// + public enum AuthState + { + SignedIn, + SignedOut, + UserUpdated, + PasswordRecovery, + TokenRefreshed, + Shutdown, + MfaChallengeVerified, + } + + /// + /// Specifies the functionality expected from the `SignIn` method + /// + public enum SignInType + { + Email, + Phone, + RefreshToken, + } + + /// + /// Represents an OAuth Flow type + /// + public enum OAuthFlowType + { + [MapTo("implicit")] + Implicit, + + [MapTo("pkce")] + PKCE, + } + + /// + /// Specifies the functionality expected from the `SignUp` method + /// + public enum SignUpType + { + Email, + Phone, + } + + public enum SignOutScope + { + [MapTo("global")] + Global, + + [MapTo("local")] + Local, + + [MapTo("others")] + Others, } } diff --git a/packages/Gotrue/Gotrue/Interfaces/IGotrueApi.cs b/packages/Gotrue/Gotrue/Interfaces/IGotrueApi.cs index 2ed2d1af..717cee8e 100644 --- a/packages/Gotrue/Gotrue/Interfaces/IGotrueApi.cs +++ b/packages/Gotrue/Gotrue/Interfaces/IGotrueApi.cs @@ -2,6 +2,7 @@ using System.Threading.Tasks; using Supabase.Core.Interfaces; using Supabase.Gotrue.Mfa; +using Supabase.Gotrue.Resend; using Supabase.Gotrue.Responses; using static Supabase.Gotrue.Constants; @@ -81,4 +82,11 @@ public interface IGotrueApi : IGettableHeaders /// Identity to be unlinked /// Task UnlinkIdentity(string token, UserIdentity userIdentity); + + /// + /// Resends a confirmation code to a user's email or phone. + /// + /// + /// + Task Resend(ResendParameters resendParameters); } diff --git a/packages/Gotrue/Gotrue/Interfaces/IGotrueClient.cs b/packages/Gotrue/Gotrue/Interfaces/IGotrueClient.cs index 9f6034dd..c26029a9 100644 --- a/packages/Gotrue/Gotrue/Interfaces/IGotrueClient.cs +++ b/packages/Gotrue/Gotrue/Interfaces/IGotrueClient.cs @@ -6,6 +6,8 @@ using Supabase.Core.Interfaces; using Supabase.Gotrue.Exceptions; using Supabase.Gotrue.Mfa; +using Supabase.Gotrue.Resend; +using Supabase.Gotrue.Responses; using static Supabase.Gotrue.Constants; #endregion @@ -532,6 +534,13 @@ public interface IGotrueClient : IGettableHeaders /// The refresh token to exchange for a new session. Task RefreshToken(string accessToken, string refreshToken); + /// + /// Resends a confirmation code to a user's email or phone. + /// + /// + /// + Task Resend(ResendParameters resendParameters); + #region MFA /// diff --git a/packages/Gotrue/Gotrue/PublicAPI.Unshipped.txt b/packages/Gotrue/Gotrue/PublicAPI.Unshipped.txt index 7dc5c581..52aac80c 100644 --- a/packages/Gotrue/Gotrue/PublicAPI.Unshipped.txt +++ b/packages/Gotrue/Gotrue/PublicAPI.Unshipped.txt @@ -1 +1,26 @@ #nullable enable +Supabase.Gotrue.Api.Resend(Supabase.Gotrue.Resend.ResendParameters! resendParameters) -> System.Threading.Tasks.Task! +Supabase.Gotrue.Client.Resend(Supabase.Gotrue.Resend.ResendParameters! resendParameters) -> System.Threading.Tasks.Task! +Supabase.Gotrue.Interfaces.IGotrueApi.Resend(Supabase.Gotrue.Resend.ResendParameters! resendParameters) -> System.Threading.Tasks.Task! +Supabase.Gotrue.Interfaces.IGotrueClient.Resend(Supabase.Gotrue.Resend.ResendParameters! resendParameters) -> System.Threading.Tasks.Task! +Supabase.Gotrue.Resend.ResendOptions +Supabase.Gotrue.Resend.ResendOptions.CaptchaToken.get -> string? +Supabase.Gotrue.Resend.ResendOptions.CaptchaToken.set -> void +Supabase.Gotrue.Resend.ResendOptions.EmailRedirectTo.get -> string? +Supabase.Gotrue.Resend.ResendOptions.EmailRedirectTo.set -> void +Supabase.Gotrue.Resend.ResendOptions.ResendOptions() -> void +Supabase.Gotrue.Resend.ResendParameters +Supabase.Gotrue.Resend.ResendParameters.Email.get -> string? +Supabase.Gotrue.Resend.ResendParameters.Email.set -> void +Supabase.Gotrue.Resend.ResendParameters.Options.get -> Supabase.Gotrue.Resend.ResendOptions? +Supabase.Gotrue.Resend.ResendParameters.Options.set -> void +Supabase.Gotrue.Resend.ResendParameters.Phone.get -> string? +Supabase.Gotrue.Resend.ResendParameters.Phone.set -> void +Supabase.Gotrue.Resend.ResendParameters.ResendParameters(Supabase.Gotrue.Resend.ResendType type) -> void +Supabase.Gotrue.Resend.ResendParameters.Type.get -> string? +Supabase.Gotrue.Resend.ResendParameters.Type.set -> void +Supabase.Gotrue.Resend.ResendType +Supabase.Gotrue.Resend.ResendType.EmailChange = 3 -> Supabase.Gotrue.Resend.ResendType +Supabase.Gotrue.Resend.ResendType.PhoneChange = 2 -> Supabase.Gotrue.Resend.ResendType +Supabase.Gotrue.Resend.ResendType.SignUp = 0 -> Supabase.Gotrue.Resend.ResendType +Supabase.Gotrue.Resend.ResendType.Sms = 1 -> Supabase.Gotrue.Resend.ResendType diff --git a/packages/Gotrue/Gotrue/Resend/ResendOptions.cs b/packages/Gotrue/Gotrue/Resend/ResendOptions.cs new file mode 100644 index 00000000..012e0ea6 --- /dev/null +++ b/packages/Gotrue/Gotrue/Resend/ResendOptions.cs @@ -0,0 +1,23 @@ +using System.Text.Json.Serialization; + +namespace Supabase.Gotrue.Resend; + +/// +/// Represents optional parameters used in the Resend API. These parameters provide +/// additional customization for the resend operation, such as including a CAPTCHA +/// token or specifying a redirection URL after email confirmation. +/// +public class ResendOptions +{ + /// + /// Verification token received when the user completes the captcha on the site. + /// + [JsonPropertyName("captchaToken")] + public string? CaptchaToken { get; set; } + + /// + /// A URL or mobile address to send the user to after they are confirmed. + /// + [JsonPropertyName("emailRedirectTo")] + public string? EmailRedirectTo { get; set; } +} diff --git a/packages/Gotrue/Gotrue/Resend/ResendParameters.cs b/packages/Gotrue/Gotrue/Resend/ResendParameters.cs new file mode 100644 index 00000000..a87087b0 --- /dev/null +++ b/packages/Gotrue/Gotrue/Resend/ResendParameters.cs @@ -0,0 +1,42 @@ +using System.Text.Json.Serialization; + +namespace Supabase.Gotrue.Resend; + +/// +/// Parameters for the Resend API. +/// +public class ResendParameters +{ + /// + /// The email address to resend to. + /// + [JsonPropertyName("email")] + public string? Email { get; set; } + + /// + /// The phone number to resend to. + /// + [JsonPropertyName("phone")] + public string? Phone { get; set; } + + /// + /// The type of resend. + /// + [JsonPropertyName("type")] + public string? Type { get; set; } + + /// + /// Additional options to customize the behavior of the Resend API. + /// Provides support for features such as CAPTCHA tokens and email redirection. + /// + [JsonPropertyName("options")] + public ResendOptions? Options { get; set; } + + /// + /// Represents the parameters used for the Resend API, allowing for resending + /// confirmation codes or links based on the provided inputs. This includes + /// support for email, phone, and custom data. + /// + public ResendParameters(ResendType type) => + this.Type = Core.Helpers.GetMappedToAttr(type).Mapping; +} diff --git a/packages/Gotrue/Gotrue/Resend/ResendType.cs b/packages/Gotrue/Gotrue/Resend/ResendType.cs new file mode 100644 index 00000000..b19c36f7 --- /dev/null +++ b/packages/Gotrue/Gotrue/Resend/ResendType.cs @@ -0,0 +1,30 @@ +using System.Runtime.Serialization; +using Supabase.Core.Attributes; + +namespace Supabase.Gotrue.Resend; + +/// +/// The type of resend. +/// +public enum ResendType +{ + /// Signup resend. + [MapTo("signup")] + [EnumMember(Value = "signup")] + SignUp, + + /// SMS resend. + [MapTo("sms")] + [EnumMember(Value = "sms")] + Sms, + + /// Phone change resend. + [MapTo("phone_change")] + [EnumMember(Value = "phone_change")] + PhoneChange, + + /// Email change resend. + [MapTo("email_change")] + [EnumMember(Value = "email_change")] + EmailChange, +} From 7b59aaa927e33969789221fe502395f1381dbdb7 Mon Sep 17 00:00:00 2001 From: Diego Santo Date: Mon, 14 Sep 2026 21:54:03 -0300 Subject: [PATCH 2/8] fix(gotrue): remove full path of parameter --- packages/Gotrue/Gotrue/Api.cs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/packages/Gotrue/Gotrue/Api.cs b/packages/Gotrue/Gotrue/Api.cs index 6d80f98f..39b92095 100644 --- a/packages/Gotrue/Gotrue/Api.cs +++ b/packages/Gotrue/Gotrue/Api.cs @@ -11,6 +11,7 @@ using Supabase.Gotrue.Exceptions; using Supabase.Gotrue.Interfaces; using Supabase.Gotrue.Mfa; +using Supabase.Gotrue.Resend; using Supabase.Gotrue.Responses; using static Supabase.Gotrue.Constants; @@ -849,7 +850,7 @@ public Task GenerateLink(string jwt, GenerateLinkOptions options) /// /// /// BaseResponse - public Task Resend(Resend.ResendParameters resendParameters) => this.MakeRequestAsync( + public Task Resend(ResendParameters resendParameters) => this.MakeRequestAsync( HttpMethod.Post, $"{this.Url}/resend", resendParameters, From 5fe607b3e4256ea329f7c9f691805b3275be3507 Mon Sep 17 00:00:00 2001 From: Diego Santo Date: Mon, 28 Sep 2026 20:09:57 -0300 Subject: [PATCH 3/8] feat(gotrue): improve patterns and tests --- .../Authentication/ResendParametersTests.cs | 65 ++++++++++++++++--- packages/Gotrue/Gotrue/Api.cs | 30 +++++++-- .../Gotrue/Gotrue/PublicAPI.Unshipped.txt | 3 +- .../Gotrue/Gotrue/Resend/ResendParameters.cs | 20 ++++-- packages/Gotrue/Gotrue/Resend/ResendType.cs | 4 -- 5 files changed, 96 insertions(+), 26 deletions(-) diff --git a/packages/Gotrue/Gotrue.Tests/Authentication/ResendParametersTests.cs b/packages/Gotrue/Gotrue.Tests/Authentication/ResendParametersTests.cs index d315c3cd..ed1d676d 100644 --- a/packages/Gotrue/Gotrue.Tests/Authentication/ResendParametersTests.cs +++ b/packages/Gotrue/Gotrue.Tests/Authentication/ResendParametersTests.cs @@ -2,6 +2,7 @@ using System.Threading.Tasks; using Gotrue.Tests.Support; using Microsoft.VisualStudio.TestTools.UnitTesting; +using Supabase.Gotrue.Exceptions; using Supabase.Gotrue.Resend; using static Gotrue.Tests.TestUtils; @@ -15,40 +16,88 @@ public class ResendParametersTests : AuthClientFixture public async Task Resend_ShouldRequestConfirmationCode_GivenSignUpType() { var email = RandomEmail(); - var resend = new ResendParameters(ResendType.SignUp) { Email = email }; + var resend = new ResendParameters { Type = ResendType.SignUp, Email = email }; var response = await this.Client.Resend(resend); Assert.IsNotNull(response); - Assert.AreEqual(HttpStatusCode.OK, response?.ResponseMessage?.StatusCode); + Assert.AreEqual(HttpStatusCode.OK, response.ResponseMessage?.StatusCode); } [TestMethod] public async Task Resend_ShouldRequestConfirmationCode_GivenSmsType() { - var resend = new ResendParameters(ResendType.Sms) { Phone = "5544989899898" }; + var resend = new ResendParameters { Type = ResendType.Sms, Phone = "5544989899898" }; var response = await this.Client.Resend(resend); Assert.IsNotNull(response); - Assert.AreEqual(HttpStatusCode.OK, response?.ResponseMessage?.StatusCode); + Assert.AreEqual(HttpStatusCode.OK, response.ResponseMessage?.StatusCode); } [TestMethod] public async Task Resend_ShouldRequestConfirmationCode_GivenPhoneChangeType() { - var resend = new ResendParameters(ResendType.PhoneChange) { Phone = "5544989899898" }; + var resend = new ResendParameters { Type = ResendType.PhoneChange, Phone = "5544989899898" }; var response = await this.Client.Resend(resend); Assert.IsNotNull(response); - Assert.AreEqual(HttpStatusCode.OK, response?.ResponseMessage?.StatusCode); + Assert.AreEqual(HttpStatusCode.OK, response.ResponseMessage?.StatusCode); } [TestMethod] public async Task Resend_ShouldRequestConfirmationCode_GivenEmailChangeType() { - var resend = new ResendParameters(ResendType.EmailChange) { Email = RandomEmail() }; + var resend = new ResendParameters { Type = ResendType.EmailChange, Email = RandomEmail() }; var response = await this.Client.Resend(resend); Assert.IsNotNull(response); - Assert.AreEqual(HttpStatusCode.OK, response?.ResponseMessage?.StatusCode); + Assert.AreEqual(HttpStatusCode.OK, response.ResponseMessage?.StatusCode); + } + + [TestMethod] + public async Task Resend_ShouldThrowErrorWhenParameterAreInvalid_GivenPhoneForTypeEmail() + { + var resend = new ResendParameters { Type = ResendType.EmailChange, Phone = "5544989899898" }; + + var action = async () => await this.Client.Resend(resend); + + var exception = await Assert.ThrowsAsync(action); + Assert.Contains("Missing email address or phone number", exception.Message); + Assert.Contains("400", exception.Message); + } + + [TestMethod] + public async Task Resend_ShouldThrowErrorWhenParameterAreInvalid_GivenEmailForTypePhone() + { + var resend = new ResendParameters { Type = ResendType.Sms, Email = RandomEmail() }; + + var action = async () => await this.Client.Resend(resend); + + var exception = await Assert.ThrowsAsync(action); + Assert.Contains("Type provided requires a phone number", exception.Message); + Assert.Contains("400", exception.Message); + } + + [TestMethod] + public async Task Resend_ShouldThrowErrorWhenParameterAreInvalid_GivenNothingForTypeEmail() + { + var resend = new ResendParameters { Type = ResendType.EmailChange }; + + var action = async () => await this.Client.Resend(resend); + + var exception = await Assert.ThrowsAsync(action); + Assert.Contains("Missing email address or phone number", exception.Message); + Assert.Contains("400", exception.Message); + } + + [TestMethod] + public async Task Resend_ShouldThrowErrorWhenParameterAreInvalid_GivenNothingForTypePhone() + { + var resend = new ResendParameters { Type = ResendType.Sms }; + + var action = async () => await this.Client.Resend(resend); + + var exception = await Assert.ThrowsAsync(action); + Assert.Contains("Type provided requires a phone number", exception.Message); + Assert.Contains("400", exception.Message); } } diff --git a/packages/Gotrue/Gotrue/Api.cs b/packages/Gotrue/Gotrue/Api.cs index 39b92095..e9a5bc33 100644 --- a/packages/Gotrue/Gotrue/Api.cs +++ b/packages/Gotrue/Gotrue/Api.cs @@ -850,10 +850,28 @@ public Task GenerateLink(string jwt, GenerateLinkOptions options) /// /// /// BaseResponse - public Task Resend(ResendParameters resendParameters) => this.MakeRequestAsync( - HttpMethod.Post, - $"{this.Url}/resend", - resendParameters, - this.Headers - ); + public Task Resend(ResendParameters resendParameters) + { + var body = new Dictionary() + { + { "type", Core.Helpers.GetMappedToAttr(resendParameters.Type).Mapping } + }; + + if (resendParameters.IsEmail() && !string.IsNullOrWhiteSpace(resendParameters.Email)) + body.Add("email", resendParameters.Email); + + if (!resendParameters.IsEmail() && !string.IsNullOrWhiteSpace(resendParameters.Phone)) + body.Add("phone", resendParameters.Phone); + + if (resendParameters.Options != null && !string.IsNullOrWhiteSpace(resendParameters.Options.CaptchaToken)) + body.Add("gotrue_meta_security", new Dictionary() { { "captcha_token", resendParameters.Options.CaptchaToken } }); + + + return this.MakeRequestAsync( + HttpMethod.Post, + $"{this.Url}/resend", + body, + this.Headers + ); + } } diff --git a/packages/Gotrue/Gotrue/PublicAPI.Unshipped.txt b/packages/Gotrue/Gotrue/PublicAPI.Unshipped.txt index 52aac80c..92d22329 100644 --- a/packages/Gotrue/Gotrue/PublicAPI.Unshipped.txt +++ b/packages/Gotrue/Gotrue/PublicAPI.Unshipped.txt @@ -12,12 +12,11 @@ Supabase.Gotrue.Resend.ResendOptions.ResendOptions() -> void Supabase.Gotrue.Resend.ResendParameters Supabase.Gotrue.Resend.ResendParameters.Email.get -> string? Supabase.Gotrue.Resend.ResendParameters.Email.set -> void +Supabase.Gotrue.Resend.ResendParameters.IsEmail() -> bool Supabase.Gotrue.Resend.ResendParameters.Options.get -> Supabase.Gotrue.Resend.ResendOptions? Supabase.Gotrue.Resend.ResendParameters.Options.set -> void Supabase.Gotrue.Resend.ResendParameters.Phone.get -> string? Supabase.Gotrue.Resend.ResendParameters.Phone.set -> void -Supabase.Gotrue.Resend.ResendParameters.ResendParameters(Supabase.Gotrue.Resend.ResendType type) -> void -Supabase.Gotrue.Resend.ResendParameters.Type.get -> string? Supabase.Gotrue.Resend.ResendParameters.Type.set -> void Supabase.Gotrue.Resend.ResendType Supabase.Gotrue.Resend.ResendType.EmailChange = 3 -> Supabase.Gotrue.Resend.ResendType diff --git a/packages/Gotrue/Gotrue/Resend/ResendParameters.cs b/packages/Gotrue/Gotrue/Resend/ResendParameters.cs index a87087b0..a796f5cc 100644 --- a/packages/Gotrue/Gotrue/Resend/ResendParameters.cs +++ b/packages/Gotrue/Gotrue/Resend/ResendParameters.cs @@ -1,3 +1,4 @@ +using System.Collections.Generic; using System.Text.Json.Serialization; namespace Supabase.Gotrue.Resend; @@ -23,7 +24,7 @@ public class ResendParameters /// The type of resend. /// [JsonPropertyName("type")] - public string? Type { get; set; } + public ResendType Type { get; set; } /// /// Additional options to customize the behavior of the Resend API. @@ -33,10 +34,17 @@ public class ResendParameters public ResendOptions? Options { get; set; } /// - /// Represents the parameters used for the Resend API, allowing for resending - /// confirmation codes or links based on the provided inputs. This includes - /// support for email, phone, and custom data. + /// Check if the ResendType belongs to email-related types. /// - public ResendParameters(ResendType type) => - this.Type = Core.Helpers.GetMappedToAttr(type).Mapping; + /// + public bool IsEmail() + { + var types = new List() + { + ResendType.EmailChange, + ResendType.SignUp + }; + + return types.Contains(Type); + } } diff --git a/packages/Gotrue/Gotrue/Resend/ResendType.cs b/packages/Gotrue/Gotrue/Resend/ResendType.cs index b19c36f7..d1f6edb5 100644 --- a/packages/Gotrue/Gotrue/Resend/ResendType.cs +++ b/packages/Gotrue/Gotrue/Resend/ResendType.cs @@ -10,21 +10,17 @@ public enum ResendType { /// Signup resend. [MapTo("signup")] - [EnumMember(Value = "signup")] SignUp, /// SMS resend. [MapTo("sms")] - [EnumMember(Value = "sms")] Sms, /// Phone change resend. [MapTo("phone_change")] - [EnumMember(Value = "phone_change")] PhoneChange, /// Email change resend. [MapTo("email_change")] - [EnumMember(Value = "email_change")] EmailChange, } From 009268be8a59aed2476447e379fb43de6288cf7e Mon Sep 17 00:00:00 2001 From: Diego Santo Date: Mon, 28 Sep 2026 20:16:06 -0300 Subject: [PATCH 4/8] feat(gotrue): update public api file --- .../Gotrue/Gotrue/PublicAPI.Unshipped.txt | 26 +++++++++++++++++++ 1 file changed, 26 insertions(+) diff --git a/packages/Gotrue/Gotrue/PublicAPI.Unshipped.txt b/packages/Gotrue/Gotrue/PublicAPI.Unshipped.txt index 67f6412b..58d6a19c 100644 --- a/packages/Gotrue/Gotrue/PublicAPI.Unshipped.txt +++ b/packages/Gotrue/Gotrue/PublicAPI.Unshipped.txt @@ -31,6 +31,7 @@ static Supabase.Gotrue.Claims.JwtHeader.operator !=(Supabase.Gotrue.Claims.JwtHe static Supabase.Gotrue.Claims.JwtHeader.operator ==(Supabase.Gotrue.Claims.JwtHeader? left, Supabase.Gotrue.Claims.JwtHeader? right) -> bool Supabase.Gotrue.Api.GetJwksAsync(System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.Task! Supabase.Gotrue.Api.GetUserAsync(string! jwt, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.Task! +Supabase.Gotrue.Api.Resend(Supabase.Gotrue.Resend.ResendParameters! resendParameters) -> System.Threading.Tasks.Task! Supabase.Gotrue.Claims.GetClaimsOptions Supabase.Gotrue.Claims.GetClaimsOptions.$() -> Supabase.Gotrue.Claims.GetClaimsOptions! Supabase.Gotrue.Claims.GetClaimsOptions.AllowExpired.get -> bool @@ -110,9 +111,34 @@ Supabase.Gotrue.Claims.JwtHeader.Kid.init -> void Supabase.Gotrue.Claims.JwtHeader.Typ.get -> string? Supabase.Gotrue.Claims.JwtHeader.Typ.init -> void Supabase.Gotrue.Client.GetClaimsAsync(string? jwt = null, Supabase.Gotrue.Claims.GetClaimsOptions? options = null, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.Task! +Supabase.Gotrue.Client.Resend(Supabase.Gotrue.Resend.ResendParameters! resendParameters) -> System.Threading.Tasks.Task! Supabase.Gotrue.Exceptions.FailureHint.Reason.InvalidJwt = 22 -> Supabase.Gotrue.Exceptions.FailureHint.Reason Supabase.Gotrue.Interfaces.IGotrueApi.GetJwksAsync(System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.Task! Supabase.Gotrue.Interfaces.IGotrueApi.GetUserAsync(string! jwt, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.Task! +Supabase.Gotrue.Interfaces.IGotrueApi.Resend(Supabase.Gotrue.Resend.ResendParameters! resendParameters) -> System.Threading.Tasks.Task! Supabase.Gotrue.Interfaces.IGotrueClient.GetClaimsAsync(string? jwt = null, Supabase.Gotrue.Claims.GetClaimsOptions? options = null, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.Task! +Supabase.Gotrue.Interfaces.IGotrueClient.Resend(Supabase.Gotrue.Resend.ResendParameters! resendParameters) -> System.Threading.Tasks.Task! +Supabase.Gotrue.Resend.ResendOptions +Supabase.Gotrue.Resend.ResendOptions.CaptchaToken.get -> string? +Supabase.Gotrue.Resend.ResendOptions.CaptchaToken.set -> void +Supabase.Gotrue.Resend.ResendOptions.EmailRedirectTo.get -> string? +Supabase.Gotrue.Resend.ResendOptions.EmailRedirectTo.set -> void +Supabase.Gotrue.Resend.ResendOptions.ResendOptions() -> void +Supabase.Gotrue.Resend.ResendParameters +Supabase.Gotrue.Resend.ResendParameters.Email.get -> string? +Supabase.Gotrue.Resend.ResendParameters.Email.set -> void +Supabase.Gotrue.Resend.ResendParameters.IsEmail() -> bool +Supabase.Gotrue.Resend.ResendParameters.Options.get -> Supabase.Gotrue.Resend.ResendOptions? +Supabase.Gotrue.Resend.ResendParameters.Options.set -> void +Supabase.Gotrue.Resend.ResendParameters.Phone.get -> string? +Supabase.Gotrue.Resend.ResendParameters.Phone.set -> void +Supabase.Gotrue.Resend.ResendParameters.ResendParameters() -> void +Supabase.Gotrue.Resend.ResendParameters.Type.get -> Supabase.Gotrue.Resend.ResendType +Supabase.Gotrue.Resend.ResendParameters.Type.set -> void +Supabase.Gotrue.Resend.ResendType +Supabase.Gotrue.Resend.ResendType.EmailChange = 3 -> Supabase.Gotrue.Resend.ResendType +Supabase.Gotrue.Resend.ResendType.PhoneChange = 2 -> Supabase.Gotrue.Resend.ResendType +Supabase.Gotrue.Resend.ResendType.SignUp = 0 -> Supabase.Gotrue.Resend.ResendType +Supabase.Gotrue.Resend.ResendType.Sms = 1 -> Supabase.Gotrue.Resend.ResendType Supabase.Gotrue.SignUpOptions.CaptchaToken.get -> string? Supabase.Gotrue.SignUpOptions.CaptchaToken.set -> void From faa43a33ac081f4e4a4e1b89e7be2034cc40feac Mon Sep 17 00:00:00 2001 From: Diego Santo Date: Mon, 28 Sep 2026 21:38:13 -0300 Subject: [PATCH 5/8] chore(gotrue): fix styles --- packages/Gotrue/Gotrue/Resend/ResendParameters.cs | 2 +- packages/Gotrue/Gotrue/Resend/ResendType.cs | 1 - 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/packages/Gotrue/Gotrue/Resend/ResendParameters.cs b/packages/Gotrue/Gotrue/Resend/ResendParameters.cs index a796f5cc..9978bffd 100644 --- a/packages/Gotrue/Gotrue/Resend/ResendParameters.cs +++ b/packages/Gotrue/Gotrue/Resend/ResendParameters.cs @@ -45,6 +45,6 @@ public bool IsEmail() ResendType.SignUp }; - return types.Contains(Type); + return types.Contains(this.Type); } } diff --git a/packages/Gotrue/Gotrue/Resend/ResendType.cs b/packages/Gotrue/Gotrue/Resend/ResendType.cs index d1f6edb5..b57fe572 100644 --- a/packages/Gotrue/Gotrue/Resend/ResendType.cs +++ b/packages/Gotrue/Gotrue/Resend/ResendType.cs @@ -1,4 +1,3 @@ -using System.Runtime.Serialization; using Supabase.Core.Attributes; namespace Supabase.Gotrue.Resend; From 8cb570cc0a2634231d1cd90613289349f2e0fc47 Mon Sep 17 00:00:00 2001 From: Diego Santo Date: Tue, 29 Sep 2026 20:49:24 -0300 Subject: [PATCH 6/8] test(gotrue): create contract tests --- ...dPayload_GivenEmailChangeType.verified.txt | 1 + ...dPayload_GivenPhoneChangeType.verified.txt | 1 + ...pectedPayload_GivenSignUpType.verified.txt | 1 + ...oExpectedPayload_GivenSmsType.verified.txt | 1 + .../ResendParametersContractTests.cs | 57 +++++++++++++++++++ 5 files changed, 61 insertions(+) create mode 100644 packages/Gotrue/Gotrue.Tests/Authentication/Data/ResendParametersContractTests.ResendRequest_ShouldSerializeToExpectedPayload_GivenEmailChangeType.verified.txt create mode 100644 packages/Gotrue/Gotrue.Tests/Authentication/Data/ResendParametersContractTests.ResendRequest_ShouldSerializeToExpectedPayload_GivenPhoneChangeType.verified.txt create mode 100644 packages/Gotrue/Gotrue.Tests/Authentication/Data/ResendParametersContractTests.ResendRequest_ShouldSerializeToExpectedPayload_GivenSignUpType.verified.txt create mode 100644 packages/Gotrue/Gotrue.Tests/Authentication/Data/ResendParametersContractTests.ResendRequest_ShouldSerializeToExpectedPayload_GivenSmsType.verified.txt create mode 100644 packages/Gotrue/Gotrue.Tests/Authentication/ResendParametersContractTests.cs diff --git a/packages/Gotrue/Gotrue.Tests/Authentication/Data/ResendParametersContractTests.ResendRequest_ShouldSerializeToExpectedPayload_GivenEmailChangeType.verified.txt b/packages/Gotrue/Gotrue.Tests/Authentication/Data/ResendParametersContractTests.ResendRequest_ShouldSerializeToExpectedPayload_GivenEmailChangeType.verified.txt new file mode 100644 index 00000000..03294a0a --- /dev/null +++ b/packages/Gotrue/Gotrue.Tests/Authentication/Data/ResendParametersContractTests.ResendRequest_ShouldSerializeToExpectedPayload_GivenEmailChangeType.verified.txt @@ -0,0 +1 @@ +{"type":"email_change","email":"user@supabase.com"} diff --git a/packages/Gotrue/Gotrue.Tests/Authentication/Data/ResendParametersContractTests.ResendRequest_ShouldSerializeToExpectedPayload_GivenPhoneChangeType.verified.txt b/packages/Gotrue/Gotrue.Tests/Authentication/Data/ResendParametersContractTests.ResendRequest_ShouldSerializeToExpectedPayload_GivenPhoneChangeType.verified.txt new file mode 100644 index 00000000..c5535cf5 --- /dev/null +++ b/packages/Gotrue/Gotrue.Tests/Authentication/Data/ResendParametersContractTests.ResendRequest_ShouldSerializeToExpectedPayload_GivenPhoneChangeType.verified.txt @@ -0,0 +1 @@ +{"type":"phone_change","phone":"+5544998989898"} diff --git a/packages/Gotrue/Gotrue.Tests/Authentication/Data/ResendParametersContractTests.ResendRequest_ShouldSerializeToExpectedPayload_GivenSignUpType.verified.txt b/packages/Gotrue/Gotrue.Tests/Authentication/Data/ResendParametersContractTests.ResendRequest_ShouldSerializeToExpectedPayload_GivenSignUpType.verified.txt new file mode 100644 index 00000000..8d89883a --- /dev/null +++ b/packages/Gotrue/Gotrue.Tests/Authentication/Data/ResendParametersContractTests.ResendRequest_ShouldSerializeToExpectedPayload_GivenSignUpType.verified.txt @@ -0,0 +1 @@ +{"type":"signup","email":"user@supabase.com"} diff --git a/packages/Gotrue/Gotrue.Tests/Authentication/Data/ResendParametersContractTests.ResendRequest_ShouldSerializeToExpectedPayload_GivenSmsType.verified.txt b/packages/Gotrue/Gotrue.Tests/Authentication/Data/ResendParametersContractTests.ResendRequest_ShouldSerializeToExpectedPayload_GivenSmsType.verified.txt new file mode 100644 index 00000000..5dd95978 --- /dev/null +++ b/packages/Gotrue/Gotrue.Tests/Authentication/Data/ResendParametersContractTests.ResendRequest_ShouldSerializeToExpectedPayload_GivenSmsType.verified.txt @@ -0,0 +1 @@ +{"type":"sms","phone":"+5544998989898"} diff --git a/packages/Gotrue/Gotrue.Tests/Authentication/ResendParametersContractTests.cs b/packages/Gotrue/Gotrue.Tests/Authentication/ResendParametersContractTests.cs new file mode 100644 index 00000000..17c14938 --- /dev/null +++ b/packages/Gotrue/Gotrue.Tests/Authentication/ResendParametersContractTests.cs @@ -0,0 +1,57 @@ +using System.Net; +using System.Threading.Tasks; +using Gotrue.Tests.Support; +using Microsoft.VisualStudio.TestTools.UnitTesting; +using Supabase.Gotrue.Exceptions; +using Supabase.Gotrue.Resend; +using static Gotrue.Tests.TestUtils; + +namespace Gotrue.Tests.Authentication; + +/// +/// Pins the exact bytes the resend requests put on the wire, including the type enum mappings +/// (signup, sms, phone_change, email_change) and whether the target is sent +/// as email or phone. +/// +[TestClass] +[TestCategory("Contract")] +public class ResendParametersContractTests : RequestApprovalFixture +{ + [TestMethod] + public async Task ResendRequest_ShouldSerializeToExpectedPayload_GivenSignUpType() + { + const string email = "user@supabase.com"; + var resend = new ResendParameters { Type = ResendType.SignUp, Email = email }; + await this.Api.Resend(resend); + + await this.Verify(this.EmittedRequestBody).UseDirectory("Data"); + } + + [TestMethod] + public async Task ResendRequest_ShouldSerializeToExpectedPayload_GivenEmailChangeType() + { + const string email = "user@supabase.com"; + var resend = new ResendParameters { Type = ResendType.EmailChange, Email = email }; + await this.Api.Resend(resend); + + await this.Verify(this.EmittedRequestBody).UseDirectory("Data"); + } + + [TestMethod] + public async Task ResendRequest_ShouldSerializeToExpectedPayload_GivenPhoneChangeType() + { + var resend = new ResendParameters { Type = ResendType.PhoneChange, Phone = "+5544998989898"}; + await this.Api.Resend(resend); + + await this.Verify(this.EmittedRequestBody).UseDirectory("Data"); + } + + [TestMethod] + public async Task ResendRequest_ShouldSerializeToExpectedPayload_GivenSmsType() + { + var resend = new ResendParameters { Type = ResendType.Sms, Phone = "+5544998989898"}; + await this.Api.Resend(resend); + + await this.Verify(this.EmittedRequestBody).UseDirectory("Data"); + } +} From 0a9c60992bece9c60042ec7c744b1a70cb481579 Mon Sep 17 00:00:00 2001 From: Diego Santo Date: Tue, 29 Sep 2026 21:04:30 -0300 Subject: [PATCH 7/8] test(gotrue): add contract tests --- ...oExpectedPayload_GivenOptions.verified.txt | 1 + ...dPayload_GivenEmailChangeType.verified.txt | 2 +- ...dPayload_GivenPhoneChangeType.verified.txt | 2 +- ...pectedPayload_GivenSignUpType.verified.txt | 2 +- ...oExpectedPayload_GivenSmsType.verified.txt | 2 +- .../ResendParametersContractTests.cs | 26 +++++++++++++++---- 6 files changed, 26 insertions(+), 9 deletions(-) create mode 100644 packages/Gotrue/Gotrue.Tests/Authentication/Data/ResendParametersContractTests.ResendRequest_ShouldSerializeCaptchaToExpectedPayload_GivenOptions.verified.txt diff --git a/packages/Gotrue/Gotrue.Tests/Authentication/Data/ResendParametersContractTests.ResendRequest_ShouldSerializeCaptchaToExpectedPayload_GivenOptions.verified.txt b/packages/Gotrue/Gotrue.Tests/Authentication/Data/ResendParametersContractTests.ResendRequest_ShouldSerializeCaptchaToExpectedPayload_GivenOptions.verified.txt new file mode 100644 index 00000000..7027963f --- /dev/null +++ b/packages/Gotrue/Gotrue.Tests/Authentication/Data/ResendParametersContractTests.ResendRequest_ShouldSerializeCaptchaToExpectedPayload_GivenOptions.verified.txt @@ -0,0 +1 @@ +{"type":"signup","email":"user@supabase.com","gotrue_meta_security":{"captcha_token":"captcha-token"}} \ No newline at end of file diff --git a/packages/Gotrue/Gotrue.Tests/Authentication/Data/ResendParametersContractTests.ResendRequest_ShouldSerializeToExpectedPayload_GivenEmailChangeType.verified.txt b/packages/Gotrue/Gotrue.Tests/Authentication/Data/ResendParametersContractTests.ResendRequest_ShouldSerializeToExpectedPayload_GivenEmailChangeType.verified.txt index 03294a0a..f05b5da4 100644 --- a/packages/Gotrue/Gotrue.Tests/Authentication/Data/ResendParametersContractTests.ResendRequest_ShouldSerializeToExpectedPayload_GivenEmailChangeType.verified.txt +++ b/packages/Gotrue/Gotrue.Tests/Authentication/Data/ResendParametersContractTests.ResendRequest_ShouldSerializeToExpectedPayload_GivenEmailChangeType.verified.txt @@ -1 +1 @@ -{"type":"email_change","email":"user@supabase.com"} +{"type":"email_change","email":"user@supabase.com"} \ No newline at end of file diff --git a/packages/Gotrue/Gotrue.Tests/Authentication/Data/ResendParametersContractTests.ResendRequest_ShouldSerializeToExpectedPayload_GivenPhoneChangeType.verified.txt b/packages/Gotrue/Gotrue.Tests/Authentication/Data/ResendParametersContractTests.ResendRequest_ShouldSerializeToExpectedPayload_GivenPhoneChangeType.verified.txt index c5535cf5..bfbfa256 100644 --- a/packages/Gotrue/Gotrue.Tests/Authentication/Data/ResendParametersContractTests.ResendRequest_ShouldSerializeToExpectedPayload_GivenPhoneChangeType.verified.txt +++ b/packages/Gotrue/Gotrue.Tests/Authentication/Data/ResendParametersContractTests.ResendRequest_ShouldSerializeToExpectedPayload_GivenPhoneChangeType.verified.txt @@ -1 +1 @@ -{"type":"phone_change","phone":"+5544998989898"} +{"type":"phone_change","phone":"+5544998989898"} \ No newline at end of file diff --git a/packages/Gotrue/Gotrue.Tests/Authentication/Data/ResendParametersContractTests.ResendRequest_ShouldSerializeToExpectedPayload_GivenSignUpType.verified.txt b/packages/Gotrue/Gotrue.Tests/Authentication/Data/ResendParametersContractTests.ResendRequest_ShouldSerializeToExpectedPayload_GivenSignUpType.verified.txt index 8d89883a..16a7bbcd 100644 --- a/packages/Gotrue/Gotrue.Tests/Authentication/Data/ResendParametersContractTests.ResendRequest_ShouldSerializeToExpectedPayload_GivenSignUpType.verified.txt +++ b/packages/Gotrue/Gotrue.Tests/Authentication/Data/ResendParametersContractTests.ResendRequest_ShouldSerializeToExpectedPayload_GivenSignUpType.verified.txt @@ -1 +1 @@ -{"type":"signup","email":"user@supabase.com"} +{"type":"signup","email":"user@supabase.com"} \ No newline at end of file diff --git a/packages/Gotrue/Gotrue.Tests/Authentication/Data/ResendParametersContractTests.ResendRequest_ShouldSerializeToExpectedPayload_GivenSmsType.verified.txt b/packages/Gotrue/Gotrue.Tests/Authentication/Data/ResendParametersContractTests.ResendRequest_ShouldSerializeToExpectedPayload_GivenSmsType.verified.txt index 5dd95978..6d7aaab2 100644 --- a/packages/Gotrue/Gotrue.Tests/Authentication/Data/ResendParametersContractTests.ResendRequest_ShouldSerializeToExpectedPayload_GivenSmsType.verified.txt +++ b/packages/Gotrue/Gotrue.Tests/Authentication/Data/ResendParametersContractTests.ResendRequest_ShouldSerializeToExpectedPayload_GivenSmsType.verified.txt @@ -1 +1 @@ -{"type":"sms","phone":"+5544998989898"} +{"type":"sms","phone":"+5544998989898"} \ No newline at end of file diff --git a/packages/Gotrue/Gotrue.Tests/Authentication/ResendParametersContractTests.cs b/packages/Gotrue/Gotrue.Tests/Authentication/ResendParametersContractTests.cs index 17c14938..334e8ac5 100644 --- a/packages/Gotrue/Gotrue.Tests/Authentication/ResendParametersContractTests.cs +++ b/packages/Gotrue/Gotrue.Tests/Authentication/ResendParametersContractTests.cs @@ -1,10 +1,7 @@ -using System.Net; using System.Threading.Tasks; using Gotrue.Tests.Support; using Microsoft.VisualStudio.TestTools.UnitTesting; -using Supabase.Gotrue.Exceptions; using Supabase.Gotrue.Resend; -using static Gotrue.Tests.TestUtils; namespace Gotrue.Tests.Authentication; @@ -40,7 +37,11 @@ public async Task ResendRequest_ShouldSerializeToExpectedPayload_GivenEmailChang [TestMethod] public async Task ResendRequest_ShouldSerializeToExpectedPayload_GivenPhoneChangeType() { - var resend = new ResendParameters { Type = ResendType.PhoneChange, Phone = "+5544998989898"}; + var resend = new ResendParameters + { + Type = ResendType.PhoneChange, + Phone = "+5544998989898", + }; await this.Api.Resend(resend); await this.Verify(this.EmittedRequestBody).UseDirectory("Data"); @@ -49,7 +50,22 @@ public async Task ResendRequest_ShouldSerializeToExpectedPayload_GivenPhoneChang [TestMethod] public async Task ResendRequest_ShouldSerializeToExpectedPayload_GivenSmsType() { - var resend = new ResendParameters { Type = ResendType.Sms, Phone = "+5544998989898"}; + var resend = new ResendParameters { Type = ResendType.Sms, Phone = "+5544998989898" }; + await this.Api.Resend(resend); + + await this.Verify(this.EmittedRequestBody).UseDirectory("Data"); + } + + [TestMethod] + public async Task ResendRequest_ShouldSerializeCaptchaToExpectedPayload_GivenOptions() + { + var resend = new ResendParameters + { + Type = ResendType.SignUp, + Email = "user@supabase.com", + Options = new ResendOptions { CaptchaToken = "captcha-token" }, + }; + await this.Api.Resend(resend); await this.Verify(this.EmittedRequestBody).UseDirectory("Data"); From a13ff1f6587105007ba94e3acfa27562f29a1c8d Mon Sep 17 00:00:00 2001 From: Diego Santo Date: Thu, 1 Oct 2026 20:45:48 -0300 Subject: [PATCH 8/8] feat(gotrue): move creation of the body to the dto --- .../ResendParametersContractTests.cs | 44 +++++++--- .../Authentication/ResendParametersTests.cs | 56 ++++++++++-- packages/Gotrue/Gotrue/Api.cs | 18 +--- .../Gotrue/Gotrue/PublicAPI.Unshipped.txt | 12 +-- .../Gotrue/Gotrue/Resend/ResendOptions.cs | 17 ++-- .../Gotrue/Gotrue/Resend/ResendParameters.cs | 88 +++++++++++++++---- 6 files changed, 167 insertions(+), 68 deletions(-) diff --git a/packages/Gotrue/Gotrue.Tests/Authentication/ResendParametersContractTests.cs b/packages/Gotrue/Gotrue.Tests/Authentication/ResendParametersContractTests.cs index 334e8ac5..999bb803 100644 --- a/packages/Gotrue/Gotrue.Tests/Authentication/ResendParametersContractTests.cs +++ b/packages/Gotrue/Gotrue.Tests/Authentication/ResendParametersContractTests.cs @@ -18,7 +18,12 @@ public class ResendParametersContractTests : RequestApprovalFixture public async Task ResendRequest_ShouldSerializeToExpectedPayload_GivenSignUpType() { const string email = "user@supabase.com"; - var resend = new ResendParameters { Type = ResendType.SignUp, Email = email }; + var resend = new ResendParameters( + type: ResendType.SignUp, + email: email, + phone: null, + options: null + ); await this.Api.Resend(resend); await this.Verify(this.EmittedRequestBody).UseDirectory("Data"); @@ -28,7 +33,12 @@ public async Task ResendRequest_ShouldSerializeToExpectedPayload_GivenSignUpType public async Task ResendRequest_ShouldSerializeToExpectedPayload_GivenEmailChangeType() { const string email = "user@supabase.com"; - var resend = new ResendParameters { Type = ResendType.EmailChange, Email = email }; + var resend = new ResendParameters( + type: ResendType.EmailChange, + email: email, + phone: null, + options: null + ); await this.Api.Resend(resend); await this.Verify(this.EmittedRequestBody).UseDirectory("Data"); @@ -37,11 +47,12 @@ public async Task ResendRequest_ShouldSerializeToExpectedPayload_GivenEmailChang [TestMethod] public async Task ResendRequest_ShouldSerializeToExpectedPayload_GivenPhoneChangeType() { - var resend = new ResendParameters - { - Type = ResendType.PhoneChange, - Phone = "+5544998989898", - }; + var resend = new ResendParameters( + type: ResendType.PhoneChange, + email: null, + phone: "+5544998989898", + options: null + ); await this.Api.Resend(resend); await this.Verify(this.EmittedRequestBody).UseDirectory("Data"); @@ -50,7 +61,12 @@ public async Task ResendRequest_ShouldSerializeToExpectedPayload_GivenPhoneChang [TestMethod] public async Task ResendRequest_ShouldSerializeToExpectedPayload_GivenSmsType() { - var resend = new ResendParameters { Type = ResendType.Sms, Phone = "+5544998989898" }; + var resend = new ResendParameters( + type: ResendType.Sms, + email: null, + phone: "+5544998989898", + options: null + ); await this.Api.Resend(resend); await this.Verify(this.EmittedRequestBody).UseDirectory("Data"); @@ -59,12 +75,12 @@ public async Task ResendRequest_ShouldSerializeToExpectedPayload_GivenSmsType() [TestMethod] public async Task ResendRequest_ShouldSerializeCaptchaToExpectedPayload_GivenOptions() { - var resend = new ResendParameters - { - Type = ResendType.SignUp, - Email = "user@supabase.com", - Options = new ResendOptions { CaptchaToken = "captcha-token" }, - }; + var resend = new ResendParameters( + type: ResendType.SignUp, + email: "user@supabase.com", + phone: null, + options: new ResendOptions(captchaToken: "captcha-token", emailRedirectTo: null) + ); await this.Api.Resend(resend); diff --git a/packages/Gotrue/Gotrue.Tests/Authentication/ResendParametersTests.cs b/packages/Gotrue/Gotrue.Tests/Authentication/ResendParametersTests.cs index ed1d676d..a92762c3 100644 --- a/packages/Gotrue/Gotrue.Tests/Authentication/ResendParametersTests.cs +++ b/packages/Gotrue/Gotrue.Tests/Authentication/ResendParametersTests.cs @@ -16,7 +16,12 @@ public class ResendParametersTests : AuthClientFixture public async Task Resend_ShouldRequestConfirmationCode_GivenSignUpType() { var email = RandomEmail(); - var resend = new ResendParameters { Type = ResendType.SignUp, Email = email }; + var resend = new ResendParameters( + type: ResendType.SignUp, + email: email, + phone: null, + options: null + ); var response = await this.Client.Resend(resend); Assert.IsNotNull(response); @@ -26,7 +31,12 @@ public async Task Resend_ShouldRequestConfirmationCode_GivenSignUpType() [TestMethod] public async Task Resend_ShouldRequestConfirmationCode_GivenSmsType() { - var resend = new ResendParameters { Type = ResendType.Sms, Phone = "5544989899898" }; + var resend = new ResendParameters( + type: ResendType.Sms, + email: null, + phone: "5544989899898", + options: null + ); var response = await this.Client.Resend(resend); Assert.IsNotNull(response); @@ -36,7 +46,12 @@ public async Task Resend_ShouldRequestConfirmationCode_GivenSmsType() [TestMethod] public async Task Resend_ShouldRequestConfirmationCode_GivenPhoneChangeType() { - var resend = new ResendParameters { Type = ResendType.PhoneChange, Phone = "5544989899898" }; + var resend = new ResendParameters( + type: ResendType.PhoneChange, + email: null, + phone: "5544989899898", + options: null + ); var response = await this.Client.Resend(resend); Assert.IsNotNull(response); @@ -46,7 +61,12 @@ public async Task Resend_ShouldRequestConfirmationCode_GivenPhoneChangeType() [TestMethod] public async Task Resend_ShouldRequestConfirmationCode_GivenEmailChangeType() { - var resend = new ResendParameters { Type = ResendType.EmailChange, Email = RandomEmail() }; + var resend = new ResendParameters( + type: ResendType.EmailChange, + email: RandomEmail(), + phone: null, + options: null + ); var response = await this.Client.Resend(resend); Assert.IsNotNull(response); @@ -56,7 +76,12 @@ public async Task Resend_ShouldRequestConfirmationCode_GivenEmailChangeType() [TestMethod] public async Task Resend_ShouldThrowErrorWhenParameterAreInvalid_GivenPhoneForTypeEmail() { - var resend = new ResendParameters { Type = ResendType.EmailChange, Phone = "5544989899898" }; + var resend = new ResendParameters( + type: ResendType.EmailChange, + email: null, + phone: "5544989899898", + options: null + ); var action = async () => await this.Client.Resend(resend); @@ -68,7 +93,12 @@ public async Task Resend_ShouldThrowErrorWhenParameterAreInvalid_GivenPhoneForTy [TestMethod] public async Task Resend_ShouldThrowErrorWhenParameterAreInvalid_GivenEmailForTypePhone() { - var resend = new ResendParameters { Type = ResendType.Sms, Email = RandomEmail() }; + var resend = new ResendParameters( + type: ResendType.Sms, + email: RandomEmail(), + phone: null, + options: null + ); var action = async () => await this.Client.Resend(resend); @@ -80,7 +110,12 @@ public async Task Resend_ShouldThrowErrorWhenParameterAreInvalid_GivenEmailForTy [TestMethod] public async Task Resend_ShouldThrowErrorWhenParameterAreInvalid_GivenNothingForTypeEmail() { - var resend = new ResendParameters { Type = ResendType.EmailChange }; + var resend = new ResendParameters( + type: ResendType.EmailChange, + email: null, + phone: null, + options: null + ); var action = async () => await this.Client.Resend(resend); @@ -92,7 +127,12 @@ public async Task Resend_ShouldThrowErrorWhenParameterAreInvalid_GivenNothingFor [TestMethod] public async Task Resend_ShouldThrowErrorWhenParameterAreInvalid_GivenNothingForTypePhone() { - var resend = new ResendParameters { Type = ResendType.Sms }; + var resend = new ResendParameters( + type: ResendType.Sms, + email: null, + phone: null, + options: null + ); var action = async () => await this.Client.Resend(resend); diff --git a/packages/Gotrue/Gotrue/Api.cs b/packages/Gotrue/Gotrue/Api.cs index 8ba6353f..c2780864 100644 --- a/packages/Gotrue/Gotrue/Api.cs +++ b/packages/Gotrue/Gotrue/Api.cs @@ -878,24 +878,12 @@ public Task GenerateLink(string jwt, GenerateLinkOptions options) /// BaseResponse public Task Resend(ResendParameters resendParameters) { - var body = new Dictionary() - { - { "type", Core.Helpers.GetMappedToAttr(resendParameters.Type).Mapping } - }; - - if (resendParameters.IsEmail() && !string.IsNullOrWhiteSpace(resendParameters.Email)) - body.Add("email", resendParameters.Email); - - if (!resendParameters.IsEmail() && !string.IsNullOrWhiteSpace(resendParameters.Phone)) - body.Add("phone", resendParameters.Phone); - - if (resendParameters.Options != null && !string.IsNullOrWhiteSpace(resendParameters.Options.CaptchaToken)) - body.Add("gotrue_meta_security", new Dictionary() { { "captcha_token", resendParameters.Options.CaptchaToken } }); - + var body = resendParameters.ToDictionary(); + var url = resendParameters.ApplyRedirectTo(this.Url); return this.MakeRequestAsync( HttpMethod.Post, - $"{this.Url}/resend", + url, body, this.Headers ); diff --git a/packages/Gotrue/Gotrue/PublicAPI.Unshipped.txt b/packages/Gotrue/Gotrue/PublicAPI.Unshipped.txt index 58d6a19c..18737efe 100644 --- a/packages/Gotrue/Gotrue/PublicAPI.Unshipped.txt +++ b/packages/Gotrue/Gotrue/PublicAPI.Unshipped.txt @@ -120,21 +120,15 @@ Supabase.Gotrue.Interfaces.IGotrueClient.GetClaimsAsync(string? Supabase.Gotrue.Interfaces.IGotrueClient.Resend(Supabase.Gotrue.Resend.ResendParameters! resendParameters) -> System.Threading.Tasks.Task! Supabase.Gotrue.Resend.ResendOptions Supabase.Gotrue.Resend.ResendOptions.CaptchaToken.get -> string? -Supabase.Gotrue.Resend.ResendOptions.CaptchaToken.set -> void Supabase.Gotrue.Resend.ResendOptions.EmailRedirectTo.get -> string? -Supabase.Gotrue.Resend.ResendOptions.EmailRedirectTo.set -> void -Supabase.Gotrue.Resend.ResendOptions.ResendOptions() -> void +Supabase.Gotrue.Resend.ResendOptions.ResendOptions(string? captchaToken, string? emailRedirectTo) -> void Supabase.Gotrue.Resend.ResendParameters Supabase.Gotrue.Resend.ResendParameters.Email.get -> string? -Supabase.Gotrue.Resend.ResendParameters.Email.set -> void -Supabase.Gotrue.Resend.ResendParameters.IsEmail() -> bool Supabase.Gotrue.Resend.ResendParameters.Options.get -> Supabase.Gotrue.Resend.ResendOptions? -Supabase.Gotrue.Resend.ResendParameters.Options.set -> void Supabase.Gotrue.Resend.ResendParameters.Phone.get -> string? -Supabase.Gotrue.Resend.ResendParameters.Phone.set -> void -Supabase.Gotrue.Resend.ResendParameters.ResendParameters() -> void +Supabase.Gotrue.Resend.ResendParameters.ResendParameters(Supabase.Gotrue.Resend.ResendType type, string? email, string? phone, Supabase.Gotrue.Resend.ResendOptions? options) -> void +Supabase.Gotrue.Resend.ResendParameters.ToDictionary() -> System.Collections.Generic.Dictionary! Supabase.Gotrue.Resend.ResendParameters.Type.get -> Supabase.Gotrue.Resend.ResendType -Supabase.Gotrue.Resend.ResendParameters.Type.set -> void Supabase.Gotrue.Resend.ResendType Supabase.Gotrue.Resend.ResendType.EmailChange = 3 -> Supabase.Gotrue.Resend.ResendType Supabase.Gotrue.Resend.ResendType.PhoneChange = 2 -> Supabase.Gotrue.Resend.ResendType diff --git a/packages/Gotrue/Gotrue/Resend/ResendOptions.cs b/packages/Gotrue/Gotrue/Resend/ResendOptions.cs index 012e0ea6..b9f21460 100644 --- a/packages/Gotrue/Gotrue/Resend/ResendOptions.cs +++ b/packages/Gotrue/Gotrue/Resend/ResendOptions.cs @@ -1,5 +1,3 @@ -using System.Text.Json.Serialization; - namespace Supabase.Gotrue.Resend; /// @@ -12,12 +10,19 @@ public class ResendOptions /// /// Verification token received when the user completes the captcha on the site. /// - [JsonPropertyName("captchaToken")] - public string? CaptchaToken { get; set; } + public string? CaptchaToken { get; } /// /// A URL or mobile address to send the user to after they are confirmed. /// - [JsonPropertyName("emailRedirectTo")] - public string? EmailRedirectTo { get; set; } + public string? EmailRedirectTo { get; } + + /// + /// Represents optional parameters for a resend operation in the Resend API. + /// + public ResendOptions(string? captchaToken, string? emailRedirectTo) + { + this.CaptchaToken = captchaToken; + this.EmailRedirectTo = emailRedirectTo; + } } diff --git a/packages/Gotrue/Gotrue/Resend/ResendParameters.cs b/packages/Gotrue/Gotrue/Resend/ResendParameters.cs index 9978bffd..2ed94893 100644 --- a/packages/Gotrue/Gotrue/Resend/ResendParameters.cs +++ b/packages/Gotrue/Gotrue/Resend/ResendParameters.cs @@ -1,5 +1,4 @@ using System.Collections.Generic; -using System.Text.Json.Serialization; namespace Supabase.Gotrue.Resend; @@ -11,40 +10,97 @@ public class ResendParameters /// /// The email address to resend to. /// - [JsonPropertyName("email")] - public string? Email { get; set; } + public string? Email { get; } /// /// The phone number to resend to. /// - [JsonPropertyName("phone")] - public string? Phone { get; set; } + public string? Phone { get; } /// /// The type of resend. /// - [JsonPropertyName("type")] - public ResendType Type { get; set; } + public ResendType Type { get; } /// /// Additional options to customize the behavior of the Resend API. /// Provides support for features such as CAPTCHA tokens and email redirection. /// - [JsonPropertyName("options")] - public ResendOptions? Options { get; set; } + public ResendOptions? Options { get; } /// - /// Check if the ResendType belongs to email-related types. + /// Represents the parameters required for a resend operation, such as resending a confirmation email + /// or a verification SMS, depending on the specified type. /// - /// - public bool IsEmail() + public ResendParameters(ResendType type, string? email, string? phone, ResendOptions? options) { - var types = new List() + this.Type = type; + this.Email = email; + this.Phone = phone; + this.Options = options; + } + + /// + /// Converts the resend parameters to a dictionary format, including the type, email, phone, + /// and any additional options, for use in API requests. + /// + /// + /// A dictionary containing the key-value pairs representing the resend parameters. + /// + public Dictionary ToDictionary() + { + var body = new Dictionary { - ResendType.EmailChange, - ResendType.SignUp + { "type", Core.Helpers.GetMappedToAttr(this.Type).Mapping }, }; - return types.Contains(this.Type); + body = this.AddEmailAndTokenField(body); + body = this.AddPhoneField(body); + + return body; + } + + internal string ApplyRedirectTo(string url) + { + var newUrl = $"{url}/resend"; + if (!this.IsEmail()) + return newUrl; + + if (!string.IsNullOrWhiteSpace(this.Options?.EmailRedirectTo)) + newUrl = $"{url}/resend?redirect_to={this.Options.EmailRedirectTo}"; + + return newUrl; + } + + private bool IsEmail() => this.Type is ResendType.EmailChange or ResendType.SignUp; + + private Dictionary AddEmailAndTokenField(Dictionary parameter) + { + if (!this.IsEmail()) + return parameter; + + var body = new Dictionary(parameter); + if (!string.IsNullOrWhiteSpace(this.Email)) + body.Add("email", this.Email); + + if (!string.IsNullOrWhiteSpace(this.Options?.CaptchaToken)) + body.Add( + "gotrue_meta_security", + new Dictionary { { "captcha_token", this.Options.CaptchaToken } } + ); + + return body; + } + + private Dictionary AddPhoneField(Dictionary parameter) + { + if (this.IsEmail()) + return parameter; + + var body = new Dictionary(parameter); + if (!string.IsNullOrWhiteSpace(this.Phone)) + body.Add("phone", this.Phone); + + return body; } }