Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
a18cda0
chore: update Flutter SDK to 25.3.0
ChiragAgg5k Jul 13, 2026
e50fec6
Commit from GitHub Actions (Format and push)
ChiragAgg5k Jul 13, 2026
1c0fb53
chore: update Flutter SDK to 25.3.0
ChiragAgg5k Jul 13, 2026
4f7e2d4
Commit from GitHub Actions (Format and push)
ChiragAgg5k Jul 13, 2026
7071616
chore: update Flutter SDK to 25.4.0
ChiragAgg5k Aug 5, 2026
4fde2ec
chore: merge main into dev for the 25.4.0 release
ChiragAgg5k Aug 5, 2026
30daa7b
Commit from GitHub Actions (Format and push)
ChiragAgg5k Aug 5, 2026
ab4db8c
chore: update Flutter SDK to 25.5.0
ChiragAgg5k Aug 24, 2026
876a6a9
chore: merge main into dev for the 25.5.0 release
ChiragAgg5k Aug 24, 2026
292f2c0
Commit from GitHub Actions (Format and push)
ChiragAgg5k Aug 24, 2026
d7f9f6d
chore: update Flutter SDK to 26.0.0
ChiragAgg5k Aug 24, 2026
9c107fb
Commit from GitHub Actions (Format and push)
ChiragAgg5k Aug 24, 2026
11e368f
chore: remove auto-formatting workflow, the generator now emits forma…
ChiragAgg5k Aug 24, 2026
8bab69b
chore: update Flutter SDK to 26.2.0
ChiragAgg5k Sep 7, 2026
d72f621
chore: merge main into dev for the 26.2.0 release
ChiragAgg5k Sep 7, 2026
2658391
docs: state the SDK targets Appwrite server version 2.0.x
ChiragAgg5k Sep 7, 2026
dc3489f
fix: regenerate with the latest sdk-generator and add auto image gravity
ChiragAgg5k Sep 7, 2026
0b5d558
chore: update Flutter SDK to 27.0.0
ChiragAgg5k Sep 24, 2026
68b413c
chore: merge main into dev for the 27.0.0 release
ChiragAgg5k Sep 24, 2026
d67a3e9
Merge remote-tracking branch 'origin/dev' into dev
ArnabChatterjee20k Oct 6, 2026
fd1381c
chore: update Flutter SDK to 27.1.0-rc.0
ArnabChatterjee20k Oct 6, 2026
a632b3b
Merge remote-tracking branch 'origin/dev' into dev
ArnabChatterjee20k Oct 6, 2026
08faf7a
chore: update Flutter SDK to 27.1.0-rc.0
ArnabChatterjee20k Oct 6, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 11 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,16 @@
# Change Log

## 27.1.0-rc.0

* Added: `Apps` service to manage OAuth2 apps, keys, secrets, and installations
* Added: `Oauth2` service for authorization, consent, device, PAR, and token flows
* Added: `Push` service for MQTT realtime push with iOS background delivery
* Added: `Topic` builder and a native `android` module for push
* Added: `Client.setPushEndpoint()` and `Client.setPushClientId()`
* Added: `Avatars.updatePhoto()` and `Avatars.deletePhoto()` methods
* Added: `App`, `Oauth2*`, and `Account` models
* Added: `webflow` to the `OAuthProvider` enum

## 27.0.0

* Breaking: removed `Account.listLogs` and the `Log`, `LogList` models
Expand Down
56 changes: 55 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ Add this to your package's `pubspec.yaml` file:

```yml
dependencies:
appwrite: ^27.0.0
appwrite: ^27.1.0-rc.0
```

You can install packages from the command line:
Expand All @@ -28,6 +28,58 @@ You can install packages from the command line:
flutter pub add appwrite
```

### Push on Android

`Push` includes a native Android plugin, so the SDK needs Android 7.0 (API 24) or later: apps
with a lower `minSdk` fail to build until they raise it. `flutter_local_notifications` also needs
core library desugaring. Add both to `android/app/build.gradle.kts` (desugaring is configured when
the app is assembled, so a plugin cannot apply it for you):

```kotlin
android {
defaultConfig {
minSdk = 24
}
compileOptions {
isCoreLibraryDesugaringEnabled = true
}
}

