diff --git a/CHANGELOG.md b/CHANGELOG.md index bef1683..e32bd1c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,11 @@ # Change Log +## 29.1.0 + +* Added: `account` passkey methods `listPasskeys`, `createPasskey`, `getPasskey`, `updatePasskey`, `deletePasskey` and `updatePasskeyVerification` +* Added: `account.createPasskeyToken` and `account.updatePasskeyToken` for passkey sign-in +* Added: `Passkey`, `PasskeyList`, `PasskeyChallenge` and `TokenSecret` models + ## 29.0.0 * Breaking: minimum Android SDK raised from 23 to 24 (Android 7.0) diff --git a/README.md b/README.md index aeffd88..535ef1f 100644 --- a/README.md +++ b/README.md @@ -38,7 +38,7 @@ repositories { Next, add the dependency to your project's `build.gradle(.kts)` file: ```groovy -implementation("io.appwrite:sdk-for-android:29.0.0") +implementation("io.appwrite:sdk-for-android:29.1.0") ``` ### Maven @@ -49,7 +49,7 @@ Add this to your project's `pom.xml` file: io.appwrite sdk-for-android - 29.0.0 + 29.1.0 ``` diff --git a/docs/examples/java/account/create-passkey-token.md b/docs/examples/java/account/create-passkey-token.md new file mode 100644 index 0000000..12db5fe --- /dev/null +++ b/docs/examples/java/account/create-passkey-token.md @@ -0,0 +1,22 @@ +```java +import android.util.Log; + +import io.appwrite.Client; +import io.appwrite.coroutines.CoroutineCallback; +import io.appwrite.services.Account; + +Client client = new Client(context) + .setEndpoint("https://.cloud.appwrite.io/v1") // Your API Endpoint + .setProject(""); // Your project ID + +Account account = new Account(client); + +account.createPasskeyToken(new CoroutineCallback<>((result, error) -> { + if (error != null) { + error.printStackTrace(); + return; + } + + Log.d("Appwrite", result.toString()); +})); +``` diff --git a/docs/examples/java/account/create-passkey.md b/docs/examples/java/account/create-passkey.md new file mode 100644 index 0000000..39a9a29 --- /dev/null +++ b/docs/examples/java/account/create-passkey.md @@ -0,0 +1,27 @@ +```java +import android.util.Log; + +import io.appwrite.Client; +import io.appwrite.coroutines.CoroutineCallback; +import io.appwrite.services.Account; + +Client client = new Client(context) + .setEndpoint("https://.cloud.appwrite.io/v1") // Your API Endpoint + .setProject(""); // Your project ID + +Account account = new Account(client); + +account.createPasskey( + "", // passkeyId (optional) + "", // name (optional) + new CoroutineCallback<>((result, error) -> { + if (error != null) { + error.printStackTrace(); + return; + } + + Log.d("Appwrite", result.toString()); + }) +); + +``` diff --git a/docs/examples/java/account/delete-passkey.md b/docs/examples/java/account/delete-passkey.md new file mode 100644 index 0000000..a59d384 --- /dev/null +++ b/docs/examples/java/account/delete-passkey.md @@ -0,0 +1,26 @@ +```java +import android.util.Log; + +import io.appwrite.Client; +import io.appwrite.coroutines.CoroutineCallback; +import io.appwrite.services.Account; + +Client client = new Client(context) + .setEndpoint("https://.cloud.appwrite.io/v1") // Your API Endpoint + .setProject(""); // Your project ID + +Account account = new Account(client); + +account.deletePasskey( + "", // passkeyId + new CoroutineCallback<>((result, error) -> { + if (error != null) { + error.printStackTrace(); + return; + } + + Log.d("Appwrite", result.toString()); + }) +); + +``` diff --git a/docs/examples/java/account/get-passkey.md b/docs/examples/java/account/get-passkey.md new file mode 100644 index 0000000..5d988e8 --- /dev/null +++ b/docs/examples/java/account/get-passkey.md @@ -0,0 +1,26 @@ +```java +import android.util.Log; + +import io.appwrite.Client; +import io.appwrite.coroutines.CoroutineCallback; +import io.appwrite.services.Account; + +Client client = new Client(context) + .setEndpoint("https://.cloud.appwrite.io/v1") // Your API Endpoint + .setProject(""); // Your project ID + +Account account = new Account(client); + +account.getPasskey( + "", // passkeyId + new CoroutineCallback<>((result, error) -> { + if (error != null) { + error.printStackTrace(); + return; + } + + Log.d("Appwrite", result.toString()); + }) +); + +``` diff --git a/docs/examples/java/account/list-passkeys.md b/docs/examples/java/account/list-passkeys.md new file mode 100644 index 0000000..6b04b1f --- /dev/null +++ b/docs/examples/java/account/list-passkeys.md @@ -0,0 +1,27 @@ +```java +import android.util.Log; + +import io.appwrite.Client; +import io.appwrite.coroutines.CoroutineCallback; +import io.appwrite.services.Account; + +Client client = new Client(context) + .setEndpoint("https://.cloud.appwrite.io/v1") // Your API Endpoint + .setProject(""); // Your project ID + +Account account = new Account(client); + +account.listPasskeys( + List.of(), // queries (optional) + false, // total (optional) + new CoroutineCallback<>((result, error) -> { + if (error != null) { + error.printStackTrace(); + return; + } + + Log.d("Appwrite", result.toString()); + }) +); + +``` diff --git a/docs/examples/java/account/update-passkey-token.md b/docs/examples/java/account/update-passkey-token.md new file mode 100644 index 0000000..6370aaa --- /dev/null +++ b/docs/examples/java/account/update-passkey-token.md @@ -0,0 +1,27 @@ +```java +import android.util.Log; + +import io.appwrite.Client; +import io.appwrite.coroutines.CoroutineCallback; +import io.appwrite.services.Account; + +Client client = new Client(context) + .setEndpoint("https://.cloud.appwrite.io/v1") // Your API Endpoint + .setProject(""); // Your project ID + +Account account = new Account(client); + +account.updatePasskeyToken( + "", // challengeId + Map.of("a", "b"), // credential + new CoroutineCallback<>((result, error) -> { + if (error != null) { + error.printStackTrace(); + return; + } + + Log.d("Appwrite", result.toString()); + }) +); + +``` diff --git a/docs/examples/java/account/update-passkey-verification.md b/docs/examples/java/account/update-passkey-verification.md new file mode 100644 index 0000000..53eed3a --- /dev/null +++ b/docs/examples/java/account/update-passkey-verification.md @@ -0,0 +1,27 @@ +```java +import android.util.Log; + +import io.appwrite.Client; +import io.appwrite.coroutines.CoroutineCallback; +import io.appwrite.services.Account; + +Client client = new Client(context) + .setEndpoint("https://.cloud.appwrite.io/v1") // Your API Endpoint + .setProject(""); // Your project ID + +Account account = new Account(client); + +account.updatePasskeyVerification( + "", // passkeyId + Map.of("a", "b"), // credential + new CoroutineCallback<>((result, error) -> { + if (error != null) { + error.printStackTrace(); + return; + } + + Log.d("Appwrite", result.toString()); + }) +); + +``` diff --git a/docs/examples/java/account/update-passkey.md b/docs/examples/java/account/update-passkey.md new file mode 100644 index 0000000..3cf1078 --- /dev/null +++ b/docs/examples/java/account/update-passkey.md @@ -0,0 +1,27 @@ +```java +import android.util.Log; + +import io.appwrite.Client; +import io.appwrite.coroutines.CoroutineCallback; +import io.appwrite.services.Account; + +Client client = new Client(context) + .setEndpoint("https://.cloud.appwrite.io/v1") // Your API Endpoint + .setProject(""); // Your project ID + +Account account = new Account(client); + +account.updatePasskey( + "", // passkeyId + "", // name + new CoroutineCallback<>((result, error) -> { + if (error != null) { + error.printStackTrace(); + return; + } + + Log.d("Appwrite", result.toString()); + }) +); + +``` diff --git a/docs/examples/kotlin/account/create-passkey-token.md b/docs/examples/kotlin/account/create-passkey-token.md new file mode 100644 index 0000000..52b6bf3 --- /dev/null +++ b/docs/examples/kotlin/account/create-passkey-token.md @@ -0,0 +1,13 @@ +```kotlin +import io.appwrite.Client +import io.appwrite.coroutines.CoroutineCallback +import io.appwrite.services.Account + +val client = Client(context) + .setEndpoint("https://.cloud.appwrite.io/v1") // Your API Endpoint + .setProject("") // Your project ID + +val account = Account(client) + +val result = account.createPasskeyToken() +``` diff --git a/docs/examples/kotlin/account/create-passkey.md b/docs/examples/kotlin/account/create-passkey.md new file mode 100644 index 0000000..69a0651 --- /dev/null +++ b/docs/examples/kotlin/account/create-passkey.md @@ -0,0 +1,16 @@ +```kotlin +import io.appwrite.Client +import io.appwrite.coroutines.CoroutineCallback +import io.appwrite.services.Account + +val client = Client(context) + .setEndpoint("https://.cloud.appwrite.io/v1") // Your API Endpoint + .setProject("") // Your project ID + +val account = Account(client) + +val result = account.createPasskey( + passkeyId = "", // (optional) + name = "", // (optional) +) +``` diff --git a/docs/examples/kotlin/account/delete-passkey.md b/docs/examples/kotlin/account/delete-passkey.md new file mode 100644 index 0000000..08bf8fd --- /dev/null +++ b/docs/examples/kotlin/account/delete-passkey.md @@ -0,0 +1,15 @@ +```kotlin +import io.appwrite.Client +import io.appwrite.coroutines.CoroutineCallback +import io.appwrite.services.Account + +val client = Client(context) + .setEndpoint("https://.cloud.appwrite.io/v1") // Your API Endpoint + .setProject("") // Your project ID + +val account = Account(client) + +val result = account.deletePasskey( + passkeyId = "", +) +``` diff --git a/docs/examples/kotlin/account/get-passkey.md b/docs/examples/kotlin/account/get-passkey.md new file mode 100644 index 0000000..ddf9e83 --- /dev/null +++ b/docs/examples/kotlin/account/get-passkey.md @@ -0,0 +1,15 @@ +```kotlin +import io.appwrite.Client +import io.appwrite.coroutines.CoroutineCallback +import io.appwrite.services.Account + +val client = Client(context) + .setEndpoint("https://.cloud.appwrite.io/v1") // Your API Endpoint + .setProject("") // Your project ID + +val account = Account(client) + +val result = account.getPasskey( + passkeyId = "", +) +``` diff --git a/docs/examples/kotlin/account/list-passkeys.md b/docs/examples/kotlin/account/list-passkeys.md new file mode 100644 index 0000000..74c8add --- /dev/null +++ b/docs/examples/kotlin/account/list-passkeys.md @@ -0,0 +1,16 @@ +```kotlin +import io.appwrite.Client +import io.appwrite.coroutines.CoroutineCallback +import io.appwrite.services.Account + +val client = Client(context) + .setEndpoint("https://.cloud.appwrite.io/v1") // Your API Endpoint + .setProject("") // Your project ID + +val account = Account(client) + +val result = account.listPasskeys( + queries = listOf(), // (optional) + total = false, // (optional) +) +``` diff --git a/docs/examples/kotlin/account/update-passkey-token.md b/docs/examples/kotlin/account/update-passkey-token.md new file mode 100644 index 0000000..f48e29f --- /dev/null +++ b/docs/examples/kotlin/account/update-passkey-token.md @@ -0,0 +1,16 @@ +```kotlin +import io.appwrite.Client +import io.appwrite.coroutines.CoroutineCallback +import io.appwrite.services.Account + +val client = Client(context) + .setEndpoint("https://.cloud.appwrite.io/v1") // Your API Endpoint + .setProject("") // Your project ID + +val account = Account(client) + +val result = account.updatePasskeyToken( + challengeId = "", + credential = mapOf( "a" to "b" ), +) +``` diff --git a/docs/examples/kotlin/account/update-passkey-verification.md b/docs/examples/kotlin/account/update-passkey-verification.md new file mode 100644 index 0000000..41c016a --- /dev/null +++ b/docs/examples/kotlin/account/update-passkey-verification.md @@ -0,0 +1,16 @@ +```kotlin +import io.appwrite.Client +import io.appwrite.coroutines.CoroutineCallback +import io.appwrite.services.Account + +val client = Client(context) + .setEndpoint("https://.cloud.appwrite.io/v1") // Your API Endpoint + .setProject("") // Your project ID + +val account = Account(client) + +val result = account.updatePasskeyVerification( + passkeyId = "", + credential = mapOf( "a" to "b" ), +) +``` diff --git a/docs/examples/kotlin/account/update-passkey.md b/docs/examples/kotlin/account/update-passkey.md new file mode 100644 index 0000000..c8c2bc3 --- /dev/null +++ b/docs/examples/kotlin/account/update-passkey.md @@ -0,0 +1,16 @@ +```kotlin +import io.appwrite.Client +import io.appwrite.coroutines.CoroutineCallback +import io.appwrite.services.Account + +val client = Client(context) + .setEndpoint("https://.cloud.appwrite.io/v1") // Your API Endpoint + .setProject("") // Your project ID + +val account = Account(client) + +val result = account.updatePasskey( + passkeyId = "", + name = "", +) +``` diff --git a/library/src/main/java/io/appwrite/Client.kt b/library/src/main/java/io/appwrite/Client.kt index c364e83..8130478 100644 --- a/library/src/main/java/io/appwrite/Client.kt +++ b/library/src/main/java/io/appwrite/Client.kt @@ -101,7 +101,7 @@ class Client @JvmOverloads constructor( "x-sdk-name" to "Android", "x-sdk-platform" to "client", "x-sdk-language" to "android", - "x-sdk-version" to "29.0.0", + "x-sdk-version" to "29.1.0", "x-appwrite-response-format" to "2.3.0" ) config = mutableMapOf() diff --git a/library/src/main/java/io/appwrite/models/Passkey.kt b/library/src/main/java/io/appwrite/models/Passkey.kt new file mode 100644 index 0000000..de2ea0a --- /dev/null +++ b/library/src/main/java/io/appwrite/models/Passkey.kt @@ -0,0 +1,61 @@ +package io.appwrite.models + +import com.google.gson.annotations.SerializedName +import io.appwrite.extensions.jsonCast + +/** + * Passkey + */ +data class Passkey( + /** + * Passkey ID. + */ + @SerializedName("\$id") + val id: String, + + /** + * Passkey creation date in ISO 8601 format. + */ + @SerializedName("\$createdAt") + val createdAt: String, + + /** + * Passkey update date in ISO 8601 format. + */ + @SerializedName("\$updatedAt") + val updatedAt: String, + + /** + * Passkey name. + */ + @SerializedName("name") + val name: String, + + /** + * Most recent sign-in with this passkey in ISO 8601 format. Empty until the passkey is used to sign in. + */ + @SerializedName("accessedAt") + val accessedAt: String, + +) { + fun toMap(): Map = mapOf( + "\$id" to id as Any, + "\$createdAt" to createdAt as Any, + "\$updatedAt" to updatedAt as Any, + "name" to name as Any, + "accessedAt" to accessedAt as Any, + ) + + companion object { + @Suppress("UNCHECKED_CAST") + fun from( + map: Map, + ) = Passkey( + id = map["\$id"] as String, + createdAt = map["\$createdAt"] as String, + updatedAt = map["\$updatedAt"] as String, + name = map["name"] as String, + accessedAt = map["accessedAt"] as String, + ) + } +} diff --git a/library/src/main/java/io/appwrite/models/PasskeyChallenge.kt b/library/src/main/java/io/appwrite/models/PasskeyChallenge.kt new file mode 100644 index 0000000..4a9f715 --- /dev/null +++ b/library/src/main/java/io/appwrite/models/PasskeyChallenge.kt @@ -0,0 +1,61 @@ +package io.appwrite.models + +import com.google.gson.annotations.SerializedName +import io.appwrite.extensions.jsonCast + +/** + * Passkey Challenge + */ +data class PasskeyChallenge( + /** + * Challenge ID. When signing in, pass it back with the credential to receive a token. + */ + @SerializedName("\$id") + val id: String, + + /** + * Challenge creation date in ISO 8601 format. + */ + @SerializedName("\$createdAt") + val createdAt: String, + + /** + * ID of the passkey being registered. Empty for sign-in challenges. + */ + @SerializedName("passkeyId") + val passkeyId: String, + + /** + * Challenge expiration date in ISO 8601 format. + */ + @SerializedName("expire") + val expire: String, + + /** + * WebAuthn options in JSON form. Pass them to `PublicKeyCredential.parseCreationOptionsFromJSON()` when registering, or `PublicKeyCredential.parseRequestOptionsFromJSON()` when signing in. + */ + @SerializedName("publicKey") + val publicKey: Map, + +) { + fun toMap(): Map = mapOf( + "\$id" to id as Any, + "\$createdAt" to createdAt as Any, + "passkeyId" to passkeyId as Any, + "expire" to expire as Any, + "publicKey" to publicKey as Any, + ) + + companion object { + @Suppress("UNCHECKED_CAST") + fun from( + map: Map, + ) = PasskeyChallenge( + id = map["\$id"] as String, + createdAt = map["\$createdAt"] as String, + passkeyId = map["passkeyId"] as String, + expire = map["expire"] as String, + publicKey = map["publicKey"] as Map, + ) + } +} diff --git a/library/src/main/java/io/appwrite/models/PasskeyList.kt b/library/src/main/java/io/appwrite/models/PasskeyList.kt new file mode 100644 index 0000000..3ab4efc --- /dev/null +++ b/library/src/main/java/io/appwrite/models/PasskeyList.kt @@ -0,0 +1,37 @@ +package io.appwrite.models + +import com.google.gson.annotations.SerializedName +import io.appwrite.extensions.jsonCast + +/** + * Passkeys List + */ +data class PasskeyList( + /** + * Total number of passkeys that matched your query. + */ + @SerializedName("total") + val total: Long, + + /** + * List of passkeys. + */ + @SerializedName("passkeys") + val passkeys: List, + +) { + fun toMap(): Map = mapOf( + "total" to total as Any, + "passkeys" to passkeys.map { it.toMap() } as Any, + ) + + companion object { + @Suppress("UNCHECKED_CAST") + fun from( + map: Map, + ) = PasskeyList( + total = (map["total"] as Number).toLong(), + passkeys = (map["passkeys"] as List>).map { Passkey.from(map = it) }, + ) + } +} diff --git a/library/src/main/java/io/appwrite/models/TokenSecret.kt b/library/src/main/java/io/appwrite/models/TokenSecret.kt new file mode 100644 index 0000000..9a6cd65 --- /dev/null +++ b/library/src/main/java/io/appwrite/models/TokenSecret.kt @@ -0,0 +1,69 @@ +package io.appwrite.models + +import com.google.gson.annotations.SerializedName +import io.appwrite.extensions.jsonCast + +/** + * Token Secret + */ +data class TokenSecret( + /** + * Token ID. + */ + @SerializedName("\$id") + val id: String, + + /** + * Token creation date in ISO 8601 format. + */ + @SerializedName("\$createdAt") + val createdAt: String, + + /** + * User ID. + */ + @SerializedName("userId") + val userId: String, + + /** + * Token secret key. Exchange it for a session with the create session endpoint. + */ + @SerializedName("secret") + val secret: String, + + /** + * Token expiration date in ISO 8601 format. + */ + @SerializedName("expire") + val expire: String, + + /** + * Security phrase of a token. Empty if security phrase was not requested when creating a token. It includes randomly generated phrase which is also sent in the external resource such as email. + */ + @SerializedName("phrase") + val phrase: String, + +) { + fun toMap(): Map = mapOf( + "\$id" to id as Any, + "\$createdAt" to createdAt as Any, + "userId" to userId as Any, + "secret" to secret as Any, + "expire" to expire as Any, + "phrase" to phrase as Any, + ) + + companion object { + @Suppress("UNCHECKED_CAST") + fun from( + map: Map, + ) = TokenSecret( + id = map["\$id"] as String, + createdAt = map["\$createdAt"] as String, + userId = map["userId"] as String, + secret = map["secret"] as String, + expire = map["expire"] as String, + phrase = map["phrase"] as String, + ) + } +} diff --git a/library/src/main/java/io/appwrite/services/Account.kt b/library/src/main/java/io/appwrite/services/Account.kt index 8df3822..61b6ce9 100644 --- a/library/src/main/java/io/appwrite/services/Account.kt +++ b/library/src/main/java/io/appwrite/services/Account.kt @@ -1200,6 +1200,219 @@ class Account(client: Client) : Service(client) { nestedType = classOf(), ) + /** + * Get the list of verified passkeys registered by the currently logged in user. + * + * @param queries Array of query strings generated using the Query class provided by the SDK. [Learn more about queries](https://appwrite.io/docs/queries). Maximum of 100 queries are allowed, each 4096 characters long. You may filter and order on the following attributes: $id, $createdAt, $updatedAt, name, accessedAt + * @param total When set to false, the total count returned will be 0 and will not be calculated. + * @return [io.appwrite.models.PasskeyList] + */ + @JvmOverloads + suspend fun listPasskeys( + queries: List? = null, + total: Boolean? = null, + ): io.appwrite.models.PasskeyList { + val apiPath = "/account/passkeys" + val apiParams = mutableMapOf( + "queries" to queries, + "total" to total, + ) + val apiHeaders = mutableMapOf( + "X-Appwrite-Project" to client.config["project"].orEmpty(), + "accept" to "application/json", + ) + val converter: (Any) -> io.appwrite.models.PasskeyList = { + @Suppress("UNCHECKED_CAST") + io.appwrite.models.PasskeyList.from(map = it as Map) + } + return client.call( + "GET", + apiPath, + apiHeaders, + apiParams, + responseType = io.appwrite.models.PasskeyList::class.java, + converter, + ) + } + + /** + * Start registering a passkey for the currently logged in user. The session must have signed in or completed an MFA challenge within the last 10 minutes. Pass the returned `publicKey` options to `navigator.credentials.create()`, then complete the registration with [Update passkey verification](/docs/references/cloud/client-web/account#updatePasskeyVerification). The passkey stays pending until verified, and the challenge expires after 5 minutes. + * + * @param passkeyId Passkey ID. Choose a custom ID or generate a random ID with `ID.unique()`. Valid chars are a-z, A-Z, 0-9, period, hyphen, and underscore. Can't start with a special char. Max length is 36 chars. + * @param name Passkey name, shown when listing passkeys. Max length: 128 chars. + * @return [io.appwrite.models.PasskeyChallenge] + */ + @JvmOverloads + suspend fun createPasskey( + passkeyId: String? = null, + name: String? = null, + ): io.appwrite.models.PasskeyChallenge { + val apiPath = "/account/passkeys" + val apiParams = mutableMapOf( + "passkeyId" to passkeyId, + "name" to name, + ) + val apiHeaders = mutableMapOf( + "X-Appwrite-Project" to client.config["project"].orEmpty(), + "content-type" to "application/json", + "accept" to "application/json", + ) + val converter: (Any) -> io.appwrite.models.PasskeyChallenge = { + @Suppress("UNCHECKED_CAST") + io.appwrite.models.PasskeyChallenge.from(map = it as Map) + } + return client.call( + "POST", + apiPath, + apiHeaders, + apiParams, + responseType = io.appwrite.models.PasskeyChallenge::class.java, + converter, + ) + } + + /** + * Get a verified passkey of the currently logged in user by its unique ID. + * + * @param passkeyId Passkey ID. + * @return [io.appwrite.models.Passkey] + */ + suspend fun getPasskey( + passkeyId: String, + ): io.appwrite.models.Passkey { + if (passkeyId.isEmpty()) { + throw AppwriteException("Missing required parameter: \"passkeyId\"") + } + val apiPath = ("/account/passkeys/{passkeyId}" + .replace("{passkeyId}", passkeyId) + ) + val apiParams = mutableMapOf() + val apiHeaders = mutableMapOf( + "X-Appwrite-Project" to client.config["project"].orEmpty(), + "accept" to "application/json", + ) + val converter: (Any) -> io.appwrite.models.Passkey = { + @Suppress("UNCHECKED_CAST") + io.appwrite.models.Passkey.from(map = it as Map) + } + return client.call( + "GET", + apiPath, + apiHeaders, + apiParams, + responseType = io.appwrite.models.Passkey::class.java, + converter, + ) + } + + /** + * Rename a passkey of the currently logged in user. + * + * @param passkeyId Passkey ID. + * @param name Passkey name. Max length: 128 chars. + * @return [io.appwrite.models.Passkey] + */ + suspend fun updatePasskey( + passkeyId: String, + name: String, + ): io.appwrite.models.Passkey { + if (passkeyId.isEmpty()) { + throw AppwriteException("Missing required parameter: \"passkeyId\"") + } + val apiPath = ("/account/passkeys/{passkeyId}" + .replace("{passkeyId}", passkeyId) + ) + val apiParams = mutableMapOf( + "name" to name, + ) + val apiHeaders = mutableMapOf( + "X-Appwrite-Project" to client.config["project"].orEmpty(), + "content-type" to "application/json", + "accept" to "application/json", + ) + val converter: (Any) -> io.appwrite.models.Passkey = { + @Suppress("UNCHECKED_CAST") + io.appwrite.models.Passkey.from(map = it as Map) + } + return client.call( + "PATCH", + apiPath, + apiHeaders, + apiParams, + responseType = io.appwrite.models.Passkey::class.java, + converter, + ) + } + + /** + * Delete a passkey from the currently logged in user. The passkey can no longer be used to sign in, although it may remain stored on the user's device. The session must have signed in or completed an MFA challenge within the last 10 minutes. + * + * @param passkeyId Passkey ID. + * @return [Any] + */ + suspend fun deletePasskey( + passkeyId: String, + ): Any { + if (passkeyId.isEmpty()) { + throw AppwriteException("Missing required parameter: \"passkeyId\"") + } + val apiPath = ("/account/passkeys/{passkeyId}" + .replace("{passkeyId}", passkeyId) + ) + val apiParams = mutableMapOf() + val apiHeaders = mutableMapOf( + "X-Appwrite-Project" to client.config["project"].orEmpty(), + "content-type" to "application/json", + "accept" to "application/json", + ) + return client.call( + "DELETE", + apiPath, + apiHeaders, + apiParams, + responseType = Any::class.java, + ) + } + + /** + * Complete a passkey registration started with [Create passkey](/docs/references/cloud/client-web/account#createPasskey). Pass the JSON form of the credential returned by `navigator.credentials.create()`, for example `credential.toJSON()`. Each registration can be verified once; a failed attempt requires starting it again. + * + * @param passkeyId Passkey ID. + * @param credential Registration credential returned by the authenticator, in the JSON form produced by `PublicKeyCredential.toJSON()`. + * @return [io.appwrite.models.Passkey] + */ + suspend fun updatePasskeyVerification( + passkeyId: String, + credential: Map, + ): io.appwrite.models.Passkey { + if (passkeyId.isEmpty()) { + throw AppwriteException("Missing required parameter: \"passkeyId\"") + } + val apiPath = ("/account/passkeys/{passkeyId}/verification" + .replace("{passkeyId}", passkeyId) + ) + val apiParams = mutableMapOf( + "credential" to credential, + ) + val apiHeaders = mutableMapOf( + "X-Appwrite-Project" to client.config["project"].orEmpty(), + "content-type" to "application/json", + "accept" to "application/json", + ) + val converter: (Any) -> io.appwrite.models.Passkey = { + @Suppress("UNCHECKED_CAST") + io.appwrite.models.Passkey.from(map = it as Map) + } + return client.call( + "PUT", + apiPath, + apiHeaders, + apiParams, + responseType = io.appwrite.models.Passkey::class.java, + converter, + ) + } + /** * Update currently logged in user password. For validation, user is required to pass in the new password, and the old password. For users created with OAuth, Team Invites and Magic URL, oldPassword is optional. * @@ -2332,6 +2545,68 @@ class Account(client: Client) : Service(client) { } } + /** + * Start signing in with a passkey. No email or user ID is needed: pass the returned `publicKey` options to `navigator.credentials.get()` and the user picks one of their passkeys. Then call [Update passkey token](/docs/references/cloud/client-web/account#updatePasskeyToken) with the credential to receive a token, and exchange it with [Create session](/docs/references/cloud/client-web/account#createSession). The challenge expires after 5 minutes. + * + * @return [io.appwrite.models.PasskeyChallenge] + */ + suspend fun createPasskeyToken(): io.appwrite.models.PasskeyChallenge { + val apiPath = "/account/tokens/passkey" + val apiParams = mutableMapOf() + val apiHeaders = mutableMapOf( + "X-Appwrite-Project" to client.config["project"].orEmpty(), + "content-type" to "application/json", + "accept" to "application/json", + ) + val converter: (Any) -> io.appwrite.models.PasskeyChallenge = { + @Suppress("UNCHECKED_CAST") + io.appwrite.models.PasskeyChallenge.from(map = it as Map) + } + return client.call( + "POST", + apiPath, + apiHeaders, + apiParams, + responseType = io.appwrite.models.PasskeyChallenge::class.java, + converter, + ) + } + + /** + * Complete a passkey sign-in started with [Create passkey token](/docs/references/cloud/client-web/account#createPasskeyToken). Pass the challenge ID and the JSON form of the credential returned by `navigator.credentials.get()`, for example `credential.toJSON()`. The returned token includes its secret and expires after 1 minute: exchange it for a session with [Create session](/docs/references/cloud/client-web/account#createSession). Passkeys require user verification, so the session also satisfies MFA. Each challenge can only be used once. + * + * @param challengeId Challenge ID returned by createPasskeyToken. + * @param credential Authentication credential returned by the authenticator, in the JSON form produced by `PublicKeyCredential.toJSON()`. + * @return [io.appwrite.models.TokenSecret] + */ + suspend fun updatePasskeyToken( + challengeId: String, + credential: Map, + ): io.appwrite.models.TokenSecret { + val apiPath = "/account/tokens/passkey" + val apiParams = mutableMapOf( + "challengeId" to challengeId, + "credential" to credential, + ) + val apiHeaders = mutableMapOf( + "X-Appwrite-Project" to client.config["project"].orEmpty(), + "content-type" to "application/json", + "accept" to "application/json", + ) + val converter: (Any) -> io.appwrite.models.TokenSecret = { + @Suppress("UNCHECKED_CAST") + io.appwrite.models.TokenSecret.from(map = it as Map) + } + return client.call( + "PUT", + apiPath, + apiHeaders, + apiParams, + responseType = io.appwrite.models.TokenSecret::class.java, + converter, + ) + } + /** * Sends the user an SMS with a secret key for creating a session. If the provided user ID has not be registered, a new user will be created. Use the returned user ID and secret and submit a request to the [POST /v1/account/sessions/token](https://appwrite.io/docs/references/cloud/client-web/account#createSession) endpoint to complete the login process. The secret sent to the user's phone is valid for 15 minutes. *