Repository: supabase-community/supabase-kt (kotlin)

Issue:
[Feature request]: Add a way to verify an OTP from a token hash
### General Info

- [X] I installed the latest version of Supabase Kt
- [X] I checked for similar feature requests

### Feature request

Add a `fun verifyEmailOtp(type: OtpType.Email, tokenHash: String)` function to verify an OTP from a token hash. This feature is present in official Supabase SDKs such as the JS SDK (see [verifyOtp](https://supabase.com/docs/reference/javascript/auth-verifyotp)) and Swift SDK (see [merged PR](https://github.com/supabase/supabase-swift/pull/451)).

### Usecase

This would be very useful when verifying an OTP coming from a link sent by email.

Pull request description:
## What kind of change does this PR introduce?

Feature (closes #655)

## What is the current behavior?

You cannot sign in via verifying a token hash received from an email

## What is the new behavior?

There is now a new overload providing support for this feature.

Patch:
diff --git a/GoTrue/src/commonMain/kotlin/io/github/jan/supabase/gotrue/Auth.kt b/GoTrue/src/commonMain/kotlin/io/github/jan/supabase/gotrue/Auth.kt
index 270ebc76b..3ed62db8b 100644
--- a/GoTrue/src/commonMain/kotlin/io/github/jan/supabase/gotrue/Auth.kt
+++ b/GoTrue/src/commonMain/kotlin/io/github/jan/supabase/gotrue/Auth.kt
@@ -268,6 +268,16 @@ sealed interface Auth : MainPlugin<AuthConfig>, CustomSerializationPlugin {
      */
     suspend fun verifyEmailOtp(type: OtpType.Email, email: String, token: String, captchaToken: String? = null)
 
+    /**
+     * Verifies a email otp token hash received via email
+     * @param type The type of the verification
+     * @param tokenHash The token hash used to verify
+     * @throws RestException or one of its subclasses if receiving an error response. If the error response contains a error code, an [AuthRestException] will be thrown which can be used to easier identify the problem.
+     * @throws HttpRequestTimeoutException if the request timed out
+     * @throws HttpRequestException on network related issues
+     */
+    suspend fun verifyEmailOtp(type: OtpType.Email, tokenHash: String, captchaToken: String? = null)
+
     /**
      * Verifies a phone/sms otp
      * @param type The type of the verification
diff --git a/GoTrue/src/commonMain/kotlin/io/github/jan/supabase/gotrue/AuthImpl.kt b/GoTrue/src/commonMain/kotlin/io/github/jan/supabase/gotrue/AuthImpl.kt
index d5a8ed563..b62c93338 100644
--- a/GoTrue/src/commonMain/kotlin/io/github/jan/supabase/gotrue/AuthImpl.kt
+++ b/GoTrue/src/commonMain/kotlin/io/github/jan/supabase/gotrue/AuthImpl.kt
@@ -295,13 +295,13 @@ internal class AuthImpl(
 
     private suspend fun verify(
         type: String,
-        token: String,
+        token: String?,
         captchaToken: String?,
         additionalData: JsonObjectBuilder.() -> Unit
     ) {
         val body = buildJsonObject {
             put("type", type)
-            put("token", token)
+            token?.let { put("token", it) }
             captchaToken?.let(::putCaptchaToken)
             additionalData()
         }
@@ -319,6 +319,14 @@ internal class AuthImpl(
         put("email", email)
     }
 
+    override suspend fun verifyEmailOtp(
+        type: OtpType.Email,
+        tokenHash: String,
+        captchaToken: String?
+    ) = verify(type.type, null, captchaToken) {
+        put("token_hash", tokenHash)
+    }
+
     override suspend fun verifyPhoneOtp(
         type: OtpType.Phone,
         phone: String,


Test patch:
diff --git a/GoTrue/src/commonTest/kotlin/AuthApiTest.kt b/GoTrue/src/commonTest/kotlin/AuthApiTest.kt
index 6b733123d..5f6c6ea0b 100644
--- a/GoTrue/src/commonTest/kotlin/AuthApiTest.kt
+++ b/GoTrue/src/commonTest/kotlin/AuthApiTest.kt
@@ -560,6 +560,31 @@ class AuthRequestTest {
         }
     }
 
+    @Test
+    fun testVerifyEmailOtpWithTokenHash() {
+        runTest {
+            val expectedType = OtpType.Email.EMAIL
+            val expectedTokenHash = "hash"
+            val expectedCaptchaToken = "captchaToken"
+            val client = createMockedSupabaseClient(configuration = configuration) {
+                assertMethodIs(HttpMethod.Post, it.method)
+                assertPathIs("/verify", it.url.pathAfterVersion())
+                val body = it.body.toJsonElement().jsonObject
+                val metaSecurity = body["gotrue_meta_security"]!!.jsonObject
+                assertEquals(
+                    expectedCaptchaToken,
+                    metaSecurity["captcha_token"]?.jsonPrimitive?.content
+                )
+                assertEquals(expectedTokenHash, body["token_hash"]?.jsonPrimitive?.content)
+                assertEquals(expectedType.name.lowercase(), body["type"]?.jsonPrimitive?.content)
+                respondJson(
+                    sampleUserSession()
+                )
+            }
+            client.auth.verifyEmailOtp(expectedType, tokenHash = expectedTokenHash, captchaToken = expectedCaptchaToken)
+        }
+    }
+
     @Test
     fun testVerifyPhoneOtp() {
         runTest {