dependencies {
coreLibraryDesugaring("com.android.tools:desugar_jdk_libs:2.1.4")
}
```

A subscription with `background: true` keeps delivering after the app is backgrounded, killed or
the device restarts, until it is unsubscribed or `push.close()` is called (do this on sign-out).
The plugin saves the subscription, and a scheduled job and alarm wake the app every 15 to 60
seconds to reconnect; the broker replays what was sent in between (`retry: true`). Messages no
in-app callback receives are posted as notifications that open the app. It reconnects with the
credential saved at subscribe time, so use a session rather than a short-lived JWT.

On Android 13 and later, notifications only appear once the user grants the
`POST_NOTIFICATIONS` runtime permission. Without it, the subscription still delivers to your
callback but posts no notification, so ask before subscribing, for example with
`flutter_local_notifications`:

```dart
await FlutterLocalNotificationsPlugin()
.resolvePlatformSpecificImplementation<AndroidFlutterLocalNotificationsPlugin>()
?.requestNotificationsPermission();

final sub = await push.subscribe('news', (message) => print(message.data),
background: true, title: 'News');

// Optional: immediate delivery even after a kill and during Doze, with a quiet ongoing
// notification (call while the app is in the foreground).
await push.setForeground(true);
```

Foreground mode runs a `remoteMessaging` foreground service, which Google Play asks apps to
declare in the Play Console. Apps that never enable it can remove the service from their merged
manifest with `tools:node="remove"` on `io.appwrite.services.PushService` and
`android.permission.FOREGROUND_SERVICE_REMOTE_MESSAGING`.


## Getting Started

Expand All @@ -38,6 +90,8 @@ From the options, choose to add a new **Flutter** platform and add your app cred

If you are building your Flutter application for multiple devices, you have to follow this process for each different device.

> On Android, iOS and macOS, call `createOAuth2Session` without the `success` and `failure` URLs. The SDK only returns to your app through the `appwrite-callback-[PROJECT_ID]` scheme, and only the default redirect carries the new session with it. A custom URL, such as an https App Link, makes the call fail (for example with `PlatformException(CANCELED)`) even when the login itself succeeded. Await the returned `Future` and navigate from your Dart code instead.

### Android
For **Android** first add your app <u>name</u> and <u>package name</u>, Your package name is generally the **applicationId** in your app-level <a href="https://github.com/appwrite/playground-for-flutter/blob/0fdbdff98384fff940ed0b1e08cf14cfe3a2be3e/android/app/build.gradle#L41" target="_blank" rel="noopener">build.gradle</a> file. By registering your new app platform, you are allowing your app to communicate with the Appwrite API.

Expand Down
39 changes: 39 additions & 0 deletions android/.editorconfig
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
root = true

[*.{kt,kts}]
ktlint_code_style = ktlint_official
ktlint_standard_annotation = disabled
ktlint_standard_argument-list-wrapping = disabled
ktlint_standard_blank-line-before-declaration = disabled
ktlint_standard_blank-line-between-when-conditions = disabled
ktlint_standard_chain-method-continuation = disabled
ktlint_standard_class-signature = disabled
ktlint_standard_function-expression-body = disabled
ktlint_standard_function-signature = disabled
ktlint_standard_function-start-of-body-spacing = disabled
ktlint_standard_if-else-bracing = disabled
ktlint_standard_if-else-wrapping = disabled
ktlint_standard_max-line-length = disabled
ktlint_standard_multiline-expression-wrapping = disabled
ktlint_standard_multiline-if-else = disabled
ktlint_standard_no-blank-line-before-rbrace = disabled
ktlint_standard_no-blank-line-in-list = disabled
ktlint_standard_no-empty-first-line-in-class-body = disabled
ktlint_standard_no-line-break-before-assignment = disabled
ktlint_standard_no-wildcard-imports = disabled
ktlint_standard_parameter-list-wrapping = disabled
ktlint_standard_spacing-between-declarations-with-annotations = disabled
ktlint_standard_spacing-between-declarations-with-comments = disabled
ktlint_standard_string-template = disabled
ktlint_standard_trailing-comma-on-call-site = disabled
ktlint_standard_trailing-comma-on-declaration-site = disabled
ktlint_standard_when-entry-bracing = disabled
ktlint_standard_wrapping = disabled

[**/Channel.kt]
# Marker types intentionally use leading underscores to distinguish channel states.
ktlint_standard_class-naming = disabled

[**/models/*.kt]
# Model imports follow schema property order so generated declarations stay deterministic.
ktlint_standard_import-ordering = disabled
56 changes: 56 additions & 0 deletions android/build.gradle
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
// The native half of Push on Android: background delivery that survives the process being
// killed, a reboot and an app update, shared with the Appwrite Android SDK.
group = "io.appwrite.flutter"
version = "27.1.0-rc.0"

buildscript {
ext.kotlin_version = "2.1.0"
repositories {
google()
mavenCentral()
}

dependencies {
classpath("com.android.tools.build:gradle:8.7.3")
classpath("org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version")
}
}

allprojects {
repositories {
google()
mavenCentral()
}
}

apply plugin: "com.android.library"
apply plugin: "kotlin-android"

android {
namespace = "io.appwrite.flutter"
compileSdk = 35

compileOptions {
sourceCompatibility = JavaVersion.VERSION_17
targetCompatibility = JavaVersion.VERSION_17
}

kotlinOptions {
jvmTarget = "17"
}

sourceSets {
main.java.srcDirs += "src/main/kotlin"
}

defaultConfig {
minSdk = 24
Comment thread
hansi-codes[bot] marked this conversation as resolved.
// R8 rules for the bundled MQTT client, applied to the app.
consumerProguardFiles("consumer-rules.pro")
}
}

dependencies {
implementation("com.hivemq:hivemq-mqtt-client-shaded:1.3.6")
implementation("androidx.core:core-ktx:1.13.1")
}
27 changes: 27 additions & 0 deletions android/consumer-rules.pro
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
# Push: rules applied to apps that shrink with R8.
#
# The shaded HiveMQ MQTT client relocates Netty and JCTools under
# com.hivemq.client.internal.shaded. Netty references optional classes that are never loaded
# on Android: native transports, the HTTP/WebSocket codecs, proxy handlers, alternative TLS
# providers (tcnative, BouncyCastle, Conscrypt, Jetty ALPN) and logging backends.
-dontwarn com.hivemq.client.internal.shaded.io.netty.channel.epoll.**
-dontwarn com.hivemq.client.internal.shaded.io.netty.channel.kqueue.**
-dontwarn com.hivemq.client.internal.shaded.io.netty.handler.codec.http.**
-dontwarn com.hivemq.client.internal.shaded.io.netty.handler.proxy.**
-dontwarn com.hivemq.client.internal.shaded.io.netty.internal.tcnative.**
-dontwarn org.bouncycastle.**
-dontwarn org.conscrypt.**
-dontwarn org.eclipse.jetty.**
-dontwarn org.apache.log4j.**
-dontwarn org.apache.logging.log4j.**
-dontwarn org.slf4j.**
-dontwarn reactor.blockhound.**

# Netty and JCTools reach their own fields by name, through atomic field updaters and Unsafe offsets.
-keepclassmembernames class com.hivemq.client.internal.shaded.io.netty.** { <fields>; }
-keepclassmembers class com.hivemq.client.internal.shaded.org.jctools.** { <fields>; }

# Netty's leak detector looks these methods up by name when its classes load.
-keepclassmembers class com.hivemq.client.internal.shaded.io.netty.buffer.AbstractByteBufAllocator { *** toLeakAwareBuffer(...); }
-keepclassmembers class com.hivemq.client.internal.shaded.io.netty.buffer.AdvancedLeakAwareByteBuf { *** recordLeakNonRefCountingOperation(...); }
-keepclassmembers class com.hivemq.client.internal.shaded.io.netty.util.ReferenceCountUtil { *** touch(...); }
1 change: 1 addition & 0 deletions android/settings.gradle
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
rootProject.name = 'appwrite'
41 changes: 41 additions & 0 deletions android/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android">

<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
<!-- Background Push delivery (subscribe(background = true)): scheduled runs that reconnect
under a short wakelock, on the current network, and resume after a reboot. Apps can also
declare SCHEDULE_EXACT_ALARM (punctual runs) and REQUEST_IGNORE_BATTERY_OPTIMIZATIONS
(a direct exemption prompt); both are optional and subject to Google Play policy. -->
<uses-permission android:name="android.permission.WAKE_LOCK" />
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
<uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED" />
<!-- Foreground-service background Push delivery (Push.setForeground). Apps that never enable it can
remove PushService and these two permissions with tools:node="remove". -->
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_REMOTE_MESSAGING" />

<application>
<service
android:name="io.appwrite.services.PushService"
android:exported="false"
android:foregroundServiceType="remoteMessaging"
android:stopWithTask="false" />
<service
android:name="io.appwrite.services.PushJobService"
android:exported="false"
android:permission="android.permission.BIND_JOB_SERVICE" />
<receiver
android:name="io.appwrite.services.PushAlarmReceiver"
android:exported="false" />
<receiver
android:name="io.appwrite.services.PushBootReceiver"
android:exported="true">
<intent-filter>
<action android:name="android.intent.action.BOOT_COMPLETED" />
<action android:name="android.intent.action.MY_PACKAGE_REPLACED" />
</intent-filter>
</receiver>
</application>

</manifest>
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
package io.appwrite.exceptions

/** A push error. When the broker explained a refusal or a disconnect, that is its message. */
class AppwriteException(
override val message: String? = null,
) : Exception(message)
108 changes: 108 additions & 0 deletions android/src/main/kotlin/io/appwrite/flutter/AppwritePushPlugin.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
package io.appwrite.flutter

import android.os.Handler
import android.os.Looper
import io.appwrite.services.PushBridge
import io.appwrite.services.PushMessage
import io.flutter.embedding.engine.plugins.FlutterPlugin
import io.flutter.plugin.common.EventChannel
import io.flutter.plugin.common.MethodCall
import io.flutter.plugin.common.MethodChannel

/**
* The Flutter side of [PushBridge]: the SDK's `Push` hosts its background subscriptions here on
* Android over a method channel, and receives their messages and errors over an event channel.
*/
class AppwritePushPlugin :
FlutterPlugin,
MethodChannel.MethodCallHandler,
EventChannel.StreamHandler {
private val main = Handler(Looper.getMainLooper())
private var methods: MethodChannel? = null
private var events: EventChannel? = null
private var sink: EventChannel.EventSink? = null
private var bridge: PushBridge? = null

override fun onAttachedToEngine(binding: FlutterPlugin.FlutterPluginBinding) {
bridge = PushBridge(
binding.applicationContext,
object : PushBridge.Events {
override fun onMessage(subscriptionId: String, message: PushMessage, ackToken: String) = send(
mapOf(
"type" to "message",
"id" to subscriptionId,
"topic" to message.topic,
"payload" to message.payload,
"qos" to message.qos,
"ackToken" to ackToken,
),
)

override fun onError(message: String) = send(mapOf("type" to "error", "message" to message))
},
)
methods = MethodChannel(binding.binaryMessenger, METHOD_CHANNEL).also { it.setMethodCallHandler(this) }
events = EventChannel(binding.binaryMessenger, EVENT_CHANNEL).also { it.setStreamHandler(this) }
}

override fun onDetachedFromEngine(binding: FlutterPlugin.FlutterPluginBinding) {
methods?.setMethodCallHandler(null)
events?.setStreamHandler(null)
methods = null
events = null
sink = null
}

override fun onMethodCall(call: MethodCall, result: MethodChannel.Result) {
val bridge = bridge ?: return result.error(ERROR_CODE, "Push plugin is not attached", null)
try {
if (call.method == "host") {
// Replies once the connection is up and every filter is subscribed, or with why not.
bridge.host(call.argument<String>("config")!!, call.argument<String>("subscriptions")!!) { error ->
main.post {
if (error == null) {
result.success(null)
} else {
result.error(ERROR_CODE, error, null)
}
}
}
return
}
result.success(
when (call.method) {
"ack" -> bridge.ack(call.argument<String>("token")!!).let { null }
"release" -> bridge.release().let { null }
"stop" -> bridge.stop().let { null }
"setForeground" -> bridge.setForeground(call.argument<Boolean>("enabled") == true).let { null }
"hasSaved" -> bridge.hasSaved()
"resume" -> bridge.resume().let { null }
"setErrorCallback" -> bridge.setErrorCallback(call.argument<Boolean>("registered") == true)
"defaultClientId" -> bridge.defaultClientId(call.argument<String>("authMethod")!!, call.argument<String>("credential")!!)
else -> return result.notImplemented()
},
)
} catch (e: Exception) {
result.error(ERROR_CODE, e.message, null)
}
}

override fun onListen(arguments: Any?, events: EventChannel.EventSink?) {
sink = events
}

override fun onCancel(arguments: Any?) {
sink = null
}

// The bridge calls back on background threads; event sinks must be used on the main thread.
private fun send(event: Map<String, Any?>) {
main.post { sink?.success(event) }
}

private companion object {
const val METHOD_CHANNEL = "appwrite.push"
const val EVENT_CHANNEL = "appwrite.push/events"
const val ERROR_CODE = "appwrite_push"
}
}
Loading
Loading