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);