diff --git a/CHANGELOG.md b/CHANGELOG.md index e32bd1c..6c3623d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,13 @@ # Change Log +## 29.2.0 + +* Added: `analytics.createEvent` in a new `Analytics` service to send tracking events +* Added: optional `state` on `account.createOAuth2Session` and `account.createOAuth2Token`, returned unchanged on the success and failure URLs +* Added: optional `membershipId` on `teams.createMembership` +* Fixed: `avatars.updatePhoto` sends `PUT`, so uploading a profile photo no longer returns 404 +* Fixed: OAuth2 query parameters (`success`, `failure`, `scopes`, `state`) are URL-encoded, so values containing `&`, `+` or `#` survive the redirect + ## 29.1.0 * Added: `account` passkey methods `listPasskeys`, `createPasskey`, `getPasskey`, `updatePasskey`, `deletePasskey` and `updatePasskeyVerification` diff --git a/README.md b/README.md index 535ef1f..388c6e6 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.1.0") +implementation("io.appwrite:sdk-for-android:29.2.0") ``` ### Maven @@ -49,7 +49,7 @@ Add this to your project's `pom.xml` file: io.appwrite sdk-for-android - 29.1.0 + 29.2.0 ``` diff --git a/docs/examples/java/account/create-o-auth-2-session.md b/docs/examples/java/account/create-o-auth-2-session.md index da54742..f2a07ef 100644 --- a/docs/examples/java/account/create-o-auth-2-session.md +++ b/docs/examples/java/account/create-o-auth-2-session.md @@ -17,6 +17,7 @@ account.createOAuth2Session( "https://example.com", // success (optional) "https://example.com", // failure (optional) List.of(), // scopes (optional) + "", // state (optional) new CoroutineCallback<>((result, error) -> { if (error != null) { error.printStackTrace(); diff --git a/docs/examples/java/account/create-o-auth-2-token.md b/docs/examples/java/account/create-o-auth-2-token.md index ae68585..885c066 100644 --- a/docs/examples/java/account/create-o-auth-2-token.md +++ b/docs/examples/java/account/create-o-auth-2-token.md @@ -17,6 +17,7 @@ account.createOAuth2Token( "https://example.com", // success (optional) "https://example.com", // failure (optional) List.of(), // scopes (optional) + "", // state (optional) new CoroutineCallback<>((result, error) -> { if (error != null) { error.printStackTrace(); diff --git a/docs/examples/java/analytics/create-event.md b/docs/examples/java/analytics/create-event.md new file mode 100644 index 0000000..e2b8dbe --- /dev/null +++ b/docs/examples/java/analytics/create-event.md @@ -0,0 +1,38 @@ +```java +import android.util.Log; + +import io.appwrite.Client; +import io.appwrite.coroutines.CoroutineCallback; +import io.appwrite.services.Analytics; + +Client client = new Client(context) + .setEndpoint("https://.cloud.appwrite.io/v1") // Your API Endpoint + .setProject(""); // Your project ID + +Analytics analytics = new Analytics(client); + +analytics.createEvent( + "", // propertyId + "", // name + "https://example.com", // url + "", // domain (optional) + "", // referrer (optional) + 0L, // screenWidth (optional) + "", // sessionHash (optional) + 0L, // scrollDepth (optional) + 0L, // engagementTime (optional) + List.of(), // props (optional) + "", // userId (optional) + "", // ip (optional) + "", // userAgent (optional) + new CoroutineCallback<>((result, error) -> { + if (error != null) { + error.printStackTrace(); + return; + } + + Log.d("Appwrite", result.toString()); + }) +); + +``` diff --git a/docs/examples/java/teams/create-membership.md b/docs/examples/java/teams/create-membership.md index d7fba7c..b6d23a6 100644 --- a/docs/examples/java/teams/create-membership.md +++ b/docs/examples/java/teams/create-membership.md @@ -19,6 +19,7 @@ teams.createMembership( "+12065550100", // phone (optional) "https://example.com", // url (optional) "", // name (optional) + "", // membershipId (optional) new CoroutineCallback<>((result, error) -> { if (error != null) { error.printStackTrace(); diff --git a/docs/examples/kotlin/account/create-o-auth-2-session.md b/docs/examples/kotlin/account/create-o-auth-2-session.md index cde941c..c9b3a92 100644 --- a/docs/examples/kotlin/account/create-o-auth-2-session.md +++ b/docs/examples/kotlin/account/create-o-auth-2-session.md @@ -15,5 +15,6 @@ account.createOAuth2Session( success = "https://example.com", // (optional) failure = "https://example.com", // (optional) scopes = listOf(), // (optional) + state = "", // (optional) ) ``` diff --git a/docs/examples/kotlin/account/create-o-auth-2-token.md b/docs/examples/kotlin/account/create-o-auth-2-token.md index 5e90564..3d5b0d7 100644 --- a/docs/examples/kotlin/account/create-o-auth-2-token.md +++ b/docs/examples/kotlin/account/create-o-auth-2-token.md @@ -15,5 +15,6 @@ account.createOAuth2Token( success = "https://example.com", // (optional) failure = "https://example.com", // (optional) scopes = listOf(), // (optional) + state = "", // (optional) ) ``` diff --git a/docs/examples/kotlin/analytics/create-event.md b/docs/examples/kotlin/analytics/create-event.md new file mode 100644 index 0000000..820e0ba --- /dev/null +++ b/docs/examples/kotlin/analytics/create-event.md @@ -0,0 +1,27 @@ +```kotlin +import io.appwrite.Client +import io.appwrite.coroutines.CoroutineCallback +import io.appwrite.services.Analytics + +val client = Client(context) + .setEndpoint("https://.cloud.appwrite.io/v1") // Your API Endpoint + .setProject("") // Your project ID + +val analytics = Analytics(client) + +val result = analytics.createEvent( + propertyId = "", + name = "", + url = "https://example.com", + domain = "", // (optional) + referrer = "", // (optional) + screenWidth = 0, // (optional) + sessionHash = "", // (optional) + scrollDepth = 0, // (optional) + engagementTime = 0, // (optional) + props = listOf(), // (optional) + userId = "", // (optional) + ip = "", // (optional) + userAgent = "", // (optional) +) +``` diff --git a/docs/examples/kotlin/teams/create-membership.md b/docs/examples/kotlin/teams/create-membership.md index 219119a..2f73c03 100644 --- a/docs/examples/kotlin/teams/create-membership.md +++ b/docs/examples/kotlin/teams/create-membership.md @@ -17,5 +17,6 @@ val result = teams.createMembership( phone = "+12065550100", // (optional) url = "https://example.com", // (optional) name = "", // (optional) + membershipId = "", // (optional) ) ``` diff --git a/library/src/main/java/io/appwrite/Client.kt b/library/src/main/java/io/appwrite/Client.kt index 8130478..32d1c60 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.1.0", + "x-sdk-version" to "29.2.0", "x-appwrite-response-format" to "2.3.0" ) config = mutableMapOf() @@ -551,10 +551,11 @@ class Client @JvmOverloads constructor( paramName: String, idParamName: String? = null, onProgress: ((UploadProgress) -> Unit)? = null, + method: String = "POST", ): T { if (params[paramName] == null) { return call( - method = "POST", + method = method, path, headers, params, @@ -585,7 +586,7 @@ class Client @JvmOverloads constructor( data ) return call( - method = "POST", + method = method, path, headers, params, @@ -656,7 +657,7 @@ class Client @JvmOverloads constructor( ) val chunkResult = call( - method = "POST", + method = method, path, chunkHeaders, chunkParams, diff --git a/library/src/main/java/io/appwrite/services/Account.kt b/library/src/main/java/io/appwrite/services/Account.kt index 61b6ce9..e6201c4 100644 --- a/library/src/main/java/io/appwrite/services/Account.kt +++ b/library/src/main/java/io/appwrite/services/Account.kt @@ -1997,6 +1997,7 @@ class Account(client: Client) : Service(client) { * @param success URL to redirect back to your app after a successful login attempt. Only URLs from hostnames in your project's platform list are allowed. This requirement helps to prevent an [open redirect](https://cheatsheetseries.owasp.org/cheatsheets/Unvalidated_Redirects_and_Forwards_Cheat_Sheet.html) attack against your project API. * @param failure URL to redirect back to your app after a failed login attempt. Only URLs from hostnames in your project's platform list are allowed. This requirement helps to prevent an [open redirect](https://cheatsheetseries.owasp.org/cheatsheets/Unvalidated_Redirects_and_Forwards_Cheat_Sheet.html) attack against your project API. * @param scopes A list of custom OAuth2 scopes. Check each provider internal docs for a list of supported scopes. Maximum of 100 scopes are allowed, each 4096 characters long. + * @param state An opaque value your app generates and keeps, for example in a cookie. It is returned unchanged as the `state` query parameter on the success and failure URLs, so your app can check that the sign-in it receives is one it started. Printable ASCII only (RFC 6749 Appendix A.5). Max length: 256 chars. */ @JvmOverloads suspend fun createOAuth2Session( @@ -2005,6 +2006,7 @@ class Account(client: Client) : Service(client) { success: String? = null, failure: String? = null, scopes: List? = null, + state: String? = null, ) { val apiPath = ("/account/sessions/oauth2/{provider}" .replace("{provider}", provider.value) @@ -2013,6 +2015,7 @@ class Account(client: Client) : Service(client) { "success" to success, "failure" to failure, "scopes" to scopes, + "state" to state, "project" to client.config["project"], ) val apiQuery = mutableListOf() @@ -2023,11 +2026,11 @@ class Account(client: Client) : Service(client) { } is List<*> -> { (it.value as List<*>).forEach { v -> - apiQuery.add("${it.key}[]=${v.toString()}") + apiQuery.add("${Uri.encode(it.key)}[]=${Uri.encode(v.toString())}") } } else -> { - apiQuery.add("${it.key}=${it.value.toString()}") + apiQuery.add("${Uri.encode(it.key)}=${Uri.encode(it.value.toString())}") } } } @@ -2481,6 +2484,7 @@ class Account(client: Client) : Service(client) { * @param success URL to redirect back to your app after a successful login attempt. Only URLs from hostnames in your project's platform list are allowed. This requirement helps to prevent an [open redirect](https://cheatsheetseries.owasp.org/cheatsheets/Unvalidated_Redirects_and_Forwards_Cheat_Sheet.html) attack against your project API. * @param failure URL to redirect back to your app after a failed login attempt. Only URLs from hostnames in your project's platform list are allowed. This requirement helps to prevent an [open redirect](https://cheatsheetseries.owasp.org/cheatsheets/Unvalidated_Redirects_and_Forwards_Cheat_Sheet.html) attack against your project API. * @param scopes A list of custom OAuth2 scopes. Check each provider internal docs for a list of supported scopes. Maximum of 100 scopes are allowed, each 4096 characters long. + * @param state An opaque value your app generates and keeps, for example in a cookie. It is returned unchanged as the `state` query parameter on the success and failure URLs, so your app can check that the sign-in it receives is one it started. Printable ASCII only (RFC 6749 Appendix A.5). Max length: 256 chars. */ @JvmOverloads suspend fun createOAuth2Token( @@ -2489,6 +2493,7 @@ class Account(client: Client) : Service(client) { success: String? = null, failure: String? = null, scopes: List? = null, + state: String? = null, ) { val apiPath = ("/account/tokens/oauth2/{provider}" .replace("{provider}", provider.value) @@ -2497,6 +2502,7 @@ class Account(client: Client) : Service(client) { "success" to success, "failure" to failure, "scopes" to scopes, + "state" to state, "project" to client.config["project"], ) val apiQuery = mutableListOf() @@ -2507,11 +2513,11 @@ class Account(client: Client) : Service(client) { } is List<*> -> { (it.value as List<*>).forEach { v -> - apiQuery.add("${it.key}[]=${v.toString()}") + apiQuery.add("${Uri.encode(it.key)}[]=${Uri.encode(v.toString())}") } } else -> { - apiQuery.add("${it.key}=${it.value.toString()}") + apiQuery.add("${Uri.encode(it.key)}=${Uri.encode(it.value.toString())}") } } } diff --git a/library/src/main/java/io/appwrite/services/Analytics.kt b/library/src/main/java/io/appwrite/services/Analytics.kt new file mode 100644 index 0000000..1949f76 --- /dev/null +++ b/library/src/main/java/io/appwrite/services/Analytics.kt @@ -0,0 +1,83 @@ +package io.appwrite.services + +import android.net.Uri +import io.appwrite.Client +import io.appwrite.Service +import io.appwrite.exceptions.AppwriteException +import io.appwrite.extensions.classOf +import io.appwrite.models.* +import okhttp3.Cookie +import java.io.File + +/** + * + */ +class Analytics(client: Client) : Service(client) { + /** + * Send a tracking event from a browser, native app, or server-side SDK. + * + * @param propertyId Analytics property ID. + * @param name Event name. "pageview" is just a conventional event name; events are not modeled specially. + * @param url Full page URL or screen identifier. + * @param domain Hostname (e.g. example.com). + * @param referrer Referrer URL. + * @param screenWidth Viewport width in CSS pixels. + * @param sessionHash Optional session hash (provided by SDK). + * @param scrollDepth Scroll depth percentage 0-100. + * @param engagementTime Engagement time in seconds, 0-4294967295. + * @param props Custom string properties as a flat key=value list (max 32 entries, alternating key,value). + * @param userId Override user ID. Requires API-key auth with analytics.write scope. + * @param ip Override IP address. Requires API-key auth with analytics.write scope. + * @param userAgent Override user agent. Requires API-key auth with analytics.write scope. + * @return [Any] + */ + @JvmOverloads + suspend fun createEvent( + propertyId: String, + name: String, + url: String, + domain: String? = null, + referrer: String? = null, + screenWidth: Long? = null, + sessionHash: String? = null, + scrollDepth: Long? = null, + engagementTime: Long? = null, + props: List? = null, + userId: String? = null, + ip: String? = null, + userAgent: String? = null, + ): Any { + if (propertyId.isEmpty()) { + throw AppwriteException("Missing required parameter: \"propertyId\"") + } + val apiPath = ("/analytics/properties/{propertyId}/events" + .replace("{propertyId}", propertyId) + ) + val apiParams = mutableMapOf( + "name" to name, + "url" to url, + "domain" to domain, + "referrer" to referrer, + "screenWidth" to screenWidth, + "sessionHash" to sessionHash, + "scrollDepth" to scrollDepth, + "engagementTime" to engagementTime, + "props" to props, + "userId" to userId, + "ip" to ip, + "userAgent" to userAgent, + ) + val apiHeaders = mutableMapOf( + "X-Appwrite-Project" to client.config["project"].orEmpty(), + "content-type" to "application/json", + "accept" to "application/json", + ) + return client.call( + "POST", + apiPath, + apiHeaders, + apiParams, + responseType = Any::class.java, + ) + } +} diff --git a/library/src/main/java/io/appwrite/services/Avatars.kt b/library/src/main/java/io/appwrite/services/Avatars.kt index 3b11d16..2223223 100644 --- a/library/src/main/java/io/appwrite/services/Avatars.kt +++ b/library/src/main/java/io/appwrite/services/Avatars.kt @@ -325,6 +325,7 @@ class Avatars(client: Client) : Service(client) { paramName = paramName, idParamName = idParamName, onProgress = onProgress, + method = "PUT", ) } diff --git a/library/src/main/java/io/appwrite/services/Teams.kt b/library/src/main/java/io/appwrite/services/Teams.kt index 7227eba..8a4bb87 100644 --- a/library/src/main/java/io/appwrite/services/Teams.kt +++ b/library/src/main/java/io/appwrite/services/Teams.kt @@ -539,6 +539,7 @@ class Teams(client: Client) : Service(client) { * @param phone Phone number. Format this number with a leading '+' and a country code, e.g., +16175551212. * @param url URL to redirect the user back to your app from the invitation email. This parameter is not required when an API key is supplied. Only URLs from hostnames in your project platform list are allowed. This requirement helps to prevent an [open redirect](https://cheatsheetseries.owasp.org/cheatsheets/Unvalidated_Redirects_and_Forwards_Cheat_Sheet.html) attack against your project API. * @param name Name of the new team member. Max length: 128 chars. + * @param membershipId Membership 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. * @return [io.appwrite.models.Membership] */ @JvmOverloads @@ -550,6 +551,7 @@ class Teams(client: Client) : Service(client) { phone: String? = null, url: String? = null, name: String? = null, + membershipId: String? = null, ): io.appwrite.models.Membership { if (teamId.isEmpty()) { throw AppwriteException("Missing required parameter: \"teamId\"") @@ -564,6 +566,7 @@ class Teams(client: Client) : Service(client) { "roles" to roles, "url" to url, "name" to name, + "membershipId" to membershipId, ) val apiHeaders = mutableMapOf( "X-Appwrite-Project" to client.config["project"].orEmpty(),