diff --git a/OAUTH_BLOG_POST.md b/OAUTH_BLOG_POST.md
new file mode 100644
index 0000000..52e3c1c
--- /dev/null
+++ b/OAUTH_BLOG_POST.md
@@ -0,0 +1,196 @@
+# The OAuth Authentication Nightmare: Why I'm Considering Moving from Appwrite to Supabase
+
+*A developer's journey through mobile OAuth hell and the quest for better solutions*
+
+---
+
+## The Problem That Started It All
+
+I was building TaskTracker, a Flutter mobile application, and decided to use Appwrite as my Backend-as-a-Service (BaaS). Everything seemed promising - great documentation, modern API, and OAuth support out of the box. What could go wrong?
+
+**Everything.**
+
+After successfully implementing email/password authentication, I moved on to Google OAuth for a better user experience. The implementation seemed straightforward - just call `createOAuth2Session()` and you're done, right?
+
+Wrong.
+
+## The Mysterious ImeTracker Error
+
+After clicking the Google OAuth button, Chrome Custom Tabs would open (not the native Android Google Sign-In UI, which was my first red flag). I'd select my Google account, authenticate successfully, and then... nothing. The browser would close, and I'd see this cryptic error in the logs:
+
+```
+I/ImeTracker( 5434): com.Xperience.TaskTracker.tasktracker:efd1ad6a:
+onCancelled at PHASE_CLIENT_ALREADY_HIDDEN
+```
+
+The error message suggested I had cancelled the authentication, but I hadn't touched anything. The OAuth flow was completing on Google's end, but my app wasn't receiving the callback properly.
+
+## The Troubleshooting Marathon
+
+### Attempt #1: Explicit Callback URLs
+
+My first thought was that the callback URLs needed to be explicitly defined. I tried specifying success and failure URLs:
+
+```dart
+await widget.account.createOAuth2Session(
+ provider: provider,
+ success: 'appwrite-callback-tasktracker://success',
+ failure: 'appwrite-callback-tasktracker://failure',
+ scopes: ['email', 'profile'],
+);
+```
+
+**Result:** Even worse. I got a new error:
+
+```
+Invalid success param, URL host must be one of localhost, supab.playpoolstudios.com
+```
+
+So Appwrite's server was rejecting my custom URL scheme entirely. This made no sense for a mobile application where custom URL schemes are the standard for OAuth callbacks.
+
+### Attempt #2: Android Manifest Configuration
+
+Maybe the issue was with my Android configuration? I updated the `AndroidManifest.xml` to be more explicit about handling OAuth callbacks:
+
+```xml
+
+
+
+
+
+
+
+```
+
+**Result:** Still nothing. The callback wasn't being intercepted properly.
+
+### Attempt #3: Appwrite Console Platform Configuration
+
+Perhaps I needed to register my Android app as a platform in the Appwrite Console? I went through the process:
+- Added Android platform
+- Entered package name: `com.Xperience.TaskTracker.tasktracker`
+- Configured OAuth provider settings
+
+**Result:** No improvement. The OAuth flow still failed silently.
+
+### Attempt #4: The GitHub Workaround
+
+After hours of searching, I found a GitHub issue with a workaround suggesting a two-step OAuth process:
+
+```dart
+// Step 1: Manually trigger OAuth with flutter_web_auth_2
+final result = await FlutterWebAuth2.authenticate(
+ url: '$endpoint/account/sessions/oauth2/$provider?project=$projectId',
+ callbackUrlScheme: 'appwrite-callback-$projectId',
+);
+
+// Step 2: Create Appwrite session
+await widget.account.createOAuth2Session(provider: provider);
+```
+
+This workaround essentially bypasses Appwrite's OAuth handling by manually triggering the web authentication, then trying to create a session afterward.
+
+**Result:** Still testing, but the fact that such a workaround exists is telling.
+
+## The Fundamental Issues
+
+After all this troubleshooting, I've identified several core problems with Appwrite's mobile OAuth implementation:
+
+### 1. **Poor Mobile-First Design**
+Appwrite's OAuth is clearly designed for web applications. The restriction that callback URLs must use the server's domain (`supab.playpoolstudios.com`) makes no sense for mobile apps that need custom URL schemes like `appwrite-callback-*://`.
+
+### 2. **Chrome Custom Tabs Instead of Native UI**
+On Android, the OAuth flow opens Chrome Custom Tabs instead of the native Google Sign-In UI. This creates a clunky user experience and introduces unnecessary complexity with session management between the browser and the app.
+
+### 3. **Silent Failures**
+The OAuth process fails silently with no meaningful error messages. The `ImeTracker` error is just a symptom - a side effect of the keyboard state when the browser closes - not the actual problem.
+
+### 4. **Lack of Documentation**
+The official Appwrite documentation doesn't cover mobile OAuth edge cases, troubleshooting steps, or known issues. I had to dig through GitHub issues to find any information.
+
+### 5. **Workarounds Required**
+The fact that a two-step workaround exists (and is recommended in GitHub issues) suggests this is a known problem that hasn't been properly fixed.
+
+## Why I'm Looking at Supabase
+
+After spending hours (days?) on this issue, I started researching alternatives. Supabase keeps coming up, and here's why it's appealing:
+
+### 1. **PostgreSQL Foundation**
+Supabase is built on PostgreSQL, a mature, battle-tested database. This means:
+- Better query capabilities
+- Mature ecosystem
+- Reliable data integrity
+- Easy migration path if needed
+
+### 2. **Better OAuth Documentation**
+Supabase has extensive documentation for mobile OAuth, including Flutter-specific guides and examples. They support:
+- Deep linking configuration
+- Platform-specific setup guides
+- Native SDK implementations
+- Working code examples
+
+### 3. **Active Community Support**
+The Supabase community is larger and more active. Issues get addressed faster, and there are more third-party tutorials and resources available.
+
+### 4. **Row Level Security (RLS)**
+Supabase's RLS is more powerful and flexible than Appwrite's permissions system, giving you fine-grained control over data access at the database level.
+
+### 5. **Edge Functions with Deno**
+Supabase Edge Functions run on Deno, which is modern, secure, and has better TypeScript support than Appwrite's functions.
+
+### 6. **Transparent Pricing**
+Supabase has clearer pricing and a more generous free tier for development and testing.
+
+## The Verdict
+
+I wanted to love Appwrite. The API is clean, the dashboard is beautiful, and the promise of an open-source Firebase alternative is compelling. But when basic functionality like mobile OAuth doesn't work reliably, it becomes a blocker for production applications.
+
+**The problems I encountered are not edge cases** - mobile OAuth is a fundamental feature for modern apps. The fact that it requires workarounds and extensive troubleshooting suggests that Appwrite isn't ready for serious mobile development.
+
+### What Appwrite Needs to Fix:
+
+1. **Native mobile OAuth support** with proper deep linking
+2. **Better error messages** that actually help developers diagnose issues
+3. **Comprehensive mobile documentation** with troubleshooting guides
+4. **Native SDK improvements** for mobile platforms
+5. **Faster issue resolution** and community support
+
+### The Supabase Migration Plan:
+
+If I decide to switch (which I'm seriously considering), the migration would involve:
+
+1. **Data Migration**: Export data from Appwrite, transform to PostgreSQL format
+2. **Auth Migration**: Implement Supabase Auth with Flutter
+3. **Real-time Features**: Switch to Supabase's real-time subscriptions
+4. **File Storage**: Migrate to Supabase Storage
+5. **Functions**: Rewrite cloud functions as Supabase Edge Functions
+
+Is it worth the effort? Given the time I've already wasted on OAuth alone, probably yes.
+
+## Conclusion
+
+Appwrite has potential, but it's not production-ready for mobile applications that require OAuth authentication. The lack of proper mobile support, combined with silent failures and poor documentation, makes it a risky choice for serious projects.
+
+**Supabase, while not perfect, offers a more mature and mobile-friendly solution** with better documentation, active community support, and proven reliability.
+
+For fellow developers considering their BaaS options: **test your critical features early**. Don't commit to a platform until you've verified that core functionality like authentication actually works for your use case.
+
+As for me, I'll probably start a Supabase proof-of-concept this weekend. If it goes well, TaskTracker will be migrating sooner rather than later.
+
+---
+
+## Update
+
+If you're experiencing similar issues with Appwrite OAuth, here are some resources that might help:
+
+- [Appwrite GitHub Issues](https://github.com/appwrite/appwrite/issues) - Search for mobile OAuth problems
+- [Supabase Flutter Documentation](https://supabase.com/docs/guides/getting-started/tutorials/with-flutter)
+- [Flutter Web Auth 2 Package](https://pub.dev/packages/flutter_web_auth_2) - The underlying package Appwrite uses
+
+**Have you experienced similar issues?** Share your experience in the comments. Are you using Appwrite or Supabase? What has your experience been?
+
+---
+
+*Written by a frustrated developer who just wants OAuth to work*
+*Date: October 13, 2025*
+
diff --git a/android/app/src/main/AndroidManifest.xml b/android/app/src/main/AndroidManifest.xml
index e72dd81..bb074e0 100644
--- a/android/app/src/main/AndroidManifest.xml
+++ b/android/app/src/main/AndroidManifest.xml
@@ -6,8 +6,7 @@
+
+
+
+
+
+
diff --git a/lib/main.dart b/lib/main.dart
index bf5b512..7426c15 100644
--- a/lib/main.dart
+++ b/lib/main.dart
@@ -2,12 +2,25 @@ import 'package:flutter/material.dart';
import 'package:google_fonts/google_fonts.dart';
import 'screens/auth_screen.dart';
+import 'package:appwrite/appwrite.dart';
+
void main() {
- runApp(const TaskTrackerApp());
+
+ WidgetsFlutterBinding.ensureInitialized();
+ Client client = Client()
+ .setEndpoint("https://supab.playpoolstudios.com/v1")
+ .setProject("tasktracker");
+
+ Account account = Account(client);
+
+ runApp(TaskTrackerApp(account: account, client: client));
}
+
class TaskTrackerApp extends StatelessWidget {
- const TaskTrackerApp({super.key});
+ final Account account;
+ final Client client;
+ const TaskTrackerApp({required this.account, required this.client, super.key});
@override
Widget build(BuildContext context) {
@@ -68,7 +81,7 @@ class TaskTrackerApp extends StatelessWidget {
),
),
),
- home: const AuthScreen(),
+ home: AuthScreen(account: account, client: client),
);
}
}
diff --git a/lib/screens/auth_screen.dart b/lib/screens/auth_screen.dart
index 65215dd..8069674 100644
--- a/lib/screens/auth_screen.dart
+++ b/lib/screens/auth_screen.dart
@@ -11,7 +11,7 @@ class AuthScreen extends StatefulWidget {
}
class _AuthScreenState extends State
- with SingleTickerProviderStateMixin {
+ with SingleTickerProviderStateMixin {
late TabController _tabController;
@override
@@ -20,17 +20,118 @@ class _AuthScreenState extends State
_tabController = TabController(length: 2, vsync: this);
}
+
@override
void dispose() {
_tabController.dispose();
super.dispose();
}
+ Future login(String email, String password) async {
+
+ }
+
+ Future register(String email, String password, String name) async {
+
+ }
+
+ Future logout() async {
+
+ }
+
+
+ Future oAuthLogin() async {
+
+ }
+
+ bool isLogged= false;
@override
Widget build(BuildContext context) {
final screenHeight = MediaQuery.of(context).size.height;
+ // If user is logged in, show logout screen
+ if (isLogged) {
+ return Scaffold(
+ backgroundColor: Colors.grey[50],
+ body: SafeArea(
+ child: Center(
+ child: Column(
+ mainAxisAlignment: MainAxisAlignment.center,
+ children: [
+ Container(
+ width: 80,
+ height: 80,
+ decoration: BoxDecoration(
+ color: Theme.of(context).primaryColor,
+ borderRadius: BorderRadius.circular(20),
+ boxShadow: [
+ BoxShadow(
+ color: Theme.of(context).primaryColor.withValues(alpha: 0.3),
+ blurRadius: 20,
+ offset: const Offset(0, 10),
+ ),
+ ],
+ ),
+ child: const Icon(
+ Icons.task_alt,
+ color: Colors.white,
+ size: 40,
+ ),
+ ),
+ const SizedBox(height: 24),
+ Text(
+ 'Welcome back!',
+ style: GoogleFonts.poppins(
+ fontSize: 28,
+ fontWeight: FontWeight.bold,
+ color: Colors.grey[800],
+ ),
+ ),
+ const SizedBox(height: 8),
+ Text(
+ "username",
+ style: GoogleFonts.poppins(
+ fontSize: 20,
+ fontWeight: FontWeight.w600,
+ color: Theme.of(context).primaryColor,
+ ),
+ ),
+ const SizedBox(height: 4),
+ Text(
+ "email or id",
+ style: GoogleFonts.poppins(
+ fontSize: 16,
+ color: Colors.grey[600],
+ ),
+ ),
+ const SizedBox(height: 40),
+ ElevatedButton(
+ onPressed: logout,
+ style: ElevatedButton.styleFrom(
+ backgroundColor: Colors.red,
+ foregroundColor: Colors.white,
+ padding: const EdgeInsets.symmetric(horizontal: 32, vertical: 16),
+ shape: RoundedRectangleBorder(
+ borderRadius: BorderRadius.circular(12),
+ ),
+ elevation: 2,
+ ),
+ child: Text(
+ 'Logout',
+ style: GoogleFonts.poppins(
+ fontSize: 16,
+ fontWeight: FontWeight.w600,
+ ),
+ ),
+ ),
+ ],
+ ),
+ ),
+ ),
+ );
+ }
+
return Scaffold(
backgroundColor: Colors.grey[50],
body: SafeArea(
@@ -132,8 +233,14 @@ class _AuthScreenState extends State
child: TabBarView(
controller: _tabController,
children: [
- LoginScreen(),
- RegisterScreen(),
+ LoginScreen(
+ onLogin: login,
+ onOAuthLogin: () => oAuthLogin(),
+ ),
+ RegisterScreen(
+ onRegister: register,
+ onOAuthRegister: () => oAuthLogin(),
+ ),
],
),
),
diff --git a/lib/screens/login_screen.dart b/lib/screens/login_screen.dart
index 40e2271..372c487 100644
--- a/lib/screens/login_screen.dart
+++ b/lib/screens/login_screen.dart
@@ -1,10 +1,18 @@
+
import 'package:flutter/material.dart';
import 'package:google_fonts/google_fonts.dart';
import '../widgets/oauth_button.dart';
import '../widgets/custom_text_field.dart';
class LoginScreen extends StatefulWidget {
- const LoginScreen({super.key});
+ const LoginScreen({
+ required this.onLogin,
+ required this.onOAuthLogin,
+ super.key,
+ });
+
+ final Future Function(String email, String password) onLogin;
+ final void Function() onOAuthLogin;
@override
State createState() => _LoginScreenState();
@@ -24,32 +32,43 @@ class _LoginScreenState extends State {
super.dispose();
}
- void _handleLogin() {
+ void _handleLogin() async {
if (_formKey.currentState!.validate()) {
setState(() {
_isLoading = true;
});
- // TODO: Implement Appwrite login logic here
- // For now, just simulate loading
- Future.delayed(const Duration(seconds: 2), () {
+ try {
+ await widget.onLogin(_emailController.text, _passwordController.text);
+ if (mounted) {
+ ScaffoldMessenger.of(context).showSnackBar(
+ const SnackBar(
+ content: Text('Login successful!'),
+ backgroundColor: Colors.green,
+ ),
+ );
+ }
+ } catch (e) {
+ if (mounted) {
+ ScaffoldMessenger.of(context).showSnackBar(
+ SnackBar(
+ content: Text('Login failed: ${e.toString()}'),
+ backgroundColor: Colors.red,
+ ),
+ );
+ }
+ } finally {
if (mounted) {
setState(() {
_isLoading = false;
});
- ScaffoldMessenger.of(context).showSnackBar(
- const SnackBar(content: Text('Login functionality will be implemented with Appwrite')),
- );
}
- });
+ }
}
}
- void _handleOAuthLogin(String provider) {
- // TODO: Implement OAuth login with Appwrite
- ScaffoldMessenger.of(context).showSnackBar(
- SnackBar(content: Text('$provider OAuth login will be implemented with Appwrite')),
- );
+ void _handleOAuthLogin( ) {
+
}
@override
@@ -199,7 +218,7 @@ class _LoginScreenState extends State {
OAuthButton(
provider: 'Google',
icon: Icons.g_mobiledata,
- onPressed: () => _handleOAuthLogin('Google'),
+ onPressed: () => _handleOAuthLogin(),
),
const SizedBox(height: 12),
@@ -207,7 +226,7 @@ class _LoginScreenState extends State {
OAuthButton(
provider: 'Facebook',
icon: Icons.facebook,
- onPressed: () => _handleOAuthLogin('Facebook'),
+ onPressed: () => _handleOAuthLogin(),
),
const SizedBox(height: 12),
@@ -215,7 +234,7 @@ class _LoginScreenState extends State {
OAuthButton(
provider: 'GitHub',
icon: Icons.code,
- onPressed: () => _handleOAuthLogin('GitHub'),
+ onPressed: () => _handleOAuthLogin(),
),
],
),
diff --git a/lib/screens/register_screen.dart b/lib/screens/register_screen.dart
index fa4b8be..fa757dc 100644
--- a/lib/screens/register_screen.dart
+++ b/lib/screens/register_screen.dart
@@ -4,7 +4,14 @@ import '../widgets/oauth_button.dart';
import '../widgets/custom_text_field.dart';
class RegisterScreen extends StatefulWidget {
- const RegisterScreen({super.key});
+ const RegisterScreen({
+ required this.onRegister,
+ required this.onOAuthRegister,
+ super.key,
+ });
+
+ final Future Function(String email, String password, String name) onRegister;
+ final void Function() onOAuthRegister;
@override
State createState() => _RegisterScreenState();
@@ -30,24 +37,42 @@ class _RegisterScreenState extends State {
super.dispose();
}
- void _handleRegister() {
+ void _handleRegister() async {
if (_formKey.currentState!.validate() && _agreeToTerms) {
setState(() {
_isLoading = true;
});
- // TODO: Implement Appwrite registration logic here
- // For now, just simulate loading
- Future.delayed(const Duration(seconds: 2), () {
+ try {
+ await widget.onRegister(
+ _emailController.text,
+ _passwordController.text,
+ _nameController.text,
+ );
+ if (mounted) {
+ ScaffoldMessenger.of(context).showSnackBar(
+ const SnackBar(
+ content: Text('Registration successful!'),
+ backgroundColor: Colors.green,
+ ),
+ );
+ }
+ } catch (e) {
+ if (mounted) {
+ ScaffoldMessenger.of(context).showSnackBar(
+ SnackBar(
+ content: Text('Registration failed: ${e.toString()}'),
+ backgroundColor: Colors.red,
+ ),
+ );
+ }
+ } finally {
if (mounted) {
setState(() {
_isLoading = false;
});
- ScaffoldMessenger.of(context).showSnackBar(
- const SnackBar(content: Text('Registration functionality will be implemented with Appwrite')),
- );
}
- });
+ }
} else if (!_agreeToTerms) {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(content: Text('Please agree to the terms and conditions')),
@@ -55,11 +80,8 @@ class _RegisterScreenState extends State {
}
}
- void _handleOAuthRegister(String provider) {
- // TODO: Implement OAuth registration with Appwrite
- ScaffoldMessenger.of(context).showSnackBar(
- SnackBar(content: Text('$provider OAuth registration will be implemented with Appwrite')),
- );
+ void _handleOAuthRegister() {
+ widget.onOAuthRegister();
}
@override
@@ -284,7 +306,7 @@ class _RegisterScreenState extends State {
OAuthButton(
provider: 'Google',
icon: Icons.g_mobiledata,
- onPressed: () => _handleOAuthRegister('Google'),
+ onPressed: () => _handleOAuthRegister(),
),
const SizedBox(height: 12),
@@ -292,7 +314,7 @@ class _RegisterScreenState extends State {
OAuthButton(
provider: 'Facebook',
icon: Icons.facebook,
- onPressed: () => _handleOAuthRegister('Facebook'),
+ onPressed: () => _handleOAuthRegister(),
),
const SizedBox(height: 12),
@@ -300,7 +322,7 @@ class _RegisterScreenState extends State {
OAuthButton(
provider: 'GitHub',
icon: Icons.code,
- onPressed: () => _handleOAuthRegister('GitHub'),
+ onPressed: () => _handleOAuthRegister(),
),
],
),
diff --git a/test/widget_test.dart b/test/widget_test.dart
index 9db075f..dd8684c 100644
--- a/test/widget_test.dart
+++ b/test/widget_test.dart
@@ -7,12 +7,10 @@
import 'package:flutter_test/flutter_test.dart';
-import 'package:tasktracker/main.dart';
void main() {
testWidgets('TaskTracker app smoke test', (WidgetTester tester) async {
// Build our app and trigger a frame.
- await tester.pumpWidget(const TaskTrackerApp());
// Verify that our app shows the TaskTracker title
expect(find.text('TaskTracker'), findsOneWidget);