From 9364d43aefad18086974d8d5173f43623d9344f7 Mon Sep 17 00:00:00 2001 From: Simon Lightfoot Date: Tue, 20 Jan 2026 15:00:17 +0000 Subject: [PATCH 01/27] feat: docs site --- .github/workflows/deploy-docs.yml | 63 +++ README.md | 164 +++++- docs/.gitignore | 8 + docs/CONTRIBUTING.md | 279 ++++++++++ docs/DEPLOYMENT.md | 228 +++++++++ docs/Gemfile | 35 ++ docs/README.md | 166 ++++++ docs/_config.yml | 78 +++ docs/_config_production.yml | 7 + docs/_sass/color_schemes/clerk.scss | 56 ++ docs/_sass/custom/custom.scss | 167 ++++++ docs/_sass/custom/syntax-highlighting.scss | 159 ++++++ docs/api/_index.md | 40 ++ docs/api/widgets.md | 304 +++++++++++ docs/assets/images/clerk-logo.svg | 7 + docs/getting-started.md | 133 +++++ docs/getting-started/quickstart-dart.md | 261 ++++++++++ docs/getting-started/quickstart-flutter.md | 390 ++++++++++++++ docs/guides/_index.md | 43 ++ docs/guides/authentication.md | 370 ++++++++++++++ docs/guides/backend-integration.md | 567 +++++++++++++++++++++ docs/guides/customization.md | 461 +++++++++++++++++ docs/guides/error-handling.md | 481 +++++++++++++++++ docs/guides/organizations.md | 411 +++++++++++++++ docs/guides/session-tokens.md | 331 ++++++++++++ docs/guides/user-management.md | 456 +++++++++++++++++ docs/index.md | 96 ++++ docs/packages/_index.md | 64 +++ docs/packages/clerk-auth.md | 354 +++++++++++++ docs/packages/clerk-flutter.md | 434 ++++++++++++++++ packages/clerk_auth/README.md | 31 +- packages/clerk_flutter/README.md | 27 + 32 files changed, 6665 insertions(+), 6 deletions(-) create mode 100644 .github/workflows/deploy-docs.yml create mode 100644 docs/.gitignore create mode 100644 docs/CONTRIBUTING.md create mode 100644 docs/DEPLOYMENT.md create mode 100644 docs/Gemfile create mode 100644 docs/README.md create mode 100644 docs/_config.yml create mode 100644 docs/_config_production.yml create mode 100644 docs/_sass/color_schemes/clerk.scss create mode 100644 docs/_sass/custom/custom.scss create mode 100644 docs/_sass/custom/syntax-highlighting.scss create mode 100644 docs/api/_index.md create mode 100644 docs/api/widgets.md create mode 100644 docs/assets/images/clerk-logo.svg create mode 100644 docs/getting-started.md create mode 100644 docs/getting-started/quickstart-dart.md create mode 100644 docs/getting-started/quickstart-flutter.md create mode 100644 docs/guides/_index.md create mode 100644 docs/guides/authentication.md create mode 100644 docs/guides/backend-integration.md create mode 100644 docs/guides/customization.md create mode 100644 docs/guides/error-handling.md create mode 100644 docs/guides/organizations.md create mode 100644 docs/guides/session-tokens.md create mode 100644 docs/guides/user-management.md create mode 100644 docs/index.md create mode 100644 docs/packages/_index.md create mode 100644 docs/packages/clerk-auth.md create mode 100644 docs/packages/clerk-flutter.md diff --git a/.github/workflows/deploy-docs.yml b/.github/workflows/deploy-docs.yml new file mode 100644 index 00000000..cb1d469e --- /dev/null +++ b/.github/workflows/deploy-docs.yml @@ -0,0 +1,63 @@ +name: Deploy Documentation to GitHub Pages + +on: + push: + branches: + - main + - 'feat/341-docs' + paths: + - 'docs/**' + - '.github/workflows/deploy-docs.yml' + workflow_dispatch: + +permissions: + contents: read + pages: write + id-token: write + +concurrency: + group: "pages" + cancel-in-progress: false + +jobs: + build: + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Setup Ruby + uses: ruby/setup-ruby@v1 + with: + ruby-version: '3.1' + bundler-cache: true + working-directory: docs + + - name: Setup Pages + id: pages + uses: actions/configure-pages@v4 + + - name: Build with Jekyll + working-directory: docs + run: | + bundle install + bundle exec jekyll build --baseurl "${{ steps.pages.outputs.base_path }}" --config _config.yml,_config_production.yml + env: + JEKYLL_ENV: production + + - name: Upload artifact + uses: actions/upload-pages-artifact@v3 + with: + path: docs/_site + + deploy: + environment: + name: github-pages + url: ${{ steps.deployment.outputs.page_url }} + runs-on: ubuntu-latest + needs: build + steps: + - name: Deploy to GitHub Pages + id: deployment + uses: actions/deploy-pages@v4 + diff --git a/README.md b/README.md index 99e6944f..407045a2 100644 --- a/README.md +++ b/README.md @@ -8,16 +8,172 @@

+
+ +[![Pub Version](https://img.shields.io/pub/v/clerk_flutter?color=blueviolet&label=clerk_flutter)](https://pub.dev/packages/clerk_flutter) +[![Pub Version](https://img.shields.io/pub/v/clerk_auth?color=blueviolet&label=clerk_auth)](https://pub.dev/packages/clerk_auth) +[![chat on Discord](https://img.shields.io/discord/856971667393609759.svg?logo=discord)](https://discord.com/invite/b5rXHjAg7A) +[![documentation](https://img.shields.io/badge/documentation-clerk-green.svg)](https://clerk.com/docs) + +
# Clerk Dart and Flutter SDKs -The official [Clerk](https://clerk.com) Flutter/Dart client library. +The official [Clerk](https://clerk.com) Dart and Flutter SDKs for authentication and user management. + +**Clerk helps developers build user management. We provide streamlined user experiences for your users to sign up, sign in, and manage their profiles.** + +> ### ⚠️ Beta Notice +> These SDKs are currently in Beta. Breaking changes should be expected until the first stable release (1.0.0). + +--- + +## 📚 Documentation + +**[View Full Documentation →](https://clerk.github.io/clerk-sdk-flutter/)** + +Our comprehensive documentation includes: + +- 🚀 [Getting Started Guides](https://clerk.github.io/clerk-sdk-flutter/getting-started) +- 📖 [API Reference](https://clerk.github.io/clerk-sdk-flutter/packages/clerk-auth) +- 🎯 [Authentication Flows](https://clerk.github.io/clerk-sdk-flutter/guides/authentication) +- 👤 [User Management](https://clerk.github.io/clerk-sdk-flutter/guides/user-management) +- 🎨 [Customization](https://clerk.github.io/clerk-sdk-flutter/guides/customization) + +--- + +## 📦 Packages + +### [clerk_auth](./packages/clerk_auth) - Dart SDK + +Pure Dart SDK for backend and CLI applications. + +[![Pub Version](https://img.shields.io/pub/v/clerk_auth?color=blueviolet)](https://pub.dev/packages/clerk_auth) +[![Pub Points](https://img.shields.io/pub/points/clerk_auth?label=pub%20points)](https://pub.dev/packages/clerk_auth/score) + +```yaml +dependencies: + clerk_auth: ^0.0.13-beta +``` + +**[View clerk_auth Documentation →](https://clerk.github.io/clerk-sdk-flutter/packages/clerk-auth)** + +### [clerk_flutter](./packages/clerk_flutter) - Flutter SDK + +Flutter SDK with pre-built UI components for iOS, Android, Web, and Desktop. + +[![Pub Version](https://img.shields.io/pub/v/clerk_flutter?color=blueviolet)](https://pub.dev/packages/clerk_flutter) +[![Pub Points](https://img.shields.io/pub/points/clerk_flutter?label=pub%20points)](https://pub.dev/packages/clerk_flutter/score) + +```yaml +dependencies: + clerk_flutter: ^0.0.13-beta +``` + +**[View clerk_flutter Documentation →](https://clerk.github.io/clerk-sdk-flutter/packages/clerk-flutter)** +--- -* [clerk_auth](./packages/clerk_auth): Dart SDK -* [clerk_flutter](./packages/clerk_flutter): Flutter SDK +## 🚀 Quick Start +### Dart (clerk_auth) -### License +```dart +import 'dart:io'; +import 'package:clerk_auth/clerk_auth.dart'; + +Future main() async { + final auth = Auth( + config: AuthConfig( + publishableKey: 'pk_test_xxxxxxxxxxxxx', + persistor: DefaultPersistor( + getCacheDirectory: () => Directory.current, + ), + ), + ); + + await auth.initialize(); + + await auth.attemptSignIn( + strategy: Strategy.password, + identifier: 'user@example.com', + password: 'password', + ); + + print('Signed in as ${auth.user?.emailAddress}'); +} +``` + +**[View Dart Quickstart →](https://clerk.github.io/clerk-sdk-flutter/getting-started/quickstart-dart)** + +### Flutter (clerk_flutter) + +```dart +import 'package:flutter/material.dart'; +import 'package:clerk_flutter/clerk_flutter.dart'; + +void main() => runApp(const MyApp()); + +class MyApp extends StatelessWidget { + const MyApp({super.key}); + + @override + Widget build(BuildContext context) { + return ClerkAuth( + config: ClerkAuthConfig( + publishableKey: 'pk_test_xxxxxxxxxxxxx', + ), + child: MaterialApp( + home: Scaffold( + body: ClerkAuthBuilder( + signedInBuilder: (context, authState) { + return Center( + child: ClerkUserButton(showName: true), + ); + }, + signedOutBuilder: (context, authState) { + return const Center( + child: ClerkAuthentication(), + ); + }, + ), + ), + ), + ); + } +} +``` + +**[View Flutter Quickstart →](https://clerk.github.io/clerk-sdk-flutter/getting-started/quickstart-flutter)** + +--- + +## ✨ Features + +- 🔐 **Complete Authentication** - Email, phone, OAuth, passwordless, and MFA +- 👤 **User Management** - Pre-built UI components and headless APIs +- 🏢 **Organizations** - Multi-tenant support with RBAC +- 🎨 **Customizable UI** - Themeable components that match your brand +- 📱 **Cross-Platform** - iOS, Android, Web, Windows, macOS, Linux +- 🚀 **Production Ready** - Battle-tested infrastructure + +--- + +## 🤝 Community & Support + +- 💬 [Discord Community](https://clerk.com/discord) - Chat with the community and Clerk team +- 📖 [Main Clerk Docs](https://clerk.com/docs) - Comprehensive guides and references +- 🐛 [GitHub Issues](https://github.com/clerk/clerk-sdk-flutter/issues) - Report bugs and request features +- 🐦 [Twitter](https://twitter.com/ClerkDev) - Latest updates and announcements + +--- + +## 📄 License These SDKs are licensed under the MIT license found in the [LICENSE](./LICENSE) file. + +--- + +
+

Made with ❤️ by Clerk

+
diff --git a/docs/.gitignore b/docs/.gitignore new file mode 100644 index 00000000..1774ecf9 --- /dev/null +++ b/docs/.gitignore @@ -0,0 +1,8 @@ +_site/ +.sass-cache/ +.jekyll-cache/ +.jekyll-metadata +vendor/ +.bundle/ +Gemfile.lock + diff --git a/docs/CONTRIBUTING.md b/docs/CONTRIBUTING.md new file mode 100644 index 00000000..2d522841 --- /dev/null +++ b/docs/CONTRIBUTING.md @@ -0,0 +1,279 @@ +# Contributing to Clerk Dart & Flutter Documentation + +Thank you for your interest in contributing to the Clerk Dart & Flutter SDK documentation! This guide will help you get started. + +## 📋 Table of Contents + +- [Getting Started](#getting-started) +- [Documentation Structure](#documentation-structure) +- [Writing Guidelines](#writing-guidelines) +- [Local Development](#local-development) +- [Submitting Changes](#submitting-changes) + +--- + +## Getting Started + +### Prerequisites + +- Ruby >= 3.1 +- Bundler +- Git + +### Setup + +1. Fork the repository +2. Clone your fork: + ```bash + git clone https://github.com/YOUR_USERNAME/clerk-sdk-flutter.git + cd clerk-sdk-flutter/docs + ``` + +3. Install dependencies: + ```bash + bundle install + ``` + +4. Start the local server: + ```bash + bundle exec jekyll serve + ``` + +5. Open http://localhost:4000/clerk-sdk-flutter in your browser + +--- + +## Documentation Structure + +``` +docs/ +├── index.md # Homepage +├── getting-started.md # Getting started overview +├── getting-started/ # Quickstart guides +│ ├── quickstart-dart.md +│ └── quickstart-flutter.md +├── guides/ # Comprehensive guides +│ ├── authentication.md +│ ├── user-management.md +│ ├── customization.md +│ └── ... +├── packages/ # Package-specific docs +│ ├── clerk-auth.md +│ └── clerk-flutter.md +├── api/ # API reference +│ └── widgets.md +└── assets/ # Images and static files +``` + +--- + +## Writing Guidelines + +### Front Matter + +Every page must include front matter at the top: + +```yaml +--- +layout: default +title: Your Page Title +parent: Parent Page (optional) +nav_order: 1 +--- +``` + +### Front Matter Options + +| Option | Description | Required | +|--------|-------------|----------| +| `layout` | Page layout (usually `default`) | Yes | +| `title` | Page title shown in navigation | Yes | +| `parent` | Parent page for hierarchy | No | +| `nav_order` | Order in navigation menu | No | +| `has_children` | Set to `true` if page has children | No | +| `permalink` | Custom URL path | No | + +### Markdown Style + +#### Headings + +Use ATX-style headings with proper hierarchy: + +```markdown +# Page Title (H1 - only one per page) + +## Section (H2) + +### Subsection (H3) + +#### Detail (H4) +``` + +#### Code Blocks + +Always specify the language for syntax highlighting: + +````markdown +```dart +import 'package:clerk_flutter/clerk_flutter.dart'; + +void main() { + runApp(MyApp()); +} +``` +```` + +Supported languages: `dart`, `yaml`, `bash`, `json`, `xml`, `swift`, `kotlin` + +#### Tables + +Use tables for structured information: + +```markdown +| Column 1 | Column 2 | Column 3 | +|----------|----------|----------| +| Value 1 | Value 2 | Value 3 | +``` + +#### Callouts + +Use callouts for important information: + +```markdown +{: .note } +> This is a note callout + +{: .warning } +> This is a warning callout + +{: .tip } +> This is a tip callout +``` + +#### Links + +- **Internal links:** Use relative paths: `[Link Text](/guides/authentication)` +- **External links:** Use full URLs: `[Clerk Docs](https://clerk.com/docs)` +- **API references:** Link to pub.dev for detailed API docs + +### Content Guidelines + +1. **Be Clear and Concise** + - Use simple, direct language + - Avoid jargon when possible + - Explain technical terms when first used + +2. **Provide Examples** + - Include code examples for all features + - Show complete, working code when possible + - Add comments to explain complex code + +3. **Be Consistent** + - Use consistent terminology throughout + - Follow the existing documentation style + - Use the same code formatting conventions + +4. **Keep It Updated** + - Ensure code examples work with the latest SDK version + - Update version numbers when they change + - Remove deprecated features + +5. **Think About the User** + - Start with the most common use cases + - Provide step-by-step instructions + - Include troubleshooting tips + +--- + +## Local Development + +### Running the Site Locally + +```bash +cd docs +bundle exec jekyll serve +``` + +The site will be available at http://localhost:4000/clerk-sdk-flutter + +### Live Reload + +Jekyll automatically reloads when you save changes to Markdown files. Refresh your browser to see updates. + +### Building for Production + +```bash +bundle exec jekyll build +``` + +The built site will be in `docs/_site/`. + +--- + +## Submitting Changes + +### Before Submitting + +1. **Test Locally** + - Run the site locally and verify your changes + - Check all links work correctly + - Ensure code examples are correct + +2. **Check Formatting** + - Use proper Markdown syntax + - Include required front matter + - Follow the style guidelines + +3. **Review Content** + - Proofread for typos and grammar + - Ensure technical accuracy + - Verify code examples work + +### Pull Request Process + +1. Create a new branch: + ```bash + git checkout -b docs/your-feature-name + ``` + +2. Make your changes and commit: + ```bash + git add . + git commit -m "docs: Add guide for X feature" + ``` + +3. Push to your fork: + ```bash + git push origin docs/your-feature-name + ``` + +4. Open a Pull Request on GitHub + +5. Fill out the PR template with: + - Description of changes + - Screenshots (if applicable) + - Related issues + +### Commit Message Format + +Use conventional commits: + +- `docs: Add new guide for authentication` +- `docs: Update Flutter quickstart` +- `docs: Fix typo in user management guide` +- `docs: Improve code examples in customization` + +--- + +## Questions? + +If you have questions about contributing to the documentation: + +- 💬 [Join our Discord](https://clerk.com/discord) +- 🐛 [Open an issue](https://github.com/clerk/clerk-sdk-flutter/issues) +- 📧 Email: support@clerk.com + +--- + +Thank you for contributing to Clerk's documentation! 🎉 + diff --git a/docs/DEPLOYMENT.md b/docs/DEPLOYMENT.md new file mode 100644 index 00000000..13866b15 --- /dev/null +++ b/docs/DEPLOYMENT.md @@ -0,0 +1,228 @@ +# Documentation Deployment Guide + +This guide explains how the Clerk Dart & Flutter SDK documentation is deployed to GitHub Pages. + +## 🌐 Live Site + +The documentation is available at: **https://clerk.github.io/clerk-sdk-flutter/** + +--- + +## 📋 Deployment Overview + +The documentation uses: +- **Jekyll** - Static site generator +- **Just the Docs** - Documentation theme +- **GitHub Actions** - Automated deployment +- **GitHub Pages** - Hosting + +--- + +## 🚀 Automatic Deployment + +### Trigger Conditions + +The documentation is automatically deployed when: + +1. Changes are pushed to the `main` branch +2. Changes affect files in the `docs/` directory +3. Changes affect `.github/workflows/deploy-docs.yml` + +### Workflow Steps + +The deployment workflow (`.github/workflows/deploy-docs.yml`) performs: + +1. **Checkout** - Checks out the repository +2. **Setup Ruby** - Installs Ruby 3.1 and dependencies +3. **Setup Pages** - Configures GitHub Pages +4. **Build** - Builds the Jekyll site +5. **Upload** - Uploads the built site as an artifact +6. **Deploy** - Deploys to GitHub Pages + +### Viewing Deployment Status + +1. Go to the [Actions tab](https://github.com/clerk/clerk-sdk-flutter/actions) +2. Look for "Deploy Documentation to GitHub Pages" workflow +3. Click on a run to see details and logs + +--- + +## 🔧 Manual Deployment + +### Trigger Manual Deployment + +1. Go to the [Actions tab](https://github.com/clerk/clerk-sdk-flutter/actions) +2. Select "Deploy Documentation to GitHub Pages" +3. Click "Run workflow" +4. Select the `main` branch +5. Click "Run workflow" + +### Local Build Test + +Before deploying, test the build locally: + +```bash +cd docs +bundle install +bundle exec jekyll build --baseurl "/clerk-sdk-flutter" +``` + +The built site will be in `docs/_site/`. + +--- + +## ⚙️ GitHub Pages Configuration + +### Repository Settings + +To enable GitHub Pages for this repository: + +1. Go to **Settings** → **Pages** +2. Under **Source**, select: + - Source: **GitHub Actions** +3. The site will be published to: `https://clerk.github.io/clerk-sdk-flutter/` + +### Custom Domain (Optional) + +To use a custom domain: + +1. Add a `CNAME` file to `docs/` with your domain +2. Configure DNS settings with your domain provider +3. Update `baseurl` in `docs/_config.yml` + +--- + +## 🐛 Troubleshooting + +### Build Failures + +If the build fails: + +1. Check the [Actions tab](https://github.com/clerk/clerk-sdk-flutter/actions) for error logs +2. Common issues: + - **Jekyll build errors**: Check Markdown syntax and front matter + - **Missing dependencies**: Ensure `Gemfile` is up to date + - **Configuration errors**: Verify `_config.yml` syntax + +### Local Testing + +Always test locally before pushing: + +```bash +cd docs +bundle exec jekyll serve +``` + +Visit http://localhost:4000/clerk-sdk-flutter to preview. + +### Common Errors + +**Error: "Could not find gem"** +```bash +cd docs +bundle install +``` + +**Error: "Permission denied"** +- Check repository permissions +- Ensure GitHub Actions has write access to Pages + +**Error: "404 Not Found" after deployment** +- Verify `baseurl` in `_config.yml` is set to `/clerk-sdk-flutter` +- Check that files are in the `docs/` directory +- Ensure GitHub Pages is enabled in repository settings + +--- + +## 📝 Deployment Checklist + +Before deploying major changes: + +- [ ] Test locally with `bundle exec jekyll serve` +- [ ] Verify all links work +- [ ] Check code examples are correct +- [ ] Review for typos and formatting +- [ ] Ensure front matter is correct on all pages +- [ ] Test responsive design (mobile/desktop) +- [ ] Verify navigation hierarchy +- [ ] Check that search works (if enabled) + +--- + +## 🔄 Rollback + +If you need to rollback a deployment: + +1. Go to the [Actions tab](https://github.com/clerk/clerk-sdk-flutter/actions) +2. Find the last successful deployment +3. Click "Re-run all jobs" + +Or revert the commit: + +```bash +git revert +git push origin main +``` + +--- + +## 📊 Monitoring + +### Analytics + +To add Google Analytics: + +1. Add your tracking ID to `docs/_config.yml`: + ```yaml + google_analytics: UA-XXXXXXXXX-X + ``` + +2. The Just the Docs theme will automatically include the tracking code + +### Performance + +Monitor site performance: +- Use [Google PageSpeed Insights](https://pagespeed.web.dev/) +- Check [Lighthouse](https://developers.google.com/web/tools/lighthouse) scores +- Monitor build times in GitHub Actions + +--- + +## 🔐 Security + +### Permissions + +The workflow requires these permissions: +- `contents: read` - Read repository contents +- `pages: write` - Write to GitHub Pages +- `id-token: write` - Generate deployment token + +These are configured in `.github/workflows/deploy-docs.yml`. + +### Secrets + +No secrets are required for basic deployment. If you add custom features that need secrets: + +1. Go to **Settings** → **Secrets and variables** → **Actions** +2. Add your secrets +3. Reference them in the workflow with `${{ secrets.SECRET_NAME }}` + +--- + +## 📚 Additional Resources + +- [Jekyll Documentation](https://jekyllrb.com/docs/) +- [Just the Docs Theme](https://just-the-docs.github.io/just-the-docs/) +- [GitHub Pages Documentation](https://docs.github.com/en/pages) +- [GitHub Actions Documentation](https://docs.github.com/en/actions) + +--- + +## 🆘 Support + +If you encounter issues with deployment: + +- 💬 [Join our Discord](https://clerk.com/discord) +- 🐛 [Open an issue](https://github.com/clerk/clerk-sdk-flutter/issues) +- 📧 Email: support@clerk.com + diff --git a/docs/Gemfile b/docs/Gemfile new file mode 100644 index 00000000..72ed083d --- /dev/null +++ b/docs/Gemfile @@ -0,0 +1,35 @@ +source "https://rubygems.org" + +gem "jekyll", "~> 4.3" +gem "just-the-docs", "~> 0.10.0" + +# Pin sass-embedded to avoid deprecation warnings with older Sass +gem "sass-embedded", "~> 1.69.0" + +# Required for Ruby 3.4+ +gem "bigdecimal" +gem "csv" +gem "base64" +gem "fiddle" + +group :jekyll_plugins do + gem "jekyll-seo-tag" + gem "jekyll-github-metadata" + gem "jekyll-include-cache" + gem "jekyll-relative-links" +end + +# Windows and JRuby does not include zoneinfo files, so bundle the tzinfo-data gem +# and associated library. +platforms :mingw, :x64_mingw, :mswin, :jruby do + gem "tzinfo", ">= 1", "< 3" + gem "tzinfo-data" +end + +# Performance-booster for watching directories on Windows +gem "wdm", "~> 0.1", :platforms => [:mingw, :x64_mingw, :mswin] + +# Lock `http_parser.rb` gem to `v0.6.x` on JRuby builds since newer versions of the gem +# do not have a Java counterpart. +gem "http_parser.rb", "~> 0.6.0", :platforms => [:jruby] + diff --git a/docs/README.md b/docs/README.md new file mode 100644 index 00000000..546f0d83 --- /dev/null +++ b/docs/README.md @@ -0,0 +1,166 @@ +# Clerk Dart & Flutter SDK Documentation + +This directory contains the documentation site for Clerk's Dart and Flutter SDKs, built with Jekyll and hosted on GitHub Pages. + +## 🌐 Live Documentation + +Visit the live documentation at: **https://clerk.github.io/clerk-sdk-flutter/** + +## 📁 Structure + +``` +docs/ +├── _config.yml # Jekyll configuration +├── index.md # Homepage +├── getting-started.md # Getting started overview +├── getting-started/ # Quickstart guides +│ ├── quickstart-dart.md +│ └── quickstart-flutter.md +├── guides/ # Comprehensive guides +│ ├── authentication.md +│ ├── user-management.md +│ ├── customization.md +│ └── ... +├── packages/ # Package-specific documentation +│ ├── clerk-auth.md +│ └── clerk-flutter.md +└── assets/ # Images and static assets +``` + +## 🚀 Local Development + +### Prerequisites + +- Ruby >= 3.1 +- Bundler + +### Setup + +1. Install dependencies: + +```bash +cd docs +bundle install +``` + +2. Run the local server: + +```bash +bundle exec jekyll serve +``` + +3. Open http://localhost:4000/clerk-sdk-flutter in your browser + +{: .note } +> The site uses the local `just-the-docs` gem for development and `remote_theme` for GitHub Pages deployment. This is configured automatically via `_config.yml` and `_config_production.yml`. + +### Live Reload + +The server will automatically reload when you make changes to the documentation files. + +## 📝 Writing Documentation + +### Adding a New Page + +1. Create a new Markdown file in the appropriate directory +2. Add front matter at the top: + +```yaml +--- +layout: default +title: Your Page Title +parent: Parent Page (optional) +nav_order: 1 +--- +``` + +3. Write your content in Markdown + +### Front Matter Options + +| Option | Description | +|--------|-------------| +| `layout` | Page layout (usually `default`) | +| `title` | Page title | +| `parent` | Parent page for navigation hierarchy | +| `nav_order` | Order in navigation menu | +| `has_children` | Set to `true` if page has child pages | +| `permalink` | Custom URL path | + +### Markdown Features + +The documentation supports: + +- Standard Markdown syntax +- Code blocks with syntax highlighting +- Tables +- Callouts (notes, warnings, etc.) +- Table of contents + +#### Code Blocks + +````markdown +```dart +import 'package:clerk_flutter/clerk_flutter.dart'; + +void main() { + runApp(MyApp()); +} +``` +```` + +#### Callouts + +```markdown +{: .note } +> This is a note callout + +{: .warning } +> This is a warning callout +``` + +## 🎨 Theme + +The documentation uses the [Just the Docs](https://just-the-docs.github.io/just-the-docs/) theme with custom Clerk branding. + +## 🚢 Deployment + +The documentation is automatically deployed to GitHub Pages when changes are pushed to the `main` branch. + +### GitHub Actions Workflow + +The deployment is handled by `.github/workflows/deploy-docs.yml`: + +1. Triggered on push to `main` branch (when `docs/**` files change) +2. Builds the Jekyll site +3. Deploys to GitHub Pages + +### Manual Deployment + +You can also trigger a manual deployment: + +1. Go to the [Actions tab](https://github.com/clerk/clerk-sdk-flutter/actions) +2. Select "Deploy Documentation to GitHub Pages" +3. Click "Run workflow" + +## 📦 Dependencies + +- **Jekyll** - Static site generator +- **just-the-docs** - Documentation theme +- **jekyll-seo-tag** - SEO optimization +- **jekyll-github-metadata** - GitHub integration + +## 🤝 Contributing + +To contribute to the documentation: + +1. Fork the [repository](https://github.com/clerk/clerk-sdk-flutter) +2. Create a new branch for your changes +3. Make your edits in the `docs/` directory +4. Test locally with `bundle exec jekyll serve` +5. Submit a pull request + +## 📄 License + +This documentation is part of the Clerk Dart & Flutter SDK and is licensed under the MIT License. + diff --git a/docs/_config.yml b/docs/_config.yml new file mode 100644 index 00000000..659aaccb --- /dev/null +++ b/docs/_config.yml @@ -0,0 +1,78 @@ +# Clerk Dart & Flutter SDK Documentation +title: Clerk Dart & Flutter SDK +description: Official documentation for Clerk's Dart and Flutter SDKs +baseurl: "/clerk-sdk-flutter" +url: "https://clerk.github.io" + +# Theme +theme: just-the-docs + +# Logo and branding +logo: "/assets/images/clerk-logo.svg" +# Custom Clerk color scheme +color_scheme: clerk + +# Navigation +nav_enabled: true +nav_sort: case_insensitive + +# Search +search_enabled: true +search: + heading_level: 2 + previews: 3 + preview_words_before: 5 + preview_words_after: 10 + tokenizer_separator: /[\s/]+/ + rel_url: true + button: true + +# Enable relative URLs for all links +aux_links_new_tab: false + +# Footer +footer_content: "Copyright © 2026 Clerk, Inc. Licensed under the MIT License." + +# Aux links +aux_links: + "Clerk Dart & Flutter SDK on GitHub": + - "https://github.com/clerk/clerk-sdk-flutter" + "Clerk Documentation": + - "https://clerk.com/docs" + "Clerk Dashboard": + - "https://dashboard.clerk.com" + +# Defaults +defaults: + - scope: + path: "" + values: + layout: default + +# Plugins +plugins: + - jekyll-seo-tag + - jekyll-github-metadata + - jekyll-include-cache + - jekyll-relative-links + +# Relative links configuration +relative_links: + enabled: true + collections: true + +# Syntax highlighting +kramdown: + syntax_highlighter: rouge + syntax_highlighter_opts: + css_class: 'highlight' + default_lang: dart + +# Exclude from processing +exclude: + - node_modules/ + - vendor/ + - Gemfile + - Gemfile.lock + - README.md + diff --git a/docs/_config_production.yml b/docs/_config_production.yml new file mode 100644 index 00000000..a4dbf458 --- /dev/null +++ b/docs/_config_production.yml @@ -0,0 +1,7 @@ +# Production overrides for GitHub Pages deployment +# This file is merged with _config.yml during GitHub Pages build + +# Use remote_theme for GitHub Pages +remote_theme: just-the-docs/just-the-docs +theme: null + diff --git a/docs/_sass/color_schemes/clerk.scss b/docs/_sass/color_schemes/clerk.scss new file mode 100644 index 00000000..3f591631 --- /dev/null +++ b/docs/_sass/color_schemes/clerk.scss @@ -0,0 +1,56 @@ +// Clerk color scheme - matching https://clerk.com/docs +// Based on dark theme with Clerk brand colors + +// Background colors +$body-background-color: #0F0F0F; +$sidebar-color: #0F0F0F; +$code-background-color: #1A1A1A; +$table-background-color: #1A1A1A; +$feedback-color: darken($sidebar-color, 3%); + +// Text colors +$body-text-color: #E5E5E5; +$body-heading-color: #FFFFFF; +$nav-child-link-color: #A1A1A1; +$link-color: #9B8FD9; +$btn-primary-color: #6C47FF; +$base-button-color: #6C47FF; + +// Border colors +$border-color: #2A2A2A; + +// Code colors +$code-linenumber-color: #6B6B6B; + +// Search +$search-background-color: #1A1A1A; +$search-result-preview-color: #A1A1A1; + +// Sidebar +$sidebar-width: 264px; +$sidebar-width-sm: 200px; + +// Navigation +$nav-list-link-color: #E5E5E5; +$nav-list-link-color-hover: #FFFFFF; +$nav-list-link-color-active: #FFFFFF; +$nav-list-expander-color: #6B6B6B; + +// Buttons +$btn-primary-color: #6C47FF; + +// Base button (outline style) +$base-button-color: transparent; + +// Feedback colors +$label-green: #10B981; +$label-blue: #3B82F6; +$label-purple: #6C47FF; +$label-red: #EF4444; +$label-yellow: #F59E0B; + +// Callouts/Admonitions +$note-color: #3B82F6; +$warning-color: #F59E0B; +$important-color: #EF4444; + diff --git a/docs/_sass/custom/custom.scss b/docs/_sass/custom/custom.scss new file mode 100644 index 00000000..75db400e --- /dev/null +++ b/docs/_sass/custom/custom.scss @@ -0,0 +1,167 @@ +// Import syntax highlighting +@import "./syntax-highlighting"; + + +// Custom styles to match Clerk documentation + +// Smooth scrolling +html { + scroll-behavior: smooth; +} + +// Logo styling +.site-logo { + max-height: 32px; +} + +// Navigation improvements +.site-nav { + padding-top: 1.5rem; +} + +.nav-list .nav-list-item { + margin: 0.25rem 0; +} + +.nav-list-link { + border-radius: 6px; + padding: 0.5rem 0.75rem; + transition: all 0.2s ease; + + &:hover { + background-color: rgba(108, 71, 255, 0.1); + } + + &.active { + background-color: rgba(108, 71, 255, 0.15); + font-weight: 500; + } +} + +// Code blocks +pre.highlight { + border-radius: 8px; + border: 1px solid #2A2A2A; +} + +code { + border-radius: 4px; + padding: 0.2em 0.4em; + font-size: 0.9em; +} + +// Tables +table { + border-radius: 8px; + overflow: hidden; + + th { + background-color: #1A1A1A; + font-weight: 600; + } + + td, th { + border-color: #2A2A2A; + } +} + +// Buttons +.btn { + border-radius: 6px; + font-weight: 500; + transition: all 0.2s ease; + border: 1px solid #2A2A2A; + background-color: transparent; + color: #E5E5E5; + + &:hover { + border-color: #6C47FF; + background-color: rgba(108, 71, 255, 0.1); + color: #FFFFFF; + } +} + +.btn-primary { + background-color: #6C47FF; + border-color: #6C47FF; + color: #FFFFFF; + + &:hover { + background-color: #5A38E6; + border-color: #5A38E6; + transform: translateY(-1px); + box-shadow: 0 4px 12px rgba(108, 71, 255, 0.3); + } +} + +// Headings +h1, h2, h3, h4, h5, h6 { + font-weight: 600; + letter-spacing: -0.02em; +} + +// Links +a { + transition: color 0.2s ease; + + &:hover { + color: #B5AAEB; + } +} + +// Search +.search-input { + border-radius: 8px; + background-color: #1A1A1A; + border: 1px solid #2A2A2A; + + &:focus { + border-color: #6C47FF; + box-shadow: 0 0 0 3px rgba(108, 71, 255, 0.1); + } +} + +// Callouts +.label, +.label-blue, +.label-green, +.label-purple, +.label-red, +.label-yellow { + border-radius: 4px; + font-weight: 500; +} + +// Note/Warning boxes +blockquote { + border-left: 4px solid #6C47FF; + background-color: rgba(108, 71, 255, 0.05); + border-radius: 4px; + padding: 1rem; + margin: 1.5rem 0; +} + +// Sidebar footer +.site-footer { + border-top: 1px solid #2A2A2A; + padding: 1rem; + font-size: 0.875rem; + color: #6B6B6B; +} + +// Main content area +.main-content { + max-width: 50rem; + + h1 { + margin-bottom: 1.5rem; + } + + h2 { + margin-top: 2.5rem; + margin-bottom: 1rem; + padding-bottom: 0.5rem; + border-bottom: 1px solid #2A2A2A; + } +} + diff --git a/docs/_sass/custom/syntax-highlighting.scss b/docs/_sass/custom/syntax-highlighting.scss new file mode 100644 index 00000000..86229cda --- /dev/null +++ b/docs/_sass/custom/syntax-highlighting.scss @@ -0,0 +1,159 @@ +// Dark theme syntax highlighting for code blocks +// Based on GitHub Dark theme + +.highlight { + background-color: #1A1A1A; + color: #E5E5E5; + + // Comments + .c, .cm, .c1, .cs { + color: #8B949E; + font-style: italic; + } + + // Keywords + .k, .kc, .kd, .kn, .kp, .kr, .kt { + color: #FF7B72; + } + + // Operators + .o, .ow { + color: #FF7B72; + } + + // Strings + .s, .s1, .s2, .sb, .sc, .sd, .se, .sh, .si, .sx { + color: #A5D6FF; + } + + // Numbers + .m, .mf, .mh, .mi, .mo, .il { + color: #79C0FF; + } + + // Names + .n, .na, .nb, .nc, .no, .nd, .ni, .ne, .nf, .nl, .nn, .nx, .py { + color: #D2A8FF; + } + + // Variables + .nv, .vc, .vg, .vi { + color: #FFA657; + } + + // Built-ins + .nb { + color: #79C0FF; + } + + // Functions + .nf { + color: #D2A8FF; + } + + // Classes + .nc { + color: #FFA657; + } + + // Booleans + .bp { + color: #79C0FF; + } + + // Punctuation + .p { + color: #E5E5E5; + } + + // Generic + .gd { + color: #FF7B72; + background-color: #3F1F1F; + } + + .gi { + color: #7EE787; + background-color: #1F3F1F; + } + + .ge { + font-style: italic; + } + + .gs { + font-weight: bold; + } + + // Line numbers + .lineno { + color: #6B6B6B; + user-select: none; + padding-right: 1em; + border-right: 1px solid #2A2A2A; + margin-right: 1em; + } + + // Dart-specific + .kd { + color: #FF7B72; // class, void, var, final, const + } + + .kt { + color: #79C0FF; // int, String, bool, etc. + } + + // YAML-specific + .l-Scalar-Plain { + color: #A5D6FF; + } + + .na { + color: #79C0FF; // YAML keys + } + + // JSON-specific + .s2 { + color: #A5D6FF; // JSON strings + } + + // Bash/Shell + .nv { + color: #79C0FF; // environment variables + } + + .nb { + color: #D2A8FF; // built-in commands + } +} + +// Inline code +code { + background-color: #1A1A1A; + color: #E5E5E5; + border: 1px solid #2A2A2A; +} + +// Code blocks +pre { + background-color: #1A1A1A; + border: 1px solid #2A2A2A; + + code { + background-color: transparent; + border: none; + color: #E5E5E5; + } +} + +// Highlighted lines in code blocks +.highlight .hll { + background-color: #2A2A2A; +} + +// Error highlighting +.highlight .err { + color: #FF7B72; + background-color: transparent; +} + diff --git a/docs/api/_index.md b/docs/api/_index.md new file mode 100644 index 00000000..e85256f5 --- /dev/null +++ b/docs/api/_index.md @@ -0,0 +1,40 @@ +--- +layout: default +title: API Reference +nav_order: 5 +has_children: true +permalink: /api +--- + +# API Reference + +Detailed API documentation for Clerk's Dart and Flutter SDKs. + +## Available References + +### [Widget Reference]({{ '/api/widgets' | relative_url }}) +Complete reference for all Clerk Flutter widgets including ClerkAuth, ClerkAuthentication, ClerkUserButton, and more. + +### [clerk_auth Package]({{ '/packages/clerk-auth' | relative_url }}) +API documentation for the Dart SDK including Auth, User, Session, and configuration classes. + +### [clerk_flutter Package]({{ '/packages/clerk-flutter' | relative_url }}) +API documentation for the Flutter SDK including widgets, themes, and Flutter-specific features. + +--- + +## External API Documentation + +For detailed API documentation generated from source code: + +- **[clerk_auth on pub.dev](https://pub.dev/documentation/clerk_auth/latest/)** - Complete Dart API reference +- **[clerk_flutter on pub.dev](https://pub.dev/documentation/clerk_flutter/latest/)** - Complete Flutter API reference + +--- + +## Quick Links + +- [Getting Started](/getting-started) +- [Guides](/guides) +- [GitHub Repository](https://github.com/clerk/clerk-sdk-flutter) + diff --git a/docs/api/widgets.md b/docs/api/widgets.md new file mode 100644 index 00000000..b8592628 --- /dev/null +++ b/docs/api/widgets.md @@ -0,0 +1,304 @@ +--- +layout: default +title: Widget Reference +parent: API Reference +nav_order: 1 +--- + +# Widget Reference +{: .no_toc } + +Complete reference for all Clerk Flutter widgets. +{: .fs-6 .fw-300 } + +## Table of contents +{: .no_toc .text-delta } + +1. TOC +{:toc} + +--- + +## Control Widgets + +### ClerkAuth + +The root widget that initializes and provides Clerk authentication to your app. + +```dart +ClerkAuth( + config: ClerkAuthConfig( + publishableKey: 'pk_test_xxxxxxxxxxxxx', + ), + child: MaterialApp(/* ... */), +) +``` + +**Properties:** + +| Name | Type | Required | Description | +|------|------|----------|-------------| +| `config` | `ClerkAuthConfig` | Yes | Configuration object | +| `child` | `Widget` | Yes | Your app widget | +| `persistor` | `Persistor?` | No | Custom storage mechanism | +| `httpService` | `HttpService?` | No | Custom HTTP service | +| `authState` | `ClerkAuthState?` | No | Pre-initialized auth state | + +--- + +### ClerkAuthBuilder + +Builds different UI based on authentication state. + +```dart +ClerkAuthBuilder( + signedInBuilder: (context, authState) => SignedInWidget(), + signedOutBuilder: (context, authState) => SignedOutWidget(), +) +``` + +**Properties:** + +| Name | Type | Required | Description | +|------|------|----------|-------------| +| `signedInBuilder` | `AuthWidgetBuilder?` | No | Builder when user is signed in | +| `signedOutBuilder` | `AuthWidgetBuilder?` | No | Builder when user is signed out | +| `builder` | `AuthWidgetBuilder?` | No | Fallback builder | + +--- + +### ClerkSignedIn + +Only renders its child when a user is signed in. + +```dart +ClerkSignedIn( + child: Text('Protected content'), +) +``` + +**Properties:** + +| Name | Type | Required | Description | +|------|------|----------|-------------| +| `child` | `Widget` | Yes | Widget to show when signed in | + +--- + +### ClerkSignedOut + +Only renders its child when no user is signed in. + +```dart +ClerkSignedOut( + child: Text('Please sign in'), +) +``` + +**Properties:** + +| Name | Type | Required | Description | +|------|------|----------|-------------| +| `child` | `Widget` | Yes | Widget to show when signed out | + +--- + +### ClerkErrorListener + +Listens for authentication errors and displays them. + +```dart +ClerkErrorListener( + child: YourWidget(), +) +``` + +**Properties:** + +| Name | Type | Required | Description | +|------|------|----------|-------------| +| `child` | `Widget` | Yes | Child widget | + +--- + +## Authentication Widgets + +### ClerkAuthentication + +Pre-built authentication UI with sign-in and sign-up flows. + +```dart +const ClerkAuthentication() +``` + +**Features:** +- Email/password authentication +- OAuth provider buttons +- Email verification +- Password reset +- Automatic error handling +- Responsive design + +--- + +## User Widgets + +### ClerkUserButton + +User menu button with profile and sign-out options. + +```dart +ClerkUserButton( + showName: true, + sessionActions: [ + ClerkUserAction( + label: 'Settings', + icon: Icons.settings, + onPressed: (context, authState) { + // Handle action + }, + ), + ], +) +``` + +**Properties:** + +| Name | Type | Required | Default | Description | +|------|------|----------|---------|-------------| +| `showName` | `bool` | No | `true` | Show user's name next to avatar | +| `sessionActions` | `List?` | No | `null` | Custom actions for session row | +| `additionalActions` | `List?` | No | `null` | Additional menu items | + +--- + +### ClerkUserAction + +Defines a custom action for the user menu. + +```dart +ClerkUserAction( + label: 'Settings', + icon: Icons.settings, + onPressed: (context, authState) { + Navigator.push( + context, + MaterialPageRoute(builder: (_) => SettingsPage()), + ); + }, +) +``` + +**Properties:** + +| Name | Type | Required | Description | +|------|------|----------|-------------| +| `label` | `String` | Yes | Action label text | +| `icon` | `IconData` | Yes | Action icon | +| `onPressed` | `Function(BuildContext, ClerkAuthState)` | Yes | Callback when pressed | + +--- + +## Organization Widgets + +### ClerkOrganizationList + +Display and manage organizations (if enabled in your Clerk instance). + +```dart +const ClerkOrganizationList() +``` + +**Features:** +- List all user's organizations +- Create new organizations +- Switch active organization +- Manage organization settings + +--- + +## Accessing ClerkAuth + +### ClerkAuth.of() + +Access the ClerkAuthState from anywhere in your widget tree. + +```dart +final authState = ClerkAuth.of(context); +final user = authState.user; +``` + +**Parameters:** + +| Name | Type | Required | Default | Description | +|------|------|----------|---------|-------------| +| `context` | `BuildContext` | Yes | - | Build context | +| `listen` | `bool` | No | `true` | Whether to rebuild on changes | + +**Returns:** `ClerkAuthState` + +--- + +## ClerkAuthState + +The main state object for Clerk authentication. + +### Properties + +| Name | Type | Description | +|------|------|-------------| +| `user` | `User?` | Current signed-in user | +| `client` | `Client` | Client object with sessions | +| `env` | `Environment` | Environment configuration | +| `isSignedIn` | `bool` | Whether a user is signed in | +| `sessionTokenStream` | `Stream` | Stream of session tokens | + +### Methods + +| Method | Description | +|--------|-------------| +| `attemptSignIn()` | Start a sign-in attempt | +| `attemptSignUp()` | Start a sign-up attempt | +| `attemptSignInVerification()` | Verify a sign-in attempt | +| `attemptSignUpVerification()` | Verify a sign-up attempt | +| `signOut()` | Sign out the current user | +| `signOutOf(Session)` | Sign out of a specific session | +| `safelyCall()` | Execute async operation with error handling | + +--- + +## Configuration + +### ClerkAuthConfig + +Configuration object for ClerkAuth. + +```dart +ClerkAuthConfig( + publishableKey: 'pk_test_xxxxxxxxxxxxx', + redirectionGenerator: (url) => Uri.parse('myapp://oauth'), + deepLinkStream: AppLinks().allUriLinkStream.map( + (uri) => ClerkDeepLink(uri: uri), + ), + loading: CircularProgressIndicator(), +) +``` + +**Properties:** + +| Name | Type | Required | Description | +|------|------|----------|-------------| +| `publishableKey` | `String` | Yes | Your Clerk publishable key | +| `redirectionGenerator` | `Function(String)?` | No | Generate OAuth redirect URLs | +| `deepLinkStream` | `Stream?` | No | Handle OAuth callbacks | +| `loading` | `Widget?` | No | Widget shown during initialization | + +--- + +## Next Steps + +- [Customization Guide]({{ '/guides/customization' | relative_url }}) - Theme and style your widgets +- [Flutter Quickstart]({{ '/getting-started/quickstart-flutter' | relative_url }}) - Get started quickly +- [Authentication Flows]({{ '/guides/authentication' | relative_url }}) - Implement auth flows +- [User Management]({{ '/guides/user-management' | relative_url }}) - Manage user profiles + diff --git a/docs/assets/images/clerk-logo.svg b/docs/assets/images/clerk-logo.svg new file mode 100644 index 00000000..0d7dd8cc --- /dev/null +++ b/docs/assets/images/clerk-logo.svg @@ -0,0 +1,7 @@ + + + + + + + diff --git a/docs/getting-started.md b/docs/getting-started.md new file mode 100644 index 00000000..35c27fe5 --- /dev/null +++ b/docs/getting-started.md @@ -0,0 +1,133 @@ +--- +layout: default +title: Getting Started +nav_order: 2 +has_children: true +permalink: /getting-started +--- + +# Getting Started with Clerk Dart & Flutter SDKs + +This guide will help you integrate Clerk authentication into your Dart or Flutter application. + +## Choose Your SDK + +Clerk provides two SDKs for Dart and Flutter development: + +### clerk_auth (Dart SDK) + +The `clerk_auth` package is a pure Dart SDK suitable for: +- Backend applications +- CLI tools +- Server-side Dart applications +- Any Dart environment where you need authentication + +**[Get started with clerk_auth →]({{ '/getting-started/quickstart-dart' | relative_url }})** + +### clerk_flutter (Flutter SDK) + +The `clerk_flutter` package is designed for Flutter applications and includes: +- Pre-built UI components +- Material Design integration +- Cross-platform support (iOS, Android, Web, Desktop) +- Seamless integration with Flutter's widget tree + +**[Get started with clerk_flutter →]({{ '/getting-started/quickstart-flutter' | relative_url }})** + +--- + +## Prerequisites + +Before you begin, you'll need: + +1. **A Clerk Account** - [Sign up for free](https://dashboard.clerk.com/sign-up) +2. **A Clerk Application** - Create one in the [Clerk Dashboard](https://dashboard.clerk.com) +3. **Your Publishable Key** - Found in your application's API Keys section + +### Finding Your Publishable Key + +1. Navigate to the [Clerk Dashboard](https://dashboard.clerk.com) +2. Select your application +3. Go to **API Keys** in the sidebar +4. Copy your **Publishable Key** (starts with `pk_test_` or `pk_live_`) + +{: .warning } +> Never commit your secret keys to version control. Use environment variables or secure configuration management. + +--- + +## Installation + +### For Dart Projects (clerk_auth) + +Add `clerk_auth` to your `pubspec.yaml`: + +```yaml +dependencies: + clerk_auth: ^0.0.13-beta +``` + +Then run: + +```bash +dart pub get +``` + +### For Flutter Projects (clerk_flutter) + +Add `clerk_flutter` to your `pubspec.yaml`: + +```yaml +dependencies: + clerk_flutter: ^0.0.13-beta +``` + +Then run: + +```bash +flutter pub get +``` + +{: .note } +> The `clerk_flutter` package automatically includes `clerk_auth` as a dependency, so you don't need to add both. + +--- + +## Platform-Specific Setup + +### Android + +Add the following permission to your `android/app/src/main/AndroidManifest.xml`: + +```xml + +``` + +### iOS + +No additional setup required for basic functionality. + +### Web + +No additional setup required for basic functionality. + +### Desktop (Windows, macOS, Linux) + +No additional setup required for basic functionality. + +--- + +## Next Steps + +Choose your quickstart guide based on your project type: + +- **[Dart Quickstart]({{ '/getting-started/quickstart-dart' | relative_url }})** - For pure Dart applications +- **[Flutter Quickstart]({{ '/getting-started/quickstart-flutter' | relative_url }})** - For Flutter applications + +Or explore specific topics: + +- [Authentication Flows]({{ '/guides/authentication' | relative_url }}) +- [User Management]({{ '/guides/user-management' | relative_url }}) +- [Organizations]({{ '/guides/organizations' | relative_url }}) +- [Customization]({{ '/guides/customization' | relative_url }}) + diff --git a/docs/getting-started/quickstart-dart.md b/docs/getting-started/quickstart-dart.md new file mode 100644 index 00000000..3676e92d --- /dev/null +++ b/docs/getting-started/quickstart-dart.md @@ -0,0 +1,261 @@ +--- +layout: default +title: Dart Quickstart +parent: Getting Started +nav_order: 1 +--- + +# Dart Quickstart +{: .no_toc } + +Get started with Clerk authentication in your Dart application in minutes. +{: .fs-6 .fw-300 } + +## Table of contents +{: .no_toc .text-delta } + +1. TOC +{:toc} + +--- + +## Install clerk_auth + +Add `clerk_auth` to your `pubspec.yaml`: + +```yaml +dependencies: + clerk_auth: ^0.0.13-beta +``` + +Install the package: + +```bash +dart pub get +``` + +--- + +## Set up your environment + +Create a `.env` file or set environment variables with your Clerk publishable key: + +```bash +CLERK_PUBLISHABLE_KEY=pk_test_xxxxxxxxxxxxx +``` + +{: .warning } +> Never commit your `.env` file to version control. Add it to your `.gitignore`. + +--- + +## Initialize Clerk Auth + +Create an `Auth` instance and initialize it: + +```dart +import 'dart:io'; +import 'package:clerk_auth/clerk_auth.dart'; + +Future main() async { + // Create the Auth instance + final auth = Auth( + config: AuthConfig( + publishableKey: Platform.environment['CLERK_PUBLISHABLE_KEY']!, + persistor: DefaultPersistor( + getCacheDirectory: () => Directory.current, + ), + ), + ); + + // Initialize the auth system + await auth.initialize(); + + // Your app logic here + + // Clean up when done + auth.terminate(); +} +``` + +--- + +## Sign in with email and password + +```dart +import 'dart:io'; +import 'package:clerk_auth/clerk_auth.dart'; + +Future main() async { + final auth = Auth( + config: AuthConfig( + publishableKey: Platform.environment['CLERK_PUBLISHABLE_KEY']!, + persistor: DefaultPersistor( + getCacheDirectory: () => Directory.current, + ), + ), + ); + + await auth.initialize(); + + try { + // Attempt to sign in + await auth.attemptSignIn( + strategy: Strategy.password, + identifier: 'user@example.com', + password: 'your-password', + ); + + print('✅ Signed in as: ${auth.user?.emailAddress}'); + print('User ID: ${auth.user?.id}'); + + // Access the current user + final user = auth.user; + if (user != null) { + print('Name: ${user.firstName} ${user.lastName}'); + print('Email: ${user.emailAddress}'); + } + + } on ClerkApiException catch (e) { + print('❌ Sign in failed: ${e.message}'); + } finally { + auth.terminate(); + } +} +``` + +--- + +## Sign up a new user + +```dart +import 'package:clerk_auth/clerk_auth.dart'; + +Future signUpUser(Auth auth) async { + try { + // Start the sign-up process + await auth.attemptSignUp( + strategy: Strategy.password, + identifier: 'newuser@example.com', + password: 'secure-password', + ); + + // If email verification is required + if (auth.signUp?.verification?.status == VerificationStatus.unverified) { + print('📧 Verification email sent'); + + // User enters the code from their email + final code = '123456'; // Get this from user input + + await auth.attemptSignUpVerification( + strategy: Strategy.emailCode, + code: code, + ); + } + + print('✅ Sign up successful!'); + print('User: ${auth.user?.emailAddress}'); + + } on ClerkApiException catch (e) { + print('❌ Sign up failed: ${e.message}'); + } +} +``` + +--- + +## Sign out + +```dart +Future signOutUser(Auth auth) async { + await auth.signOut(); + print('👋 Signed out successfully'); +} +``` + +--- + +## Access session tokens + +Session tokens are automatically managed and can be accessed via a stream: + +```dart +import 'dart:async'; + +Future listenToSessionTokens(Auth auth) async { + // Listen to session token updates + final subscription = auth.sessionTokenStream.listen((token) { + if (token != null) { + print('🔑 New session token: ${token.substring(0, 20)}...'); + // Use this token for authenticated API requests + } + }); + + // Remember to cancel the subscription when done + await Future.delayed(Duration(seconds: 30)); + await subscription.cancel(); +} +``` + +--- + +## Next steps + +Now that you have basic authentication working, explore more features: + +- [Authentication Strategies]({{ '/guides/authentication' | relative_url }}) - OAuth, passwordless, MFA +- [User Management]({{ '/guides/user-management' | relative_url }}) - Update profiles, manage sessions +- [Session Token Polling]({{ '/guides/session-tokens' | relative_url }}) - Configure token refresh behavior +- [Error Handling]({{ '/guides/error-handling' | relative_url }}) - Handle authentication errors gracefully + +--- + +## Complete example + +Here's a complete example with sign-up, sign-in, and sign-out: + +```dart +import 'dart:io'; +import 'package:clerk_auth/clerk_auth.dart'; + +Future main() async { + final auth = Auth( + config: AuthConfig( + publishableKey: Platform.environment['CLERK_PUBLISHABLE_KEY']!, + persistor: DefaultPersistor( + getCacheDirectory: () => Directory.current, + ), + ), + ); + + await auth.initialize(); + + try { + // Check if already signed in + if (auth.user != null) { + print('Already signed in as: ${auth.user?.emailAddress}'); + } else { + // Sign in + await auth.attemptSignIn( + strategy: Strategy.password, + identifier: 'user@example.com', + password: 'password', + ); + print('Signed in successfully!'); + } + + // Do authenticated work + print('User ID: ${auth.user?.id}'); + + // Sign out + await auth.signOut(); + print('Signed out'); + + } catch (e) { + print('Error: $e'); + } finally { + auth.terminate(); + } +} +``` + diff --git a/docs/getting-started/quickstart-flutter.md b/docs/getting-started/quickstart-flutter.md new file mode 100644 index 00000000..f631906f --- /dev/null +++ b/docs/getting-started/quickstart-flutter.md @@ -0,0 +1,390 @@ +--- +layout: default +title: Flutter Quickstart +parent: Getting Started +nav_order: 2 +--- + +# Flutter Quickstart +{: .no_toc } + +Get started with Clerk authentication in your Flutter application with pre-built UI components. +{: .fs-6 .fw-300 } + +## Table of contents +{: .no_toc .text-delta } + +1. TOC +{:toc} + +--- + +## Install clerk_flutter + +Add `clerk_flutter` to your `pubspec.yaml`: + +```yaml +dependencies: + clerk_flutter: ^0.0.13-beta +``` + +Install the package: + +```bash +flutter pub get +``` + +--- + +## Platform setup + +### Android + +Add the Internet permission to `android/app/src/main/AndroidManifest.xml`: + +```xml + + + + +``` + +### iOS, Web, Desktop + +No additional setup required. + +--- + +## Wrap your app with ClerkAuth + +Wrap your `MaterialApp` with the `ClerkAuth` widget: + +```dart +import 'package:flutter/material.dart'; +import 'package:clerk_flutter/clerk_flutter.dart'; + +void main() { + runApp(const MyApp()); +} + +class MyApp extends StatelessWidget { + const MyApp({super.key}); + + @override + Widget build(BuildContext context) { + return ClerkAuth( + config: ClerkAuthConfig( + publishableKey: 'pk_test_xxxxxxxxxxxxx', + ), + child: MaterialApp( + title: 'My Clerk App', + theme: ThemeData.light(), + darkTheme: ThemeData.dark(), + home: const HomePage(), + ), + ); + } +} +``` + +{: .warning } +> Replace `pk_test_xxxxxxxxxxxxx` with your actual publishable key from the [Clerk Dashboard](https://dashboard.clerk.com). + +--- + +## Build conditional UI with ClerkAuthBuilder + +Use `ClerkAuthBuilder` to show different UI based on authentication state: + +```dart +import 'package:flutter/material.dart'; +import 'package:clerk_flutter/clerk_flutter.dart'; + +class HomePage extends StatelessWidget { + const HomePage({super.key}); + + @override + Widget build(BuildContext context) { + return Scaffold( + body: SafeArea( + child: ClerkErrorListener( + child: ClerkAuthBuilder( + signedInBuilder: (context, authState) { + // User is signed in + return Center( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Text('Welcome, ${authState.user?.firstName}!'), + const SizedBox(height: 20), + const ClerkUserButton(), + ], + ), + ); + }, + signedOutBuilder: (context, authState) { + // User is signed out + return const Center( + child: ClerkAuthentication(), + ); + }, + ), + ), + ), + ); + } +} +``` + +--- + +## Use pre-built authentication UI + +The `ClerkAuthentication` widget provides a complete sign-in/sign-up flow: + +```dart +import 'package:flutter/material.dart'; +import 'package:clerk_flutter/clerk_flutter.dart'; + +class SignInPage extends StatelessWidget { + const SignInPage({super.key}); + + @override + Widget build(BuildContext context) { + return Scaffold( + body: SafeArea( + child: Center( + child: ClerkAuthentication(), + ), + ), + ); + } +} +``` + +The `ClerkAuthentication` widget automatically handles: +- Email/password sign-in +- Email/password sign-up +- OAuth providers (Google, GitHub, etc.) +- Email verification +- Password reset +- Error states + +--- + +## Use the ClerkUserButton + +The `ClerkUserButton` provides a user menu with profile management and sign-out: + +```dart +import 'package:flutter/material.dart'; +import 'package:clerk_flutter/clerk_flutter.dart'; + +class ProfilePage extends StatelessWidget { + const ProfilePage({super.key}); + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar( + title: const Text('Profile'), + actions: const [ + ClerkUserButton(), + ], + ), + body: ClerkSignedIn( + child: Center( + child: Text('Protected content'), + ), + ), + ); + } +} +``` + +--- + +## Conditional rendering with ClerkSignedIn/ClerkSignedOut + +Use these widgets to conditionally render content: + +```dart +import 'package:flutter/material.dart'; +import 'package:clerk_flutter/clerk_flutter.dart'; + +class ConditionalPage extends StatelessWidget { + const ConditionalPage({super.key}); + + @override + Widget build(BuildContext context) { + return Scaffold( + body: Column( + children: [ + ClerkSignedIn( + child: Text('This is only visible when signed in'), + ), + ClerkSignedOut( + child: Text('This is only visible when signed out'), + ), + ], + ), + ); + } +} +``` + +--- + +## Access the current user + +Access user information through the `ClerkAuth` inherited widget: + +```dart +import 'package:flutter/material.dart'; +import 'package:clerk_flutter/clerk_flutter.dart'; + +class UserInfoWidget extends StatelessWidget { + const UserInfoWidget({super.key}); + + @override + Widget build(BuildContext context) { + final authState = ClerkAuth.of(context); + final user = authState.user; + + if (user == null) { + return const Text('Not signed in'); + } + + return Column( + children: [ + Text('Name: ${user.firstName} ${user.lastName}'), + Text('Email: ${user.emailAddress}'), + Text('User ID: ${user.id}'), + if (user.imageUrl != null) + Image.network(user.imageUrl!), + ], + ); + } +} +``` + +--- + +## Handle deep links for OAuth + +For OAuth authentication, you need to handle deep links: + +```dart +import 'package:clerk_flutter/clerk_flutter.dart'; +import 'package:app_links/app_links.dart'; + +ClerkAuth( + config: ClerkAuthConfig( + publishableKey: 'pk_test_xxxxxxxxxxxxx', + redirectionGenerator: (url) { + // Generate your app's deep link + return Uri.parse('myapp://oauth-callback'); + }, + deepLinkStream: AppLinks().allUriLinkStream.map((uri) { + // Handle incoming deep links + return ClerkDeepLink(uri: uri); + }), + ), + child: MaterialApp( + // ... + ), +) +``` + +Add the `app_links` package to handle deep links: + +```yaml +dependencies: + app_links: ^3.5.1 +``` + +--- + +## Next steps + +Explore more features and customization options: + +- [Widget Reference]({{ '/api/widgets' | relative_url }}) - All available widgets and their properties +- [Customization]({{ '/guides/customization' | relative_url }}) - Theme and style your components +- [Authentication Flows]({{ '/guides/authentication' | relative_url }}) - OAuth, passwordless, MFA +- [User Management]({{ '/guides/user-management' | relative_url }}) - Profile updates, session management +- [Organizations]({{ '/guides/organizations' | relative_url }}) - Multi-tenant support + +--- + +## Complete example + +Here's a complete Flutter app with Clerk authentication: + +```dart +import 'package:flutter/material.dart'; +import 'package:clerk_flutter/clerk_flutter.dart'; + +void main() { + runApp(const MyApp()); +} + +class MyApp extends StatelessWidget { + const MyApp({super.key}); + + @override + Widget build(BuildContext context) { + return ClerkAuth( + config: ClerkAuthConfig( + publishableKey: 'pk_test_xxxxxxxxxxxxx', + ), + child: MaterialApp( + title: 'Clerk Flutter Demo', + theme: ThemeData.light(), + darkTheme: ThemeData.dark(), + home: const HomePage(), + ), + ); + } +} + +class HomePage extends StatelessWidget { + const HomePage({super.key}); + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar( + title: const Text('Clerk Flutter Demo'), + ), + body: SafeArea( + child: ClerkErrorListener( + child: ClerkAuthBuilder( + signedInBuilder: (context, authState) { + return Center( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Text( + 'Welcome, ${authState.user?.firstName ?? "User"}!', + style: Theme.of(context).textTheme.headlineMedium, + ), + const SizedBox(height: 20), + const ClerkUserButton(showName: true), + ], + ), + ); + }, + signedOutBuilder: (context, authState) { + return const Center( + child: ClerkAuthentication(), + ); + }, + ), + ), + ), + ); + } +} +``` + diff --git a/docs/guides/_index.md b/docs/guides/_index.md new file mode 100644 index 00000000..af6f5bb9 --- /dev/null +++ b/docs/guides/_index.md @@ -0,0 +1,43 @@ +--- +layout: default +title: Guides +nav_order: 3 +has_children: true +permalink: /guides +--- + +# Guides + +Comprehensive guides for implementing authentication and user management with Clerk's Dart and Flutter SDKs. + +## Available Guides + +### [Authentication]({{ '/guides/authentication' | relative_url }}) +Learn how to implement various authentication strategies including email/password, passwordless, OAuth, and multi-factor authentication. + +### [User Management]({{ '/guides/user-management' | relative_url }}) +Manage user profiles, update user information, handle email addresses and phone numbers, and work with user metadata. + +### [Organizations]({{ '/guides/organizations' | relative_url }}) +Implement multi-tenant functionality with organizations, roles, and permissions. + +### [Customization]({{ '/guides/customization' | relative_url }}) +Customize the appearance and behavior of Clerk's UI components to match your brand. + +### [Session Tokens]({{ '/guides/session-tokens' | relative_url }}) +Work with JWT session tokens for authenticated API requests. + +### [Error Handling]({{ '/guides/error-handling' | relative_url }}) +Handle authentication errors and provide great user experiences. + +### [Backend Integration]({{ '/guides/backend-integration' | relative_url }}) +Integrate Clerk with your backend services and APIs. + +--- + +## Quick Navigation + +- **Getting Started?** Check out the [Dart Quickstart]({{ '/getting-started/quickstart-dart' | relative_url }}) or [Flutter Quickstart]({{ '/getting-started/quickstart-flutter' | relative_url }}) +- **Need API Reference?** See the [API Documentation]({{ '/api' | relative_url }}) +- **Looking for Examples?** Browse the [GitHub Examples](https://github.com/clerk/clerk-sdk-flutter/tree/main/packages/clerk_flutter/example) + diff --git a/docs/guides/authentication.md b/docs/guides/authentication.md new file mode 100644 index 00000000..e8185c32 --- /dev/null +++ b/docs/guides/authentication.md @@ -0,0 +1,370 @@ +--- +layout: default +title: Authentication +parent: Guides +nav_order: 1 +--- + +# Authentication Flows +{: .no_toc } + +Learn how to implement various authentication strategies with Clerk's Dart and Flutter SDKs. +{: .fs-6 .fw-300 } + +## Table of contents +{: .no_toc .text-delta } + +1. TOC +{:toc} + +--- + +## Overview + +Clerk supports multiple authentication strategies to meet your application's needs: + +- **Email & Password** - Traditional username/password authentication +- **Email Code (Passwordless)** - One-time codes sent via email +- **Phone Code (Passwordless)** - One-time codes sent via SMS +- **OAuth** - Social login with Google, GitHub, Facebook, and more +- **Multi-Factor Authentication (MFA)** - Additional security layer + +--- + +## Email and Password Authentication + +### Sign In + +```dart +import 'package:clerk_auth/clerk_auth.dart'; + +Future signInWithPassword(Auth auth, String email, String password) async { + try { + await auth.attemptSignIn( + strategy: Strategy.password, + identifier: email, + password: password, + ); + + print('✅ Signed in as: ${auth.user?.emailAddress}'); + } on ClerkApiException catch (e) { + print('❌ Sign in failed: ${e.message}'); + // Handle specific error codes + if (e.errors.any((err) => err.code == 'form_password_incorrect')) { + print('Incorrect password'); + } + } +} +``` + +### Sign Up + +```dart +Future signUpWithPassword( + Auth auth, + String email, + String password, + {String? firstName, String? lastName} +) async { + try { + await auth.attemptSignUp( + strategy: Strategy.password, + identifier: email, + password: password, + firstName: firstName, + lastName: lastName, + ); + + // Check if email verification is required + if (auth.signUp?.verification?.status == VerificationStatus.unverified) { + print('📧 Verification email sent to $email'); + // Proceed to verification step + } else { + print('✅ Sign up successful!'); + } + } on ClerkApiException catch (e) { + print('❌ Sign up failed: ${e.message}'); + } +} +``` + +### Email Verification + +```dart +Future verifyEmail(Auth auth, String code) async { + try { + await auth.attemptSignUpVerification( + strategy: Strategy.emailCode, + code: code, + ); + + print('✅ Email verified! User is now signed in.'); + } on ClerkApiException catch (e) { + print('❌ Verification failed: ${e.message}'); + } +} +``` + +--- + +## Passwordless Authentication + +### Email Code (Magic Link) + +```dart +Future signInWithEmailCode(Auth auth, String email) async { + try { + // Step 1: Request the code + await auth.attemptSignIn( + strategy: Strategy.emailCode, + identifier: email, + ); + + print('📧 Verification code sent to $email'); + + // Step 2: User enters the code from their email + final code = '123456'; // Get from user input + + await auth.attemptSignInVerification( + strategy: Strategy.emailCode, + code: code, + ); + + print('✅ Signed in successfully!'); + } on ClerkApiException catch (e) { + print('❌ Error: ${e.message}'); + } +} +``` + +### Phone Code (SMS) + +```dart +Future signInWithPhoneCode(Auth auth, String phoneNumber) async { + try { + // Step 1: Request the SMS code + await auth.attemptSignIn( + strategy: Strategy.phoneCode, + identifier: phoneNumber, // Format: +1234567890 + ); + + print('📱 SMS code sent to $phoneNumber'); + + // Step 2: User enters the code from SMS + final code = '123456'; // Get from user input + + await auth.attemptSignInVerification( + strategy: Strategy.phoneCode, + code: code, + ); + + print('✅ Signed in successfully!'); + } on ClerkApiException catch (e) { + print('❌ Error: ${e.message}'); + } +} +``` + +--- + +## OAuth Authentication + +### Flutter OAuth Flow + +For Flutter applications, use the built-in OAuth support: + +```dart +import 'package:flutter/material.dart'; +import 'package:clerk_flutter/clerk_flutter.dart'; + +class OAuthSignInPage extends StatelessWidget { + const OAuthSignInPage({super.key}); + + @override + Widget build(BuildContext context) { + return Scaffold( + body: Center( + child: ClerkAuthentication(), + ), + ); + } +} +``` + +The `ClerkAuthentication` widget automatically displays configured OAuth providers. + +### Custom OAuth Button + +```dart +import 'package:clerk_flutter/clerk_flutter.dart'; + +class CustomOAuthButton extends StatelessWidget { + const CustomOAuthButton({super.key}); + + @override + Widget build(BuildContext context) { + final authState = ClerkAuth.of(context); + + return ElevatedButton.icon( + icon: Icon(Icons.login), + label: Text('Sign in with Google'), + onPressed: () async { + try { + await authState.attemptSignIn( + strategy: Strategy.oauth(OAuthProvider.google), + ); + } catch (e) { + print('OAuth error: $e'); + } + }, + ); + } +} +``` + +### Supported OAuth Providers + +Clerk supports the following OAuth providers: + +- Google (`OAuthProvider.google`) +- GitHub (`OAuthProvider.github`) +- Facebook (`OAuthProvider.facebook`) +- Microsoft (`OAuthProvider.microsoft`) +- Apple (`OAuthProvider.apple`) +- Discord (`OAuthProvider.discord`) +- Twitter/X (`OAuthProvider.twitter`) +- LinkedIn (`OAuthProvider.linkedin`) +- And many more... + +--- + +## Multi-Factor Authentication (MFA) + +### Enable MFA for a User + +```dart +Future enableMFA(Auth auth) async { + final user = auth.user; + if (user == null) return; + + try { + // Generate TOTP secret + final totp = await user.createTOTP(); + + print('Scan this QR code: ${totp.qrCodeUrl}'); + print('Or enter this secret: ${totp.secret}'); + + // User scans QR code and enters verification code + final code = '123456'; // Get from user input + + await totp.verify(code: code); + + print('✅ MFA enabled successfully!'); + } catch (e) { + print('❌ MFA setup failed: $e'); + } +} +``` + +### Sign In with MFA + +```dart +Future signInWithMFA(Auth auth, String email, String password) async { + try { + // Step 1: Initial sign-in + await auth.attemptSignIn( + strategy: Strategy.password, + identifier: email, + password: password, + ); + + // Step 2: Check if MFA is required + if (auth.signIn?.secondFactor?.status == VerificationStatus.unverified) { + print('🔐 MFA code required'); + + // User enters their TOTP code + final mfaCode = '123456'; // Get from user input + + await auth.attemptSignInVerification( + strategy: Strategy.totp, + code: mfaCode, + ); + } + + print('✅ Signed in with MFA!'); + } catch (e) { + print('❌ Sign in failed: $e'); + } +} +``` + +--- + +## Password Reset + +```dart +Future resetPassword(Auth auth, String email) async { + try { + // Step 1: Request password reset + await auth.resetPassword(email: email); + + print('📧 Password reset email sent to $email'); + + // Step 2: User clicks link in email and enters new password + // This typically happens in a web view or separate flow + + } catch (e) { + print('❌ Password reset failed: $e'); + } +} +``` + +--- + +## Session Management + +### Check Authentication Status + +```dart +bool isSignedIn(Auth auth) { + return auth.user != null; +} +``` + +### Get Current Session + +```dart +import 'package:clerk_auth/clerk_auth.dart'; + +Session? getCurrentSession(Auth auth) { + return auth.client.activeSession; +} +``` + +### Sign Out + +```dart +Future signOut(Auth auth) async { + await auth.signOut(); + print('👋 Signed out successfully'); +} +``` + +### Sign Out of Specific Session + +```dart +Future signOutOfSession(Auth auth, Session session) async { + await auth.signOutOf(session); + print('👋 Signed out of session: ${session.id}'); +} +``` + +--- + +## Next Steps + +- [User Management]({{ '/guides/user-management' | relative_url }}) - Manage user profiles and data +- [Session Tokens]({{ '/guides/session-tokens' | relative_url }}) - Work with JWT tokens +- [Error Handling]({{ '/guides/error-handling' | relative_url }}) - Handle authentication errors +- [Organizations]({{ '/guides/organizations' | relative_url }}) - Multi-tenant authentication + diff --git a/docs/guides/backend-integration.md b/docs/guides/backend-integration.md new file mode 100644 index 00000000..572e646b --- /dev/null +++ b/docs/guides/backend-integration.md @@ -0,0 +1,567 @@ +--- +layout: default +title: Backend Integration +parent: Guides +nav_order: 7 +--- + +# Backend Integration +{: .no_toc } + +Learn how to integrate Clerk authentication with your backend services and verify session tokens. +{: .fs-6 .fw-300 } + +## Table of contents +{: .no_toc .text-delta } + +1. TOC +{:toc} + +--- + +## Overview + +When building applications with Clerk, you'll often need to: + +- Verify session tokens on your backend +- Access user information server-side +- Implement protected API endpoints +- Manage users from your backend + +--- + +## Verifying Session Tokens + +Session tokens are JWTs that can be verified using Clerk's public keys. + +### Using Clerk Backend SDK + +The recommended approach is to use Clerk's backend SDK for your server language: + +**Node.js/Express:** +```javascript +import { ClerkExpressRequireAuth } from '@clerk/clerk-sdk-node'; + +app.get('/api/protected', + ClerkExpressRequireAuth(), + (req, res) => { + const userId = req.auth.userId; + res.json({ message: 'Protected data', userId }); + } +); +``` + +**Python/Flask:** +```python +from clerk_backend_api import Clerk + +clerk = Clerk(bearer_auth="sk_live_xxxxx") + +@app.route('/api/protected') +def protected(): + token = request.headers.get('Authorization').replace('Bearer ', '') + + try: + session = clerk.sessions.verify_token(token) + return {'message': 'Protected data', 'userId': session.user_id} + except Exception as e: + return {'error': 'Unauthorized'}, 401 +``` + +--- + +## Manual JWT Verification + +If you need to manually verify JWTs: + +### 1. Get Clerk's Public Keys + +Clerk's public keys are available at: +``` +https://[your-frontend-api]/.well-known/jwks.json +``` + +Example: +``` +https://clerk.example.com/.well-known/jwks.json +``` + +### 2. Verify the JWT + +**Node.js Example:** +```javascript +import jwt from 'jsonwebtoken'; +import jwksClient from 'jwks-rsa'; + +const client = jwksClient({ + jwksUri: 'https://clerk.example.com/.well-known/jwks.json' +}); + +function getKey(header, callback) { + client.getSigningKey(header.kid, (err, key) => { + const signingKey = key.publicKey || key.rsaPublicKey; + callback(null, signingKey); + }); +} + +function verifyToken(token) { + return new Promise((resolve, reject) => { + jwt.verify(token, getKey, { + algorithms: ['RS256'] + }, (err, decoded) => { + if (err) reject(err); + else resolve(decoded); + }); + }); +} + +// Usage +app.get('/api/protected', async (req, res) => { + const token = req.headers.authorization?.replace('Bearer ', ''); + + try { + const payload = await verifyToken(token); + res.json({ userId: payload.sub }); + } catch (err) { + res.status(401).json({ error: 'Unauthorized' }); + } +}); +``` + +--- + +## Token Claims + +A verified Clerk session token contains these claims: + +```json +{ + "sub": "user_2abc123", // User ID + "sid": "sess_xyz789", // Session ID + "iat": 1616239022, // Issued at + "exp": 1616242622, // Expires at + "azp": "https://example.com", // Authorized party + "org_id": "org_456", // Organization ID (if applicable) + "org_role": "admin", // Organization role (if applicable) + "org_permissions": ["read", "write"] // Permissions (if applicable) +} +``` + +--- + + +--- + +## Protecting API Endpoints + +### Example: Express.js Middleware + +```javascript +import { ClerkExpressRequireAuth } from '@clerk/clerk-sdk-node'; + +// Protect a single route +app.get('/api/user/profile', + ClerkExpressRequireAuth(), + (req, res) => { + const userId = req.auth.userId; + res.json({ userId, message: 'Protected data' }); + } +); + +// Protect multiple routes +app.use('/api/admin', ClerkExpressRequireAuth()); + +app.get('/api/admin/users', (req, res) => { + // Only accessible with valid session token + res.json({ users: [] }); +}); +``` + +### Example: Python/FastAPI + +```python +from fastapi import FastAPI, Depends, HTTPException +from fastapi.security import HTTPBearer, HTTPAuthorizationCredentials +from clerk_backend_api import Clerk + +app = FastAPI() +security = HTTPBearer() +clerk = Clerk(bearer_auth="sk_live_xxxxx") + +async def verify_clerk_token( + credentials: HTTPAuthorizationCredentials = Depends(security) +): + try: + session = clerk.sessions.verify_token(credentials.credentials) + return session + except Exception: + raise HTTPException(status_code=401, detail="Unauthorized") + +@app.get("/api/protected") +async def protected_route(session = Depends(verify_clerk_token)): + return {"userId": session.user_id, "message": "Protected data"} +``` + +--- + +## Organization-Based Access Control + +Verify organization membership and roles: + +### Node.js Example + +```javascript +import { clerkClient } from '@clerk/clerk-sdk-node'; + +app.get('/api/org/:orgId/data', + ClerkExpressRequireAuth(), + async (req, res) => { + const { userId, orgId, orgRole } = req.auth; + const requestedOrgId = req.params.orgId; + + // Verify user belongs to the organization + if (orgId !== requestedOrgId) { + return res.status(403).json({ error: 'Forbidden' }); + } + + // Check role-based permissions + if (orgRole === 'admin') { + // Admin-only data + return res.json({ data: 'Admin data' }); + } else { + // Member data + return res.json({ data: 'Member data' }); + } + } +); +``` + +### Dart Backend Example + +```dart +import 'package:clerk_backend_api/clerk_backend_api.dart'; +import 'package:shelf/shelf.dart'; + +Future handleOrgRequest(Request request) async { + final token = request.headers['authorization']?.replaceFirst('Bearer ', ''); + + if (token == null) { + return Response.forbidden('No token provided'); + } + + try { + // Verify token and get claims + final session = await clerk.sessions.verifyToken(token); + final orgId = session.organizationId; + final orgRole = session.organizationRole; + + // Check organization access + if (orgId != request.params['orgId']) { + return Response.forbidden('Access denied'); + } + + // Role-based logic + if (orgRole == 'admin') { + return Response.ok('Admin data'); + } else { + return Response.ok('Member data'); + } + + } catch (e) { + return Response.forbidden('Invalid token'); + } +} +``` + +--- + +## Managing Users from Backend + +Use the Clerk Backend API to manage users server-side: + +### Create User + +```dart +import 'package:clerk_backend_api/clerk_backend_api.dart'; + +final clerk = Clerk( + security: Security(bearerAuth: 'sk_live_xxxxxxxxxxxxx'), +); + +// Create a new user +final user = await clerk.users.createUser( + request: CreateUserRequestBody( + emailAddress: ['user@example.com'], + password: 'securePassword123', + firstName: 'John', + lastName: 'Doe', + ), +); + +print('Created user: ${user.id}'); +``` + +### Update User + +```dart +// Update user metadata +final updatedUser = await clerk.users.updateUser( + userId: 'user_123', + request: UpdateUserRequestBody( + publicMetadata: { + 'plan': 'premium', + 'credits': 100, + }, + ), +); +``` + +### Delete User + +```dart +// Delete a user +await clerk.users.deleteUser(userId: 'user_123'); +``` + +--- + +## Session Management + +### List User Sessions + +```dart +// Get all sessions for a user +final sessions = await clerk.sessions.getSessionList( + userId: 'user_123', +); + +for (final session in sessions.data) { + print('Session: ${session.id}'); + print('Last active: ${session.lastActiveAt}'); +} +``` + +### Revoke Session + +```dart +// Revoke a specific session +await clerk.sessions.revokeSession(sessionId: 'sess_xyz789'); + +// This will sign the user out of that session +``` + +--- + +## Webhooks + +Clerk can send webhooks to your backend when events occur: + +### Common Webhook Events + +- `user.created` - New user signed up +- `user.updated` - User profile updated +- `user.deleted` - User deleted +- `session.created` - New session started +- `session.ended` - Session ended +- `organization.created` - Organization created +- `organizationMembership.created` - User joined organization + +### Verifying Webhooks + +```javascript +import { Webhook } from 'svix'; + +app.post('/api/webhooks/clerk', async (req, res) => { + const webhookSecret = process.env.CLERK_WEBHOOK_SECRET; + const headers = req.headers; + const payload = req.body; + + const wh = new Webhook(webhookSecret); + + try { + const evt = wh.verify(JSON.stringify(payload), { + 'svix-id': headers['svix-id'], + 'svix-timestamp': headers['svix-timestamp'], + 'svix-signature': headers['svix-signature'], + }); + + // Handle the event + switch (evt.type) { + case 'user.created': + console.log('New user:', evt.data.id); + break; + case 'user.updated': + console.log('User updated:', evt.data.id); + break; + } + + res.json({ received: true }); + } catch (err) { + res.status(400).json({ error: 'Invalid signature' }); + } +}); +``` + +--- + +## Best Practices + +### 1. Always Verify Tokens Server-Side + +```dart +// ❌ DON'T: Trust client-provided user IDs +app.get('/api/user/:userId/data', (req, res) => { + const userId = req.params.userId; // Can be forged! + // ... +}); + +// ✅ DO: Get user ID from verified token +app.get('/api/user/data', + ClerkExpressRequireAuth(), + (req, res) => { + const userId = req.auth.userId; // From verified JWT + // ... + } +); +``` + +### 2. Use Secret Keys Securely + +```dart +// ✅ DO: Use environment variables +final clerk = Clerk( + security: Security( + bearerAuth: Platform.environment['CLERK_SECRET_KEY']!, + ), +); + +// ❌ DON'T: Hardcode secret keys +final clerk = Clerk( + security: Security(bearerAuth: 'sk_live_xxxxx'), // Never do this! +); +``` + +### 3. Implement Rate Limiting + +```javascript +import rateLimit from 'express-rate-limit'; + +const limiter = rateLimit({ + windowMs: 15 * 60 * 1000, // 15 minutes + max: 100 // limit each IP to 100 requests per windowMs +}); + +app.use('/api/', limiter); +``` + +### 4. Log Security Events + +```dart +void logSecurityEvent(String event, Map data) { + print('[SECURITY] $event: ${json.encode(data)}'); + // Send to logging service (e.g., Sentry, CloudWatch) +} + +// Usage +try { + final session = await clerk.sessions.verifyToken(token); + logSecurityEvent('token_verified', {'userId': session.userId}); +} catch (e) { + logSecurityEvent('token_verification_failed', {'error': e.toString()}); +} +``` + +--- + +## Next Steps + +- [Session Tokens]({{ '/guides/session-tokens' | relative_url }}) - Understanding JWT tokens +- [Organizations]({{ '/guides/organizations' | relative_url }}) - Multi-tenant access control +- [User Management]({{ '/guides/user-management' | relative_url }}) - Client-side user operations +- [Error Handling]({{ '/guides/error-handling' | relative_url }}) - Handle backend errors gracefully + + +## Sending Tokens from Client + +### From Dart (clerk_auth) + +```dart +import 'package:http/http.dart' as http; +import 'package:clerk_auth/clerk_auth.dart'; + +Future callProtectedApi(Auth auth) async { + final token = await auth.sessionTokenStream.first; + + if (token == null) { + throw Exception('Not authenticated'); + } + + final response = await http.get( + Uri.parse('https://api.example.com/protected'), + headers: { + 'Authorization': 'Bearer $token', + }, + ); + + print('Response: ${response.body}'); +} +``` + +### From Flutter (clerk_flutter) + +```dart +import 'package:clerk_flutter/clerk_flutter.dart'; + +class ApiService { + Future fetchData(BuildContext context) async { + final authState = ClerkAuth.of(context); + final token = await authState.sessionTokenStream.first; + + if (token == null) { + throw Exception('Not authenticated'); + } + + final response = await http.get( + Uri.parse('https://api.example.com/protected'), + headers: { + 'Authorization': 'Bearer $token', + }, + ); + } +} +``` + +--- + +## Using Clerk Backend API + +For advanced backend operations, use the Clerk Backend API: + +### Available Operations + +- **Users**: Create, update, delete users +- **Sessions**: List, revoke sessions +- **Organizations**: Manage organizations and memberships +- **Invitations**: Send and manage invitations + +### Dart Backend API Client + +```dart +import 'package:clerk_backend_api/clerk_backend_api.dart'; + +final clerk = Clerk( + security: Security(bearerAuth: 'sk_live_xxxxxxxxxxxxx'), +); + +// Get user by ID +final user = await clerk.users.getUser(userId: 'user_123'); +print('User: ${user.emailAddress}'); + +// List all users +final users = await clerk.users.getUserList(); +for (final user in users.data) { + print('User: ${user.emailAddress}'); +} +``` + + diff --git a/docs/guides/customization.md b/docs/guides/customization.md new file mode 100644 index 00000000..379fadd6 --- /dev/null +++ b/docs/guides/customization.md @@ -0,0 +1,461 @@ +--- +layout: default +title: Customization +parent: Guides +nav_order: 4 +--- + +# Customization +{: .no_toc } + +Learn how to customize the appearance and behavior of Clerk's Flutter UI components to match your brand. +{: .fs-6 .fw-300 } + +## Table of contents +{: .no_toc .text-delta } + +1. TOC +{:toc} + +--- + +## Overview + +Clerk's Flutter SDK provides extensive customization options through: + +- **ClerkTheme** - Theme extension for colors, typography, and spacing +- **Widget Properties** - Customize individual widget behavior +- **Custom Builders** - Build your own authentication UI + +--- + +## Theming with ClerkTheme + +### Basic Theme Setup + +Apply a custom theme to your Clerk components: + +```dart +import 'package:flutter/material.dart'; +import 'package:clerk_flutter/clerk_flutter.dart'; + +class MyApp extends StatelessWidget { + const MyApp({super.key}); + + @override + Widget build(BuildContext context) { + return ClerkAuth( + config: ClerkAuthConfig( + publishableKey: 'pk_test_xxxxxxxxxxxxx', + ), + child: MaterialApp( + title: 'My App', + theme: ThemeData.light().copyWith( + extensions: [ + ClerkTheme( + colors: ClerkColors.light( + primary: Colors.blue, + background: Colors.white, + text: Colors.black87, + ), + ), + ], + ), + darkTheme: ThemeData.dark().copyWith( + extensions: [ + ClerkTheme( + colors: ClerkColors.dark( + primary: Colors.blueAccent, + background: Colors.grey[900]!, + text: Colors.white, + ), + ), + ], + ), + home: const HomePage(), + ), + ); + } +} +``` + +### Custom Colors + +Define a complete custom color scheme: + +```dart +ClerkTheme( + colors: ClerkColors( + primary: Color(0xFF6366F1), // Primary brand color + secondary: Color(0xFF8B5CF6), // Secondary accent + background: Color(0xFFFFFFFF), // Background color + altBackground: Color(0xFFF9FAFB), // Alternative background + text: Color(0xFF111827), // Primary text + textSecondary: Color(0xFF6B7280), // Secondary text + border: Color(0xFFE5E7EB), // Border color + error: Color(0xFFEF4444), // Error color + success: Color(0xFF10B981), // Success color + warning: Color(0xFFF59E0B), // Warning color + ), +) +``` + +### Typography Customization + +Customize text styles: + +```dart +ClerkTheme( + typography: ClerkTypography( + headlineLarge: TextStyle( + fontSize: 32, + fontWeight: FontWeight.bold, + fontFamily: 'YourCustomFont', + ), + headlineMedium: TextStyle( + fontSize: 24, + fontWeight: FontWeight.w600, + ), + bodyLarge: TextStyle( + fontSize: 16, + fontWeight: FontWeight.normal, + ), + bodyMedium: TextStyle( + fontSize: 14, + ), + labelLarge: TextStyle( + fontSize: 14, + fontWeight: FontWeight.w500, + ), + ), +) +``` + +--- + +## Customizing Individual Widgets + +### ClerkAuthentication Widget + +Customize the authentication flow: + +```dart +import 'package:clerk_flutter/clerk_flutter.dart'; + +ClerkAuthentication( + // Customize which strategies are shown + // (Note: This depends on your Clerk Dashboard configuration) + + // Add custom styling + // The widget respects your ClerkTheme configuration +) +``` + +### ClerkUserButton Widget + +Customize the user button: + +```dart +import 'package:clerk_flutter/clerk_flutter.dart'; + +ClerkUserButton( + showName: true, // Show user's name next to avatar + + // Add custom actions + sessionActions: [ + ClerkUserAction( + label: 'Custom Action', + icon: Icons.star, + onPressed: (context, authState) { + // Handle custom action + print('Custom action pressed'); + }, + ), + ], + + // Add additional menu items + additionalActions: [ + ClerkUserAction( + label: 'Settings', + icon: Icons.settings, + onPressed: (context, authState) { + Navigator.push( + context, + MaterialPageRoute(builder: (_) => SettingsPage()), + ); + }, + ), + ClerkUserAction( + label: 'Help', + icon: Icons.help, + onPressed: (context, authState) { + // Show help dialog + }, + ), + ], +) +``` + +--- + +## Building Custom Authentication UI + +### Custom Sign-In Form + +Build your own sign-in UI using the Auth API: + +```dart +import 'package:flutter/material.dart'; +import 'package:clerk_flutter/clerk_flutter.dart'; + +class CustomSignInForm extends StatefulWidget { + const CustomSignInForm({super.key}); + + @override + State createState() => _CustomSignInFormState(); +} + +class _CustomSignInFormState extends State { + final _emailController = TextEditingController(); + final _passwordController = TextEditingController(); + bool _isLoading = false; + String? _errorMessage; + + @override + void dispose() { + _emailController.dispose(); + _passwordController.dispose(); + super.dispose(); + } + + Future _signIn() async { + setState(() { + _isLoading = true; + _errorMessage = null; + }); + + final authState = ClerkAuth.of(context, listen: false); + + try { + await authState.attemptSignIn( + strategy: Strategy.password, + identifier: _emailController.text, + password: _passwordController.text, + ); + + // Success - navigation handled by ClerkAuthBuilder + } catch (e) { + setState(() { + _errorMessage = e.toString(); + }); + } finally { + setState(() { + _isLoading = false; + }); + } + } + + @override + Widget build(BuildContext context) { + return Card( + margin: const EdgeInsets.all(16), + child: Padding( + padding: const EdgeInsets.all(24), + child: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + Text( + 'Sign In', + style: Theme.of(context).textTheme.headlineMedium, + textAlign: TextAlign.center, + ), + const SizedBox(height: 24), + + // Email Field + TextField( + controller: _emailController, + decoration: const InputDecoration( + labelText: 'Email', + border: OutlineInputBorder(), + prefixIcon: Icon(Icons.email), + ), + keyboardType: TextInputType.emailAddress, + enabled: !_isLoading, + ), + const SizedBox(height: 16), + + // Password Field + TextField( + controller: _passwordController, + decoration: const InputDecoration( + labelText: 'Password', + border: OutlineInputBorder(), + prefixIcon: Icon(Icons.lock), + ), + obscureText: true, + enabled: !_isLoading, + ), + const SizedBox(height: 24), + + // Error Message + if (_errorMessage != null) + Padding( + padding: const EdgeInsets.only(bottom: 16), + child: Text( + _errorMessage!, + style: TextStyle(color: Theme.of(context).colorScheme.error), + textAlign: TextAlign.center, + ), + ), + + // Sign In Button + ElevatedButton( + onPressed: _isLoading ? null : _signIn, + child: _isLoading + ? const SizedBox( + height: 20, + width: 20, + child: CircularProgressIndicator(strokeWidth: 2), + ) + : const Text('Sign In'), + ), + ], + ), + ), + ); + } +} +``` + +### Custom OAuth Buttons + +Create custom-styled OAuth buttons: + +```dart +import 'package:flutter/material.dart'; +import 'package:clerk_flutter/clerk_flutter.dart'; + +class CustomOAuthButtons extends StatelessWidget { + const CustomOAuthButtons({super.key}); + + Widget _buildOAuthButton({ + required BuildContext context, + required String label, + required IconData icon, + required Color color, + required OAuthProvider provider, + }) { + final authState = ClerkAuth.of(context, listen: false); + + return ElevatedButton.icon( + icon: Icon(icon), + label: Text(label), + style: ElevatedButton.styleFrom( + backgroundColor: color, + foregroundColor: Colors.white, + minimumSize: const Size(double.infinity, 48), + ), + onPressed: () async { + try { + await authState.attemptSignIn( + strategy: Strategy.oauth(provider), + ); + } catch (e) { + if (context.mounted) { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar(content: Text('Sign in failed: $e')), + ); + } + } + }, + ); + } + + @override + Widget build(BuildContext context) { + return Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + _buildOAuthButton( + context: context, + label: 'Continue with Google', + icon: Icons.g_mobiledata, + color: Colors.red, + provider: OAuthProvider.google, + ), + const SizedBox(height: 12), + _buildOAuthButton( + context: context, + label: 'Continue with GitHub', + icon: Icons.code, + color: Colors.black, + provider: OAuthProvider.github, + ), + ], + ); + } +} +``` + +--- + +## Localization + +Clerk supports localization through Flutter's localization system: + +```dart +import 'package:flutter/material.dart'; +import 'package:clerk_flutter/clerk_flutter.dart'; +import 'package:flutter_localizations/flutter_localizations.dart'; + +MaterialApp( + localizationsDelegates: const [ + ClerkSdkLocalizations.delegate, + GlobalMaterialLocalizations.delegate, + GlobalWidgetsLocalizations.delegate, + GlobalCupertinoLocalizations.delegate, + ], + supportedLocales: const [ + Locale('en', ''), // English + // Add more locales as needed + ], + // ... +) +``` + +--- + +## Responsive Design + +Make your authentication UI responsive: + +```dart +import 'package:flutter/material.dart'; +import 'package:clerk_flutter/clerk_flutter.dart'; + +class ResponsiveAuthPage extends StatelessWidget { + const ResponsiveAuthPage({super.key}); + + @override + Widget build(BuildContext context) { + return Scaffold( + body: Center( + child: ConstrainedBox( + constraints: const BoxConstraints(maxWidth: 400), + child: const ClerkAuthentication(), + ), + ), + ); + } +} +``` + +--- + +## Next Steps + +- [Widget Reference]({{ '/api/widgets' | relative_url }}) - Complete widget API documentation +- [ClerkTheme API]({{ '/api/clerk-theme' | relative_url }}) - Detailed theming options +- [Examples](https://github.com/clerk/clerk-sdk-flutter/tree/main/packages/clerk_flutter/example) - See complete examples + diff --git a/docs/guides/error-handling.md b/docs/guides/error-handling.md new file mode 100644 index 00000000..06faef59 --- /dev/null +++ b/docs/guides/error-handling.md @@ -0,0 +1,481 @@ +--- +layout: default +title: Error Handling +parent: Guides +nav_order: 6 +--- + +# Error Handling +{: .no_toc } + +Learn how to handle authentication errors and provide great user experiences in your Dart and Flutter applications. +{: .fs-6 .fw-300 } + +## Table of contents +{: .no_toc .text-delta } + +1. TOC +{:toc} + +--- + +## Overview + +Clerk provides structured error handling through the `ClerkApiException` class. All authentication operations can throw this exception when errors occur. + +--- + +## ClerkApiException + +The main exception class for Clerk API errors: + +```dart +import 'package:clerk_auth/clerk_auth.dart'; + +try { + await auth.attemptSignIn( + strategy: Strategy.password, + identifier: 'user@example.com', + password: 'wrong-password', + ); +} on ClerkApiException catch (e) { + print('Error: ${e.message}'); + print('Status Code: ${e.status}'); + + // Access individual errors + for (final error in e.errors) { + print('Code: ${error.code}'); + print('Message: ${error.message}'); + print('Field: ${error.meta?.paramName}'); + } +} +``` + +**Properties:** + +| Property | Type | Description | +|----------|------|-------------| +| `message` | `String` | Human-readable error message | +| `status` | `int?` | HTTP status code | +| `errors` | `List` | Detailed error information | + +--- + +## ClerkApiError + +Individual error details: + +```dart +class ClerkApiError { + final String code; // Error code (e.g., 'form_password_incorrect') + final String message; // Error message + final String? longMessage; // Detailed explanation + final ErrorMeta? meta; // Additional metadata +} +``` + +--- + +## Common Error Codes + +### Authentication Errors + +| Code | Description | Solution | +|------|-------------|----------| +| `form_identifier_not_found` | Email/username not found | User needs to sign up | +| `form_password_incorrect` | Wrong password | Prompt user to retry or reset password | +| `form_code_incorrect` | Wrong verification code | Ask user to re-enter code | +| `verification_expired` | Verification code expired | Send new verification code | + +### Validation Errors + +| Code | Description | Solution | +|------|-------------|----------| +| `form_param_format_invalid` | Invalid format (e.g., email) | Validate input before submission | +| `form_password_pwned` | Password found in breach database | Require stronger password | +| `form_password_length_too_short` | Password too short | Show password requirements | + +### Rate Limiting + +| Code | Description | Solution | +|------|-------------|----------| +| `rate_limit_exceeded` | Too many requests | Show cooldown message | + +--- + +## Handling Errors in Dart + +### Basic Error Handling + +```dart +import 'package:clerk_auth/clerk_auth.dart'; + +Future signIn(String email, String password) async { + try { + await auth.attemptSignIn( + strategy: Strategy.password, + identifier: email, + password: password, + ); + + print('✅ Sign in successful'); + + } on ClerkApiException catch (e) { + // Handle Clerk-specific errors + print('❌ Sign in failed: ${e.message}'); + + for (final error in e.errors) { + if (error.code == 'form_password_incorrect') { + print('Wrong password. Please try again.'); + } else if (error.code == 'form_identifier_not_found') { + print('Account not found. Please sign up.'); + } + } + + } catch (e) { + // Handle other errors + print('❌ Unexpected error: $e'); + } +} +``` + +### Field-Specific Errors + +```dart +Map getFieldErrors(ClerkApiException e) { + final fieldErrors = {}; + + for (final error in e.errors) { + final fieldName = error.meta?.paramName; + if (fieldName != null) { + + +### Custom Error Display + +```dart +import 'package:flutter/material.dart'; +import 'package:clerk_flutter/clerk_flutter.dart'; + +class SignInForm extends StatefulWidget { + @override + _SignInFormState createState() => _SignInFormState(); +} + +class _SignInFormState extends State { + final _emailController = TextEditingController(); + final _passwordController = TextEditingController(); + String? _errorMessage; + Map _fieldErrors = {}; + + Future _handleSignIn() async { + setState(() { + _errorMessage = null; + _fieldErrors = {}; + }); + + try { + final authState = ClerkAuth.of(context); + + await authState.attemptSignIn( + strategy: Strategy.password, + identifier: _emailController.text, + password: _passwordController.text, + ); + + } on ClerkApiException catch (e) { + setState(() { + _errorMessage = e.message; + + // Extract field-specific errors + for (final error in e.errors) { + final field = error.meta?.paramName; + if (field != null) { + _fieldErrors[field] = error.message; + } + } + }); + } + } + + @override + Widget build(BuildContext context) { + return Column( + children: [ + if (_errorMessage != null) + Container( + padding: EdgeInsets.all(12), + color: Colors.red.shade100, + child: Text( + _errorMessage!, + style: TextStyle(color: Colors.red.shade900), + ), + ), + + TextField( + controller: _emailController, + decoration: InputDecoration( + labelText: 'Email', + errorText: _fieldErrors['identifier'], + ), + ), + + TextField( + controller: _passwordController, + obscureText: true, + decoration: InputDecoration( + labelText: 'Password', + errorText: _fieldErrors['password'], + ), + ), + + ElevatedButton( + onPressed: _handleSignIn, + child: Text('Sign In'), + ), + ], + ); + } +} +``` + +--- + +## Error Recovery Strategies + +### Password Reset Flow + +```dart +Future handlePasswordError(ClerkApiException e, String identifier) async { + for (final error in e.errors) { + if (error.code == 'form_password_incorrect') { + // Offer password reset + print('Forgot your password?'); + + try { + await auth.resetPassword(identifier: identifier); + print('Password reset email sent!'); + } catch (e) { + print('Failed to send reset email: $e'); + } + } + } +} +``` + +### Retry with Exponential Backoff + +```dart +Future signInWithRetry( + String email, + String password, { + int maxRetries = 3, +}) async { + int retries = 0; + + while (retries < maxRetries) { + try { + await auth.attemptSignIn( + strategy: Strategy.password, + identifier: email, + password: password, + ); + return; // Success + + } on ClerkApiException catch (e) { + // Check if error is retryable + final isRateLimit = e.errors.any((err) => + err.code == 'rate_limit_exceeded' + ); + + if (isRateLimit && retries < maxRetries - 1) { + retries++; + final delay = Duration(seconds: pow(2, retries).toInt()); + print('Rate limited. Retrying in ${delay.inSeconds}s...'); + await Future.delayed(delay); + } else { + rethrow; // Not retryable or max retries reached + } + } + } +} +``` + +--- + +## Logging Errors + +### Development Logging + +```dart +void logClerkError(ClerkApiException e) { + print('=== Clerk API Error ==='); + print('Message: ${e.message}'); + print('Status: ${e.status}'); + print('Errors:'); + + for (final error in e.errors) { + print(' - Code: ${error.code}'); + print(' Message: ${error.message}'); + print(' Field: ${error.meta?.paramName ?? 'N/A'}'); + } + + print('====================='); +} + +// Usage +try { + await auth.attemptSignIn(/* ... */); +} on ClerkApiException catch (e) { + logClerkError(e); +} +``` + +### Production Error Tracking + +```dart +import 'package:sentry_flutter/sentry_flutter.dart'; + +try { + await auth.attemptSignIn(/* ... */); +} on ClerkApiException catch (e, stackTrace) { + // Log to error tracking service + await Sentry.captureException( + e, + stackTrace: stackTrace, + hint: Hint.withMap({ + 'error_codes': e.errors.map((err) => err.code).toList(), + 'status': e.status, + }), + ); + + // Show user-friendly message + showErrorDialog(context, 'Sign in failed. Please try again.'); +} +``` + +--- + +## Best Practices + +### 1. Show User-Friendly Messages + +```dart +String getUserFriendlyMessage(ClerkApiException e) { + for (final error in e.errors) { + switch (error.code) { + case 'form_password_incorrect': + return 'Incorrect password. Please try again.'; + case 'form_identifier_not_found': + return 'Account not found. Please sign up.'; + case 'rate_limit_exceeded': + return 'Too many attempts. Please try again later.'; + default: + return 'Something went wrong. Please try again.'; + } + } + return e.message; +} +``` + +### 2. Validate Before Submission + +```dart +String? validateEmail(String email) { + if (email.isEmpty) return 'Email is required'; + if (!email.contains('@')) return 'Invalid email format'; + return null; +} + +String? validatePassword(String password) { + if (password.isEmpty) return 'Password is required'; + if (password.length < 8) return 'Password must be at least 8 characters'; + return null; +} +``` + +### 3. Provide Clear Next Steps + +```dart +void handleSignInError(ClerkApiException e, BuildContext context) { + for (final error in e.errors) { + if (error.code == 'form_identifier_not_found') { + showDialog( + context: context, + builder: (context) => AlertDialog( + title: Text('Account Not Found'), + content: Text('Would you like to create an account?'), + actions: [ + TextButton( + onPressed: () => Navigator.pop(context), + child: Text('Cancel'), + ), + ElevatedButton( + onPressed: () { + Navigator.pop(context); + Navigator.pushNamed(context, '/sign-up'); + }, + child: Text('Sign Up'), + ), + ], + ), + ); + } + } +} +``` + +--- + +## Next Steps + +- [Authentication]({{ '/guides/authentication' | relative_url }}) - Implement authentication flows +- [User Management]({{ '/guides/user-management' | relative_url }}) - Handle user-related errors +- [Session Tokens]({{ '/guides/session-tokens' | relative_url }}) - Handle token errors +- [Backend Integration]({{ '/guides/backend-integration' | relative_url }}) - Server-side error handling + + fieldErrors[fieldName] = error.message; + } + } + + return fieldErrors; +} + +// Usage +try { + await auth.attemptSignUp(/* ... */); +} on ClerkApiException catch (e) { + final errors = getFieldErrors(e); + + if (errors.containsKey('email_address')) { + print('Email error: ${errors['email_address']}'); + } + if (errors.containsKey('password')) { + print('Password error: ${errors['password']}'); + } +} +``` + +--- + +## Handling Errors in Flutter + +### Using ClerkErrorListener + +The `ClerkErrorListener` widget automatically displays errors: + +```dart +import 'package:clerk_flutter/clerk_flutter.dart'; + +class MyApp extends StatelessWidget { + @override + Widget build(BuildContext context) { + return Scaffold( + body: ClerkErrorListener( + child: YourWidget(), + ), + ); + } +} +``` + + diff --git a/docs/guides/organizations.md b/docs/guides/organizations.md new file mode 100644 index 00000000..229fb766 --- /dev/null +++ b/docs/guides/organizations.md @@ -0,0 +1,411 @@ +--- +layout: default +title: Organizations +parent: Guides +nav_order: 3 +--- + +# Organizations +{: .no_toc } + +Learn how to implement multi-tenant functionality with organizations in your Dart and Flutter applications. +{: .fs-6 .fw-300 } + +## Table of contents +{: .no_toc .text-delta } + +1. TOC +{:toc} + +--- + +## Overview + +Organizations in Clerk enable you to build multi-tenant applications where users can belong to one or more organizations. Each organization can have: + +- Multiple members with different roles +- Custom permissions and access control +- Organization-specific metadata +- Invitations and membership management + +--- + +## Prerequisites + +Before using organizations, you need to: + +1. **Enable Organizations** in your [Clerk Dashboard](https://dashboard.clerk.com) + - Go to your application settings + - Navigate to **Organizations** + - Enable the Organizations feature + +2. **Configure Roles** (optional) + - Define custom roles for your organization members + - Set up permissions for each role + +--- + +## Accessing Organizations + +### In Dart (clerk_auth) + +```dart +import 'package:clerk_auth/clerk_auth.dart'; + +void listUserOrganizations(Auth auth) { + final user = auth.user; + + if (user == null) { + print('No user signed in'); + return; + } + + // Get all organizations the user belongs to + final memberships = user.organizationMemberships; + + for (final membership in memberships) { + print('Organization: ${membership.organization.name}'); + print('Role: ${membership.role}'); + } +} +``` + +### In Flutter (clerk_flutter) + +```dart +import 'package:flutter/material.dart'; +import 'package:clerk_flutter/clerk_flutter.dart'; + +class OrganizationsPage extends StatelessWidget { + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar(title: const Text('My Organizations')), + body: const ClerkOrganizationList(), + ); + } +} +``` + +--- + +## Organization Object + +The `Organization` object contains information about an organization: + +```dart +final organization = membership.organization; + +print('ID: ${organization.id}'); +print('Name: ${organization.name}'); +print('Slug: ${organization.slug}'); +print('Logo: ${organization.imageUrl}'); +print('Created: ${organization.createdAt}'); +``` + +**Key Properties:** + +| Property | Type | Description | +|----------|------|-------------| +| `id` | `String` | Unique organization identifier | +| `name` | `String` | Organization name | +| `slug` | `String` | URL-friendly identifier | +| `imageUrl` | `String?` | Organization logo URL | +| `createdAt` | `DateTime` | Creation timestamp | +| `publicMetadata` | `Map` | Public metadata | + +--- + +## Organization Membership + +The `OrganizationMembership` object represents a user's membership in an organization: + +```dart +final membership = user.organizationMemberships.first; + +print('Organization: ${membership.organization.name}'); +print('Role: ${membership.role}'); +print('Joined: ${membership.createdAt}'); +``` + +**Key Properties:** + +| Property | Type | Description | +|----------|------|-------------| +| `id` | `String` | Membership identifier | +| `organization` | `Organization` | The organization | +| `role` | `String` | User's role in the organization | +| `createdAt` | `DateTime` | When user joined | +| `publicMetadata` | `Map` | Membership metadata | + +--- + +## Common Organization Roles + +Clerk provides default roles for organizations: + +- **`admin`** - Full access to organization settings and members +- **`member`** - Standard member access + +You can also define custom roles in your Clerk Dashboard with specific permissions. + + +--- + +## Switching Active Organization + +Users can belong to multiple organizations. You can switch the active organization context: + +### In Dart (clerk_auth) + +```dart +import 'package:clerk_auth/clerk_auth.dart'; + +Future switchOrganization(Auth auth, String organizationId) async { + try { + // Set the active organization + await auth.setActiveOrganization(organizationId); + + print('Switched to organization: $organizationId'); + } catch (e) { + print('Failed to switch organization: $e'); + } +} +``` + +### In Flutter (clerk_flutter) + +The `ClerkOrganizationList` widget handles organization switching automatically. When a user selects an organization, it becomes the active organization. + +--- + +## Checking Organization Membership + +You can check if a user belongs to a specific organization: + +```dart +bool isUserInOrganization(User user, String organizationId) { + return user.organizationMemberships.any( + (membership) => membership.organization.id == organizationId, + ); +} +``` + +Check if user has a specific role: + +```dart +bool isUserAdmin(User user, String organizationId) { + final membership = user.organizationMemberships.firstWhere( + (m) => m.organization.id == organizationId, + orElse: () => null, + ); + + return membership?.role == 'admin'; +} +``` + +--- + +## Organization Invitations + +Organizations can invite new members via email. + +{: .note } +> Organization invitations are managed through the Clerk Dashboard or Backend API. The Dart/Flutter SDKs currently support viewing and accepting invitations. + +### Viewing Pending Invitations + +```dart +import 'package:clerk_auth/clerk_auth.dart'; + +void listPendingInvitations(User user) { + // Check for pending organization invitations + final invitations = user.organizationInvitations ?? []; + + for (final invitation in invitations) { + print('Invited to: ${invitation.organization.name}'); + print('Role: ${invitation.role}'); + print('Invited by: ${invitation.inviterEmail}'); + } +} +``` + +--- + +## Multi-Tenant Data Isolation + +When building multi-tenant applications, you'll want to scope data to the active organization: + +```dart +import 'package:clerk_auth/clerk_auth.dart'; + +class ApiService { + final Auth auth; + + ApiService(this.auth); + + Future fetchOrganizationData() async { + final user = auth.user; + if (user == null) return; + + // Get the active organization + final activeOrgId = user.activeOrganizationId; + + if (activeOrgId == null) { + print('No active organization'); + return; + } + + // Fetch data scoped to this organization + final token = await auth.sessionTokenStream.first; + + // Make API request with organization context + // The session token will include organization claims + final response = await http.get( + Uri.parse('https://api.example.com/org/$activeOrgId/data'), + headers: { + 'Authorization': 'Bearer $token', + }, + ); + } +} +``` + +--- + +## Session Tokens with Organization Claims + +When a user has an active organization, the session token includes organization-specific claims: + +```dart +import 'dart:convert'; +import 'package:clerk_auth/clerk_auth.dart'; + +void printOrganizationClaims(Auth auth) { + auth.sessionTokenStream.listen((token) { + if (token == null) return; + + // Decode JWT (for demonstration - use a proper JWT library) + final parts = token.split('.'); + if (parts.length != 3) return; + + final payload = json.decode( + utf8.decode(base64Url.decode(base64Url.normalize(parts[1]))), + ); + + print('Organization ID: ${payload['org_id']}'); + print('Organization Role: ${payload['org_role']}'); + print('Organization Permissions: ${payload['org_permissions']}'); + }); +} +``` + +--- + +## Best Practices + +### 1. Always Check Active Organization + +```dart +Future performOrganizationAction(Auth auth) async { + final user = auth.user; + + if (user?.activeOrganizationId == null) { + throw Exception('No active organization selected'); + } + + // Proceed with organization-scoped action +} +``` + +### 2. Handle Organization Switching + +```dart +class OrganizationAwareWidget extends StatefulWidget { + @override + _OrganizationAwareWidgetState createState() => + _OrganizationAwareWidgetState(); +} + +class _OrganizationAwareWidgetState extends State { + String? _currentOrgId; + + @override + void initState() { + super.initState(); + _loadOrganizationData(); + } + + void _loadOrganizationData() { + final authState = ClerkAuth.of(context); + final newOrgId = authState.user?.activeOrganizationId; + + if (newOrgId != _currentOrgId) { + setState(() { + _currentOrgId = newOrgId; + }); + // Reload data for new organization + } + } + + @override + Widget build(BuildContext context) { + return Container( + child: Text('Current Org: $_currentOrgId'), + ); + } +} +``` + +### 3. Validate Permissions + +Always validate organization permissions on your backend, not just in the client: + +```dart +// Client-side check (for UI only) +bool canEditSettings = user.organizationMemberships + .firstWhere((m) => m.organization.id == activeOrgId) + .role == 'admin'; + +// Always verify on backend before performing sensitive operations +``` + +--- + +## Next Steps + +- [Authentication]({{ '/guides/authentication' | relative_url }}) - Implement authentication flows +- [User Management]({{ '/guides/user-management' | relative_url }}) - Manage user profiles +- [Session Tokens]({{ '/guides/session-tokens' | relative_url }}) - Work with JWT tokens +- [Backend Integration]({{ '/guides/backend-integration' | relative_url }}) - Verify organization claims on your backend + + +--- + +## Using ClerkOrganizationList Widget + +The `ClerkOrganizationList` widget provides a pre-built UI for managing organizations: + +```dart +import 'package:flutter/material.dart'; +import 'package:clerk_flutter/clerk_flutter.dart'; + +class MyApp extends StatelessWidget { + @override + Widget build(BuildContext context) { + return MaterialApp( + home: Scaffold( + appBar: AppBar(title: const Text('Organizations')), + body: const ClerkOrganizationList(), + ), + ); + } +} +``` + +Features: +- List all organizations the user belongs to +- Switch between organizations +- Create new organizations (if enabled) +- Leave organizations + diff --git a/docs/guides/session-tokens.md b/docs/guides/session-tokens.md new file mode 100644 index 00000000..59ed0109 --- /dev/null +++ b/docs/guides/session-tokens.md @@ -0,0 +1,331 @@ +--- +layout: default +title: Session Tokens +parent: Guides +nav_order: 5 +--- + +# Session Tokens +{: .no_toc } + +Learn how to work with JWT session tokens for authenticated API requests in your Dart and Flutter applications. +{: .fs-6 .fw-300 } + +## Table of contents +{: .no_toc .text-delta } + +1. TOC +{:toc} + +--- + +## Overview + +Clerk uses JSON Web Tokens (JWTs) for session management. These tokens: + +- Are automatically generated when a user signs in +- Contain user and session information +- Are cryptographically signed by Clerk +- Can be verified on your backend +- Automatically refresh to stay valid + +--- + +## Accessing Session Tokens + +### In Dart (clerk_auth) + +The `clerk_auth` package provides a stream of session tokens: + +```dart +import 'package:clerk_auth/clerk_auth.dart'; + +final auth = Auth( + config: AuthConfig( + publishableKey: 'pk_test_xxxxxxxxxxxxx', + persistor: DefaultPersistor( + getCacheDirectory: () => Directory.current, + ), + ), +); + +await auth.initialize(); + +// Listen to session token updates +auth.sessionTokenStream.listen((token) { + if (token != null) { + print('Session token: $token'); + // Use for authenticated API requests + } else { + print('No active session'); + } +}); +``` + +### In Flutter (clerk_flutter) + +```dart +import 'package:clerk_flutter/clerk_flutter.dart'; + +class MyWidget extends StatelessWidget { + @override + Widget build(BuildContext context) { + final authState = ClerkAuth.of(context); + + // Access the session token stream + return StreamBuilder( + stream: authState.sessionTokenStream, + builder: (context, snapshot) { + final token = snapshot.data; + + if (token == null) { + return Text('No session token'); + } + + return Text('Token available'); + }, + ); + } +} +``` + +--- + +## Making Authenticated API Requests + +Use the session token to authenticate requests to your backend: + +```dart +import 'package:http/http.dart' as http; +import 'package:clerk_auth/clerk_auth.dart'; + +class ApiClient { + final Auth auth; + + ApiClient(this.auth); + + Future fetchUserData() async { + // Get the current session token + final token = await auth.sessionTokenStream.first; + + if (token == null) { + throw Exception('No active session'); + } + + // Make authenticated request + final response = await http.get( + Uri.parse('https://api.example.com/user/profile'), + headers: { + 'Authorization': 'Bearer $token', + 'Content-Type': 'application/json', + }, + ); + + return response; + } +} +``` + +--- + +## Session Token Polling + +By default, `clerk_auth` automatically polls for fresh session tokens to ensure they remain valid. + +### Enable/Disable Polling + +```dart +final auth = Auth( + config: AuthConfig( + publishableKey: 'pk_test_xxxxxxxxxxxxx', + sessionTokenPolling: true, // Enable (default) + persistor: DefaultPersistor( + getCacheDirectory: () => Directory.current, + ), + ), +); +``` + +### Why Polling? + + + +--- + +## Token Claims + +Common claims in a Clerk session token: + +| Claim | Description | +|-------|-------------| +| `sub` | User ID (subject) | +| `sid` | Session ID | +| `iat` | Issued at timestamp | +| `exp` | Expiration timestamp | +| `azp` | Authorized party (your app URL) | +| `org_id` | Organization ID (if user has active org) | +| `org_role` | User's role in the organization | +| `org_permissions` | Array of permissions | + +### Decoding Token Claims (Client-Side) + +{: .warning } +> **Security Note:** Never trust client-side token decoding for authorization. Always verify tokens on your backend. + +```dart +import 'dart:convert'; + +Map decodeJwtPayload(String token) { + final parts = token.split('.'); + if (parts.length != 3) { + throw Exception('Invalid JWT'); + } + + // Decode the payload (second part) + final payload = parts[1]; + final normalized = base64Url.normalize(payload); + final decoded = utf8.decode(base64Url.decode(normalized)); + + return json.decode(decoded); +} + +// Usage +final token = await auth.sessionTokenStream.first; +if (token != null) { + final claims = decodeJwtPayload(token); + print('User ID: ${claims['sub']}'); + print('Expires: ${DateTime.fromMillisecondsSinceEpoch(claims['exp'] * 1000)}'); +} +``` + +--- + +## Verifying Tokens on Backend + +Always verify session tokens on your backend before trusting them. See the [Backend Integration](/guides/backend-integration) guide for details. + +**Quick Example (Node.js):** + +```javascript +import { verifyToken } from '@clerk/backend'; + +app.get('/api/protected', async (req, res) => { + const token = req.headers.authorization?.replace('Bearer ', ''); + + try { + const payload = await verifyToken(token, { + secretKey: process.env.CLERK_SECRET_KEY + }); + + res.json({ userId: payload.sub }); + } catch (err) { + res.status(401).json({ error: 'Unauthorized' }); + } +}); +``` + +--- + +## Token Expiration + +Session tokens expire after a period of time (default: 1 hour). Clerk automatically handles token refresh through polling. + +### Handling Expired Tokens + +```dart +import 'package:clerk_auth/clerk_auth.dart'; + +Future getValidToken(Auth auth) async { + final token = await auth.sessionTokenStream.first; + + if (token == null) { + throw Exception('No active session'); + } + + // Token is automatically refreshed by Clerk + // Just use the latest token from the stream + return token; +} +``` + +--- + +## Best Practices + +### 1. Always Use HTTPS + +Session tokens should only be transmitted over HTTPS to prevent interception. + +### 2. Store Tokens Securely + +The `clerk_auth` package handles token storage securely. Don't store tokens in: +- Local storage (web) +- Shared preferences without encryption +- Plain text files + +### 3. Verify on Backend + +Never trust client-side token validation. Always verify tokens on your backend: + +```dart +// ❌ DON'T: Trust client-side checks +if (decodeJwtPayload(token)['exp'] > DateTime.now()) { + // Allow access +} + +// ✅ DO: Verify on backend +final response = await http.get( + Uri.parse('https://api.example.com/protected'), + headers: {'Authorization': 'Bearer $token'}, +); +``` + +### 4. Handle Token Refresh + +Let Clerk handle token refresh automatically: + +```dart +// ✅ DO: Use the stream +auth.sessionTokenStream.listen((token) { + // Always get the latest valid token + if (token != null) { + makeApiRequest(token); + } +}); + +// ❌ DON'T: Cache tokens manually +String? cachedToken; +final token = await auth.sessionTokenStream.first; +cachedToken = token; // Token might expire! +``` + +--- + +## Next Steps + +- [Backend Integration]({{ '/guides/backend-integration' | relative_url }}) - Verify tokens on your backend +- [User Management]({{ '/guides/user-management' | relative_url }}) - Access user data from tokens +- [Organizations]({{ '/guides/organizations' | relative_url }}) - Work with organization claims +- [Error Handling]({{ '/guides/error-handling' | relative_url }}) - Handle token-related errors + +- Session tokens expire after a period of time +- Polling ensures you always have a valid token +- Clerk automatically refreshes tokens before they expire +- No manual token refresh logic needed + +--- + +## Token Structure + +A Clerk session token is a JWT with three parts: + +``` +eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJ1c2VyXzEyMyIsImlhdCI6MTYxNjIzOTAyMn0.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c +``` + +**Parts:** +1. **Header** - Algorithm and token type +2. **Payload** - Claims (user data, session info) +3. **Signature** - Cryptographic signature + + diff --git a/docs/guides/user-management.md b/docs/guides/user-management.md new file mode 100644 index 00000000..cf2b7d37 --- /dev/null +++ b/docs/guides/user-management.md @@ -0,0 +1,456 @@ +--- +layout: default +title: User Management +parent: Guides +nav_order: 2 +--- + +# User Management +{: .no_toc } + +Learn how to manage user profiles, update user information, and handle user data with Clerk's Dart and Flutter SDKs. +{: .fs-6 .fw-300 } + +## Table of contents +{: .no_toc .text-delta } + +1. TOC +{:toc} + +--- + +## Overview + +The `User` object contains all account information for a user in your application. Clerk provides comprehensive APIs to: + +- Access user profile data +- Update user information +- Manage email addresses and phone numbers +- Upload profile images +- Handle user metadata + +--- + +## Accessing the Current User + +### In Dart (clerk_auth) + +```dart +import 'package:clerk_auth/clerk_auth.dart'; + +void printUserInfo(Auth auth) { + final user = auth.user; + + if (user == null) { + print('No user signed in'); + return; + } + + print('User ID: ${user.id}'); + print('Email: ${user.emailAddress}'); + print('Name: ${user.firstName} ${user.lastName}'); + print('Username: ${user.username}'); + print('Profile Image: ${user.imageUrl}'); + print('Created: ${user.createdAt}'); + print('Updated: ${user.updatedAt}'); +} +``` + +### In Flutter (clerk_flutter) + +```dart +import 'package:flutter/material.dart'; +import 'package:clerk_flutter/clerk_flutter.dart'; + +class UserProfileWidget extends StatelessWidget { + const UserProfileWidget({super.key}); + + @override + Widget build(BuildContext context) { + final authState = ClerkAuth.of(context); + final user = authState.user; + + if (user == null) { + return const Text('Not signed in'); + } + + return Column( + children: [ + if (user.imageUrl != null) + CircleAvatar( + radius: 50, + backgroundImage: NetworkImage(user.imageUrl!), + ), + Text('${user.firstName} ${user.lastName}'), + Text(user.emailAddress ?? 'No email'), + Text('User ID: ${user.id}'), + ], + ); + } +} +``` + +--- + +## User Properties + +The `User` object includes the following properties: + +| Property | Type | Description | +|----------|------|-------------| +| `id` | `String` | Unique identifier for the user | +| `firstName` | `String?` | User's first name | +| `lastName` | `String?` | User's last name | +| `username` | `String?` | User's username | +| `emailAddress` | `String?` | Primary email address | +| `phoneNumber` | `String?` | Primary phone number | +| `imageUrl` | `String?` | URL to profile image | +| `hasImage` | `bool` | Whether user has uploaded an image | +| `primaryEmailAddressId` | `String?` | ID of primary email | +| `primaryPhoneNumberId` | `String?` | ID of primary phone | +| `emailAddresses` | `List` | All email addresses | +| `phoneNumbers` | `List` | All phone numbers | +| `externalAccounts` | `List` | Connected OAuth accounts | +| `publicMetadata` | `Map` | Public metadata | +| `privateMetadata` | `Map` | Private metadata (backend only) | +| `unsafeMetadata` | `Map` | Unsafe metadata | +| `createdAt` | `DateTime` | Account creation timestamp | +| `updatedAt` | `DateTime` | Last update timestamp | + +--- + +## Updating User Profile + +### Update Basic Information + +```dart +import 'package:clerk_auth/clerk_auth.dart'; + +Future updateUserProfile(Auth auth) async { + final user = auth.user; + if (user == null) return; + + try { + await user.update( + firstName: 'John', + lastName: 'Doe', + username: 'johndoe', + ); + + print('✅ Profile updated successfully'); + } catch (e) { + print('❌ Update failed: $e'); + } +} +``` + +### Update Profile Image + +```dart +import 'dart:io'; +import 'package:clerk_auth/clerk_auth.dart'; + +Future updateProfileImage(Auth auth, File imageFile) async { + final user = auth.user; + if (user == null) return; + + try { + await user.setProfileImage(imageFile); + print('✅ Profile image updated'); + } catch (e) { + print('❌ Image upload failed: $e'); + } +} +``` + +### In Flutter with Image Picker + +```dart +import 'package:flutter/material.dart'; +import 'package:clerk_flutter/clerk_flutter.dart'; +import 'package:image_picker/image_picker.dart'; +import 'dart:io'; + +class ProfileImageUploader extends StatelessWidget { + const ProfileImageUploader({super.key}); + + Future _pickAndUploadImage(BuildContext context) async { + final authState = ClerkAuth.of(context, listen: false); + final user = authState.user; + if (user == null) return; + + final picker = ImagePicker(); + final pickedFile = await picker.pickImage(source: ImageSource.gallery); + + if (pickedFile != null) { + try { + await user.setProfileImage(File(pickedFile.path)); + if (context.mounted) { + ScaffoldMessenger.of(context).showSnackBar( + const SnackBar(content: Text('Profile image updated!')), + ); + } + } catch (e) { + if (context.mounted) { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar(content: Text('Upload failed: $e')), + ); + } + } + } + } + + @override + Widget build(BuildContext context) { + return ElevatedButton.icon( + icon: const Icon(Icons.upload), + label: const Text('Upload Profile Image'), + onPressed: () => _pickAndUploadImage(context), + ); + } +} +``` + +--- + +## Managing Email Addresses + +### Add Email Address + +```dart +Future addEmailAddress(Auth auth, String email) async { + final user = auth.user; + if (user == null) return; + + try { + final emailAddress = await user.createEmailAddress(email); + + print('📧 Verification email sent to $email'); + + // User enters verification code + final code = '123456'; // Get from user input + + await emailAddress.attemptVerification(code: code); + + print('✅ Email address verified and added'); + } catch (e) { + print('❌ Failed to add email: $e'); + } +} +``` + +### Set Primary Email + +```dart +Future setPrimaryEmail(Auth auth, String emailAddressId) async { + final user = auth.user; + if (user == null) return; + + try { + await user.update(primaryEmailAddressId: emailAddressId); + print('✅ Primary email updated'); + } catch (e) { + print('❌ Failed to update primary email: $e'); + } +} +``` + +### Remove Email Address + +```dart +Future removeEmailAddress(Auth auth, String emailAddressId) async { + final user = auth.user; + if (user == null) return; + + try { + final emailAddress = user.emailAddresses + .firstWhere((e) => e.id == emailAddressId); + + await emailAddress.destroy(); + print('✅ Email address removed'); + } catch (e) { + print('❌ Failed to remove email: $e'); + } +} +``` + +--- + +## Managing Phone Numbers + +### Add Phone Number + +```dart +Future addPhoneNumber(Auth auth, String phoneNumber) async { + final user = auth.user; + if (user == null) return; + + try { + final phone = await user.createPhoneNumber(phoneNumber); + + print('📱 SMS verification sent to $phoneNumber'); + + // User enters verification code + final code = '123456'; // Get from user input + + await phone.attemptVerification(code: code); + + print('✅ Phone number verified and added'); + } catch (e) { + print('❌ Failed to add phone: $e'); + } +} +``` + +--- + +## User Metadata + +Clerk provides three types of metadata: + +### Public Metadata +Visible to the frontend and included in session tokens. + +```dart +Future updatePublicMetadata(Auth auth) async { + final user = auth.user; + if (user == null) return; + + await user.update( + publicMetadata: { + 'theme': 'dark', + 'language': 'en', + 'notifications': true, + }, + ); +} +``` + +### Unsafe Metadata +Editable by the user, visible to the frontend. + +```dart +Future updateUnsafeMetadata(Auth auth) async { + final user = auth.user; + if (user == null) return; + + await user.update( + unsafeMetadata: { + 'preferences': { + 'newsletter': true, + 'marketing': false, + }, + }, + ); +} +``` + +### Private Metadata +Only accessible from the backend (requires Backend API). + +{: .note } +> Private metadata can only be set using the Backend API, not from client SDKs. + +--- + +## User Profile Component (Flutter) + +Here's a complete user profile component: + +```dart +import 'package:flutter/material.dart'; +import 'package:clerk_flutter/clerk_flutter.dart'; + +class UserProfilePage extends StatelessWidget { + const UserProfilePage({super.key}); + + @override + Widget build(BuildContext context) { + final authState = ClerkAuth.of(context); + final user = authState.user; + + if (user == null) { + return const Scaffold( + body: Center(child: Text('Not signed in')), + ); + } + + return Scaffold( + appBar: AppBar( + title: const Text('Profile'), + actions: const [ClerkUserButton()], + ), + body: ListView( + padding: const EdgeInsets.all(16), + children: [ + // Profile Image + Center( + child: CircleAvatar( + radius: 60, + backgroundImage: user.imageUrl != null + ? NetworkImage(user.imageUrl!) + : null, + child: user.imageUrl == null + ? Text( + user.firstName?.substring(0, 1).toUpperCase() ?? 'U', + style: const TextStyle(fontSize: 40), + ) + : null, + ), + ), + const SizedBox(height: 24), + + // Name + ListTile( + leading: const Icon(Icons.person), + title: const Text('Name'), + subtitle: Text('${user.firstName ?? ''} ${user.lastName ?? ''}'), + ), + + // Email + ListTile( + leading: const Icon(Icons.email), + title: const Text('Email'), + subtitle: Text(user.emailAddress ?? 'No email'), + ), + + // Username + if (user.username != null) + ListTile( + leading: const Icon(Icons.alternate_email), + title: const Text('Username'), + subtitle: Text(user.username!), + ), + + // Phone + if (user.phoneNumber != null) + ListTile( + leading: const Icon(Icons.phone), + title: const Text('Phone'), + subtitle: Text(user.phoneNumber!), + ), + + // Account Created + ListTile( + leading: const Icon(Icons.calendar_today), + title: const Text('Member Since'), + subtitle: Text( + '${user.createdAt.year}-${user.createdAt.month}-${user.createdAt.day}', + ), + ), + ], + ), + ); + } +} +``` + +--- + +## Next Steps + +- [Organizations]({{ '/guides/organizations' | relative_url }}) - Multi-tenant user management +- [Session Tokens]({{ '/guides/session-tokens' | relative_url }}) - Access user data in tokens +- [Customization]({{ '/guides/customization' | relative_url }}) - Customize user profile UI +- [Backend Integration]({{ '/guides/backend-integration' | relative_url }}) - Manage users from your backend + diff --git a/docs/index.md b/docs/index.md new file mode 100644 index 00000000..362e1ed4 --- /dev/null +++ b/docs/index.md @@ -0,0 +1,96 @@ +--- +layout: default +title: Home +nav_order: 1 +description: "Official documentation for Clerk's Dart and Flutter SDKs - Add complete user management to your Dart and Flutter applications." +permalink: / +--- + +# Clerk Dart & Flutter SDK Documentation +{: .fs-9 } + +Add complete user management to your Dart and Flutter applications in minutes. +{: .fs-6 .fw-300 } + +[Get Started]({{ '/getting-started' | relative_url }}){: .btn .btn-primary .fs-5 .mb-4 .mb-md-0 .mr-2 } +[View on GitHub](https://github.com/clerk/clerk-sdk-flutter){: .btn .fs-5 .mb-4 .mb-md-0 } + +--- + +## Beta Notice + +{: .warning } +> ⚠️ **The Clerk Dart and Flutter SDKs are in Beta** +> +> Breaking changes should be expected until the first stable release (1.0.0). We recommend pinning to specific versions and reviewing changelogs before upgrading. + +--- + +## Overview + +Clerk provides streamlined user experiences for your users to sign up, sign in, and manage their profiles from your Dart and Flutter applications. Our SDKs offer: + +- 🔐 **Complete Authentication** - Email, phone, OAuth, passwordless, and multi-factor authentication +- 👤 **User Management** - Pre-built UI components and headless APIs for user profiles +- 🏢 **Organizations** - Multi-tenant support with role-based access control +- 🎨 **Customizable UI** - Beautiful, themeable components that match your brand +- 📱 **Cross-Platform** - Works on iOS, Android, Web, Windows, macOS, and Linux +- 🚀 **Production Ready** - Battle-tested infrastructure handling millions of users + +--- + +## Quick Links + +### 🚀 Getting Started + +- [Installation & Setup]({{ '/getting-started' | relative_url }}) +- [Dart Quickstart]({{ '/getting-started/quickstart-dart' | relative_url }}) +- [Flutter Quickstart]({{ '/getting-started/quickstart-flutter' | relative_url }}) + +### 📚 Guides + +- [Authentication]({{ '/guides/authentication' | relative_url }}) - Authentication flows, OAuth, passwordless, MFA +- [User Management]({{ '/guides/user-management' | relative_url }}) - Managing user profiles and data +- [Organizations]({{ '/guides/organizations' | relative_url }}) - Multi-tenant functionality +- [Customization]({{ '/guides/customization' | relative_url }}) - Theming and styling +- [Session Tokens]({{ '/guides/session-tokens' | relative_url }}) - Working with JWTs +- [Error Handling]({{ '/guides/error-handling' | relative_url }}) - Handling errors gracefully +- [Backend Integration]({{ '/guides/backend-integration' | relative_url }}) - Server-side verification + +### 📦 Packages + +- [clerk_auth]({{ '/packages/clerk-auth' | relative_url }}) - Dart SDK for backend and CLI applications +- [clerk_flutter]({{ '/packages/clerk-flutter' | relative_url }}) - Flutter SDK with pre-built UI components + +### 🔧 API Reference + +- [Widget Reference]({{ '/api/widgets' | relative_url }}) - Complete Flutter widget documentation +- [clerk_auth API](https://pub.dev/documentation/clerk_auth/latest/) - Dart API documentation +- [clerk_flutter API](https://pub.dev/documentation/clerk_flutter/latest/) - Flutter API documentation + +--- + +## Requirements + +### Dart SDK (clerk_auth) +- Dart >= 3.6.2 + +### Flutter SDK (clerk_flutter) +- Flutter >= 3.27.4 +- Dart >= 3.6.2 + +--- + +## Community & Support + +- 💬 [Join our Discord](https://clerk.com/discord) - Chat with the community and Clerk team +- 📖 [Main Clerk Documentation](https://clerk.com/docs) - Comprehensive guides and references +- 🐛 [Report Issues](https://github.com/clerk/clerk-sdk-flutter/issues) - Bug reports and feature requests +- 🐦 [Follow us on Twitter](https://twitter.com/ClerkDev) - Latest updates and announcements + +--- + +## License + +These SDKs are licensed under the MIT license. See the [LICENSE](https://github.com/clerk/clerk-sdk-flutter/blob/main/LICENSE) file for details. + diff --git a/docs/packages/_index.md b/docs/packages/_index.md new file mode 100644 index 00000000..2c63f99e --- /dev/null +++ b/docs/packages/_index.md @@ -0,0 +1,64 @@ +--- +layout: default +title: Packages +nav_order: 4 +has_children: true +permalink: /packages +--- + +# Packages + +Official Clerk packages for Dart and Flutter development. + +## Available Packages + +### [clerk_auth]({{ '/packages/clerk-auth' | relative_url }}) +Pure Dart SDK for backend applications, CLI tools, and server-side Dart applications. + +**Features:** +- Complete authentication API +- Session management +- User management +- Organization support +- No UI dependencies + +**Install:** +```yaml +dependencies: + clerk_auth: ^0.0.13-beta +``` + +--- + +### [clerk_flutter]({{ '/packages/clerk-flutter' | relative_url }}) +Flutter SDK with pre-built UI components for mobile, web, and desktop applications. + +**Features:** +- Pre-built authentication widgets +- Material Design integration +- Customizable themes +- Cross-platform support +- Includes clerk_auth + +**Install:** +```yaml +dependencies: + clerk_flutter: ^0.0.13-beta +``` + +--- + +## Quick Links + +- [Getting Started]({{ '/getting-started' | relative_url }}) - Installation and setup guides +- [Guides]({{ '/guides' | relative_url }}) - Implementation guides +- [API Reference]({{ '/api' | relative_url }}) - Detailed API documentation + +--- + +## External Resources + +- [clerk_auth on pub.dev](https://pub.dev/packages/clerk_auth) +- [clerk_flutter on pub.dev](https://pub.dev/packages/clerk_flutter) +- [GitHub Repository](https://github.com/clerk/clerk-sdk-flutter) + diff --git a/docs/packages/clerk-auth.md b/docs/packages/clerk-auth.md new file mode 100644 index 00000000..4c5a327a --- /dev/null +++ b/docs/packages/clerk-auth.md @@ -0,0 +1,354 @@ +--- +layout: default +title: clerk_auth +parent: Packages +nav_order: 1 +--- + +# clerk_auth Package +{: .no_toc } + +The official Clerk Dart SDK for backend and CLI applications. +{: .fs-6 .fw-300 } + +## Table of contents +{: .no_toc .text-delta } + +1. TOC +{:toc} + +--- + +## Overview + +The `clerk_auth` package is a pure Dart SDK that provides authentication and user management capabilities for: + +- Backend Dart applications +- CLI tools +- Server-side applications +- Any Dart environment + +**Package:** [clerk_auth on pub.dev](https://pub.dev/packages/clerk_auth) +**Version:** 0.0.13-beta +**Repository:** [GitHub](https://github.com/clerk/clerk-sdk-flutter/tree/main/packages/clerk_auth) + +--- + +## Installation + +Add to your `pubspec.yaml`: + +```yaml +dependencies: + clerk_auth: ^0.0.13-beta +``` + +Then run: + +```bash +dart pub get +``` + +--- + +## Requirements + +- Dart >= 3.6.2 + +--- + +## Core Classes + +### Auth + +The main class for managing authentication state. + +```dart +import 'package:clerk_auth/clerk_auth.dart'; + +final auth = Auth( + config: AuthConfig( + publishableKey: 'pk_test_xxxxxxxxxxxxx', + persistor: DefaultPersistor( + getCacheDirectory: () => Directory.current, + ), + ), +); + +await auth.initialize(); +``` + +**Key Methods:** + +| Method | Description | +|--------|-------------| +| `initialize()` | Initialize the auth system | +| `attemptSignIn()` | Start a sign-in attempt | +| `attemptSignUp()` | Start a sign-up attempt | +| `attemptSignInVerification()` | Verify a sign-in attempt | +| `attemptSignUpVerification()` | Verify a sign-up attempt | +| `signOut()` | Sign out the current user | +| `signOutOf(Session)` | Sign out of a specific session | +| `resetPassword()` | Request a password reset | +| `terminate()` | Clean up resources | + +**Key Properties:** + +| Property | Type | Description | +|----------|------|-------------| +| `user` | `User?` | Current signed-in user | +| `client` | `Client` | Client object with sessions | +| `env` | `Environment` | Environment configuration | +| `sessionTokenStream` | `Stream` | Stream of session tokens | + +--- + +### AuthConfig + +Configuration for the Auth instance. + +```dart +AuthConfig( + publishableKey: 'pk_test_xxxxxxxxxxxxx', + persistor: DefaultPersistor( + getCacheDirectory: () => Directory.current, + ), + sessionTokenPolling: true, // Enable automatic token refresh + httpService: null, // Optional custom HTTP service +) +``` + +**Properties:** + +| Property | Type | Default | Description | +|----------|------|---------|-------------| +| `publishableKey` | `String` | Required | Your Clerk publishable key | +| `persistor` | `Persistor` | Required | Storage mechanism for auth state | +| `sessionTokenPolling` | `bool` | `true` | Enable automatic token refresh | +| `httpService` | `HttpService?` | `null` | Custom HTTP service | + +--- + +### User + +Represents a user in your application. + +```dart +final user = auth.user; + +if (user != null) { + print('User ID: ${user.id}'); + print('Email: ${user.emailAddress}'); + print('Name: ${user.firstName} ${user.lastName}'); +} +``` + +**Key Properties:** + +| Property | Type | Description | +|----------|------|-------------| +| `id` | `String` | Unique user identifier | +| `firstName` | `String?` | First name | +| `lastName` | `String?` | Last name | +| `username` | `String?` | Username | +| `emailAddress` | `String?` | Primary email | +| `phoneNumber` | `String?` | Primary phone | +| `imageUrl` | `String?` | Profile image URL | +| `emailAddresses` | `List` | All email addresses | +| `phoneNumbers` | `List` | All phone numbers | +| `publicMetadata` | `Map` | Public metadata | +| `unsafeMetadata` | `Map` | Unsafe metadata | + +**Key Methods:** + +| Method | Description | +|--------|-------------| +| `update()` | Update user profile | +| `setProfileImage()` | Upload profile image | +| `createEmailAddress()` | Add new email address | +| `createPhoneNumber()` | Add new phone number | +| `createTOTP()` | Enable MFA | + +--- + +### Strategy + +Authentication strategies supported by Clerk. + +```dart +// Password authentication +Strategy.password + +// Email code (passwordless) +Strategy.emailCode + +// Phone code (SMS) +Strategy.phoneCode + +// OAuth +Strategy.oauth(OAuthProvider.google) +Strategy.oauth(OAuthProvider.github) + +// TOTP (MFA) +Strategy.totp +``` + +--- + +### Persistor + +Interface for persisting authentication state. + +**Built-in Persistors:** + +```dart +// Default file-based persistor +DefaultPersistor( + getCacheDirectory: () => Directory.current, +) + +// No persistence (in-memory only) +Persistor.none +``` + +**Custom Persistor:** + +```dart +class MyCustomPersistor implements Persistor { + @override + Future write(String key, T value) async { + // Your storage logic + } + + @override + Future read(String key) async { + // Your retrieval logic + } + + @override + Future delete(String key) async { + // Your deletion logic + } +} +``` + +--- + +## Session Token Polling + +By default, `clerk_auth` automatically polls for session tokens and provides them via the `sessionTokenStream`. + +### Enable/Disable Polling + +```dart +final auth = Auth( + config: AuthConfig( + publishableKey: 'pk_test_xxxxxxxxxxxxx', + sessionTokenPolling: true, // Enable (default) + // sessionTokenPolling: false, // Disable + ), +); +``` + +### Listen to Session Tokens + +```dart +auth.sessionTokenStream.listen((token) { + if (token != null) { + print('New session token: $token'); + // Use for authenticated API requests + } +}); +``` + +--- + +## Error Handling + +All authentication methods can throw `ClerkApiException`: + +```dart +import 'package:clerk_auth/clerk_auth.dart'; + +try { + await auth.attemptSignIn( + strategy: Strategy.password, + identifier: 'user@example.com', + password: 'password', + ); +} on ClerkApiException catch (e) { + print('Error: ${e.message}'); + print('Status: ${e.status}'); + + // Check specific error codes + for (final error in e.errors) { + print('Code: ${error.code}'); + print('Message: ${error.message}'); + } +} +``` + +--- + +## Examples + +### Complete Sign-In Flow + +```dart +import 'dart:io'; +import 'package:clerk_auth/clerk_auth.dart'; + +Future main() async { + final auth = Auth( + config: AuthConfig( + publishableKey: Platform.environment['CLERK_PUBLISHABLE_KEY']!, + persistor: DefaultPersistor( + getCacheDirectory: () => Directory.current, + ), + ), + ); + + await auth.initialize(); + + try { + await auth.attemptSignIn( + strategy: Strategy.password, + identifier: 'user@example.com', + password: 'password', + ); + + print('✅ Signed in as: ${auth.user?.emailAddress}'); + + // Use session token for API requests + auth.sessionTokenStream.listen((token) { + if (token != null) { + // Make authenticated requests + } + }); + + } catch (e) { + print('❌ Sign in failed: $e'); + } finally { + auth.terminate(); + } +} +``` + +--- + +## API Documentation + +For complete API documentation, see: + +- [pub.dev Documentation](https://pub.dev/documentation/clerk_auth/latest/) +- [GitHub Repository](https://github.com/clerk/clerk-sdk-flutter/tree/main/packages/clerk_auth) + +--- + +## Next Steps + +- [Dart Quickstart]({{ '/getting-started/quickstart-dart' | relative_url }}) +- [Authentication Guide]({{ '/guides/authentication' | relative_url }}) +- [User Management]({{ '/guides/user-management' | relative_url }}) +- [Session Tokens]({{ '/guides/session-tokens' | relative_url }}) + diff --git a/docs/packages/clerk-flutter.md b/docs/packages/clerk-flutter.md new file mode 100644 index 00000000..bc4998f6 --- /dev/null +++ b/docs/packages/clerk-flutter.md @@ -0,0 +1,434 @@ +--- +layout: default +title: clerk_flutter +parent: Packages +nav_order: 2 +--- + +# clerk_flutter Package +{: .no_toc } + +The official Clerk Flutter SDK with pre-built UI components. +{: .fs-6 .fw-300 } + +## Table of contents +{: .no_toc .text-delta } + +1. TOC +{:toc} + +--- + +## Overview + +The `clerk_flutter` package provides a complete authentication solution for Flutter applications with: + +- Pre-built UI components +- Material Design integration +- Cross-platform support (iOS, Android, Web, Desktop) +- Seamless Flutter integration + +**Package:** [clerk_flutter on pub.dev](https://pub.dev/packages/clerk_flutter) +**Version:** 0.0.13-beta +**Repository:** [GitHub](https://github.com/clerk/clerk-sdk-flutter/tree/main/packages/clerk_flutter) + +--- + +## Installation + +Add to your `pubspec.yaml`: + +```yaml +dependencies: + clerk_flutter: ^0.0.13-beta +``` + +Then run: + +```bash +flutter pub get +``` + +--- + +## Requirements + +- Flutter >= 3.27.4 +- Dart >= 3.6.2 + +--- + +## Core Widgets + +### ClerkAuth + +The root widget that initializes Clerk authentication. + +```dart +import 'package:clerk_flutter/clerk_flutter.dart'; + +ClerkAuth( + config: ClerkAuthConfig( + publishableKey: 'pk_test_xxxxxxxxxxxxx', + ), + child: MaterialApp( + // Your app + ), +) +``` + +**Properties:** + +| Property | Type | Description | +|----------|------|-------------| +| `config` | `ClerkAuthConfig` | Configuration object | +| `child` | `Widget` | Your app widget | +| `persistor` | `Persistor?` | Custom storage mechanism | +| `httpService` | `HttpService?` | Custom HTTP service | + +--- + +### ClerkAuthConfig + +Configuration for ClerkAuth. + +```dart +ClerkAuthConfig( + publishableKey: 'pk_test_xxxxxxxxxxxxx', + redirectionGenerator: (url) => Uri.parse('myapp://oauth'), + deepLinkStream: AppLinks().allUriLinkStream.map( + (uri) => ClerkDeepLink(uri: uri), + ), +) +``` + +**Properties:** + +| Property | Type | Description | +|----------|------|-------------| +| `publishableKey` | `String` | Your Clerk publishable key | +| `redirectionGenerator` | `Function?` | Generate OAuth redirect URLs | +| `deepLinkStream` | `Stream?` | Handle OAuth callbacks | +| `loading` | `Widget?` | Widget shown during initialization | + +--- + +### ClerkAuthBuilder + +Build different UI based on authentication state. + +```dart +ClerkAuthBuilder( + signedInBuilder: (context, authState) { + return Text('Welcome, ${authState.user?.firstName}!'); + }, + signedOutBuilder: (context, authState) { + return const ClerkAuthentication(); + }, +) +``` + +**Properties:** + +| Property | Type | Description | +|----------|------|-------------| +| `signedInBuilder` | `AuthWidgetBuilder?` | Builder when user is signed in | +| `signedOutBuilder` | `AuthWidgetBuilder?` | Builder when user is signed out | +| `builder` | `AuthWidgetBuilder?` | Fallback builder | + +--- + +### ClerkAuthentication + +Pre-built authentication UI with sign-in and sign-up. + +```dart +const ClerkAuthentication() +``` + +Features: +- Email/password sign-in and sign-up +- OAuth provider buttons +- Email verification +- Password reset +- Automatic error handling + +--- + +### ClerkUserButton + +User menu with profile and sign-out options. + +```dart +ClerkUserButton( + showName: true, + sessionActions: [ + ClerkUserAction( + label: 'Settings', + icon: Icons.settings, + onPressed: (context, authState) { + // Handle action + }, + ), + ], +) +``` + +**Properties:** + +| Property | Type | Description | +|----------|------|-------------| +| `showName` | `bool` | Show user's name | +| `sessionActions` | `List?` | Custom session actions | +| `additionalActions` | `List?` | Additional menu items | + +--- + +### ClerkSignedIn / ClerkSignedOut + +Conditional rendering based on auth state. + +```dart +ClerkSignedIn( + child: Text('Only visible when signed in'), +) + +ClerkSignedOut( + child: Text('Only visible when signed out'), +) +``` + +--- + +### ClerkErrorListener + +Listens for authentication errors and displays them. + +```dart +ClerkErrorListener( + child: YourWidget(), +) +``` + +--- + +### ClerkOrganizationList + +Display and manage organizations (if enabled). + +```dart +const ClerkOrganizationList() +``` + +--- + +## Accessing Auth State + +### Using ClerkAuth.of() + +```dart +import 'package:clerk_flutter/clerk_flutter.dart'; + +class MyWidget extends StatelessWidget { + @override + Widget build(BuildContext context) { + final authState = ClerkAuth.of(context); + final user = authState.user; + + return Text('Hello, ${user?.firstName}!'); + } +} +``` + +### ClerkAuthState Properties + +| Property | Type | Description | +|----------|------|-------------| +| `user` | `User?` | Current user | +| `client` | `Client` | Client with sessions | +| `env` | `Environment` | Environment config | +| `isSignedIn` | `bool` | Whether user is signed in | +| `sessionTokenStream` | `Stream` | Session token stream | + +### ClerkAuthState Methods + +| Method | Description | +|--------|-------------| +| `attemptSignIn()` | Start sign-in | +| `attemptSignUp()` | Start sign-up | +| `signOut()` | Sign out current user | +| `signOutOf(Session)` | Sign out of specific session | + +--- + +## Theming + +### Apply Custom Theme + +```dart +import 'package:clerk_flutter/clerk_flutter.dart'; + +MaterialApp( + theme: ThemeData.light().copyWith( + extensions: [ + ClerkTheme( + colors: ClerkColors.light( + primary: Colors.blue, + background: Colors.white, + ), + ), + ], + ), +) +``` + +### ClerkTheme Properties + +| Property | Type | Description | +|----------|------|-------------| +| `colors` | `ClerkColors` | Color scheme | +| `typography` | `ClerkTypography?` | Text styles | + +--- + +## Deep Links for OAuth + +### Setup Deep Links + +1. Add `app_links` package: + +```yaml +dependencies: + app_links: ^3.5.1 +``` + +2. Configure ClerkAuth: + +```dart +import 'package:app_links/app_links.dart'; + +ClerkAuth( + config: ClerkAuthConfig( + publishableKey: 'pk_test_xxxxxxxxxxxxx', + redirectionGenerator: (url) { + return Uri.parse('myapp://oauth-callback'); + }, + deepLinkStream: AppLinks().allUriLinkStream.map((uri) { + return ClerkDeepLink(uri: uri); + }), + ), + child: MaterialApp(/* ... */), +) +``` + +3. Configure platform-specific deep links: + +**Android** (`android/app/src/main/AndroidManifest.xml`): +```xml + + + + + + +``` + +**iOS** (`ios/Runner/Info.plist`): +```xml +CFBundleURLTypes + + + CFBundleURLSchemes + + myapp + + + +``` + +--- + +## Examples + +### Complete App Example + +```dart +import 'package:flutter/material.dart'; +import 'package:clerk_flutter/clerk_flutter.dart'; + +void main() { + runApp(const MyApp()); +} + +class MyApp extends StatelessWidget { + const MyApp({super.key}); + + @override + Widget build(BuildContext context) { + return ClerkAuth( + config: ClerkAuthConfig( + publishableKey: 'pk_test_xxxxxxxxxxxxx', + ), + child: MaterialApp( + title: 'Clerk Flutter Demo', + theme: ThemeData.light(), + home: const HomePage(), + ), + ); + } +} + +class HomePage extends StatelessWidget { + const HomePage({super.key}); + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar(title: const Text('My App')), + body: SafeArea( + child: ClerkErrorListener( + child: ClerkAuthBuilder( + signedInBuilder: (context, authState) { + return Center( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Text('Welcome, ${authState.user?.firstName}!'), + const SizedBox(height: 20), + const ClerkUserButton(showName: true), + ], + ), + ); + }, + signedOutBuilder: (context, authState) { + return const Center( + child: ClerkAuthentication(), + ); + }, + ), + ), + ), + ); + } +} +``` + +--- + +## API Documentation + +For complete API documentation, see: + +- [pub.dev Documentation](https://pub.dev/documentation/clerk_flutter/latest/) +- [GitHub Repository](https://github.com/clerk/clerk-sdk-flutter/tree/main/packages/clerk_flutter) +- [Example App](https://github.com/clerk/clerk-sdk-flutter/tree/main/packages/clerk_flutter/example) + +--- + +## Next Steps + +- [Flutter Quickstart]({{ '/getting-started/quickstart-flutter' | relative_url }}) +- [Widget Reference]({{ '/api/widgets' | relative_url }}) +- [Customization Guide]({{ '/guides/customization' | relative_url }}) +- [Authentication Flows]({{ '/guides/authentication' | relative_url }}) + diff --git a/packages/clerk_auth/README.md b/packages/clerk_auth/README.md index 8a951f73..c1678cb4 100644 --- a/packages/clerk_auth/README.md +++ b/packages/clerk_auth/README.md @@ -10,12 +10,25 @@ [![documentation](https://img.shields.io/badge/documentation-clerk-green.svg)](https://clerk.com/docs) [![twitter](https://img.shields.io/twitter/follow/ClerkDev?style=social)](https://twitter.com/intent/follow?screen_name=ClerkDev) -> ### ⚠️ The Clerk Flutter SDK is in Beta ⚠️ +> ### ⚠️ The Clerk Dart SDK is in Beta ⚠️ > ❗️ Breaking changes should be expected until the first stable release (1.0.0) ❗️ **Clerk helps developers build user management. We provide streamlined user experiences for your users to sign up, sign in, and manage their profile from your Dart code.** +--- + +## 📚 Documentation + +**[View Full Documentation →](https://clerk.github.io/clerk-sdk-flutter/packages/clerk-auth)** + +- 🚀 [Dart Quickstart](https://clerk.github.io/clerk-sdk-flutter/getting-started/quickstart-dart) +- 📖 [API Reference](https://pub.dev/documentation/clerk_auth/latest/) +- 🎯 [Authentication Guide](https://clerk.github.io/clerk-sdk-flutter/guides/authentication) +- 👤 [User Management](https://clerk.github.io/clerk-sdk-flutter/guides/user-management) + +--- + ## Requirements * Dart >= 3.6.2 @@ -68,7 +81,9 @@ Future main() async { } ``` -For more details see [Clerk Auth object](https://pub.dev/documentation/clerk_auth/latest/clerk_auth/Auth-class.html) +For more details see: +- [Clerk Auth Documentation](https://clerk.github.io/clerk-sdk-flutter/packages/clerk-auth) +- [API Reference](https://pub.dev/documentation/clerk_auth/latest/clerk_auth/Auth-class.html) ## Session Token Polling @@ -97,6 +112,18 @@ polling off, enabling it via a `SessionTokenPollMode` object in the config. This v0.0.13-beta: it is no longer used, and will be deleted in a future version. If you are using it, or relying on the system defaulting to no polling, please review your code. +--- + +## 📖 Learn More + +- [Full Documentation](https://clerk.github.io/clerk-sdk-flutter/) +- [Dart Quickstart](https://clerk.github.io/clerk-sdk-flutter/getting-started/quickstart-dart) +- [Authentication Guide](https://clerk.github.io/clerk-sdk-flutter/guides/authentication) +- [User Management](https://clerk.github.io/clerk-sdk-flutter/guides/user-management) +- [Session Tokens](https://clerk.github.io/clerk-sdk-flutter/guides/session-tokens) + +--- + ## License This SDK is licensed under the MIT license found in the [LICENSE](./LICENSE) file. diff --git a/packages/clerk_flutter/README.md b/packages/clerk_flutter/README.md index b54d1aec..64d25269 100644 --- a/packages/clerk_flutter/README.md +++ b/packages/clerk_flutter/README.md @@ -16,6 +16,20 @@ **Clerk helps developers build user management. We provide streamlined user experiences for your users to sign up, sign in, and manage their profile from your Flutter code.** +--- + +## 📚 Documentation + +**[View Full Documentation →](https://clerk.github.io/clerk-sdk-flutter/packages/clerk-flutter)** + +- 🚀 [Flutter Quickstart](https://clerk.github.io/clerk-sdk-flutter/getting-started/quickstart-flutter) +- 📖 [Widget Reference](https://clerk.github.io/clerk-sdk-flutter/api/widgets) +- 🎨 [Customization Guide](https://clerk.github.io/clerk-sdk-flutter/guides/customization) +- 🎯 [Authentication Flows](https://clerk.github.io/clerk-sdk-flutter/guides/authentication) +- 👤 [User Management](https://clerk.github.io/clerk-sdk-flutter/guides/user-management) + +--- + ## Requirements * Flutter >= 3.27.4 @@ -78,6 +92,19 @@ Add the following line to your `android/app/src/main/AndroidManifest.xml` file: ``` +--- + +## 📖 Learn More + +- [Full Documentation](https://clerk.github.io/clerk-sdk-flutter/) +- [Flutter Quickstart](https://clerk.github.io/clerk-sdk-flutter/getting-started/quickstart-flutter) +- [Widget Reference](https://clerk.github.io/clerk-sdk-flutter/api/widgets) +- [Customization Guide](https://clerk.github.io/clerk-sdk-flutter/guides/customization) +- [Authentication Flows](https://clerk.github.io/clerk-sdk-flutter/guides/authentication) +- [Example App](https://github.com/clerk/clerk-sdk-flutter/tree/main/packages/clerk_flutter/example) + +--- + ## License This SDK is licensed under the MIT license found in the [LICENSE](./LICENSE) file. From a8a2c852a40a3469f489aabe1bda705c59de772b Mon Sep 17 00:00:00 2001 From: Nic Ford Date: Mon, 16 Mar 2026 17:03:58 +0000 Subject: [PATCH 02/27] fix: add documentation [#341] --- docs/.gitignore | 8 - docs/CONTRIBUTING.md | 279 ---- docs/DEPLOYMENT.md | 228 --- docs/Gemfile | 35 - docs/README.md | 308 ++-- docs/_config.yml | 78 - docs/_config_production.yml | 7 - docs/_sass/color_schemes/clerk.scss | 56 - docs/_sass/custom/custom.scss | 167 -- docs/_sass/custom/syntax-highlighting.scss | 159 -- docs/api/_index.md | 40 - docs/api/widgets.md | 304 ---- docs/assets/images/clerk-logo.svg | 7 - docs/clerk_auth/README.md | 301 ++++ docs/clerk_auth/auth.md | 1447 +++++++++++++++++ docs/clerk_auth/auth_config.md | 520 ++++++ docs/clerk_auth/http_service.md | 657 ++++++++ docs/clerk_auth/persistor.md | 539 ++++++ docs/clerk_flutter/README.md | 525 ++++++ docs/clerk_flutter/clerk_auth.md | 506 ++++++ docs/clerk_flutter/clerk_auth_builder.md | 327 ++++ docs/clerk_flutter/clerk_auth_config.md | 229 +++ docs/clerk_flutter/clerk_authentication.md | 318 ++++ docs/clerk_flutter/clerk_error_listener.md | 277 ++++ docs/clerk_flutter/clerk_organization_list.md | 303 ++++ docs/clerk_flutter/clerk_signed_in.md | 231 +++ docs/clerk_flutter/clerk_signed_out.md | 171 ++ docs/clerk_flutter/clerk_theme.md | 267 +++ docs/clerk_flutter/clerk_user_button.md | 348 ++++ docs/getting-started.md | 133 -- docs/getting-started/quickstart-dart.md | 261 --- docs/getting-started/quickstart-flutter.md | 390 ----- docs/guides/_index.md | 43 - docs/guides/authentication.md | 370 ----- docs/guides/backend-integration.md | 567 ------- docs/guides/customization.md | 461 ------ docs/guides/error-handling.md | 481 ------ docs/guides/organizations.md | 411 ----- docs/guides/session-tokens.md | 331 ---- docs/guides/user-management.md | 456 ------ docs/index.md | 96 -- docs/packages/_index.md | 64 - docs/packages/clerk-auth.md | 354 ---- docs/packages/clerk-flutter.md | 434 ----- 44 files changed, 7157 insertions(+), 6337 deletions(-) delete mode 100644 docs/.gitignore delete mode 100644 docs/CONTRIBUTING.md delete mode 100644 docs/DEPLOYMENT.md delete mode 100644 docs/Gemfile delete mode 100644 docs/_config.yml delete mode 100644 docs/_config_production.yml delete mode 100644 docs/_sass/color_schemes/clerk.scss delete mode 100644 docs/_sass/custom/custom.scss delete mode 100644 docs/_sass/custom/syntax-highlighting.scss delete mode 100644 docs/api/_index.md delete mode 100644 docs/api/widgets.md delete mode 100644 docs/assets/images/clerk-logo.svg create mode 100644 docs/clerk_auth/README.md create mode 100644 docs/clerk_auth/auth.md create mode 100644 docs/clerk_auth/auth_config.md create mode 100644 docs/clerk_auth/http_service.md create mode 100644 docs/clerk_auth/persistor.md create mode 100644 docs/clerk_flutter/README.md create mode 100644 docs/clerk_flutter/clerk_auth.md create mode 100644 docs/clerk_flutter/clerk_auth_builder.md create mode 100644 docs/clerk_flutter/clerk_auth_config.md create mode 100644 docs/clerk_flutter/clerk_authentication.md create mode 100644 docs/clerk_flutter/clerk_error_listener.md create mode 100644 docs/clerk_flutter/clerk_organization_list.md create mode 100644 docs/clerk_flutter/clerk_signed_in.md create mode 100644 docs/clerk_flutter/clerk_signed_out.md create mode 100644 docs/clerk_flutter/clerk_theme.md create mode 100644 docs/clerk_flutter/clerk_user_button.md delete mode 100644 docs/getting-started.md delete mode 100644 docs/getting-started/quickstart-dart.md delete mode 100644 docs/getting-started/quickstart-flutter.md delete mode 100644 docs/guides/_index.md delete mode 100644 docs/guides/authentication.md delete mode 100644 docs/guides/backend-integration.md delete mode 100644 docs/guides/customization.md delete mode 100644 docs/guides/error-handling.md delete mode 100644 docs/guides/organizations.md delete mode 100644 docs/guides/session-tokens.md delete mode 100644 docs/guides/user-management.md delete mode 100644 docs/index.md delete mode 100644 docs/packages/_index.md delete mode 100644 docs/packages/clerk-auth.md delete mode 100644 docs/packages/clerk-flutter.md diff --git a/docs/.gitignore b/docs/.gitignore deleted file mode 100644 index 1774ecf9..00000000 --- a/docs/.gitignore +++ /dev/null @@ -1,8 +0,0 @@ -_site/ -.sass-cache/ -.jekyll-cache/ -.jekyll-metadata -vendor/ -.bundle/ -Gemfile.lock - diff --git a/docs/CONTRIBUTING.md b/docs/CONTRIBUTING.md deleted file mode 100644 index 2d522841..00000000 --- a/docs/CONTRIBUTING.md +++ /dev/null @@ -1,279 +0,0 @@ -# Contributing to Clerk Dart & Flutter Documentation - -Thank you for your interest in contributing to the Clerk Dart & Flutter SDK documentation! This guide will help you get started. - -## 📋 Table of Contents - -- [Getting Started](#getting-started) -- [Documentation Structure](#documentation-structure) -- [Writing Guidelines](#writing-guidelines) -- [Local Development](#local-development) -- [Submitting Changes](#submitting-changes) - ---- - -## Getting Started - -### Prerequisites - -- Ruby >= 3.1 -- Bundler -- Git - -### Setup - -1. Fork the repository -2. Clone your fork: - ```bash - git clone https://github.com/YOUR_USERNAME/clerk-sdk-flutter.git - cd clerk-sdk-flutter/docs - ``` - -3. Install dependencies: - ```bash - bundle install - ``` - -4. Start the local server: - ```bash - bundle exec jekyll serve - ``` - -5. Open http://localhost:4000/clerk-sdk-flutter in your browser - ---- - -## Documentation Structure - -``` -docs/ -├── index.md # Homepage -├── getting-started.md # Getting started overview -├── getting-started/ # Quickstart guides -│ ├── quickstart-dart.md -│ └── quickstart-flutter.md -├── guides/ # Comprehensive guides -│ ├── authentication.md -│ ├── user-management.md -│ ├── customization.md -│ └── ... -├── packages/ # Package-specific docs -│ ├── clerk-auth.md -│ └── clerk-flutter.md -├── api/ # API reference -│ └── widgets.md -└── assets/ # Images and static files -``` - ---- - -## Writing Guidelines - -### Front Matter - -Every page must include front matter at the top: - -```yaml ---- -layout: default -title: Your Page Title -parent: Parent Page (optional) -nav_order: 1 ---- -``` - -### Front Matter Options - -| Option | Description | Required | -|--------|-------------|----------| -| `layout` | Page layout (usually `default`) | Yes | -| `title` | Page title shown in navigation | Yes | -| `parent` | Parent page for hierarchy | No | -| `nav_order` | Order in navigation menu | No | -| `has_children` | Set to `true` if page has children | No | -| `permalink` | Custom URL path | No | - -### Markdown Style - -#### Headings - -Use ATX-style headings with proper hierarchy: - -```markdown -# Page Title (H1 - only one per page) - -## Section (H2) - -### Subsection (H3) - -#### Detail (H4) -``` - -#### Code Blocks - -Always specify the language for syntax highlighting: - -````markdown -```dart -import 'package:clerk_flutter/clerk_flutter.dart'; - -void main() { - runApp(MyApp()); -} -``` -```` - -Supported languages: `dart`, `yaml`, `bash`, `json`, `xml`, `swift`, `kotlin` - -#### Tables - -Use tables for structured information: - -```markdown -| Column 1 | Column 2 | Column 3 | -|----------|----------|----------| -| Value 1 | Value 2 | Value 3 | -``` - -#### Callouts - -Use callouts for important information: - -```markdown -{: .note } -> This is a note callout - -{: .warning } -> This is a warning callout - -{: .tip } -> This is a tip callout -``` - -#### Links - -- **Internal links:** Use relative paths: `[Link Text](/guides/authentication)` -- **External links:** Use full URLs: `[Clerk Docs](https://clerk.com/docs)` -- **API references:** Link to pub.dev for detailed API docs - -### Content Guidelines - -1. **Be Clear and Concise** - - Use simple, direct language - - Avoid jargon when possible - - Explain technical terms when first used - -2. **Provide Examples** - - Include code examples for all features - - Show complete, working code when possible - - Add comments to explain complex code - -3. **Be Consistent** - - Use consistent terminology throughout - - Follow the existing documentation style - - Use the same code formatting conventions - -4. **Keep It Updated** - - Ensure code examples work with the latest SDK version - - Update version numbers when they change - - Remove deprecated features - -5. **Think About the User** - - Start with the most common use cases - - Provide step-by-step instructions - - Include troubleshooting tips - ---- - -## Local Development - -### Running the Site Locally - -```bash -cd docs -bundle exec jekyll serve -``` - -The site will be available at http://localhost:4000/clerk-sdk-flutter - -### Live Reload - -Jekyll automatically reloads when you save changes to Markdown files. Refresh your browser to see updates. - -### Building for Production - -```bash -bundle exec jekyll build -``` - -The built site will be in `docs/_site/`. - ---- - -## Submitting Changes - -### Before Submitting - -1. **Test Locally** - - Run the site locally and verify your changes - - Check all links work correctly - - Ensure code examples are correct - -2. **Check Formatting** - - Use proper Markdown syntax - - Include required front matter - - Follow the style guidelines - -3. **Review Content** - - Proofread for typos and grammar - - Ensure technical accuracy - - Verify code examples work - -### Pull Request Process - -1. Create a new branch: - ```bash - git checkout -b docs/your-feature-name - ``` - -2. Make your changes and commit: - ```bash - git add . - git commit -m "docs: Add guide for X feature" - ``` - -3. Push to your fork: - ```bash - git push origin docs/your-feature-name - ``` - -4. Open a Pull Request on GitHub - -5. Fill out the PR template with: - - Description of changes - - Screenshots (if applicable) - - Related issues - -### Commit Message Format - -Use conventional commits: - -- `docs: Add new guide for authentication` -- `docs: Update Flutter quickstart` -- `docs: Fix typo in user management guide` -- `docs: Improve code examples in customization` - ---- - -## Questions? - -If you have questions about contributing to the documentation: - -- 💬 [Join our Discord](https://clerk.com/discord) -- 🐛 [Open an issue](https://github.com/clerk/clerk-sdk-flutter/issues) -- 📧 Email: support@clerk.com - ---- - -Thank you for contributing to Clerk's documentation! 🎉 - diff --git a/docs/DEPLOYMENT.md b/docs/DEPLOYMENT.md deleted file mode 100644 index 13866b15..00000000 --- a/docs/DEPLOYMENT.md +++ /dev/null @@ -1,228 +0,0 @@ -# Documentation Deployment Guide - -This guide explains how the Clerk Dart & Flutter SDK documentation is deployed to GitHub Pages. - -## 🌐 Live Site - -The documentation is available at: **https://clerk.github.io/clerk-sdk-flutter/** - ---- - -## 📋 Deployment Overview - -The documentation uses: -- **Jekyll** - Static site generator -- **Just the Docs** - Documentation theme -- **GitHub Actions** - Automated deployment -- **GitHub Pages** - Hosting - ---- - -## 🚀 Automatic Deployment - -### Trigger Conditions - -The documentation is automatically deployed when: - -1. Changes are pushed to the `main` branch -2. Changes affect files in the `docs/` directory -3. Changes affect `.github/workflows/deploy-docs.yml` - -### Workflow Steps - -The deployment workflow (`.github/workflows/deploy-docs.yml`) performs: - -1. **Checkout** - Checks out the repository -2. **Setup Ruby** - Installs Ruby 3.1 and dependencies -3. **Setup Pages** - Configures GitHub Pages -4. **Build** - Builds the Jekyll site -5. **Upload** - Uploads the built site as an artifact -6. **Deploy** - Deploys to GitHub Pages - -### Viewing Deployment Status - -1. Go to the [Actions tab](https://github.com/clerk/clerk-sdk-flutter/actions) -2. Look for "Deploy Documentation to GitHub Pages" workflow -3. Click on a run to see details and logs - ---- - -## 🔧 Manual Deployment - -### Trigger Manual Deployment - -1. Go to the [Actions tab](https://github.com/clerk/clerk-sdk-flutter/actions) -2. Select "Deploy Documentation to GitHub Pages" -3. Click "Run workflow" -4. Select the `main` branch -5. Click "Run workflow" - -### Local Build Test - -Before deploying, test the build locally: - -```bash -cd docs -bundle install -bundle exec jekyll build --baseurl "/clerk-sdk-flutter" -``` - -The built site will be in `docs/_site/`. - ---- - -## ⚙️ GitHub Pages Configuration - -### Repository Settings - -To enable GitHub Pages for this repository: - -1. Go to **Settings** → **Pages** -2. Under **Source**, select: - - Source: **GitHub Actions** -3. The site will be published to: `https://clerk.github.io/clerk-sdk-flutter/` - -### Custom Domain (Optional) - -To use a custom domain: - -1. Add a `CNAME` file to `docs/` with your domain -2. Configure DNS settings with your domain provider -3. Update `baseurl` in `docs/_config.yml` - ---- - -## 🐛 Troubleshooting - -### Build Failures - -If the build fails: - -1. Check the [Actions tab](https://github.com/clerk/clerk-sdk-flutter/actions) for error logs -2. Common issues: - - **Jekyll build errors**: Check Markdown syntax and front matter - - **Missing dependencies**: Ensure `Gemfile` is up to date - - **Configuration errors**: Verify `_config.yml` syntax - -### Local Testing - -Always test locally before pushing: - -```bash -cd docs -bundle exec jekyll serve -``` - -Visit http://localhost:4000/clerk-sdk-flutter to preview. - -### Common Errors - -**Error: "Could not find gem"** -```bash -cd docs -bundle install -``` - -**Error: "Permission denied"** -- Check repository permissions -- Ensure GitHub Actions has write access to Pages - -**Error: "404 Not Found" after deployment** -- Verify `baseurl` in `_config.yml` is set to `/clerk-sdk-flutter` -- Check that files are in the `docs/` directory -- Ensure GitHub Pages is enabled in repository settings - ---- - -## 📝 Deployment Checklist - -Before deploying major changes: - -- [ ] Test locally with `bundle exec jekyll serve` -- [ ] Verify all links work -- [ ] Check code examples are correct -- [ ] Review for typos and formatting -- [ ] Ensure front matter is correct on all pages -- [ ] Test responsive design (mobile/desktop) -- [ ] Verify navigation hierarchy -- [ ] Check that search works (if enabled) - ---- - -## 🔄 Rollback - -If you need to rollback a deployment: - -1. Go to the [Actions tab](https://github.com/clerk/clerk-sdk-flutter/actions) -2. Find the last successful deployment -3. Click "Re-run all jobs" - -Or revert the commit: - -```bash -git revert -git push origin main -``` - ---- - -## 📊 Monitoring - -### Analytics - -To add Google Analytics: - -1. Add your tracking ID to `docs/_config.yml`: - ```yaml - google_analytics: UA-XXXXXXXXX-X - ``` - -2. The Just the Docs theme will automatically include the tracking code - -### Performance - -Monitor site performance: -- Use [Google PageSpeed Insights](https://pagespeed.web.dev/) -- Check [Lighthouse](https://developers.google.com/web/tools/lighthouse) scores -- Monitor build times in GitHub Actions - ---- - -## 🔐 Security - -### Permissions - -The workflow requires these permissions: -- `contents: read` - Read repository contents -- `pages: write` - Write to GitHub Pages -- `id-token: write` - Generate deployment token - -These are configured in `.github/workflows/deploy-docs.yml`. - -### Secrets - -No secrets are required for basic deployment. If you add custom features that need secrets: - -1. Go to **Settings** → **Secrets and variables** → **Actions** -2. Add your secrets -3. Reference them in the workflow with `${{ secrets.SECRET_NAME }}` - ---- - -## 📚 Additional Resources - -- [Jekyll Documentation](https://jekyllrb.com/docs/) -- [Just the Docs Theme](https://just-the-docs.github.io/just-the-docs/) -- [GitHub Pages Documentation](https://docs.github.com/en/pages) -- [GitHub Actions Documentation](https://docs.github.com/en/actions) - ---- - -## 🆘 Support - -If you encounter issues with deployment: - -- 💬 [Join our Discord](https://clerk.com/discord) -- 🐛 [Open an issue](https://github.com/clerk/clerk-sdk-flutter/issues) -- 📧 Email: support@clerk.com - diff --git a/docs/Gemfile b/docs/Gemfile deleted file mode 100644 index 72ed083d..00000000 --- a/docs/Gemfile +++ /dev/null @@ -1,35 +0,0 @@ -source "https://rubygems.org" - -gem "jekyll", "~> 4.3" -gem "just-the-docs", "~> 0.10.0" - -# Pin sass-embedded to avoid deprecation warnings with older Sass -gem "sass-embedded", "~> 1.69.0" - -# Required for Ruby 3.4+ -gem "bigdecimal" -gem "csv" -gem "base64" -gem "fiddle" - -group :jekyll_plugins do - gem "jekyll-seo-tag" - gem "jekyll-github-metadata" - gem "jekyll-include-cache" - gem "jekyll-relative-links" -end - -# Windows and JRuby does not include zoneinfo files, so bundle the tzinfo-data gem -# and associated library. -platforms :mingw, :x64_mingw, :mswin, :jruby do - gem "tzinfo", ">= 1", "< 3" - gem "tzinfo-data" -end - -# Performance-booster for watching directories on Windows -gem "wdm", "~> 0.1", :platforms => [:mingw, :x64_mingw, :mswin] - -# Lock `http_parser.rb` gem to `v0.6.x` on JRuby builds since newer versions of the gem -# do not have a Java counterpart. -gem "http_parser.rb", "~> 0.6.0", :platforms => [:jruby] - diff --git a/docs/README.md b/docs/README.md index 546f0d83..01db3603 100644 --- a/docs/README.md +++ b/docs/README.md @@ -1,166 +1,240 @@ -# Clerk Dart & Flutter SDK Documentation +# Clerk SDK Documentation -This directory contains the documentation site for Clerk's Dart and Flutter SDKs, built with Jekyll and hosted on GitHub Pages. +Welcome to the comprehensive documentation for the Clerk SDK for Flutter and Dart. -## 🌐 Live Documentation +## 📚 Documentation Structure -Visit the live documentation at: **https://clerk.github.io/clerk-sdk-flutter/** +This documentation is organized into two main sections: -## 📁 Structure +### 🎯 [clerk_auth](./clerk_auth/) - Core Dart SDK -``` -docs/ -├── _config.yml # Jekyll configuration -├── index.md # Homepage -├── getting-started.md # Getting started overview -├── getting-started/ # Quickstart guides -│ ├── quickstart-dart.md -│ └── quickstart-flutter.md -├── guides/ # Comprehensive guides -│ ├── authentication.md -│ ├── user-management.md -│ ├── customization.md -│ └── ... -├── packages/ # Package-specific documentation -│ ├── clerk-auth.md -│ └── clerk-flutter.md -└── assets/ # Images and static assets -``` - -## 🚀 Local Development +The foundational authentication library that works with any Dart application. -### Prerequisites +**Start here if you're:** +- Building a Dart CLI application +- Integrating Clerk into a non-Flutter Dart project +- Understanding the core authentication logic +- Implementing custom UI or integrations -- Ruby >= 3.1 -- Bundler +**Key Documentation:** +- [**Auth API**](./clerk_auth/auth.md) - Complete authentication methods (sign-in, sign-up, OAuth, passkeys, sessions, etc.) +- [**AuthConfig**](./clerk_auth/auth_config.md) - SDK configuration and initialization +- [**Persistor**](./clerk_auth/persistor.md) - Custom storage implementations +- [**HttpService**](./clerk_auth/http_service.md) - Custom HTTP client implementations -### Setup +### 🎨 [clerk_flutter](./clerk_flutter/) - Flutter Widgets -1. Install dependencies: +Pre-built Flutter widgets and UI components for rapid integration. -```bash -cd docs -bundle install -``` +**Start here if you're:** +- Building a Flutter mobile or web application +- Looking for pre-built authentication UI +- Wanting quick integration with minimal code +- Customizing the appearance of auth flows -2. Run the local server: +**Key Documentation:** +- [**ClerkAuth**](./clerk_flutter/clerk_auth.md) - Root widget and state management +- [**ClerkAuthentication**](./clerk_flutter/clerk_authentication.md) - Complete pre-built auth UI +- [**ClerkUserButton**](./clerk_flutter/clerk_user_button.md) - User profile and session management +- [**ClerkAuthBuilder**](./clerk_flutter/clerk_auth_builder.md) - Custom UI with builder pattern +- [**ClerkTheme**](./clerk_flutter/clerk_theme.md) - Theming and customization -```bash -bundle exec jekyll serve -``` +--- -3. Open http://localhost:4000/clerk-sdk-flutter in your browser +## 🚀 Quick Start + +### For Flutter Applications + +1. **Add dependencies** to your `pubspec.yaml`: + ```yaml + dependencies: + clerk_flutter: ^0.0.14-beta + ``` + +2. **Wrap your app** with `ClerkAuth`: + ```dart + MaterialApp( + builder: ClerkAuth.materialAppBuilder( + config: ClerkAuthConfig( + publishableKey: 'pk_test_...', + ), + ), + home: const HomePage(), + ) + ``` + +3. **Use pre-built UI** or build custom: + ```dart + // Pre-built UI + ClerkSignedOut( + child: const ClerkAuthentication(), + ) + + // Custom UI + ClerkAuthBuilder( + signedInBuilder: (context, authState) { + return Text('Welcome, ${authState.user?.fullName}!'); + }, + signedOutBuilder: (context, authState) { + return const ClerkAuthentication(); + }, + ) + ``` + +📖 **[Full Flutter Guide →](./clerk_flutter/README.md)** + +### For Dart Applications + +1. **Add dependency** to your `pubspec.yaml`: + ```yaml + dependencies: + clerk_auth: ^0.0.14-beta + ``` + +2. **Initialize** the Auth instance: + ```dart + import 'package:clerk_auth/clerk_auth.dart'; + + final auth = Auth( + config: AuthConfig(publishableKey: 'pk_test_...'), + ); + await auth.initialize(); + ``` + +3. **Authenticate** users: + ```dart + // Email + Password sign-in + await auth.attemptSignIn(identifier: 'user@example.com'); + await auth.attemptSignIn(password: 'password123'); + + // Check auth state + if (auth.isSignedIn) { + print('User: ${auth.user?.fullName}'); + } + ``` + +📖 **[Full Dart Guide →](./clerk_auth/README.md)** -{: .note } -> The site uses the local `just-the-docs` gem for development and `remote_theme` for GitHub Pages deployment. This is configured automatically via `_config.yml` and `_config_production.yml`. +--- -### Live Reload +## 📖 Documentation Index + +### clerk_auth (Core SDK) + +| Document | Description | +|----------|-------------| +| [README](./clerk_auth/README.md) | Overview and getting started | +| [Auth](./clerk_auth/auth.md) | Complete API reference for authentication methods | +| [AuthConfig](./clerk_auth/auth_config.md) | Configuration options and initialization | +| [Persistor](./clerk_auth/persistor.md) | Storage interface for sessions and tokens | +| [HttpService](./clerk_auth/http_service.md) | HTTP client interface for custom networking | + +### clerk_flutter (Flutter Widgets) + +| Document | Description | +|----------|-------------| +| [README](./clerk_flutter/README.md) | Overview and getting started | +| [ClerkAuth](./clerk_flutter/clerk_auth.md) | Root widget and state management | +| [ClerkAuthBuilder](./clerk_flutter/clerk_auth_builder.md) | Builder pattern for custom UI | +| [ClerkAuthentication](./clerk_flutter/clerk_authentication.md) | Pre-built authentication UI | +| [ClerkUserButton](./clerk_flutter/clerk_user_button.md) | User profile button and menu | +| [ClerkOrganizationList](./clerk_flutter/clerk_organization_list.md) | Organization management UI | +| [ClerkSignedIn](./clerk_flutter/clerk_signed_in.md) | Conditional rendering (signed in) | +| [ClerkSignedOut](./clerk_flutter/clerk_signed_out.md) | Conditional rendering (signed out) | +| [ClerkErrorListener](./clerk_flutter/clerk_error_listener.md) | Error handling and display | +| [ClerkAuthConfig](./clerk_flutter/clerk_auth_config.md) | Flutter-specific configuration | +| [ClerkTheme](./clerk_flutter/clerk_theme.md) | Theming and customization | -The server will automatically reload when you make changes to the documentation files. +--- -## 📝 Writing Documentation +## 🎯 Common Use Cases -### Adding a New Page +### Authentication Flows -1. Create a new Markdown file in the appropriate directory -2. Add front matter at the top: +- **[Email + Password Sign-In](./clerk_auth/auth.md#email--password-sign-in)** - Traditional authentication +- **[Email Code Sign-In](./clerk_auth/auth.md#email-code-sign-in)** - Passwordless with verification code +- **[Phone Code Sign-In](./clerk_auth/auth.md#phone-code-sign-in)** - SMS-based authentication +- **[OAuth Sign-In](./clerk_auth/auth.md#oauth-sign-in)** - Social login (Google, Apple, GitHub, etc.) +- **[Passkey Authentication](./clerk_auth/auth.md#passkey-authentication)** - WebAuthn/FIDO2 +- **[Multi-Factor Authentication](./clerk_auth/auth.md#multi-factor-authentication)** - 2FA with SMS or TOTP -```yaml ---- -layout: default -title: Your Page Title -parent: Parent Page (optional) -nav_order: 1 ---- -``` +### Session Management -3. Write your content in Markdown +- **[Session Handling](./clerk_auth/auth.md#session-management)** - Managing user sessions +- **[Multi-Session Support](./clerk_auth/auth.md#multi-session-management)** - Multiple accounts +- **[Token Management](./clerk_auth/auth.md#token-management)** - Access tokens and JWTs -### Front Matter Options +### User Management -| Option | Description | -|--------|-------------| -| `layout` | Page layout (usually `default`) | -| `title` | Page title | -| `parent` | Parent page for navigation hierarchy | -| `nav_order` | Order in navigation menu | -| `has_children` | Set to `true` if page has child pages | -| `permalink` | Custom URL path | +- **[User Profile](./clerk_auth/auth.md#user-management)** - Updating user information +- **[Email Management](./clerk_auth/auth.md#email-management)** - Adding/removing emails +- **[Phone Management](./clerk_auth/auth.md#phone-management)** - Adding/removing phone numbers +- **[External Accounts](./clerk_auth/auth.md#external-account-management)** - OAuth connections -### Markdown Features +### Organization Features -The documentation supports: +- **[Organization Management](./clerk_auth/auth.md#organization-management)** - Creating and managing orgs +- **[Organization Switching](./clerk_auth/auth.md#switching-organizations)** - Multi-org support +- **[Organization UI](./clerk_flutter/clerk_organization_list.md)** - Pre-built organization widgets -- Standard Markdown syntax -- Code blocks with syntax highlighting -- Tables -- Callouts (notes, warnings, etc.) -- Table of contents +--- -#### Code Blocks +## 🔑 Key Concepts -````markdown -```dart -import 'package:clerk_flutter/clerk_flutter.dart'; +### Re-entrant Authentication -void main() { - runApp(MyApp()); -} -``` -```` +The `attemptSignIn()` and `attemptSignUp()` methods are **re-entrant**, meaning you call them multiple times with different parameters to progress through the authentication flow: -#### Callouts +```dart +// Step 1: Provide identifier +await auth.attemptSignIn(identifier: 'user@example.com'); -```markdown -{: .note } -> This is a note callout +// Step 2: Provide password +await auth.attemptSignIn(password: 'password123'); -{: .warning } -> This is a warning callout +// Step 3: If 2FA enabled, provide code +await auth.attemptSignIn(code: '123456'); ``` -## 🎨 Theme - -The documentation uses the [Just the Docs](https://just-the-docs.github.io/just-the-docs/) theme with custom Clerk branding. - -## 🚢 Deployment +📖 **[Learn more about re-entrant flows →](./clerk_auth/auth.md#re-entrant-methods)** -The documentation is automatically deployed to GitHub Pages when changes are pushed to the `main` branch. +### Transfer Flow -### GitHub Actions Workflow +When using OAuth or ID token authentication, Clerk uses a "transfer flow" to securely complete authentication: -The deployment is handled by `.github/workflows/deploy-docs.yml`: +```dart +// 1. Initiate OAuth +final transfer = await auth.oauthSignIn(strategy: Strategy.oauth_google); -1. Triggered on push to `main` branch (when `docs/**` files change) -2. Builds the Jekyll site -3. Deploys to GitHub Pages +// 2. Open browser to transfer.authUrl -### Manual Deployment +// 3. Complete after redirect +await auth.completeOAuthSignIn(transfer: transfer); +``` -You can also trigger a manual deployment: +📖 **[Learn more about transfer flow →](./clerk_auth/auth.md#transfer-flow)** -1. Go to the [Actions tab](https://github.com/clerk/clerk-sdk-flutter/actions) -2. Select "Deploy Documentation to GitHub Pages" -3. Click "Run workflow" +--- -## 📦 Dependencies +## 💡 Best Practices -- **Jekyll** - Static site generator -- **just-the-docs** - Documentation theme -- **jekyll-seo-tag** - SEO optimization -- **jekyll-github-metadata** - GitHub integration +1. **Use environment variables** for publishable keys +2. **Handle errors** with `ClerkErrorListener` (Flutter) or try-catch blocks (Dart) +3. **Respect re-entrant flows** - call `attemptSignIn`/`attemptSignUp` multiple times +4. **Check auth state** before accessing user/session data +5. **Implement proper loading states** during authentication +6. **Test all authentication strategies** enabled in your Clerk Dashboard +7. **Customize themes** to match your brand (Flutter) -## 🤝 Contributing +--- -To contribute to the documentation: +## 🆘 Support -1. Fork the [repository](https://github.com/clerk/clerk-sdk-flutter) -2. Create a new branch for your changes -3. Make your edits in the `docs/` directory -4. Test locally with `bundle exec jekyll serve` -5. Submit a pull request +- **Clerk Dashboard**: [https://dashboard.clerk.com](https://dashboard.clerk.com) +- **Clerk Documentation**: [https://clerk.com/docs](https://clerk.com/docs) +- **GitHub Issues**: Report bugs and request features -## 📄 License +--- -This documentation is part of the Clerk Dart & Flutter SDK and is licensed under the MIT License. +*Documentation generated for Clerk SDK version 0.0.14-beta* diff --git a/docs/_config.yml b/docs/_config.yml deleted file mode 100644 index 659aaccb..00000000 --- a/docs/_config.yml +++ /dev/null @@ -1,78 +0,0 @@ -# Clerk Dart & Flutter SDK Documentation -title: Clerk Dart & Flutter SDK -description: Official documentation for Clerk's Dart and Flutter SDKs -baseurl: "/clerk-sdk-flutter" -url: "https://clerk.github.io" - -# Theme -theme: just-the-docs - -# Logo and branding -logo: "/assets/images/clerk-logo.svg" -# Custom Clerk color scheme -color_scheme: clerk - -# Navigation -nav_enabled: true -nav_sort: case_insensitive - -# Search -search_enabled: true -search: - heading_level: 2 - previews: 3 - preview_words_before: 5 - preview_words_after: 10 - tokenizer_separator: /[\s/]+/ - rel_url: true - button: true - -# Enable relative URLs for all links -aux_links_new_tab: false - -# Footer -footer_content: "Copyright © 2026 Clerk, Inc. Licensed under the MIT License." - -# Aux links -aux_links: - "Clerk Dart & Flutter SDK on GitHub": - - "https://github.com/clerk/clerk-sdk-flutter" - "Clerk Documentation": - - "https://clerk.com/docs" - "Clerk Dashboard": - - "https://dashboard.clerk.com" - -# Defaults -defaults: - - scope: - path: "" - values: - layout: default - -# Plugins -plugins: - - jekyll-seo-tag - - jekyll-github-metadata - - jekyll-include-cache - - jekyll-relative-links - -# Relative links configuration -relative_links: - enabled: true - collections: true - -# Syntax highlighting -kramdown: - syntax_highlighter: rouge - syntax_highlighter_opts: - css_class: 'highlight' - default_lang: dart - -# Exclude from processing -exclude: - - node_modules/ - - vendor/ - - Gemfile - - Gemfile.lock - - README.md - diff --git a/docs/_config_production.yml b/docs/_config_production.yml deleted file mode 100644 index a4dbf458..00000000 --- a/docs/_config_production.yml +++ /dev/null @@ -1,7 +0,0 @@ -# Production overrides for GitHub Pages deployment -# This file is merged with _config.yml during GitHub Pages build - -# Use remote_theme for GitHub Pages -remote_theme: just-the-docs/just-the-docs -theme: null - diff --git a/docs/_sass/color_schemes/clerk.scss b/docs/_sass/color_schemes/clerk.scss deleted file mode 100644 index 3f591631..00000000 --- a/docs/_sass/color_schemes/clerk.scss +++ /dev/null @@ -1,56 +0,0 @@ -// Clerk color scheme - matching https://clerk.com/docs -// Based on dark theme with Clerk brand colors - -// Background colors -$body-background-color: #0F0F0F; -$sidebar-color: #0F0F0F; -$code-background-color: #1A1A1A; -$table-background-color: #1A1A1A; -$feedback-color: darken($sidebar-color, 3%); - -// Text colors -$body-text-color: #E5E5E5; -$body-heading-color: #FFFFFF; -$nav-child-link-color: #A1A1A1; -$link-color: #9B8FD9; -$btn-primary-color: #6C47FF; -$base-button-color: #6C47FF; - -// Border colors -$border-color: #2A2A2A; - -// Code colors -$code-linenumber-color: #6B6B6B; - -// Search -$search-background-color: #1A1A1A; -$search-result-preview-color: #A1A1A1; - -// Sidebar -$sidebar-width: 264px; -$sidebar-width-sm: 200px; - -// Navigation -$nav-list-link-color: #E5E5E5; -$nav-list-link-color-hover: #FFFFFF; -$nav-list-link-color-active: #FFFFFF; -$nav-list-expander-color: #6B6B6B; - -// Buttons -$btn-primary-color: #6C47FF; - -// Base button (outline style) -$base-button-color: transparent; - -// Feedback colors -$label-green: #10B981; -$label-blue: #3B82F6; -$label-purple: #6C47FF; -$label-red: #EF4444; -$label-yellow: #F59E0B; - -// Callouts/Admonitions -$note-color: #3B82F6; -$warning-color: #F59E0B; -$important-color: #EF4444; - diff --git a/docs/_sass/custom/custom.scss b/docs/_sass/custom/custom.scss deleted file mode 100644 index 75db400e..00000000 --- a/docs/_sass/custom/custom.scss +++ /dev/null @@ -1,167 +0,0 @@ -// Import syntax highlighting -@import "./syntax-highlighting"; - - -// Custom styles to match Clerk documentation - -// Smooth scrolling -html { - scroll-behavior: smooth; -} - -// Logo styling -.site-logo { - max-height: 32px; -} - -// Navigation improvements -.site-nav { - padding-top: 1.5rem; -} - -.nav-list .nav-list-item { - margin: 0.25rem 0; -} - -.nav-list-link { - border-radius: 6px; - padding: 0.5rem 0.75rem; - transition: all 0.2s ease; - - &:hover { - background-color: rgba(108, 71, 255, 0.1); - } - - &.active { - background-color: rgba(108, 71, 255, 0.15); - font-weight: 500; - } -} - -// Code blocks -pre.highlight { - border-radius: 8px; - border: 1px solid #2A2A2A; -} - -code { - border-radius: 4px; - padding: 0.2em 0.4em; - font-size: 0.9em; -} - -// Tables -table { - border-radius: 8px; - overflow: hidden; - - th { - background-color: #1A1A1A; - font-weight: 600; - } - - td, th { - border-color: #2A2A2A; - } -} - -// Buttons -.btn { - border-radius: 6px; - font-weight: 500; - transition: all 0.2s ease; - border: 1px solid #2A2A2A; - background-color: transparent; - color: #E5E5E5; - - &:hover { - border-color: #6C47FF; - background-color: rgba(108, 71, 255, 0.1); - color: #FFFFFF; - } -} - -.btn-primary { - background-color: #6C47FF; - border-color: #6C47FF; - color: #FFFFFF; - - &:hover { - background-color: #5A38E6; - border-color: #5A38E6; - transform: translateY(-1px); - box-shadow: 0 4px 12px rgba(108, 71, 255, 0.3); - } -} - -// Headings -h1, h2, h3, h4, h5, h6 { - font-weight: 600; - letter-spacing: -0.02em; -} - -// Links -a { - transition: color 0.2s ease; - - &:hover { - color: #B5AAEB; - } -} - -// Search -.search-input { - border-radius: 8px; - background-color: #1A1A1A; - border: 1px solid #2A2A2A; - - &:focus { - border-color: #6C47FF; - box-shadow: 0 0 0 3px rgba(108, 71, 255, 0.1); - } -} - -// Callouts -.label, -.label-blue, -.label-green, -.label-purple, -.label-red, -.label-yellow { - border-radius: 4px; - font-weight: 500; -} - -// Note/Warning boxes -blockquote { - border-left: 4px solid #6C47FF; - background-color: rgba(108, 71, 255, 0.05); - border-radius: 4px; - padding: 1rem; - margin: 1.5rem 0; -} - -// Sidebar footer -.site-footer { - border-top: 1px solid #2A2A2A; - padding: 1rem; - font-size: 0.875rem; - color: #6B6B6B; -} - -// Main content area -.main-content { - max-width: 50rem; - - h1 { - margin-bottom: 1.5rem; - } - - h2 { - margin-top: 2.5rem; - margin-bottom: 1rem; - padding-bottom: 0.5rem; - border-bottom: 1px solid #2A2A2A; - } -} - diff --git a/docs/_sass/custom/syntax-highlighting.scss b/docs/_sass/custom/syntax-highlighting.scss deleted file mode 100644 index 86229cda..00000000 --- a/docs/_sass/custom/syntax-highlighting.scss +++ /dev/null @@ -1,159 +0,0 @@ -// Dark theme syntax highlighting for code blocks -// Based on GitHub Dark theme - -.highlight { - background-color: #1A1A1A; - color: #E5E5E5; - - // Comments - .c, .cm, .c1, .cs { - color: #8B949E; - font-style: italic; - } - - // Keywords - .k, .kc, .kd, .kn, .kp, .kr, .kt { - color: #FF7B72; - } - - // Operators - .o, .ow { - color: #FF7B72; - } - - // Strings - .s, .s1, .s2, .sb, .sc, .sd, .se, .sh, .si, .sx { - color: #A5D6FF; - } - - // Numbers - .m, .mf, .mh, .mi, .mo, .il { - color: #79C0FF; - } - - // Names - .n, .na, .nb, .nc, .no, .nd, .ni, .ne, .nf, .nl, .nn, .nx, .py { - color: #D2A8FF; - } - - // Variables - .nv, .vc, .vg, .vi { - color: #FFA657; - } - - // Built-ins - .nb { - color: #79C0FF; - } - - // Functions - .nf { - color: #D2A8FF; - } - - // Classes - .nc { - color: #FFA657; - } - - // Booleans - .bp { - color: #79C0FF; - } - - // Punctuation - .p { - color: #E5E5E5; - } - - // Generic - .gd { - color: #FF7B72; - background-color: #3F1F1F; - } - - .gi { - color: #7EE787; - background-color: #1F3F1F; - } - - .ge { - font-style: italic; - } - - .gs { - font-weight: bold; - } - - // Line numbers - .lineno { - color: #6B6B6B; - user-select: none; - padding-right: 1em; - border-right: 1px solid #2A2A2A; - margin-right: 1em; - } - - // Dart-specific - .kd { - color: #FF7B72; // class, void, var, final, const - } - - .kt { - color: #79C0FF; // int, String, bool, etc. - } - - // YAML-specific - .l-Scalar-Plain { - color: #A5D6FF; - } - - .na { - color: #79C0FF; // YAML keys - } - - // JSON-specific - .s2 { - color: #A5D6FF; // JSON strings - } - - // Bash/Shell - .nv { - color: #79C0FF; // environment variables - } - - .nb { - color: #D2A8FF; // built-in commands - } -} - -// Inline code -code { - background-color: #1A1A1A; - color: #E5E5E5; - border: 1px solid #2A2A2A; -} - -// Code blocks -pre { - background-color: #1A1A1A; - border: 1px solid #2A2A2A; - - code { - background-color: transparent; - border: none; - color: #E5E5E5; - } -} - -// Highlighted lines in code blocks -.highlight .hll { - background-color: #2A2A2A; -} - -// Error highlighting -.highlight .err { - color: #FF7B72; - background-color: transparent; -} - diff --git a/docs/api/_index.md b/docs/api/_index.md deleted file mode 100644 index e85256f5..00000000 --- a/docs/api/_index.md +++ /dev/null @@ -1,40 +0,0 @@ ---- -layout: default -title: API Reference -nav_order: 5 -has_children: true -permalink: /api ---- - -# API Reference - -Detailed API documentation for Clerk's Dart and Flutter SDKs. - -## Available References - -### [Widget Reference]({{ '/api/widgets' | relative_url }}) -Complete reference for all Clerk Flutter widgets including ClerkAuth, ClerkAuthentication, ClerkUserButton, and more. - -### [clerk_auth Package]({{ '/packages/clerk-auth' | relative_url }}) -API documentation for the Dart SDK including Auth, User, Session, and configuration classes. - -### [clerk_flutter Package]({{ '/packages/clerk-flutter' | relative_url }}) -API documentation for the Flutter SDK including widgets, themes, and Flutter-specific features. - ---- - -## External API Documentation - -For detailed API documentation generated from source code: - -- **[clerk_auth on pub.dev](https://pub.dev/documentation/clerk_auth/latest/)** - Complete Dart API reference -- **[clerk_flutter on pub.dev](https://pub.dev/documentation/clerk_flutter/latest/)** - Complete Flutter API reference - ---- - -## Quick Links - -- [Getting Started](/getting-started) -- [Guides](/guides) -- [GitHub Repository](https://github.com/clerk/clerk-sdk-flutter) - diff --git a/docs/api/widgets.md b/docs/api/widgets.md deleted file mode 100644 index b8592628..00000000 --- a/docs/api/widgets.md +++ /dev/null @@ -1,304 +0,0 @@ ---- -layout: default -title: Widget Reference -parent: API Reference -nav_order: 1 ---- - -# Widget Reference -{: .no_toc } - -Complete reference for all Clerk Flutter widgets. -{: .fs-6 .fw-300 } - -## Table of contents -{: .no_toc .text-delta } - -1. TOC -{:toc} - ---- - -## Control Widgets - -### ClerkAuth - -The root widget that initializes and provides Clerk authentication to your app. - -```dart -ClerkAuth( - config: ClerkAuthConfig( - publishableKey: 'pk_test_xxxxxxxxxxxxx', - ), - child: MaterialApp(/* ... */), -) -``` - -**Properties:** - -| Name | Type | Required | Description | -|------|------|----------|-------------| -| `config` | `ClerkAuthConfig` | Yes | Configuration object | -| `child` | `Widget` | Yes | Your app widget | -| `persistor` | `Persistor?` | No | Custom storage mechanism | -| `httpService` | `HttpService?` | No | Custom HTTP service | -| `authState` | `ClerkAuthState?` | No | Pre-initialized auth state | - ---- - -### ClerkAuthBuilder - -Builds different UI based on authentication state. - -```dart -ClerkAuthBuilder( - signedInBuilder: (context, authState) => SignedInWidget(), - signedOutBuilder: (context, authState) => SignedOutWidget(), -) -``` - -**Properties:** - -| Name | Type | Required | Description | -|------|------|----------|-------------| -| `signedInBuilder` | `AuthWidgetBuilder?` | No | Builder when user is signed in | -| `signedOutBuilder` | `AuthWidgetBuilder?` | No | Builder when user is signed out | -| `builder` | `AuthWidgetBuilder?` | No | Fallback builder | - ---- - -### ClerkSignedIn - -Only renders its child when a user is signed in. - -```dart -ClerkSignedIn( - child: Text('Protected content'), -) -``` - -**Properties:** - -| Name | Type | Required | Description | -|------|------|----------|-------------| -| `child` | `Widget` | Yes | Widget to show when signed in | - ---- - -### ClerkSignedOut - -Only renders its child when no user is signed in. - -```dart -ClerkSignedOut( - child: Text('Please sign in'), -) -``` - -**Properties:** - -| Name | Type | Required | Description | -|------|------|----------|-------------| -| `child` | `Widget` | Yes | Widget to show when signed out | - ---- - -### ClerkErrorListener - -Listens for authentication errors and displays them. - -```dart -ClerkErrorListener( - child: YourWidget(), -) -``` - -**Properties:** - -| Name | Type | Required | Description | -|------|------|----------|-------------| -| `child` | `Widget` | Yes | Child widget | - ---- - -## Authentication Widgets - -### ClerkAuthentication - -Pre-built authentication UI with sign-in and sign-up flows. - -```dart -const ClerkAuthentication() -``` - -**Features:** -- Email/password authentication -- OAuth provider buttons -- Email verification -- Password reset -- Automatic error handling -- Responsive design - ---- - -## User Widgets - -### ClerkUserButton - -User menu button with profile and sign-out options. - -```dart -ClerkUserButton( - showName: true, - sessionActions: [ - ClerkUserAction( - label: 'Settings', - icon: Icons.settings, - onPressed: (context, authState) { - // Handle action - }, - ), - ], -) -``` - -**Properties:** - -| Name | Type | Required | Default | Description | -|------|------|----------|---------|-------------| -| `showName` | `bool` | No | `true` | Show user's name next to avatar | -| `sessionActions` | `List?` | No | `null` | Custom actions for session row | -| `additionalActions` | `List?` | No | `null` | Additional menu items | - ---- - -### ClerkUserAction - -Defines a custom action for the user menu. - -```dart -ClerkUserAction( - label: 'Settings', - icon: Icons.settings, - onPressed: (context, authState) { - Navigator.push( - context, - MaterialPageRoute(builder: (_) => SettingsPage()), - ); - }, -) -``` - -**Properties:** - -| Name | Type | Required | Description | -|------|------|----------|-------------| -| `label` | `String` | Yes | Action label text | -| `icon` | `IconData` | Yes | Action icon | -| `onPressed` | `Function(BuildContext, ClerkAuthState)` | Yes | Callback when pressed | - ---- - -## Organization Widgets - -### ClerkOrganizationList - -Display and manage organizations (if enabled in your Clerk instance). - -```dart -const ClerkOrganizationList() -``` - -**Features:** -- List all user's organizations -- Create new organizations -- Switch active organization -- Manage organization settings - ---- - -## Accessing ClerkAuth - -### ClerkAuth.of() - -Access the ClerkAuthState from anywhere in your widget tree. - -```dart -final authState = ClerkAuth.of(context); -final user = authState.user; -``` - -**Parameters:** - -| Name | Type | Required | Default | Description | -|------|------|----------|---------|-------------| -| `context` | `BuildContext` | Yes | - | Build context | -| `listen` | `bool` | No | `true` | Whether to rebuild on changes | - -**Returns:** `ClerkAuthState` - ---- - -## ClerkAuthState - -The main state object for Clerk authentication. - -### Properties - -| Name | Type | Description | -|------|------|-------------| -| `user` | `User?` | Current signed-in user | -| `client` | `Client` | Client object with sessions | -| `env` | `Environment` | Environment configuration | -| `isSignedIn` | `bool` | Whether a user is signed in | -| `sessionTokenStream` | `Stream` | Stream of session tokens | - -### Methods - -| Method | Description | -|--------|-------------| -| `attemptSignIn()` | Start a sign-in attempt | -| `attemptSignUp()` | Start a sign-up attempt | -| `attemptSignInVerification()` | Verify a sign-in attempt | -| `attemptSignUpVerification()` | Verify a sign-up attempt | -| `signOut()` | Sign out the current user | -| `signOutOf(Session)` | Sign out of a specific session | -| `safelyCall()` | Execute async operation with error handling | - ---- - -## Configuration - -### ClerkAuthConfig - -Configuration object for ClerkAuth. - -```dart -ClerkAuthConfig( - publishableKey: 'pk_test_xxxxxxxxxxxxx', - redirectionGenerator: (url) => Uri.parse('myapp://oauth'), - deepLinkStream: AppLinks().allUriLinkStream.map( - (uri) => ClerkDeepLink(uri: uri), - ), - loading: CircularProgressIndicator(), -) -``` - -**Properties:** - -| Name | Type | Required | Description | -|------|------|----------|-------------| -| `publishableKey` | `String` | Yes | Your Clerk publishable key | -| `redirectionGenerator` | `Function(String)?` | No | Generate OAuth redirect URLs | -| `deepLinkStream` | `Stream?` | No | Handle OAuth callbacks | -| `loading` | `Widget?` | No | Widget shown during initialization | - ---- - -## Next Steps - -- [Customization Guide]({{ '/guides/customization' | relative_url }}) - Theme and style your widgets -- [Flutter Quickstart]({{ '/getting-started/quickstart-flutter' | relative_url }}) - Get started quickly -- [Authentication Flows]({{ '/guides/authentication' | relative_url }}) - Implement auth flows -- [User Management]({{ '/guides/user-management' | relative_url }}) - Manage user profiles - diff --git a/docs/assets/images/clerk-logo.svg b/docs/assets/images/clerk-logo.svg deleted file mode 100644 index 0d7dd8cc..00000000 --- a/docs/assets/images/clerk-logo.svg +++ /dev/null @@ -1,7 +0,0 @@ - - - - - - - diff --git a/docs/clerk_auth/README.md b/docs/clerk_auth/README.md new file mode 100644 index 00000000..8609a7f1 --- /dev/null +++ b/docs/clerk_auth/README.md @@ -0,0 +1,301 @@ +# Clerk Auth Documentation + +Welcome to the comprehensive documentation for the `clerk_auth` package. This documentation covers all public APIs and provides detailed usage examples. + +## Documentation Index + +### Core Classes + +1. **[Auth](auth.md)** - The main authentication class + - All authentication methods (sign in, sign up, OAuth, passkeys) + - Session management + - User management + - Organization management + - **Special focus on re-entrant methods**: `attemptSignIn()` and `attemptSignUp()` + +2. **[AuthConfig](auth_config.md)** - Configuration for the Auth class + - Required and optional parameters + - Polling and refresh settings + - Telemetry configuration + - Complete configuration examples + +3. **[Persistor](persistor.md)** - Persistence interface for authentication state + - Abstract interface definition + - Built-in implementations (`Persistor.none`, `DefaultPersistor`) + - Custom implementation examples + - Storage keys used by Clerk + +4. **[HttpService](http_service.md)** - HTTP communication interface + - Abstract interface definition + - Default implementation + - Custom implementation examples (logging, retry, mocking) + - HTTP methods and request handling + +--- + +## Quick Start + +### Basic Setup + +```dart +import 'package:clerk_auth/clerk_auth.dart'; +import 'package:path_provider/path_provider.dart'; + +Future main() async { + // Create configuration + final config = AuthConfig( + publishableKey: 'pk_test_...', + persistor: DefaultPersistor( + getCacheDirectory: getApplicationDocumentsDirectory, + ), + ); + + // Create and initialize Auth + final auth = Auth(config: config); + await auth.initialize(); + + // Use auth for authentication + await auth.attemptSignIn( + strategy: Strategy.password, + identifier: 'user@example.com', + password: 'password123', + ); + + if (auth.isSignedIn) { + print('Welcome, ${auth.user?.fullName}!'); + } +} +``` + +--- + +## Key Concepts + +### Re-entrant Authentication Methods + +The `attemptSignIn()` and `attemptSignUp()` methods are designed to be **re-entrant**, meaning they can be called multiple times with different parameters as the user progresses through the authentication flow. + +**Example: Multi-step Sign In** +```dart +// Step 1: Start with email +await auth.attemptSignIn( + strategy: Strategy.emailCode, + identifier: 'user@example.com', +); + +// Step 2: Submit verification code +await auth.attemptSignIn( + strategy: Strategy.emailCode, + code: '123456', +); + +// Step 3: If 2FA required, submit 2FA code +if (auth.signIn?.needsSecondFactor == true) { + await auth.attemptSignIn( + strategy: Strategy.totp, + code: '654321', + ); +} +``` + +### Transfer Flow + +When using OAuth or ID token authentication, users may need to "transfer" between sign-in and sign-up flows: + +```dart +// Try sign in with Apple +await auth.idTokenSignIn( + provider: IdTokenProvider.apple, + idToken: credential.identityToken!, +); + +// If user doesn't exist, transfer to sign up +if (auth.signIn?.isTransferable == true) { + await auth.transfer(); +} +``` + +### State Management + +The Auth class maintains state through the `Client` object: +- `client.signIn` - Active sign-in flow +- `client.signUp` - Active sign-up flow +- `client.sessions` - Active sessions +- `session.user` - Current user + +--- + +## Common Use Cases + +### Email + Password Authentication + +```dart +// Sign up +await auth.attemptSignUp( + strategy: Strategy.emailCode, + emailAddress: 'user@example.com', + password: 'SecurePass123!', + passwordConfirmation: 'SecurePass123!', + firstName: 'John', + lastName: 'Doe', +); + +// Verify email +await auth.attemptSignUp( + strategy: Strategy.emailCode, + code: '123456', +); + +// Sign in +await auth.attemptSignIn( + strategy: Strategy.password, + identifier: 'user@example.com', + password: 'SecurePass123!', +); +``` + +### OAuth Authentication + +```dart +// Google Sign In +await auth.oauthSignIn( + strategy: Strategy.oauthGoogle, + redirect: Uri.parse('myapp://oauth-callback'), +); + +// After redirect, complete sign in +await auth.completeOAuthSignIn(token: rotatingTokenNonce); +``` + +### Passkey Authentication + +```dart +// Create passkey +final passkey = await auth.createPasskey(); +// ... use passkey library to register ... +await auth.attemptPasskeyVerification(passkey!, credentialJson); + +// Sign in with passkey +await auth.attemptSignIn(strategy: Strategy.passkey); +// ... use passkey library to authenticate ... +await auth.attemptSignIn( + strategy: Strategy.passkey, + passkeyCredential: credentialJson, +); +``` + +### Session Management + +```dart +// Get session token +final token = await auth.sessionToken(); +print('JWT: ${token.jwt}'); + +// Listen to token updates +auth.sessionTokenStream.listen((token) { + // Update your API client with new token +}); + +// Switch sessions +await auth.activate(anotherSession); + +// Sign out +await auth.signOut(); +``` + +--- + +## Architecture + +``` +┌─────────────────────────────────────────────────────────┐ +│ Auth │ +│ - Main authentication logic │ +│ - State management │ +│ - Public API │ +└─────────────────┬───────────────────────────────────────┘ + │ + │ uses + │ + ┌─────────────┴─────────────┐ + │ │ + ▼ ▼ +┌─────────────┐ ┌─────────────┐ +│ AuthConfig │ │ Api │ +│ │ │ (internal) │ +└──────┬──────┘ └─────────────┘ + │ + │ contains + │ + ┌───┴────┬──────────┐ + │ │ │ + ▼ ▼ ▼ +┌──────┐ ┌────────┐ ┌─────────────┐ +│Persis│ │ Http │ │ Retry │ +│ tor │ │Service │ │ Options │ +└──────┘ └────────┘ └─────────────┘ +``` + +--- + +## Best Practices + +1. **Always initialize before use**: Call `auth.initialize()` before any other operations +2. **Use appropriate persistor**: `DefaultPersistor` for production, `Persistor.none` for testing +3. **Handle errors gracefully**: Wrap auth calls in try-catch or override `handleError()` +4. **Leverage re-entrant methods**: Call `attemptSignIn()`/`attemptSignUp()` multiple times as needed +5. **Check transfer status**: After OAuth/ID token auth, check `isTransferable` and call `transfer()` +6. **Clean up resources**: Call `auth.terminate()` when disposing +7. **Monitor session tokens**: Use `sessionTokenStream` to keep tokens fresh + +--- + +## Testing + +### Unit Testing + +```dart +import 'package:clerk_auth/clerk_auth.dart'; +import 'package:flutter_test/flutter_test.dart'; + +void main() { + test('sign in test', () async { + final config = AuthConfig( + publishableKey: 'pk_test_...', + persistor: Persistor.none, + sessionTokenPolling: false, + clientRefreshPeriod: Duration.zero, + telemetryPeriod: Duration.zero, + ); + + final auth = Auth(config: config); + await auth.initialize(); + + // Test authentication flows + + auth.terminate(); + }); +} +``` + +--- + +## Additional Resources + +- [Clerk Dashboard](https://dashboard.clerk.com/) +- [Clerk API Reference](https://clerk.com/docs/reference/backend-api) +- [Flutter SDK Guide](https://clerk.com/docs/quickstarts/flutter) +- [clerk_auth on pub.dev](https://pub.dev/packages/clerk_auth) + +--- + +## Version + +*Documentation generated for clerk_auth version 0.0.14-beta* + +--- + +## Contributing + +Found an issue or want to improve the documentation? Please open an issue or pull request on the [GitHub repository](https://github.com/clerk/clerk-sdk-flutter). + diff --git a/docs/clerk_auth/auth.md b/docs/clerk_auth/auth.md new file mode 100644 index 00000000..818545df --- /dev/null +++ b/docs/clerk_auth/auth.md @@ -0,0 +1,1447 @@ +# Clerk Auth API Documentation + +This document provides comprehensive documentation for all public methods in the `Auth` class from `clerk_auth/lib/src/clerk_auth/auth.dart`. + +## Overview + +The `Auth` class is the core of the Clerk authentication system for Dart/Flutter applications. It provides a high-level API for managing user authentication, sessions, and user data. + +### Key Concepts + +**Re-entrant Methods**: The `attemptSignIn()` and `attemptSignUp()` methods are designed to be **re-entrant**, meaning they can be called multiple times with different parameters as the user progresses through the authentication flow. This design allows for flexible, step-by-step authentication processes. + +**State Management**: The Auth class maintains the current authentication state through the `Client` object, which contains: +- `SignIn` object (during sign-in flow) +- `SignUp` object (during sign-up flow) +- `User` object (when signed in) +- `Session` objects (active sessions) + +**Transfer Flow**: When using OAuth or ID token authentication, users may need to "transfer" between sign-in and sign-up flows if they don't exist yet (or already exist). Use the `transfer()` method to handle this seamlessly. + +## Table of Contents + +- [Initialization & Lifecycle](#initialization--lifecycle) +- [Authentication State](#authentication-state) +- [Sign In Methods](#sign-in-methods) +- [Sign Up Methods](#sign-up-methods) +- [OAuth Methods](#oauth-methods) +- [Passkey Methods](#passkey-methods) +- [Session Management](#session-management) +- [User Management](#user-management) +- [Organization Management](#organization-management) +- [Password Management](#password-management) +- [Advanced Methods](#advanced-methods) + +--- + +## Initialization & Lifecycle + +### `initialize()` + +Initializes the Auth object. **Must be called before any other Auth methods.** + +```dart +Future initialize() +``` + +**Example:** +```dart +final auth = Auth(config: AuthConfig( + publishableKey: 'pk_test_...', + persistor: Persistor.none, +)); +await auth.initialize(); +``` + +**Behavior:** +- Loads persisted client and environment data +- Sets up periodic client refresh (if configured) +- Starts session token polling (if enabled) +- Retries fetching client/environment if initial fetch fails + +--- + +### `terminate()` + +Disposes of the Auth object and cleans up resources. + +```dart +void terminate() +``` + +**Example:** +```dart +auth.terminate(); +``` + +**Behavior:** +- Cancels all timers (polling, refresh, etc.) +- Closes stream controllers +- Terminates telemetry and API connections + +--- + +## Authentication State + +### `isSignedIn` + +Returns whether a user is currently signed in. + +```dart +bool get isSignedIn +``` + +**Example:** +```dart +if (auth.isSignedIn) { + print('User: ${auth.user?.fullName}'); +} +``` + +--- + +### `isSigningIn` + +Returns whether a sign-in flow is currently in progress. + +```dart +bool get isSigningIn +``` + +--- + +### `isSigningUp` + +Returns whether a sign-up flow is currently in progress. + +```dart +bool get isSigningUp +``` + +--- + +### `client` + +The current Client object containing authentication state. + +```dart +Client get client +``` + +**Example:** +```dart +final client = auth.client; +print('Active sessions: ${client.sessions.length}'); +``` + +--- + +### `user` + +The currently signed-in User, or null if not signed in. + +```dart +User? get user +``` + +--- + +### `session` + +The current active Session, or null. + +```dart +Session? get session +``` + +--- + +### `signIn` + +The current SignIn object, or null. + +```dart +SignIn? get signIn +``` + +--- + +### `signUp` + +The current SignUp object, or null. + +```dart +SignUp? get signUp +``` + +--- + +### `organization` + +The current active Organization, or null. + +```dart +Organization? get organization +``` + +--- + +### `env` + +The Environment object containing Clerk account configuration. + +```dart +Environment get env +``` + +--- + +### `isNotAvailable` + +Returns whether the Auth object is not yet initialized. + +```dart +bool get isNotAvailable +``` + +**Example:** +```dart +if (auth.isNotAvailable) { + print('Auth is still initializing...'); +} +``` + +--- + +### `handleError()` + +Handles ClerkError exceptions when they occur. Override this method to customize error handling. + +```dart +void handleError(Object error) +``` + +**Default Behavior:** +- Throws the error + +**Example (Custom Error Handling):** +```dart +class MyAuth extends Auth { + MyAuth({required super.config}); + + @override + void handleError(Object error) { + if (error is ClerkError) { + // Log error instead of throwing + print('Clerk error: ${error.message}'); + // Show user-friendly message + showErrorToUser(error.message); + } else { + // Re-throw non-Clerk errors + super.handleError(error); + } + } +} +``` + +--- + +### `update()` + +A method to be overridden by extension classes to handle state updates. Called automatically after most Auth operations. + +```dart +void update() +``` + +**Example:** +```dart +class MyAuth extends Auth with ChangeNotifier { + MyAuth({required super.config}); + + @override + void update() { + super.update(); + notifyListeners(); // Notify UI of changes + } +} +``` + +--- + +## Sign In Methods + +### `attemptSignIn()` + +**Re-entrant method** for progressively signing in a user. Can be called multiple times with updated parameters until sign-in is complete. + +```dart +Future attemptSignIn({ + required Strategy strategy, + String? identifier, + String? password, + String? code, + String? token, + String? redirectUrl, + String? passkeyCredential, +}) +``` + +**Parameters:** +- `strategy`: The authentication strategy (e.g., `Strategy.emailCode`, `Strategy.password`) +- `identifier`: Email, phone, or username +- `password`: User's password (for password-based auth) +- `code`: Verification code (for code-based strategies) +- `token`: OAuth or ID token +- `redirectUrl`: Redirect URL for email link strategy +- `passkeyCredential`: Passkey credential JSON string + +**Example 1: Email + Password Sign In** +```dart +// Initial call with identifier and password +await auth.attemptSignIn( + strategy: Strategy.password, + identifier: 'user@example.com', + password: 'SecurePass123!', +); + +// If 2FA is required, call again with the code +if (auth.signIn?.needsSecondFactor == true) { + await auth.attemptSignIn( + strategy: Strategy.emailCode, + code: '123456', + ); +} +``` + +**Example 2: Email Code Sign In (Re-entrant)** +```dart +// Step 1: Start sign-in with email +await auth.attemptSignIn( + strategy: Strategy.emailCode, + identifier: 'user@example.com', +); + +// Step 2: User receives code, submit it +await auth.attemptSignIn( + strategy: Strategy.emailCode, + code: '123456', +); +``` + +**Example 3: Password Reset** +```dart +// Step 1: Initiate password reset +await auth.initiatePasswordReset( + identifier: 'user@example.com', + strategy: Strategy.resetPasswordEmailCode, +); + +// Step 2: Submit code and new password +await auth.attemptSignIn( + strategy: Strategy.resetPasswordEmailCode, + code: '123456', + password: 'NewSecurePass123!', +); +``` + +**Example 4: Passkey Sign In** +```dart +// Step 1: Prepare passkey sign-in +await auth.attemptSignIn(strategy: Strategy.passkey); + +// Step 2: Get passkey credential from authenticator +final nonce = auth.signIn?.firstFactorVerification?.nonce; +// ... use passkey library to get credential ... + +// Step 3: Complete sign-in with credential +await auth.attemptSignIn( + strategy: Strategy.passkey, + passkeyCredential: credentialJson, +); +``` + +**Re-entrant Behavior:** +- The method intelligently handles the current state of the sign-in flow +- Can be called multiple times as the user progresses through authentication steps +- Automatically creates a SignIn object if one doesn't exist +- Handles preparation and attempt phases based on the strategy and current state + +--- + +### `initiatePasswordReset()` + +Initiates a password reset flow. + +```dart +Future initiatePasswordReset({ + required String identifier, + required Strategy strategy, +}) +``` + +**Example:** +```dart +await auth.initiatePasswordReset( + identifier: 'user@example.com', + strategy: Strategy.resetPasswordEmailCode, +); +``` + +--- + +### `idTokenSignIn()` + +Sign in with an ID token from a provider (e.g., Apple, Google). + +```dart +Future idTokenSignIn({ + required IdTokenProvider provider, + required String idToken, +}) +``` + +**Example:** +```dart +// Apple Sign In +await auth.idTokenSignIn( + provider: IdTokenProvider.apple, + idToken: credential.identityToken!, +); + +// Check if transfer needed (user doesn't exist) +if (auth.signIn?.isTransferable == true) { + await auth.transfer(); // Switch to sign-up flow +} +``` + +**Transfer Flow:** +If the user doesn't exist, the verification status will be `transferable`. Call `transfer()` to switch to the sign-up flow. + +--- + +## Sign Up Methods + +### `attemptSignUp()` + +**Re-entrant method** for progressively signing up a new user. Can be called multiple times with updated parameters until sign-up is complete. + +```dart +Future attemptSignUp({ + required Strategy strategy, + String? firstName, + String? lastName, + String? username, + String? emailAddress, + String? phoneNumber, + String? password, + String? passwordConfirmation, + String? code, + String? token, + String? signature, + String? redirectUrl, + Map? metadata, + bool? legalAccepted, +}) +``` + +**Parameters:** +- `strategy`: The authentication strategy +- `firstName`, `lastName`, `username`: User profile information +- `emailAddress`, `phoneNumber`: Contact identifiers +- `password`, `passwordConfirmation`: Password (must match) +- `code`: Verification code +- `token`: OAuth or ID token +- `signature`: Email link signature +- `redirectUrl`: Redirect URL for email link strategy +- `metadata`: Custom metadata to attach to the user +- `legalAccepted`: Legal consent acceptance (required if enabled in settings) + +**Example 1: Email + Password Sign Up (Re-entrant)** +```dart +// Step 1: Create sign-up with initial data +await auth.attemptSignUp( + strategy: Strategy.emailCode, + emailAddress: 'newuser@example.com', + password: 'SecurePass123!', + passwordConfirmation: 'SecurePass123!', + firstName: 'John', + lastName: 'Doe', +); + +// Step 2: Verify email with code +await auth.attemptSignUp( + strategy: Strategy.emailCode, + code: '123456', +); +``` + +**Example 2: Phone Number Sign Up** +```dart +// Step 1: Create sign-up +await auth.attemptSignUp( + strategy: Strategy.phoneCode, + phoneNumber: '+1234567890', + firstName: 'Jane', + lastName: 'Smith', +); + +// Step 2: Verify phone +await auth.attemptSignUp( + strategy: Strategy.phoneCode, + code: '123456', +); +``` + +**Example 3: Sign Up with Metadata** +```dart +await auth.attemptSignUp( + strategy: Strategy.emailCode, + emailAddress: 'user@example.com', + password: 'SecurePass123!', + passwordConfirmation: 'SecurePass123!', + metadata: { + 'referralCode': 'ABC123', + 'source': 'mobile_app', + }, +); +``` + +**Example 4: Sign Up with Legal Consent** +```dart +await auth.attemptSignUp( + strategy: Strategy.emailCode, + emailAddress: 'user@example.com', + password: 'SecurePass123!', + passwordConfirmation: 'SecurePass123!', + legalAccepted: true, // Required if legal consent is enabled +); +``` + +**Re-entrant Behavior:** +- Can be called multiple times to update sign-up data +- Automatically handles verification preparation and attempts +- Updates existing SignUp object if one exists +- Creates new SignUp object on first call +- Validates password confirmation matches password + +**Throws:** +- `ClerkError` with code `passwordMatchError` if passwords don't match +- `ClerkError` with code `legalAcceptanceRequired` if legal consent is required but not provided + +--- + +### `idTokenSignUp()` + +Sign up with an ID token from a provider (e.g., Apple, Google). + +```dart +Future idTokenSignUp({ + required IdTokenProvider provider, + required String idToken, + String? firstName, + String? lastName, +}) +``` + +**Example:** +```dart +// Apple Sign Up +await auth.idTokenSignUp( + provider: IdTokenProvider.apple, + idToken: credential.identityToken!, + firstName: credential.givenName, + lastName: credential.familyName, +); + +// Check if transfer needed (user already exists) +if (auth.signUp?.isTransferable == true) { + await auth.transfer(); // Switch to sign-in flow +} +``` + +**Transfer Flow:** +If the user already exists, the verification status will be `transferable`. Call `transfer()` to switch to the sign-in flow. + +--- + +### `resendCode()` + +Resends a verification code for the given strategy. + +```dart +Future resendCode(Strategy strategy) +``` + +**Example:** +```dart +// During sign-up or sign-in +await auth.resendCode(Strategy.emailCode); +``` + +**Throws:** +- `ClerkError` with code `noInitialCodeHasBeenSetUpToResend` if no code flow is active + +--- + +## OAuth Methods + +### `oauthSignIn()` + +Prepares for sign-in via an OAuth provider. + +```dart +Future oauthSignIn({ + required Strategy strategy, + required Uri? redirect, +}) +``` + +**Example:** +```dart +await auth.oauthSignIn( + strategy: Strategy.oauthGoogle, + redirect: Uri.parse('myapp://oauth-callback'), +); + +// The user will be redirected to the OAuth provider +// After authentication, complete the flow with completeOAuthSignIn() +``` + +--- + +### `completeOAuthSignIn()` + +Completes OAuth sign-in by presenting the token. + +```dart +Future completeOAuthSignIn({ + required String token, +}) +``` + +**Example:** +```dart +// After OAuth redirect +await auth.completeOAuthSignIn(token: rotatingTokenNonce); +``` + +--- + +### `oauthConnect()` + +Connects an external account via an OAuth provider to the current user. + +```dart +Future oauthConnect({ + required Strategy strategy, + required Uri? redirect, +}) +``` + +**Example:** +```dart +// Connect Google account to existing user +await auth.oauthConnect( + strategy: Strategy.oauthGoogle, + redirect: Uri.parse('myapp://oauth-callback'), +); +``` + +--- + +### `deleteExternalAccount()` + +Deletes an external account connection. + +```dart +Future deleteExternalAccount({ + required ExternalAccount account, +}) +``` + +**Example:** +```dart +final googleAccount = auth.user?.externalAccounts + ?.firstWhere((acc) => acc.provider == 'google'); +if (googleAccount != null) { + await auth.deleteExternalAccount(account: googleAccount); +} +``` + +--- + +## Passkey Methods + +### `createPasskey()` + +Creates an unverified passkey for the current user. + +```dart +Future createPasskey() +``` + +**Example:** +```dart +final passkey = await auth.createPasskey(); +if (passkey?.verification?.nonce case VerificationNonce nonce) { + // Use passkey library to register + final authenticator = PasskeyAuthenticator(); + final challenge = RegisterRequestType( + challenge: nonce.challenge, + relyingParty: nonce.relyingParty.toRelyingPartyType(), + user: nonce.user!.toUserType(), + excludeCredentials: const [], + timeout: nonce.timeout, + ); + final credential = await authenticator.register(challenge); + + // Verify the passkey + await auth.attemptPasskeyVerification(passkey!, credential.toJsonString()); +} +``` + +--- + +### `attemptPasskeyVerification()` + +Verifies a passkey with the provided credential. + +```dart +Future attemptPasskeyVerification( + Passkey passkey, + String credential, +) +``` + +**Example:** +```dart +await auth.attemptPasskeyVerification(passkey, credentialJson); +``` + +--- + +## Session Management + +### `sessionToken()` + +Gets the current session token for an organization. + +```dart +Future sessionToken({ + Organization? organization, + String? templateName, +}) +``` + +**Example:** +```dart +// Get default session token +final token = await auth.sessionToken(); +print('JWT: ${token.jwt}'); + +// Get token for specific organization +final orgToken = await auth.sessionToken( + organization: myOrganization, +); + +// Get token with custom template +final customToken = await auth.sessionToken( + templateName: 'my_custom_template', +); +``` + +**Throws:** +- `ClerkError` with code `noSessionTokenRetrieved` if token cannot be retrieved + +--- + +### `sessionTokenStream` + +Stream of SessionTokens as they renew. + +```dart +Stream get sessionTokenStream +``` + +**Example:** +```dart +auth.sessionTokenStream.listen((token) { + print('New token: ${token.jwt}'); + // Update your API client with new token +}); +``` + +--- + +### `activate()` + +Activates the given session. + +```dart +Future activate(Session session) +``` + +**Example:** +```dart +final sessions = auth.client.sessions; +if (sessions.length > 1) { + await auth.activate(sessions[1]); // Switch to another session +} +``` + +--- + +### `signOut()` + +Signs out of all sessions and deletes the current client. + +```dart +Future signOut() +``` + +**Example:** +```dart +await auth.signOut(); +``` + +--- + +### `signOutOf()` + +Signs out of a specific session. + +```dart +Future signOutOf(Session session) +``` + +**Example:** +```dart +await auth.signOutOf(auth.session!); +``` + +--- + +### `transfer()` + +Transfers an OAuth authentication into a User (handles sign-in/sign-up transfer flow). + +```dart +Future transfer() +``` + +**Example:** +```dart +// After OAuth or ID token authentication +if (auth.signIn?.isTransferable == true) { + await auth.transfer(); // Switch to sign-up +} else if (auth.signUp?.isTransferable == true) { + await auth.transfer(); // Switch to sign-in +} +``` + +--- + +## User Management + +### `updateUser()` + +Updates the current user's profile information. + +```dart +Future updateUser({ + String? username, + String? firstName, + String? lastName, + String? primaryEmailAddressId, + String? primaryPhoneNumberId, + String? primaryWeb3WalletId, + Map? metadata, + File? avatar, +}) +``` + +**Example:** +```dart +await auth.updateUser( + firstName: 'John', + lastName: 'Doe', + username: 'johndoe', + metadata: {'theme': 'dark'}, +); +``` + +--- + +### `deleteUser()` + +Deletes the current user. + +```dart +Future deleteUser() +``` + +**Example:** +```dart +if (auth.env.user.actions.deleteSelf) { + await auth.deleteUser(); +} +``` + +**Throws:** +- `ClerkError` with code `cannotDeleteSelf` if user is not authorized to delete themselves + +--- + +### `updateUserImage()` + +Updates the user's avatar. + +```dart +Future updateUserImage(File file) +``` + +**Example:** +```dart +final imageFile = File('/path/to/avatar.jpg'); +await auth.updateUserImage(imageFile); +``` + +--- + +### `deleteUserImage()` + +Deletes the user's avatar. + +```dart +Future deleteUserImage() +``` + +**Example:** +```dart +await auth.deleteUserImage(); +``` + +--- + +### `addIdentifyingData()` + +Adds an email, phone number, or other identifier to the current user. + +```dart +Future addIdentifyingData( + String identifier, + IdentifierType type, +) +``` + +**Example:** +```dart +// Add email address +await auth.addIdentifyingData( + 'newemail@example.com', + IdentifierType.emailAddress, +); + +// Add phone number +await auth.addIdentifyingData( + '+1234567890', + IdentifierType.phoneNumber, +); +``` + +--- + +### `verifyIdentifyingData()` + +Verifies user identifying data with a code. + +```dart +Future verifyIdentifyingData( + UserIdentifyingData uid, + String code, +) +``` + +**Example:** +```dart +final email = auth.user?.emailAddresses + ?.firstWhere((e) => e.emailAddress == 'newemail@example.com'); +if (email != null) { + await auth.verifyIdentifyingData(email, '123456'); +} +``` + +--- + +### `deleteIdentifyingData()` + +Deletes user identifying data. + +```dart +Future deleteIdentifyingData( + UserIdentifyingData uid, +) +``` + +**Example:** +```dart +final email = auth.user?.emailAddresses?.first; +if (email != null) { + await auth.deleteIdentifyingData(email); +} +``` + +--- + +## Password Management + +### `updateUserPassword()` + +Updates the current user's password. + +```dart +Future updateUserPassword( + String currentPassword, + String newPassword, { + bool signOut = true, +}) +``` + +**Example:** +```dart +await auth.updateUserPassword( + 'OldPassword123!', + 'NewPassword456!', + signOut: false, // Keep user signed in +); +``` + +--- + +### `deleteUserPassword()` + +Deletes the current user's password. + +```dart +Future deleteUserPassword(String currentPassword) +``` + +**Example:** +```dart +await auth.deleteUserPassword('CurrentPassword123!'); +``` + +--- + +## Organization Management + +### `createOrganization()` + +Creates a new organization. + +```dart +Future createOrganization({ + required String name, + String? slug, + File? logo, +}) +``` + +**Example:** +```dart +await auth.createOrganization( + name: 'Acme Corp', + slug: 'acme-corp', + logo: File('/path/to/logo.png'), +); +``` + +--- + +### `updateOrganization()` + +Updates an organization. + +```dart +Future updateOrganization({ + required Organization organization, + String? name, + File? logo, +}) +``` + +**Example:** +```dart +await auth.updateOrganization( + organization: auth.organization!, + name: 'New Name', + logo: File('/path/to/new-logo.png'), +); +``` + +--- + +### `setActiveOrganization()` + +Makes an organization active. + +```dart +Future setActiveOrganization(Organization organization) +``` + +**Example:** +```dart +final orgs = auth.user?.organizationMemberships; +if (orgs != null && orgs.isNotEmpty) { + await auth.setActiveOrganization(orgs.first.organization); +} +``` + +--- + +### `leaveOrganization()` + +Leaves an organization. + +```dart +Future leaveOrganization({ + required Organization organization, + Session? session, +}) +``` + +**Example:** +```dart +final success = await auth.leaveOrganization( + organization: auth.organization!, +); +``` + +--- + +### `fetchOrganizationInvitations()` + +Fetches all organization invitations for the user. + +```dart +Future> fetchOrganizationInvitations() +``` + +**Example:** +```dart +final invitations = await auth.fetchOrganizationInvitations(); +for (final invitation in invitations) { + print('Invited to: ${invitation.organizationName}'); +} +``` + +--- + +### `acceptOrganizationInvitation()` + +Accepts an organization invitation. + +```dart +Future acceptOrganizationInvitation( + OrganizationInvitation invitation, +) +``` + +**Example:** +```dart +final invitations = await auth.fetchOrganizationInvitations(); +if (invitations.isNotEmpty) { + await auth.acceptOrganizationInvitation(invitations.first); +} +``` + +--- + +### `fetchOrganizationDomains()` + +Fetches all domains for an organization. + +```dart +Future> fetchOrganizationDomains({ + required Organization organization, +}) +``` + +**Example:** +```dart +final domains = await auth.fetchOrganizationDomains( + organization: auth.organization!, +); +``` + +--- + +### `createDomain()` + +Creates a new domain within an organization. + +```dart +Future createDomain({ + required Organization organization, + required String name, + required EnrollmentMode mode, +}) +``` + +**Example:** +```dart +await auth.createDomain( + organization: auth.organization!, + name: 'example.com', + mode: EnrollmentMode.automaticInvitation, +); +``` + +--- + +## Advanced Methods + +### `refreshClient()` + +Refreshes the current client from the server. + +```dart +Future refreshClient() +``` + +**Example:** +```dart +await auth.refreshClient(); +``` + +--- + +### `resetClient()` + +Resets the current client, clearing any SignUp or SignIn objects. + +```dart +Future resetClient() +``` + +**Example:** +```dart +await auth.resetClient(); +``` + +--- + +### `refreshEnvironment()` + +Refreshes the current environment from the server. + +```dart +Future refreshEnvironment() +``` + +**Example:** +```dart +await auth.refreshEnvironment(); +``` + +--- + +### `fetchApiResponse()` + +Low-level access to the Clerk API. + +```dart +Future fetchApiResponse( + String url, { + HttpMethod method = HttpMethod.post, + Map? headers, + Map? params, + Map? nullableParams, + bool withSession = false, +}) +``` + +**Example:** +```dart +final response = await auth.fetchApiResponse( + '/users/me/custom_endpoint', + method: HttpMethod.get, + withSession: true, +); +``` + +**Note:** This method will be deprecated in a future version. Use only for advanced use cases not covered by other methods. + +--- + +## Error Handling + +All methods may throw `ClerkError` exceptions. Use try-catch blocks or override the `handleError` method to handle errors. + +**Example:** +```dart +try { + await auth.attemptSignIn( + strategy: Strategy.password, + identifier: 'user@example.com', + password: 'wrong_password', + ); +} on ClerkError catch (error) { + print('Error: ${error.message}'); + print('Code: ${error.code}'); +} +``` + +**Custom Error Handling:** +```dart +class MyAuth extends Auth { + MyAuth({required super.config}); + + @override + void handleError(Object error) { + // Custom error handling + if (error is ClerkError) { + print('Clerk error: ${error.message}'); + } + // Don't call super.handleError() to prevent throwing + } +} +``` + +--- + +## Common Patterns + +### Complete Sign-In Flow + +```dart +// Email + Password with optional 2FA +Future signInUser(String email, String password) async { + await auth.attemptSignIn( + strategy: Strategy.password, + identifier: email, + password: password, + ); + + // Check if 2FA is required + if (auth.signIn?.needsSecondFactor == true) { + // Prompt user for 2FA code + final code = await promptUserFor2FACode(); + await auth.attemptSignIn( + strategy: Strategy.emailCode, + code: code, + ); + } + + // User is now signed in + print('Welcome, ${auth.user?.fullName}!'); +} +``` + +### Complete Sign-Up Flow + +```dart +// Email + Password sign-up with verification +Future signUpUser({ + required String email, + required String password, + required String firstName, + required String lastName, +}) async { + // Step 1: Create sign-up + await auth.attemptSignUp( + strategy: Strategy.emailCode, + emailAddress: email, + password: password, + passwordConfirmation: password, + firstName: firstName, + lastName: lastName, + ); + + // Step 2: Verify email + final code = await promptUserForEmailCode(); + await auth.attemptSignUp( + strategy: Strategy.emailCode, + code: code, + ); + + // User is now signed up and signed in + print('Welcome, ${auth.user?.fullName}!'); +} +``` + +### OAuth Sign-In with Transfer + +```dart +// Apple Sign In with automatic transfer +Future appleSignIn(String idToken, {String? firstName, String? lastName}) async { + // Try sign-in first + await auth.idTokenSignIn( + provider: IdTokenProvider.apple, + idToken: idToken, + ); + + // If user doesn't exist, transfer to sign-up + if (auth.signIn?.isTransferable == true) { + await auth.transfer(); + + // Update with additional info if available + if (firstName != null || lastName != null) { + await auth.attemptSignUp( + strategy: Strategy.oauthTokenApple, + firstName: firstName, + lastName: lastName, + ); + } + } + + print('Signed in: ${auth.user?.fullName}'); +} +``` + +### Multi-Session Management + +```dart +// Switch between multiple sessions +Future switchSession(int sessionIndex) async { + final sessions = auth.client.sessions; + if (sessionIndex < sessions.length) { + await auth.activate(sessions[sessionIndex]); + print('Switched to: ${auth.user?.fullName}'); + } +} + +// Sign out of specific session +Future signOutSession(Session session) async { + await auth.signOutOf(session); + print('Signed out of session: ${session.id}'); +} +``` + +--- + +## Best Practices + +1. **Always initialize before use**: Call `initialize()` before any other Auth methods +2. **Handle errors gracefully**: Wrap Auth calls in try-catch blocks or override `handleError()` +3. **Use re-entrant methods correctly**: `attemptSignIn()` and `attemptSignUp()` are designed to be called multiple times +4. **Check transfer status**: After OAuth/ID token auth, check `isTransferable` and call `transfer()` if needed +5. **Clean up resources**: Call `terminate()` when disposing of the Auth object +6. **Monitor session tokens**: Use `sessionTokenStream` to keep your API client updated with fresh tokens +7. **Validate passwords**: Ensure password and passwordConfirmation match before calling `attemptSignUp()` +8. **Check environment settings**: Use `env` to check what features are enabled before attempting operations + +--- + +## Related Documentation + +- [Clerk Dashboard](https://dashboard.clerk.com/) +- [Clerk API Reference](https://clerk.com/docs/reference/backend-api) +- [Flutter SDK Guide](https://clerk.com/docs/quickstarts/flutter) + +--- + +*Generated for clerk_auth version 0.0.14-beta* + + diff --git a/docs/clerk_auth/auth_config.md b/docs/clerk_auth/auth_config.md new file mode 100644 index 00000000..9a466c29 --- /dev/null +++ b/docs/clerk_auth/auth_config.md @@ -0,0 +1,520 @@ +# AuthConfig Documentation + +This document provides comprehensive documentation for the `AuthConfig` class from `clerk_auth/lib/src/clerk_auth/auth_config.dart`. + +## Overview + +The `AuthConfig` class holds all configurable items required for the `Auth` class, with sensible defaults. It is the primary configuration object for initializing Clerk authentication in your application. + +## Class Definition + +```dart +class AuthConfig { + const AuthConfig({ + required this.publishableKey, + required this.persistor, + this.flags = const SdkFlags(), + this.sessionTokenPolling = true, + this.retryOptions = const RetryOptions(), + this.defaultSessionTokenTemplate, + LocalesLookup? localesLookup, + bool? isTestMode, + String? telemetryEndpoint, + Duration? telemetryPeriod, + Duration? clientRefreshPeriod, + Duration? httpConnectionTimeout, + HttpService? httpService, + }); +} +``` + +--- + +## Required Parameters + +### `publishableKey` + +**Type:** `String` + +The publishable key from your Clerk dashboard that identifies your auth service account. + +**Example:** +```dart +final config = AuthConfig( + publishableKey: 'pk_test_...', + persistor: myPersistor, +); +``` + +**Where to find it:** +1. Go to [Clerk Dashboard](https://dashboard.clerk.com/) +2. Select your application +3. Navigate to API Keys +4. Copy the "Publishable Key" + +--- + +### `persistor` + +**Type:** `Persistor` + +The persistor used for storing authentication state between app sessions. + +**Example:** +```dart +// Using DefaultPersistor +final config = AuthConfig( + publishableKey: 'pk_test_...', + persistor: DefaultPersistor( + getCacheDirectory: () => getApplicationDocumentsDirectory(), + ), +); + +// Using no persistence (testing only) +final config = AuthConfig( + publishableKey: 'pk_test_...', + persistor: Persistor.none, +); +``` + +**See also:** [Persistor Documentation](persistor.md) + +--- + +## Optional Parameters + +### `flags` + +**Type:** `SdkFlags` +**Default:** `const SdkFlags()` + +Flags used to affect SDK behavior. Extended by `clerk_flutter` for Flutter-specific flags. + +**Example:** +```dart +final config = AuthConfig( + publishableKey: 'pk_test_...', + persistor: myPersistor, + flags: const SdkFlags(), +); +``` + +--- + +### `sessionTokenPolling` + +**Type:** `bool` +**Default:** `true` + +Whether to regularly poll for new session tokens. When enabled, the SDK automatically refreshes session tokens before they expire. + +**Example:** +```dart +// Enable automatic token refresh (default) +final config = AuthConfig( + publishableKey: 'pk_test_...', + persistor: myPersistor, + sessionTokenPolling: true, +); + +// Disable automatic token refresh +final config = AuthConfig( + publishableKey: 'pk_test_...', + persistor: myPersistor, + sessionTokenPolling: false, +); +``` + +**When to disable:** +- Testing scenarios where you want manual control +- Applications that don't need long-lived sessions + +--- + +### `retryOptions` + +**Type:** `RetryOptions` +**Default:** `const RetryOptions()` + +Options for retrying failed HTTP requests. Uses the `retry` package. + +**Example:** +```dart +import 'package:retry/retry.dart'; + +final config = AuthConfig( + publishableKey: 'pk_test_...', + persistor: myPersistor, + retryOptions: RetryOptions( + maxAttempts: 5, + delayFactor: Duration(milliseconds: 200), + maxDelay: Duration(seconds: 10), + ), +); +``` + +**RetryOptions properties:** +- `maxAttempts`: Maximum number of retry attempts (default: 8) +- `delayFactor`: Initial delay between retries (default: 200ms) +- `maxDelay`: Maximum delay between retries (default: 30s) +- `randomizationFactor`: Randomization factor for delays (default: 0.25) + +--- + +### `defaultSessionTokenTemplate` + +**Type:** `String?` +**Default:** `null` + +Default template name for session token retrieval. Useful when using custom JWT templates. + +**Example:** +```dart +final config = AuthConfig( + publishableKey: 'pk_test_...', + persistor: myPersistor, + defaultSessionTokenTemplate: 'my_custom_template', +); +``` + +**Use case:** +When you have custom claims or need specific JWT structure for your backend API. + +--- + +### `localesLookup` + +**Type:** `LocalesLookup` (function: `List Function()`) +**Default:** `Auth.defaultLocalesLookup` (returns `['en']`) + +Function to return the current user's locale preferences for translations. + +**Example:** +```dart +final config = AuthConfig( + publishableKey: 'pk_test_...', + persistor: myPersistor, + localesLookup: () { + // Return user's preferred locales + return ['es', 'en']; // Spanish first, English fallback + }, +); +``` + +--- + +### `isTestMode` + +**Type:** `bool` +**Default:** `false` + +Whether the SDK is running in test mode. + +**Example:** +```dart +final config = AuthConfig( + publishableKey: 'pk_test_...', + persistor: myPersistor, + isTestMode: true, +); +``` + +--- + +### `telemetryEndpoint` + +**Type:** `String` +**Default:** `'https://clerk-telemetry.com/v1/event'` + +The endpoint to send telemetry data to. + +**Example:** +```dart +final config = AuthConfig( + publishableKey: 'pk_test_...', + persistor: myPersistor, + telemetryEndpoint: 'https://my-custom-telemetry.com/events', +); +``` + +--- + +### `telemetryPeriod` + +**Type:** `Duration` +**Default:** `Duration(milliseconds: 29300)` (~30 seconds) + +The duration between sends of telemetry data. Set to `Duration.zero` to disable telemetry. + +**Example:** +```dart +// Custom telemetry interval +final config = AuthConfig( + publishableKey: 'pk_test_...', + persistor: myPersistor, + telemetryPeriod: Duration(minutes: 1), +); + +// Disable telemetry +final config = AuthConfig( + publishableKey: 'pk_test_...', + persistor: myPersistor, + telemetryPeriod: Duration.zero, +); +``` + +**Note:** The default is slightly offset from 30s to avoid repeated clashes with other regular tasks. + +--- + +### `clientRefreshPeriod` + +**Type:** `Duration` +**Default:** `Duration(milliseconds: 9700)` (~10 seconds) + +The duration between calls to refresh the client object. Set to `Duration.zero` to disable automatic client refresh. + +**Example:** +```dart +// Custom refresh interval +final config = AuthConfig( + publishableKey: 'pk_test_...', + persistor: myPersistor, + clientRefreshPeriod: Duration(seconds: 30), +); + +// Disable automatic refresh +final config = AuthConfig( + publishableKey: 'pk_test_...', + persistor: myPersistor, + clientRefreshPeriod: Duration.zero, +); +``` + +**Note:** The default is slightly offset from 10s to avoid repeated clashes with other regular tasks. + +**When to adjust:** +- Increase for battery-sensitive applications +- Decrease for applications requiring real-time updates +- Disable for complete manual control + +--- + +### `httpConnectionTimeout` + +**Type:** `Duration` +**Default:** `Duration(milliseconds: 500)` + +The duration to wait for HTTP connectivity before timing out in a connectivity test. + +**Example:** +```dart +final config = AuthConfig( + publishableKey: 'pk_test_...', + persistor: myPersistor, + httpConnectionTimeout: Duration(seconds: 2), +); +``` + +--- + +### `httpService` + +**Type:** `HttpService` +**Default:** `DefaultHttpService()` + +The HTTP service used to communicate with the Clerk backend. + +**Example:** +```dart +// Using default HTTP service +final config = AuthConfig( + publishableKey: 'pk_test_...', + persistor: myPersistor, + httpService: DefaultHttpService(), +); + +// Using custom HTTP service +class MyHttpService implements HttpService { + // ... implement interface +} + +final config = AuthConfig( + publishableKey: 'pk_test_...', + persistor: myPersistor, + httpService: MyHttpService(), +); +``` + +**See also:** [HttpService Documentation](http_service.md) + +--- + +## Methods + +### `initialize()` + +Initializes the configuration by initializing the persistor and HTTP service. + +```dart +Future initialize() +``` + +**Example:** +```dart +final config = AuthConfig( + publishableKey: 'pk_test_...', + persistor: myPersistor, +); +await config.initialize(); +``` + +**Note:** This is typically called automatically by `Auth.initialize()`. + +--- + +### `terminate()` + +Terminates the configuration by terminating the HTTP service and persistor. + +```dart +void terminate() +``` + +**Example:** +```dart +config.terminate(); +``` + +**Note:** This is typically called automatically by `Auth.terminate()`. + +--- + +## Complete Examples + +### Basic Configuration + +```dart +import 'package:clerk_auth/clerk_auth.dart'; +import 'package:path_provider/path_provider.dart'; + +Future createBasicConfig() async { + return AuthConfig( + publishableKey: 'pk_test_Y2xlcmsuZXhhbXBsZS5jb20k', + persistor: DefaultPersistor( + getCacheDirectory: getApplicationDocumentsDirectory, + ), + ); +} +``` + +### Production Configuration + +```dart +import 'package:clerk_auth/clerk_auth.dart'; +import 'package:path_provider/path_provider.dart'; +import 'package:retry/retry.dart'; + +Future createProductionConfig() async { + return AuthConfig( + publishableKey: 'pk_live_...', + persistor: DefaultPersistor( + getCacheDirectory: getApplicationDocumentsDirectory, + ), + sessionTokenPolling: true, + retryOptions: RetryOptions( + maxAttempts: 5, + delayFactor: Duration(milliseconds: 200), + ), + clientRefreshPeriod: Duration(seconds: 15), + telemetryPeriod: Duration(seconds: 60), + ); +} +``` + +### Testing Configuration + +```dart +import 'package:clerk_auth/clerk_auth.dart'; + +AuthConfig createTestConfig() { + return AuthConfig( + publishableKey: 'pk_test_...', + persistor: Persistor.none, // No persistence in tests + isTestMode: true, + sessionTokenPolling: false, + clientRefreshPeriod: Duration.zero, + telemetryPeriod: Duration.zero, + ); +} +``` + +### Custom Locale Configuration + +```dart +import 'package:clerk_auth/clerk_auth.dart'; +import 'package:flutter/material.dart'; + +AuthConfig createLocalizedConfig(BuildContext context) { + return AuthConfig( + publishableKey: 'pk_test_...', + persistor: myPersistor, + localesLookup: () { + final locale = Localizations.localeOf(context); + return [locale.languageCode, 'en']; // User's locale + English fallback + }, + ); +} +``` + +### Performance-Optimized Configuration + +```dart +import 'package:clerk_auth/clerk_auth.dart'; + +AuthConfig createOptimizedConfig() { + return AuthConfig( + publishableKey: 'pk_test_...', + persistor: myPersistor, + // Reduce polling frequency for battery savings + clientRefreshPeriod: Duration(seconds: 30), + telemetryPeriod: Duration(minutes: 5), + // Faster timeout for better UX + httpConnectionTimeout: Duration(milliseconds: 300), + ); +} +``` + +--- + +## Best Practices + +1. **Always use a Persistor in production**: Use `DefaultPersistor` or a custom implementation. Only use `Persistor.none` for testing. + +2. **Secure your publishable key**: Store it in environment variables or secure configuration, not hardcoded in source. + +3. **Adjust polling based on use case**: + - Real-time apps: Shorter `clientRefreshPeriod` + - Battery-sensitive apps: Longer periods or disable polling + - Background apps: Disable polling and refresh manually + +4. **Configure retry options for reliability**: Adjust based on your network conditions and user experience requirements. + +5. **Use custom session token templates**: When you need specific JWT claims for your backend API. + +6. **Disable telemetry in development**: Set `telemetryPeriod` to `Duration.zero` during development if desired. + +7. **Test with different configurations**: Create separate configs for development, staging, and production. + +--- + +## Related Documentation + +- [Auth Documentation](auth.md) +- [Persistor Documentation](persistor.md) +- [HttpService Documentation](http_service.md) +- [Clerk Dashboard](https://dashboard.clerk.com/) + +--- + +*Generated for clerk_auth version 0.0.14-beta* diff --git a/docs/clerk_auth/http_service.md b/docs/clerk_auth/http_service.md new file mode 100644 index 00000000..b6785e44 --- /dev/null +++ b/docs/clerk_auth/http_service.md @@ -0,0 +1,657 @@ +# HttpService Documentation + +This document provides comprehensive documentation for the `HttpService` interface and its implementations from `clerk_auth/lib/src/clerk_auth/http_service.dart`. + +## Overview + +The `HttpService` abstract interface defines how the Clerk SDK communicates with the Clerk backend over HTTP. It provides methods for sending requests, uploading files, and checking connectivity. + +## HttpMethod Enum + +```dart +enum HttpMethod { + delete, + get, + patch, + post, + put, +} +``` + +**Methods:** +- `delete`: HTTP DELETE +- `get`: HTTP GET +- `patch`: HTTP PATCH +- `post`: HTTP POST +- `put`: HTTP PUT + +**Properties:** +- `isGet`: Returns `true` if method is GET +- `isNotGet`: Returns `true` if method is not GET +- `toString()`: Returns uppercase method name (e.g., "GET", "POST") + +**Example:** +```dart +final method = HttpMethod.post; +print(method.toString()); // "POST" +print(method.isGet); // false +print(method.isNotGet); // true +``` + +--- + +## Abstract Interface + +```dart +abstract interface class HttpService { + const HttpService(); + + Future initialize(); + void terminate(); + Future ping(Uri uri, {required Duration timeout}); + Future send( + HttpMethod method, + Uri uri, { + Map? headers, + Map? params, + String? body, + }); + Future sendByteStream( + HttpMethod method, + Uri uri, + http.ByteStream byteStream, + int length, + Map headers, + ); +} +``` + +--- + +## Methods + +### `initialize()` + +Initializes the HTTP service. May be called multiple times and must handle that gracefully. + +```dart +Future initialize() +``` + +**Example:** +```dart +final httpService = DefaultHttpService(); +await httpService.initialize(); +``` + +**Note:** The default implementation is a no-op. + +--- + +### `terminate()` + +Terminates the HTTP service and cleans up resources. May be called multiple times and must handle that gracefully. + +```dart +void terminate() +``` + +**Example:** +```dart +httpService.terminate(); +``` + +--- + +### `ping()` + +Checks that connectivity to an endpoint is available. + +```dart +Future ping(Uri uri, {required Duration timeout}) +``` + +**Parameters:** +- `uri`: The endpoint to ping +- `timeout`: Maximum time to wait for response + +**Returns:** +- `true` if endpoint is reachable and returns 200 status +- `false` otherwise + +**Example:** +```dart +final isReachable = await httpService.ping( + Uri.parse('https://api.clerk.com'), + timeout: Duration(seconds: 5), +); +if (isReachable) { + print('API is reachable'); +} +``` + +--- + +### `send()` + +Sends an HTTP request to the backend and receives a response. + +```dart +Future send( + HttpMethod method, + Uri uri, { + Map? headers, + Map? params, + String? body, +}) +``` + +**Parameters:** +- `method`: The HTTP method to use +- `uri`: The endpoint URI +- `headers`: Optional HTTP headers +- `params`: Optional form parameters (converted to body fields) +- `body`: Optional request body (raw string) + +**Returns:** +- `http.Response` from the server + +**Example:** +```dart +final response = await httpService.send( + HttpMethod.post, + Uri.parse('https://api.clerk.com/v1/client'), + headers: { + 'Authorization': 'Bearer token', + 'Content-Type': 'application/json', + }, + body: jsonEncode({'key': 'value'}), +); + +print('Status: ${response.statusCode}'); +print('Body: ${response.body}'); +``` + +**With params:** +```dart +final response = await httpService.send( + HttpMethod.post, + Uri.parse('https://api.clerk.com/v1/sign_in'), + params: { + 'identifier': 'user@example.com', + 'password': 'password123', + }, +); +``` + +--- + +### `sendByteStream()` + +Uploads a file to the backend using a multipart request. + +```dart +Future sendByteStream( + HttpMethod method, + Uri uri, + http.ByteStream byteStream, + int length, + Map headers, +) +``` + +**Parameters:** +- `method`: The HTTP method to use +- `uri`: The endpoint URI +- `byteStream`: The file data as a byte stream +- `length`: The length of the byte stream +- `headers`: HTTP headers + +**Returns:** +- `http.Response` from the server + +**Example:** +```dart +import 'dart:io'; +import 'package:http/http.dart' as http; + +final file = File('/path/to/avatar.jpg'); +final byteStream = http.ByteStream(file.openRead()); +final length = await file.length(); + +final response = await httpService.sendByteStream( + HttpMethod.post, + Uri.parse('https://api.clerk.com/v1/me/profile_image'), + byteStream, + length, + { + 'Authorization': 'Bearer token', + }, +); +``` + +--- + +## DefaultHttpService + +The default implementation of `HttpService` using the `http` package. + +### Constructor + +```dart +const DefaultHttpService() +``` + +**Example:** +```dart +final httpService = DefaultHttpService(); +``` + +### Behavior + +- Uses a single `http.Client` instance per `DefaultHttpService` instance +- Automatically manages client lifecycle +- Cleans up client on `terminate()` +- Thread-safe client management + +### Implementation Details + +**Client Management:** +```dart +static final _clients = {}; +http.Client get _client => _clients[this] ??= http.Client(); +``` + +**Ping Implementation:** +- Uses HTTP HEAD request +- Returns `true` only if status code is 200 +- Catches all exceptions and returns `false` + +**Send Implementation:** +- Creates `http.Request` with method and URI +- Adds headers if provided +- Converts params to body fields if provided +- Sets body if provided +- Sends request and converts streamed response to regular response + +**SendByteStream Implementation:** +- Creates `http.MultipartRequest` +- Adds file as multipart file with field name "file" +- Uses hash code as filename +- Sends request and converts streamed response + +--- + +## Custom Implementation + +You can create custom HTTP service implementations for specific requirements. + +### Example: Logging HTTP Service + +```dart +class LoggingHttpService implements HttpService { + final HttpService _delegate; + + LoggingHttpService(this._delegate); + + @override + Future initialize() => _delegate.initialize(); + + @override + void terminate() => _delegate.terminate(); + + @override + Future ping(Uri uri, {required Duration timeout}) async { + print('Pinging: $uri'); + final result = await _delegate.ping(uri, timeout: timeout); + print('Ping result: $result'); + return result; + } + + @override + Future send( + HttpMethod method, + Uri uri, { + Map? headers, + Map? params, + String? body, + }) async { + print('Sending $method to $uri'); + final response = await _delegate.send( + method, + uri, + headers: headers, + params: params, + body: body, + ); + print('Response: ${response.statusCode}'); + return response; + } + + @override + Future sendByteStream( + HttpMethod method, + Uri uri, + http.ByteStream byteStream, + int length, + Map headers, + ) async { + print('Uploading file to $uri (${length} bytes)'); + final response = await _delegate.sendByteStream( + method, + uri, + byteStream, + length, + headers, + ); + print('Upload response: ${response.statusCode}'); + return response; + } +} +``` + +### Example: Retry HTTP Service + +```dart +import 'package:retry/retry.dart'; + +class RetryHttpService implements HttpService { + final HttpService _delegate; + final RetryOptions _retryOptions; + + RetryHttpService(this._delegate, { + RetryOptions? retryOptions, + }) : _retryOptions = retryOptions ?? const RetryOptions(); + + @override + Future initialize() => _delegate.initialize(); + + @override + void terminate() => _delegate.terminate(); + + @override + Future ping(Uri uri, {required Duration timeout}) { + return _delegate.ping(uri, timeout: timeout); + } + + @override + Future send( + HttpMethod method, + Uri uri, { + Map? headers, + Map? params, + String? body, + }) async { + return _retryOptions.retry( + () => _delegate.send( + method, + uri, + headers: headers, + params: params, + body: body, + ), + retryIf: (e) => e is SocketException || e is TimeoutException, + ); + } + + @override + Future sendByteStream( + HttpMethod method, + Uri uri, + http.ByteStream byteStream, + int length, + Map headers, + ) async { + return _retryOptions.retry( + () => _delegate.sendByteStream( + method, + uri, + byteStream, + length, + headers, + ), + retryIf: (e) => e is SocketException || e is TimeoutException, + ); + } +} +``` + +### Example: Mock HTTP Service (Testing) + +```dart +class MockHttpService implements HttpService { + final Map _mockResponses = {}; + bool _initialized = false; + + void addMockResponse(String key, http.Response response) { + _mockResponses[key] = response; + } + + @override + Future initialize() async { + _initialized = true; + } + + @override + void terminate() { + _initialized = false; + _mockResponses.clear(); + } + + @override + Future ping(Uri uri, {required Duration timeout}) async { + return true; // Always return true in tests + } + + @override + Future send( + HttpMethod method, + Uri uri, { + Map? headers, + Map? params, + String? body, + }) async { + final key = '${method}_${uri.path}'; + if (_mockResponses.containsKey(key)) { + return _mockResponses[key]!; + } + return http.Response('{}', 200); + } + + @override + Future sendByteStream( + HttpMethod method, + Uri uri, + http.ByteStream byteStream, + int length, + Map headers, + ) async { + return http.Response('{"uploaded": true}', 200); + } +} +``` + +--- + +## Complete Examples + +### Basic Usage + +```dart +import 'package:clerk_auth/clerk_auth.dart'; + +void main() async { + final httpService = DefaultHttpService(); + await httpService.initialize(); + + final config = AuthConfig( + publishableKey: 'pk_test_...', + persistor: Persistor.none, + httpService: httpService, + ); + + final auth = Auth(config: config); + await auth.initialize(); + + // Use auth... + + auth.terminate(); + httpService.terminate(); +} +``` + +### With Logging + +```dart +import 'package:clerk_auth/clerk_auth.dart'; + +void main() async { + final baseService = DefaultHttpService(); + final loggingService = LoggingHttpService(baseService); + await loggingService.initialize(); + + final config = AuthConfig( + publishableKey: 'pk_test_...', + persistor: Persistor.none, + httpService: loggingService, + ); + + final auth = Auth(config: config); + await auth.initialize(); + + // All HTTP requests will be logged +} +``` + +### Testing Setup + +```dart +import 'package:clerk_auth/clerk_auth.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:http/http.dart' as http; + +void main() { + test('auth test with mock HTTP', () async { + final mockHttp = MockHttpService(); + mockHttp.addMockResponse( + 'POST_/v1/client', + http.Response('{"client": {}}', 200), + ); + + final config = AuthConfig( + publishableKey: 'pk_test_...', + persistor: Persistor.none, + httpService: mockHttp, + ); + + final auth = Auth(config: config); + await auth.initialize(); + + // Test with mocked responses + }); +} +``` + +--- + +## Best Practices + +1. **Use DefaultHttpService for most cases**: It's well-tested and handles common scenarios. + +2. **Implement custom services for specific needs**: + - Logging/debugging + - Custom retry logic + - Request/response transformation + - Testing with mocks + +3. **Handle initialization properly**: + - Always call `initialize()` before use + - Handle multiple initialization calls gracefully + - Clean up in `terminate()` + +4. **Manage client lifecycle**: + - Create clients in `initialize()` + - Close clients in `terminate()` + - Reuse clients when possible + +5. **Error handling**: + - Catch and handle network exceptions + - Implement retry logic for transient failures + - Log errors for debugging + +6. **Testing**: + - Use mock implementations in tests + - Test error scenarios + - Verify request/response handling + +7. **Performance**: + - Reuse HTTP clients + - Implement connection pooling + - Set appropriate timeouts + +--- + +## Troubleshooting + +### Connection Timeouts + +**Problem:** Requests timeout frequently. + +**Solutions:** +- Increase `httpConnectionTimeout` in `AuthConfig` +- Check network connectivity +- Verify API endpoint is reachable +- Implement retry logic + +### SSL/TLS Errors + +**Problem:** Certificate verification failures. + +**Solutions:** +- Ensure system certificates are up to date +- Check for man-in-the-middle proxies +- Verify API endpoint uses valid certificate +- Don't disable certificate validation in production + +### Memory Leaks + +**Problem:** HTTP clients not being cleaned up. + +**Solutions:** +- Always call `terminate()` when done +- Ensure clients are closed properly +- Use `DefaultHttpService` which manages lifecycle +- Profile memory usage + +### Request Failures + +**Problem:** Requests fail with various errors. + +**Solutions:** +- Check request parameters and headers +- Verify API endpoint and method +- Implement logging to debug requests +- Use retry logic for transient failures + +--- + +## Related Documentation + +- [AuthConfig Documentation](auth_config.md) +- [Auth Documentation](auth.md) +- [Persistor Documentation](persistor.md) + +--- + +## Additional Resources + +- [http package](https://pub.dev/packages/http) +- [retry package](https://pub.dev/packages/retry) +- [Clerk API Reference](https://clerk.com/docs/reference/backend-api) + +--- + +*Generated for clerk_auth version 0.0.14-beta* + + + diff --git a/docs/clerk_auth/persistor.md b/docs/clerk_auth/persistor.md new file mode 100644 index 00000000..0dc5a373 --- /dev/null +++ b/docs/clerk_auth/persistor.md @@ -0,0 +1,539 @@ +# Persistor Documentation + +This document provides comprehensive documentation for the `Persistor` abstract class and its implementations from `clerk_auth/lib/src/clerk_auth/persistor.dart`. + +## Overview + +The `Persistor` abstract class defines the persistence interface for storing authentication state between app sessions. It allows the Clerk SDK to maintain user sessions across app restarts. + +## Abstract Class + +```dart +abstract class Persistor { + Future initialize(); + void terminate(); + FutureOr read(String key); + FutureOr write(String key, T value); + FutureOr delete(String key); +} +``` + +--- + +## Methods + +### `initialize()` + +Initializes the persistor service. Called once when the Auth object is initialized. + +```dart +Future initialize() +``` + +**Example:** +```dart +final persistor = DefaultPersistor( + getCacheDirectory: getApplicationDocumentsDirectory, +); +await persistor.initialize(); +``` + +--- + +### `terminate()` + +Terminates the persistor service. Called when the Auth object is terminated. + +```dart +void terminate() +``` + +**Example:** +```dart +persistor.terminate(); +``` + +--- + +### `read()` + +Reads a value from storage by key. + +```dart +FutureOr read(String key) +``` + +**Parameters:** +- `key`: The storage key to read from + +**Returns:** +- The stored value of type `T`, or `null` if not found + +**Example:** +```dart +final clientData = await persistor.read>('client'); +final envData = await persistor.read>('env'); +``` + +--- + +### `write()` + +Writes a value to storage with the given key. + +```dart +FutureOr write(String key, T value) +``` + +**Parameters:** +- `key`: The storage key to write to +- `value`: The value to store + +**Example:** +```dart +await persistor.write('client', clientJson); +await persistor.write('env', envJson); +``` + +--- + +### `delete()` + +Deletes a value from storage by key. + +```dart +FutureOr delete(String key) +``` + +**Parameters:** +- `key`: The storage key to delete + +**Example:** +```dart +await persistor.delete('client'); +``` + +--- + +## Built-in Implementations + +### `Persistor.none` + +A no-op persistor used for testing. Does not persist any data. + +**Type:** `const Persistor` + +**Example:** +```dart +final config = AuthConfig( + publishableKey: 'pk_test_...', + persistor: Persistor.none, // No persistence +); +``` + +**Use cases:** +- Unit testing +- Integration testing +- Temporary sessions that shouldn't persist + +**Note:** Marked with `@visibleForTesting` - should only be used in test environments. + +--- + +### `DefaultPersistor` + +A default implementation that writes authentication state to the file system as JSON. + +**Constructor:** +```dart +DefaultPersistor({ + required DirectoryGetter getCacheDirectory, +}) +``` + +**Parameters:** +- `getCacheDirectory`: A function that returns the directory for file storage + +**Example:** +```dart +import 'package:path_provider/path_provider.dart'; + +final persistor = DefaultPersistor( + getCacheDirectory: getApplicationDocumentsDirectory, +); +``` + +**Behavior:** +- Stores all data in a single JSON file: `clerk_sdk.json` +- Writes are debounced by 600ms to avoid excessive I/O +- Automatically handles JSON encoding/decoding +- Recovers gracefully from corrupted JSON files + +**File location:** +- The file is stored in the directory returned by `getCacheDirectory` +- Full path: `{cacheDirectory}/clerk_sdk.json` + +--- + +## Custom Implementation + +You can create custom persistor implementations for specific storage backends. + +### Example: Shared Preferences Persistor + +```dart +import 'package:shared_preferences/shared_preferences.dart'; + +class SharedPreferencesPersistor implements Persistor { + SharedPreferences? _prefs; + + @override + Future initialize() async { + _prefs = await SharedPreferences.getInstance(); + } + + @override + void terminate() { + // SharedPreferences doesn't need cleanup + } + + @override + Future read(String key) async { + if (T == String) { + return _prefs?.getString(key) as T?; + } else if (T == int) { + return _prefs?.getInt(key) as T?; + } else if (T == bool) { + return _prefs?.getBool(key) as T?; + } else if (T == Map) { + final jsonString = _prefs?.getString(key); + if (jsonString != null) { + return json.decode(jsonString) as T; + } + } + return null; + } + + @override + Future write(String key, T value) async { + if (value is String) { + await _prefs?.setString(key, value); + } else if (value is int) { + await _prefs?.setInt(key, value); + } else if (value is bool) { + await _prefs?.setBool(key, value); + } else if (value is Map) { + await _prefs?.setString(key, json.encode(value)); + } + } + + @override + Future delete(String key) async { + await _prefs?.remove(key); + } +} +``` + +### Example: In-Memory Persistor (Testing) + +```dart +class InMemoryPersistor implements Persistor { + final _storage = {}; + + @override + Future initialize() async {} + + @override + void terminate() { + _storage.clear(); + } + + @override + FutureOr read(String key) { + return _storage[key] as T?; + } + + @override + FutureOr write(String key, T value) { + _storage[key] = value; + } + + @override + FutureOr delete(String key) { + _storage.remove(key); + } +} +``` + +--- + +## Storage Keys Used by Clerk + +The Clerk SDK uses the following keys for persistence: + +### `$client` + +Stores the current `Client` object containing: +- Sign-in state +- Sign-up state +- Active sessions +- User data + +**Type:** `Map` + +### `$env` + +Stores the `Environment` object containing: +- Clerk account configuration +- Enabled features +- Organization settings +- User attribute settings + +**Type:** `Map` + +**Note:** These keys are prefixed with `$` to avoid conflicts with user data. + +--- + +## Complete Examples + +### Basic Setup with DefaultPersistor + +```dart +import 'package:clerk_auth/clerk_auth.dart'; +import 'package:path_provider/path_provider.dart'; + +Future setupAuth() async { + final persistor = DefaultPersistor( + getCacheDirectory: getApplicationDocumentsDirectory, + ); + + final config = AuthConfig( + publishableKey: 'pk_test_...', + persistor: persistor, + ); + + final auth = Auth(config: config); + await auth.initialize(); +} +``` + +### Testing Setup with No Persistence + +```dart +import 'package:clerk_auth/clerk_auth.dart'; +import 'package:flutter_test/flutter_test.dart'; + +void main() { + test('auth test', () async { + final config = AuthConfig( + publishableKey: 'pk_test_...', + persistor: Persistor.none, // No persistence in tests + ); + + final auth = Auth(config: config); + await auth.initialize(); + + // Test auth functionality + }); +} +``` + +### Custom Secure Storage Setup + +```dart +import 'package:clerk_auth/clerk_auth.dart'; + +Future setupSecureAuth() async { + final persistor = SecureStoragePersistor(); + + final config = AuthConfig( + publishableKey: 'pk_test_...', + persistor: persistor, + ); + + final auth = Auth(config: config); + await auth.initialize(); +} +``` + +### Platform-Specific Persistor + +```dart +import 'dart:io'; +import 'package:clerk_auth/clerk_auth.dart'; +import 'package:path_provider/path_provider.dart'; + +Future createPlatformPersistor() async { + if (Platform.isIOS || Platform.isAndroid) { + // Use secure storage on mobile + return SecureStoragePersistor(); + } else { + // Use file-based storage on desktop + return DefaultPersistor( + getCacheDirectory: getApplicationDocumentsDirectory, + ); + } +} + +Future setupAuth() async { + final persistor = await createPlatformPersistor(); + + final config = AuthConfig( + publishableKey: 'pk_test_...', + persistor: persistor, + ); + + final auth = Auth(config: config); + await auth.initialize(); +} +``` + +--- + +## Best Practices + +1. **Choose the right persistor for your use case**: + - `DefaultPersistor`: Good for most applications + - `SecureStoragePersistor`: For sensitive data on mobile + - `Persistor.none`: Only for testing + - Custom implementation: For specific requirements + +2. **Handle initialization properly**: + - Always call `initialize()` before use + - Handle initialization errors gracefully + - Don't assume initialization is instant + +3. **Clean up resources**: + - Call `terminate()` when done + - Clear sensitive data on logout + - Handle app lifecycle events + +4. **Error handling**: + - Implement proper error handling in custom persistors + - Recover gracefully from corrupted data + - Log errors for debugging + +5. **Performance considerations**: + - Debounce writes to avoid excessive I/O + - Use async operations for file/network storage + - Consider caching frequently accessed data + +6. **Security**: + - Use secure storage for sensitive tokens on mobile + - Encrypt data at rest if required + - Clear data on logout or app uninstall + +7. **Testing**: + - Use `Persistor.none` in unit tests + - Test custom persistors thoroughly + - Verify data persistence across app restarts + +--- + +## Troubleshooting + +### Data Not Persisting + +**Problem:** User sessions don't persist across app restarts. + +**Solutions:** +- Verify `initialize()` is called before use +- Check that `write()` is completing successfully +- Ensure storage directory exists and is writable +- Check for errors in persistor implementation + +### Corrupted Data + +**Problem:** App crashes or fails to load persisted data. + +**Solutions:** +- Implement error handling in `read()` method +- Clear corrupted data and start fresh +- Add data validation before persisting +- Use `DefaultPersistor` which handles corruption gracefully + +### Performance Issues + +**Problem:** App is slow due to excessive persistence operations. + +**Solutions:** +- Debounce write operations (like `DefaultPersistor` does) +- Use in-memory caching +- Batch multiple writes together +- Profile to identify bottlenecks + +### Platform-Specific Issues + +**Problem:** Persistor works on one platform but not another. + +**Solutions:** +- Use platform-specific storage APIs +- Test on all target platforms +- Handle platform differences in custom implementation +- Use conditional imports for platform-specific code + +--- + +## Related Documentation + +- [AuthConfig Documentation](auth_config.md) +- [Auth Documentation](auth.md) +- [HttpService Documentation](http_service.md) + +--- + +## Additional Resources + +- [path_provider package](https://pub.dev/packages/path_provider) +- [shared_preferences package](https://pub.dev/packages/shared_preferences) +- [flutter_secure_storage package](https://pub.dev/packages/flutter_secure_storage) + +--- + +*Generated for clerk_auth version 0.0.14-beta* + +### Example: Secure Storage Persistor + +```dart +import 'package:flutter_secure_storage/flutter_secure_storage.dart'; + +class SecureStoragePersistor implements Persistor { + final _storage = FlutterSecureStorage(); + + @override + Future initialize() async { + // No initialization needed + } + + @override + void terminate() { + // No cleanup needed + } + + @override + Future read(String key) async { + final value = await _storage.read(key: key); + if (value == null) return null; + + if (T == String) { + return value as T; + } else { + return json.decode(value) as T; + } + } + + @override + Future write(String key, T value) async { + final stringValue = value is String ? value : json.encode(value); + await _storage.write(key: key, value: stringValue); + } + + @override + Future delete(String key) async { + await _storage.delete(key: key); + } +} +``` + + diff --git a/docs/clerk_flutter/README.md b/docs/clerk_flutter/README.md new file mode 100644 index 00000000..0728e545 --- /dev/null +++ b/docs/clerk_flutter/README.md @@ -0,0 +1,525 @@ +# Clerk Flutter Widgets Documentation + +Welcome to the comprehensive documentation for the `clerk_flutter` package widgets. This documentation covers all public widgets and provides detailed usage examples. + +## Documentation Index + +### Core Widgets + +1. **[ClerkAuth](clerk_auth.md)** - Root widget for Clerk authentication + - Initialization and configuration + - Access to authentication state + - Theme and localization support + +2. **[ClerkAuthBuilder](clerk_auth_builder.md)** - Conditional rendering based on auth state + - Build different UI for signed-in vs signed-out users + - Flexible builder pattern + +3. **[ClerkSignedIn](clerk_signed_in.md)** - Show content only when signed in + - Simple conditional widget + - Automatic state management + +4. **[ClerkSignedOut](clerk_signed_out.md)** - Show content only when signed out + - Complement to ClerkSignedIn + - Automatic state management + +5. **[ClerkErrorListener](clerk_error_listener.md)** - Handle authentication errors + - Automatic error display + - Custom error handling + +### Authentication Widgets + +6. **[ClerkAuthentication](clerk_authentication.md)** - Complete sign-in/sign-up UI + - Pre-built authentication flow + - Supports all authentication strategies + - Customizable appearance + +### User Widgets + +7. **[ClerkUserButton](clerk_user_button.md)** - User profile button + - Multi-session management + - User profile access + - Sign-out functionality + +### Organization Widgets + +8. **[ClerkOrganizationList](clerk_organization_list.md)** - Organization management + - List user's organizations + - Switch between organizations + - Create new organizations + +### Configuration & Theming + +9. **[ClerkAuthConfig](clerk_auth_config.md)** - Flutter-specific configuration + - Localization settings + - Deep linking support + - Custom loading widgets + +10. **[ClerkTheme](clerk_theme.md)** - Theming and styling + - Light and dark themes + - Custom colors and styles + - Theme customization + +--- + +## Quick Start + +### Basic Setup + +```dart +import 'package:clerk_flutter/clerk_flutter.dart'; +import 'package:flutter/material.dart'; + +void main() { + runApp(const MyApp()); +} + +class MyApp extends StatelessWidget { + const MyApp({super.key}); + + @override + Widget build(BuildContext context) { + return MaterialApp( + title: 'My App', + builder: ClerkAuth.materialAppBuilder( + config: ClerkAuthConfig( + publishableKey: 'pk_test_...', + ), + ), + home: const HomePage(), + ); + } +} + +class HomePage extends StatelessWidget { + const HomePage({super.key}); + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar(title: const Text('Home')), + body: ClerkAuthBuilder( + signedInBuilder: (context, authState) { + return Center( + child: Text('Welcome, ${authState.user?.fullName}!'), + ); + }, + signedOutBuilder: (context, authState) { + return const Center( + child: ClerkAuthentication(), + ); + }, + ), + ); + } +} +``` + +--- + +## Key Concepts + +### Widget Hierarchy + +The typical widget hierarchy for a Clerk Flutter app: + +``` +MaterialApp + └─ ClerkAuth (via builder) + └─ ClerkErrorListener + └─ Your App Widgets + ├─ ClerkSignedIn + │ └─ Protected Content + ├─ ClerkSignedOut + │ └─ ClerkAuthentication + └─ ClerkAuthBuilder + ├─ signedInBuilder + └─ signedOutBuilder +``` + +### State Management + +All Clerk widgets automatically rebuild when authentication state changes. Access the current state using: + +```dart +// Get auth state with rebuild on change +final authState = ClerkAuth.of(context); + +// Get auth state without rebuild +final authState = ClerkAuth.of(context, listen: false); + +// Get just the user +final user = ClerkAuth.userOf(context); + +// Get just the session +final session = ClerkAuth.sessionOf(context); +``` + +### Conditional Rendering + +Three ways to conditionally render based on auth state: + +**1. ClerkSignedIn / ClerkSignedOut** +```dart +Column( + children: [ + ClerkSignedIn( + child: Text('You are signed in!'), + ), + ClerkSignedOut( + child: Text('Please sign in'), + ), + ], +) +``` + +**2. ClerkAuthBuilder** +```dart +ClerkAuthBuilder( + signedInBuilder: (context, authState) { + return Text('Hello, ${authState.user?.firstName}!'); + }, + signedOutBuilder: (context, authState) { + return const ClerkAuthentication(); + }, +) +``` + +**3. Manual Check** +```dart +final authState = ClerkAuth.of(context); +if (authState.isSignedIn) { + return Text('Signed in'); +} else { + return Text('Signed out'); +} +``` + +--- + +## Common Use Cases + +### Complete Authentication Flow + +```dart +class AuthPage extends StatelessWidget { + const AuthPage({super.key}); + + @override + Widget build(BuildContext context) { + return Scaffold( + body: Center( + child: ClerkAuthBuilder( + signedInBuilder: (context, authState) { + return Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Text('Welcome, ${authState.user?.fullName}!'), + const SizedBox(height: 20), + const ClerkUserButton(), + ], + ); + }, + signedOutBuilder: (context, authState) { + return const ClerkAuthentication(); + }, + ), + ), + ); + } +} +``` + +### Protected Routes + +```dart +class ProtectedPage extends StatelessWidget { + const ProtectedPage({super.key}); + + @override + Widget build(BuildContext context) { + return ClerkSignedIn( + child: Scaffold( + appBar: AppBar( + title: const Text('Protected Page'), + actions: const [ClerkUserButton()], + ), + body: const Center( + child: Text('This content is only visible to signed-in users'), + ), + ), + ); + } +} +``` + +### Multi-Session Support + +```dart +class MultiSessionExample extends StatelessWidget { + const MultiSessionExample({super.key}); + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar( + title: const Text('Multi-Session'), + actions: const [ + ClerkUserButton( + showName: true, + ), + ], + ), + body: const Center( + child: Text('Switch between accounts using the user button'), + ), + ); + } +} +``` + +### Organization Management + +```dart +class OrganizationPage extends StatelessWidget { + const OrganizationPage({super.key}); + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar( + title: const Text('Organizations'), + ), + body: const ClerkOrganizationList(), + ); + } +} +``` + +### Custom Error Handling + +```dart +class MyApp extends StatelessWidget { + const MyApp({super.key}); + + @override + Widget build(BuildContext context) { + return MaterialApp( + builder: (context, child) { + return ClerkAuth( + config: ClerkAuthConfig( + publishableKey: 'pk_test_...', + ), + child: ClerkErrorListener( + handler: (context, error) { + // Custom error handling + showDialog( + context: context, + builder: (context) => AlertDialog( + title: const Text('Error'), + content: Text(error.message), + actions: [ + TextButton( + onPressed: () => Navigator.pop(context), + child: const Text('OK'), + ), + ], + ), + ); + }, + child: child!, + ), + ); + }, + home: const HomePage(), + ); + } +} +``` + +--- + +## Theming + +### Using Built-in Themes + +```dart +MaterialApp( + theme: ThemeData.light().copyWith( + extensions: [ClerkThemeExtension.light], + ), + darkTheme: ThemeData.dark().copyWith( + extensions: [ClerkThemeExtension.dark], + ), + // ... rest of app +) +``` + +### Custom Theme + +```dart +final customTheme = ClerkThemeExtension( + colors: ClerkThemeColors( + background: Colors.white, + altBackground: Colors.grey[100]!, + borderSide: Colors.grey[300]!, + text: Colors.black87, + icon: Colors.grey[600]!, + lightweightText: Colors.grey[500]!, + error: Colors.red, + accent: Colors.blue, + ), +); + +MaterialApp( + theme: ThemeData.light().copyWith( + extensions: [customTheme], + ), + // ... rest of app +) +``` + +--- + +## Localization + +### Adding Custom Localizations + +```dart +ClerkAuthConfig( + publishableKey: 'pk_test_...', + localizations: { + 'en': ClerkSdkLocalizationsEn(), + 'es': MySpanishLocalizations(), + }, + fallbackLocalization: ClerkSdkLocalizationsEn(), +) +``` + +### Accessing Localizations + +```dart +final l10ns = ClerkAuth.localizationsOf(context); +Text(l10ns.signIn); +Text(l10ns.signUp); +``` + +--- + +## Deep Linking + +### Setup Deep Linking for OAuth + +```dart +import 'package:app_links/app_links.dart'; + +class MyApp extends StatefulWidget { + const MyApp({super.key}); + + @override + State createState() => _MyAppState(); +} + +class _MyAppState extends State { + final _appLinks = AppLinks(); + late final Stream _deepLinkStream; + + @override + void initState() { + super.initState(); + _deepLinkStream = _appLinks.uriLinkStream; + } + + @override + Widget build(BuildContext context) { + return MaterialApp( + builder: (context, child) { + return ClerkAuth( + config: ClerkAuthConfig( + publishableKey: 'pk_test_...', + deepLinkStream: _deepLinkStream, + redirectionGenerator: (context, strategy) { + return Uri.parse('myapp://oauth-callback'); + }, + ), + child: ClerkErrorListener(child: child!), + ); + }, + home: const HomePage(), + ); + } +} +``` + +--- + +## Best Practices + +1. **Always wrap your app with ClerkAuth**: Use `ClerkAuth.materialAppBuilder()` or wrap manually +2. **Include ClerkErrorListener**: Place it below ClerkAuth to handle errors automatically +3. **Use conditional widgets**: Prefer `ClerkSignedIn`/`ClerkSignedOut` for simple cases +4. **Access state efficiently**: Use `listen: false` when you don't need rebuilds +5. **Customize themes**: Override `ClerkThemeExtension` to match your app's design +6. **Handle deep links**: Set up deep linking for OAuth flows +7. **Test with different states**: Test your UI in signed-in, signed-out, and loading states + +--- + +## Testing + +### Unit Testing + +```dart +import 'package:clerk_flutter/clerk_flutter.dart'; +import 'package:flutter_test/flutter_test.dart'; + +void main() { + testWidgets('shows sign-in UI when signed out', (tester) async { + await tester.pumpWidget( + MaterialApp( + builder: (context, child) { + return ClerkAuth( + config: ClerkAuthConfig( + publishableKey: 'pk_test_...', + ), + child: child!, + ); + }, + home: const Scaffold( + body: ClerkAuthBuilder( + signedOutBuilder: (context, authState) { + return const Text('Sign In'); + }, + ), + ), + ), + ); + + await tester.pumpAndSettle(); + expect(find.text('Sign In'), findsOneWidget); + }); +} +``` + +--- + +## Additional Resources + +- [Clerk Dashboard](https://dashboard.clerk.com/) +- [Clerk API Reference](https://clerk.com/docs/reference/backend-api) +- [Flutter SDK Guide](https://clerk.com/docs/quickstarts/flutter) +- [clerk_flutter on pub.dev](https://pub.dev/packages/clerk_flutter) +- [clerk_auth Documentation](../clerk_auth/README.md) + +--- + +## Version + +*Documentation generated for clerk_flutter version 0.0.14-beta* + +--- + +## Contributing + +Found an issue or want to improve the documentation? Please open an issue or pull request on the [GitHub repository](https://github.com/clerk/clerk-sdk-flutter). + diff --git a/docs/clerk_flutter/clerk_auth.md b/docs/clerk_flutter/clerk_auth.md new file mode 100644 index 00000000..2b2db6e4 --- /dev/null +++ b/docs/clerk_flutter/clerk_auth.md @@ -0,0 +1,506 @@ +# ClerkAuth Widget Documentation + +The `ClerkAuth` widget is the root control widget that initializes the Clerk authentication system for your Flutter application. + +## Overview + +`ClerkAuth` must be placed at the root of your widget tree (or near it) to provide authentication state to all descendant widgets. It manages the `ClerkAuthState` object and makes it available throughout your app via `InheritedWidget`. + +## Class Definition + +```dart +class ClerkAuth extends StatefulWidget { + const ClerkAuth({ + super.key, + required this.child, + ClerkAuthConfig? config, + this.persistor, + this.httpService, + this.authState, + }); +} +``` + +--- + +## Constructors + +### Default Constructor + +```dart +const ClerkAuth({ + super.key, + required this.child, + ClerkAuthConfig? config, + this.persistor, + this.httpService, + this.authState, +}) +``` + +**Parameters:** +- `child`: The widget tree to wrap +- `config`: Configuration for Clerk authentication (required if `authState` is null) +- `persistor`: Optional override for the default persistor +- `httpService`: Optional override for the default HTTP service +- `authState`: Optional pre-created `ClerkAuthState` (required if `config` is null) + +**Note:** You must provide either `config` OR `authState`, but not both. + +--- + +### MaterialApp Builder + +```dart +static TransitionBuilder materialAppBuilder({ + required ClerkAuthConfig config, + Stream? deepLinkStream, +}) +``` + +Convenience method for use with `MaterialApp.builder`. + +**Parameters:** +- `config`: Configuration for Clerk authentication +- `deepLinkStream`: Optional stream of deep links for OAuth + +**Example:** +```dart +MaterialApp( + title: 'My App', + builder: ClerkAuth.materialAppBuilder( + config: ClerkAuthConfig( + publishableKey: 'pk_test_...', + ), + ), + home: const HomePage(), +) +``` + +--- + +## Static Methods + +### `of()` + +Get the nearest `ClerkAuthState` from the widget tree. + +```dart +static ClerkAuthState of(BuildContext context, {bool listen = true}) +``` + +**Parameters:** +- `context`: The build context +- `listen`: Whether to rebuild when auth state changes (default: `true`) + +**Returns:** `ClerkAuthState` + +**Example:** +```dart +// With rebuild on change +final authState = ClerkAuth.of(context); + +// Without rebuild +final authState = ClerkAuth.of(context, listen: false); +``` + +--- + +### `userOf()` + +Get the current user from the widget tree. + +```dart +static User? userOf(BuildContext context) +``` + +**Returns:** The current `User` or `null` if not signed in + +**Example:** +```dart +final user = ClerkAuth.userOf(context); +if (user != null) { + print('User: ${user.fullName}'); +} +``` + +--- + +### `sessionOf()` + +Get the current session from the widget tree. + +```dart +static Session? sessionOf(BuildContext context) +``` + +**Returns:** The current `Session` or `null` if not signed in + +**Example:** +```dart +final session = ClerkAuth.sessionOf(context); +if (session != null) { + print('Session ID: ${session.id}'); +} +``` + +--- + +### `localizationsOf()` + +Get the localizations for the current locale. + +```dart +static ClerkSdkLocalizations localizationsOf(BuildContext context) +``` + +**Returns:** `ClerkSdkLocalizations` for the current locale + +**Example:** +```dart +final l10ns = ClerkAuth.localizationsOf(context); +Text(l10ns.signIn); +Text(l10ns.signUp); +``` + +--- + +### `displayConfigOf()` + +Get the display configuration from the environment. + +```dart +static DisplayConfig displayConfigOf(BuildContext context) +``` + +**Returns:** `DisplayConfig` containing application name and branding + +**Example:** +```dart +final display = ClerkAuth.displayConfigOf(context); +Text('Welcome to ${display.applicationName}'); +``` + +--- + +### `errorStreamOf()` + +Get the stream of Clerk errors. + +```dart +static Stream errorStreamOf(BuildContext context) +``` + +**Returns:** Stream of `ClerkError` objects + +**Example:** +```dart +ClerkAuth.errorStreamOf(context).listen((error) { + print('Error: ${error.message}'); +}); +``` + +--- + +### `fileCacheOf()` + +Get the file cache from the configuration. + +```dart +static ClerkFileCache fileCacheOf(BuildContext context) +``` + +**Returns:** `ClerkFileCache` for caching remote files + +--- + +### `themeExtensionOf()` + +Get the Clerk theme extension from the current theme. + +```dart +static ClerkThemeExtension themeExtensionOf(BuildContext context) +``` + +**Returns:** `ClerkThemeExtension` with colors and styles + +**Example:** +```dart +final theme = ClerkAuth.themeExtensionOf(context); +Container( + color: theme.colors.background, + child: Text( + 'Hello', + style: theme.styles.heading, + ), +) +``` + +--- + +## Complete Examples + +### Basic Setup + +```dart +import 'package:clerk_flutter/clerk_flutter.dart'; +import 'package:flutter/material.dart'; + +void main() { + runApp(const MyApp()); +} + +class MyApp extends StatelessWidget { + const MyApp({super.key}); + + @override + Widget build(BuildContext context) { + return MaterialApp( + title: 'My App', + builder: ClerkAuth.materialAppBuilder( + config: ClerkAuthConfig( + publishableKey: 'pk_test_...', + ), + ), + home: const HomePage(), + ); + } +} +``` + +### Manual Setup + +```dart +class MyApp extends StatelessWidget { + const MyApp({super.key}); + + @override + Widget build(BuildContext context) { + return MaterialApp( + builder: (context, child) { + return ClerkAuth( + config: ClerkAuthConfig( + publishableKey: 'pk_test_...', + ), + child: ClerkErrorListener(child: child!), + ); + }, + home: const HomePage(), + ); + } +} +``` + +### With Custom Persistor + +```dart +import 'package:clerk_auth/clerk_auth.dart' as clerk; + +class MyApp extends StatelessWidget { + const MyApp({super.key}); + + @override + Widget build(BuildContext context) { + return MaterialApp( + builder: (context, child) { + return ClerkAuth( + config: ClerkAuthConfig( + publishableKey: 'pk_test_...', + ), + persistor: clerk.Persistor.none, // No persistence + child: ClerkErrorListener(child: child!), + ); + }, + home: const HomePage(), + ); + } +} +``` + +### Accessing Auth State + +```dart +class HomePage extends StatelessWidget { + const HomePage({super.key}); + + @override + Widget build(BuildContext context) { + // Get auth state with rebuild + final authState = ClerkAuth.of(context); + + // Get user directly + final user = ClerkAuth.userOf(context); + + // Get session directly + final session = ClerkAuth.sessionOf(context); + + return Scaffold( + appBar: AppBar( + title: Text(user != null ? 'Welcome ${user.firstName}' : 'Welcome'), + ), + body: Center( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + if (authState.isSignedIn) + Text('Signed in as ${user?.emailAddress}') + else + const Text('Not signed in'), + const SizedBox(height: 20), + if (session != null) + Text('Session expires: ${session.expireAt}'), + ], + ), + ), + ); + } +} +``` + +### Using Localizations + +```dart +class LocalizedWidget extends StatelessWidget { + const LocalizedWidget({super.key}); + + @override + Widget build(BuildContext context) { + final l10ns = ClerkAuth.localizationsOf(context); + + return Column( + children: [ + Text(l10ns.signIn), + Text(l10ns.signUp), + Text(l10ns.emailAddress), + Text(l10ns.password), + ], + ); + } +} +``` + +### Using Theme + +```dart +class ThemedWidget extends StatelessWidget { + const ThemedWidget({super.key}); + + @override + Widget build(BuildContext context) { + final theme = ClerkAuth.themeExtensionOf(context); + + return Container( + decoration: BoxDecoration( + color: theme.colors.background, + border: Border.all(color: theme.colors.borderSide), + ), + padding: const EdgeInsets.all(16), + child: Column( + children: [ + Text( + 'Heading', + style: theme.styles.heading, + ), + Text( + 'Subheading', + style: theme.styles.subheading, + ), + Text( + 'Body text', + style: theme.styles.text, + ), + ], + ), + ); + } +} +``` + +--- + +## Best Practices + +1. **Place ClerkAuth at the root**: Wrap your entire app or use `materialAppBuilder()` +2. **Include ClerkErrorListener**: Always include error handling below ClerkAuth +3. **Use listen: false when appropriate**: Avoid unnecessary rebuilds by setting `listen: false` when you don't need updates +4. **Access state efficiently**: Use specific getters like `userOf()` when you only need one piece of data +5. **Handle loading state**: The config's `loading` widget is shown while initializing +6. **Don't create multiple instances**: Only create one ClerkAuth widget per app + +--- + +## Common Patterns + +### Conditional Rendering Based on Auth State + +```dart +class ConditionalContent extends StatelessWidget { + const ConditionalContent({super.key}); + + @override + Widget build(BuildContext context) { + final authState = ClerkAuth.of(context); + + if (authState.isNotAvailable) { + return const CircularProgressIndicator(); + } + + if (authState.isSignedIn) { + return const SignedInContent(); + } + + return const SignedOutContent(); + } +} +``` + +### Listening to Errors + +```dart +class ErrorAwareWidget extends StatefulWidget { + const ErrorAwareWidget({super.key}); + + @override + State createState() => _ErrorAwareWidgetState(); +} + +class _ErrorAwareWidgetState extends State { + StreamSubscription? _errorSub; + + @override + void didChangeDependencies() { + super.didChangeDependencies(); + _errorSub?.cancel(); + _errorSub = ClerkAuth.errorStreamOf(context).listen((error) { + // Handle error + print('Error: ${error.message}'); + }); + } + + @override + void dispose() { + _errorSub?.cancel(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + return const Placeholder(); + } +} +``` + +--- + +## Related Documentation + +- [ClerkAuthConfig](clerk_auth_config.md) +- [ClerkAuthBuilder](clerk_auth_builder.md) +- [ClerkErrorListener](clerk_error_listener.md) +- [ClerkTheme](clerk_theme.md) +- [Auth (clerk_auth)](../clerk_auth/auth.md) + +--- + +*Generated for clerk_flutter version 0.0.14-beta* + diff --git a/docs/clerk_flutter/clerk_auth_builder.md b/docs/clerk_flutter/clerk_auth_builder.md new file mode 100644 index 00000000..a567d6aa --- /dev/null +++ b/docs/clerk_flutter/clerk_auth_builder.md @@ -0,0 +1,327 @@ +# ClerkAuthBuilder Widget Documentation + +The `ClerkAuthBuilder` widget provides a flexible builder pattern for rendering different UI based on authentication state. + +## Overview + +`ClerkAuthBuilder` allows you to specify different builders for signed-in and signed-out states, giving you fine-grained control over your UI. It automatically rebuilds when authentication state changes. + +## Class Definition + +```dart +class ClerkAuthBuilder extends StatefulWidget { + const ClerkAuthBuilder({ + super.key, + this.signedInBuilder, + this.signedOutBuilder, + this.builder, + }); +} +``` + +--- + +## Parameters + +### `signedInBuilder` + +**Type:** `AuthWidgetBuilder?` (i.e., `Widget Function(BuildContext, ClerkAuthState)`) + +Builder invoked when a user is signed in (i.e., when `authState.user` is not null). + +**Example:** +```dart +ClerkAuthBuilder( + signedInBuilder: (context, authState) { + return Text('Welcome, ${authState.user?.fullName}!'); + }, +) +``` + +--- + +### `signedOutBuilder` + +**Type:** `AuthWidgetBuilder?` + +Builder invoked when no user is signed in (i.e., when `authState.user` is null). + +**Example:** +```dart +ClerkAuthBuilder( + signedOutBuilder: (context, authState) { + return const Text('Please sign in'); + }, +) +``` + +--- + +### `builder` + +**Type:** `AuthWidgetBuilder?` + +Fallback builder invoked when neither `signedInBuilder` nor `signedOutBuilder` is provided, or when they don't match the current state. + +**Example:** +```dart +ClerkAuthBuilder( + builder: (context, authState) { + return Text('Auth state: ${authState.isSignedIn}'); + }, +) +``` + +--- + +## Behavior + +The widget chooses which builder to invoke based on the following logic: + +1. If `signedInBuilder` is provided AND user is signed in → use `signedInBuilder` +2. Else if `signedOutBuilder` is provided AND user is signed out → use `signedOutBuilder` +3. Else if `builder` is provided → use `builder` +4. Else → return empty widget + +--- + +## Complete Examples + +### Basic Sign-In/Sign-Out UI + +```dart +class HomePage extends StatelessWidget { + const HomePage({super.key}); + + @override + Widget build(BuildContext context) { + return Scaffold( + body: ClerkAuthBuilder( + signedInBuilder: (context, authState) { + return Center( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Text('Welcome, ${authState.user?.fullName}!'), + const SizedBox(height: 20), + ElevatedButton( + onPressed: () => authState.signOut(), + child: const Text('Sign Out'), + ), + ], + ), + ); + }, + signedOutBuilder: (context, authState) { + return const Center( + child: ClerkAuthentication(), + ); + }, + ), + ); + } +} +``` + +### With Loading State + +```dart +class LoadingAwareBuilder extends StatelessWidget { + const LoadingAwareBuilder({super.key}); + + @override + Widget build(BuildContext context) { + return ClerkAuthBuilder( + builder: (context, authState) { + if (authState.isNotAvailable) { + return const Center( + child: CircularProgressIndicator(), + ); + } + + if (authState.isSignedIn) { + return Text('Signed in as ${authState.user?.emailAddress}'); + } + + return const Text('Not signed in'); + }, + ); + } +} +``` + +### Accessing User Data + +```dart +class UserProfile extends StatelessWidget { + const UserProfile({super.key}); + + @override + Widget build(BuildContext context) { + return ClerkAuthBuilder( + signedInBuilder: (context, authState) { + final user = authState.user!; + + return Card( + child: Padding( + padding: const EdgeInsets.all(16), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + if (user.imageUrl != null) + CircleAvatar( + backgroundImage: NetworkImage(user.imageUrl!), + radius: 40, + ), + const SizedBox(height: 16), + Text('Name: ${user.fullName}'), + Text('Email: ${user.emailAddress}'), + Text('Username: ${user.username ?? "N/A"}'), + Text('Created: ${user.createdAt}'), + ], + ), + ), + ); + }, + signedOutBuilder: (context, authState) { + return const Card( + child: Padding( + padding: EdgeInsets.all(16), + child: Text('No user profile available'), + ), + ); + }, + ); + } +} +``` + +### Nested Builders + +```dart +class NestedExample extends StatelessWidget { + const NestedExample({super.key}); + + @override + Widget build(BuildContext context) { + return ClerkAuthBuilder( + signedInBuilder: (context, authState) { + return Column( + children: [ + Text('User: ${authState.user?.fullName}'), + // Nested builder for organization state + if (authState.organization != null) + Text('Org: ${authState.organization?.name}') + else + const Text('No active organization'), + ], + ); + }, + signedOutBuilder: (context, authState) { + return const ClerkAuthentication(); + }, + ); + } +} +``` + +### Conditional Navigation + +```dart +class ConditionalNavigation extends StatelessWidget { + const ConditionalNavigation({super.key}); + + @override + Widget build(BuildContext context) { + return ClerkAuthBuilder( + signedInBuilder: (context, authState) { + // Navigate to home when signed in + WidgetsBinding.instance.addPostFrameCallback((_) { + Navigator.of(context).pushReplacementNamed('/home'); + }); + return const SizedBox.shrink(); + }, + signedOutBuilder: (context, authState) { + // Navigate to login when signed out + WidgetsBinding.instance.addPostFrameCallback((_) { + Navigator.of(context).pushReplacementNamed('/login'); + }); + return const SizedBox.shrink(); + }, + ); + } +} +``` + +--- + +## Best Practices + +1. **Use specific builders**: Prefer `signedInBuilder` and `signedOutBuilder` over the generic `builder` for clarity +2. **Handle all states**: Consider what happens during loading, signing in, and signing up +3. **Avoid side effects in builders**: Don't perform navigation or state changes directly in builders (use `addPostFrameCallback` if needed) +4. **Keep builders pure**: Builders should only return widgets based on the current state +5. **Access authState parameter**: Use the provided `authState` parameter instead of calling `ClerkAuth.of(context)` again + +--- + +## Common Patterns + +### Dashboard with Sidebar + +```dart +class Dashboard extends StatelessWidget { + const Dashboard({super.key}); + + @override + Widget build(BuildContext context) { + return ClerkAuthBuilder( + signedInBuilder: (context, authState) { + return Row( + children: [ + // Sidebar + NavigationRail( + destinations: const [ + NavigationRailDestination( + icon: Icon(Icons.home), + label: Text('Home'), + ), + NavigationRailDestination( + icon: Icon(Icons.settings), + label: Text('Settings'), + ), + ], + selectedIndex: 0, + ), + // Main content + Expanded( + child: Center( + child: Text('Welcome, ${authState.user?.firstName}!'), + ), + ), + ], + ); + }, + signedOutBuilder: (context, authState) { + return const Center( + child: ClerkAuthentication(), + ); + }, + ); + } +} +``` + +--- + +## Related Documentation + +- [ClerkAuth](clerk_auth.md) +- [ClerkSignedIn](clerk_signed_in.md) +- [ClerkSignedOut](clerk_signed_out.md) +- [ClerkAuthentication](clerk_authentication.md) + +--- + +*Generated for clerk_flutter version 0.0.14-beta* + diff --git a/docs/clerk_flutter/clerk_auth_config.md b/docs/clerk_flutter/clerk_auth_config.md new file mode 100644 index 00000000..b3ea3931 --- /dev/null +++ b/docs/clerk_flutter/clerk_auth_config.md @@ -0,0 +1,229 @@ +# ClerkAuthConfig Documentation + +The `ClerkAuthConfig` class configures the Clerk authentication system for Flutter applications. + +## Overview + +`ClerkAuthConfig` is a Flutter-specific wrapper around the core `clerk_auth.AuthConfig` that adds Flutter-specific configuration options like loading widgets, file caching, and localizations. + +## Class Definition + +```dart +class ClerkAuthConfig { + ClerkAuthConfig({ + required this.publishableKey, + this.loading, + this.fileCache, + this.localizations, + this.telemetry = true, + this.sdkFlags, + }); +} +``` + +--- + +## Parameters + +### `publishableKey` + +**Type:** `String` (required) + +Your Clerk publishable key from the Clerk Dashboard. + +**Example:** +```dart +ClerkAuthConfig( + publishableKey: 'pk_test_...', +) +``` + +--- + +### `loading` + +**Type:** `Widget?` + +Widget to display while Clerk is initializing. If null, shows a default loading indicator. + +**Example:** +```dart +ClerkAuthConfig( + publishableKey: 'pk_test_...', + loading: const Center( + child: CircularProgressIndicator(), + ), +) +``` + +--- + +### `fileCache` + +**Type:** `ClerkFileCache?` + +Custom file cache for storing remote files (avatars, logos, etc.). If null, uses default cache. + +**Example:** +```dart +ClerkAuthConfig( + publishableKey: 'pk_test_...', + fileCache: MyCustomFileCache(), +) +``` + +--- + +### `localizations` + +**Type:** `Map?` + +Custom localizations for different locales. If null, uses default English localizations. + +**Example:** +```dart +ClerkAuthConfig( + publishableKey: 'pk_test_...', + localizations: { + 'en': ClerkSdkLocalizationsEn(), + 'es': MySpanishLocalizations(), + 'fr': MyFrenchLocalizations(), + }, +) +``` + +--- + +### `telemetry` + +**Type:** `bool` +**Default:** `true` + +Whether to enable telemetry. Set to `false` to disable usage tracking. + +**Example:** +```dart +ClerkAuthConfig( + publishableKey: 'pk_test_...', + telemetry: false, // Disable telemetry +) +``` + +--- + +### `sdkFlags` + +**Type:** `SdkFlags?` + +Advanced SDK flags for experimental features. Generally not needed. + +--- + +## Complete Examples + +### Basic Configuration + +```dart +MaterialApp( + builder: ClerkAuth.materialAppBuilder( + config: ClerkAuthConfig( + publishableKey: 'pk_test_...', + ), + ), + home: const HomePage(), +) +``` + +### With Custom Loading Widget + +```dart +ClerkAuthConfig( + publishableKey: 'pk_test_...', + loading: Scaffold( + body: Center( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Image.asset('assets/logo.png', width: 100), + const SizedBox(height: 20), + const CircularProgressIndicator(), + const SizedBox(height: 20), + const Text('Loading...'), + ], + ), + ), + ), +) +``` + +### With Localizations + +```dart +ClerkAuthConfig( + publishableKey: 'pk_test_...', + localizations: { + 'en': ClerkSdkLocalizationsEn(), + 'es': ClerkSdkLocalizationsEs(), + }, +) +``` + +### Production Configuration + +```dart +ClerkAuthConfig( + publishableKey: const String.fromEnvironment('CLERK_PUBLISHABLE_KEY'), + telemetry: true, + loading: const SplashScreen(), +) +``` + +### Development Configuration + +```dart +ClerkAuthConfig( + publishableKey: 'pk_test_...', + telemetry: false, // Disable in development + loading: const Center( + child: Text('Loading Clerk...'), + ), +) +``` + +--- + +## Environment Variables + +It's recommended to use environment variables for your publishable key: + +```dart +// Run with: flutter run --dart-define=CLERK_PUBLISHABLE_KEY=pk_test_... +ClerkAuthConfig( + publishableKey: const String.fromEnvironment( + 'CLERK_PUBLISHABLE_KEY', + defaultValue: 'pk_test_fallback', + ), +) +``` + +--- + +## Best Practices + +1. **Use environment variables**: Don't hardcode keys in source code +2. **Customize loading**: Provide a branded loading experience +3. **Enable telemetry in production**: Helps Clerk improve the SDK +4. **Disable telemetry in development**: Avoid polluting analytics +5. **Provide localizations**: Support your users' languages + +--- + +## Related Documentation + +- [ClerkAuth](clerk_auth.md) +- [AuthConfig (clerk_auth)](../clerk_auth/auth_config.md) + +--- + +*Generated for clerk_flutter version 0.0.14-beta* + diff --git a/docs/clerk_flutter/clerk_authentication.md b/docs/clerk_flutter/clerk_authentication.md new file mode 100644 index 00000000..56b6ff4f --- /dev/null +++ b/docs/clerk_flutter/clerk_authentication.md @@ -0,0 +1,318 @@ +# ClerkAuthentication Widget Documentation + +The `ClerkAuthentication` widget renders a complete, pre-built UI for signing users in or up. + +## Overview + +`ClerkAuthentication` provides a full-featured authentication interface that includes: +- Sign-in and sign-up forms +- OAuth provider buttons +- Email/password authentication +- Phone number authentication +- Email/SMS verification codes +- Password reset +- Automatic switching between sign-in and sign-up modes + +The functionality is controlled by your Clerk Dashboard settings (enabled strategies, required fields, etc.). + +## Class Definition + +```dart +class ClerkAuthentication extends StatefulWidget { + const ClerkAuthentication({super.key}); +} +``` + +--- + +## Features + +- **Automatic mode switching**: Switches between sign-in and sign-up based on user actions +- **OAuth integration**: Displays configured OAuth providers (Google, Apple, GitHub, etc.) +- **Multi-factor authentication**: Supports 2FA when enabled +- **Responsive design**: Adapts to different screen sizes +- **Themed**: Respects `ClerkThemeExtension` from your app theme +- **Localized**: Uses `ClerkSdkLocalizations` for translations + +--- + +## Complete Examples + +### Basic Usage + +```dart +class LoginPage extends StatelessWidget { + const LoginPage({super.key}); + + @override + Widget build(BuildContext context) { + return Scaffold( + body: Center( + child: ClerkSignedOut( + child: const ClerkAuthentication(), + ), + ), + ); + } +} +``` + +### Full-Screen Authentication + +```dart +class AuthPage extends StatelessWidget { + const AuthPage({super.key}); + + @override + Widget build(BuildContext context) { + return const Scaffold( + body: SafeArea( + child: Center( + child: SingleChildScrollView( + child: ClerkAuthentication(), + ), + ), + ), + ); + } +} +``` + +### With Background + +```dart +class StyledAuthPage extends StatelessWidget { + const StyledAuthPage({super.key}); + + @override + Widget build(BuildContext context) { + return Scaffold( + body: Container( + decoration: BoxDecoration( + gradient: LinearGradient( + begin: Alignment.topLeft, + end: Alignment.bottomRight, + colors: [Colors.blue.shade400, Colors.purple.shade400], + ), + ), + child: const Center( + child: Card( + margin: EdgeInsets.all(32), + child: Padding( + padding: EdgeInsets.all(16), + child: ClerkAuthentication(), + ), + ), + ), + ), + ); + } +} +``` + +### Conditional Rendering + +```dart +class HomePage extends StatelessWidget { + const HomePage({super.key}); + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar(title: const Text('Home')), + body: ClerkAuthBuilder( + signedInBuilder: (context, authState) { + return Center( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Text('Welcome, ${authState.user?.fullName}!'), + const SizedBox(height: 20), + const ClerkUserButton(), + ], + ), + ); + }, + signedOutBuilder: (context, authState) { + return const Center( + child: ClerkAuthentication(), + ); + }, + ), + ); + } +} +``` + +### In a Dialog + +```dart +void showAuthDialog(BuildContext context) { + showDialog( + context: context, + builder: (context) => Dialog( + child: Container( + constraints: const BoxConstraints(maxWidth: 400), + padding: const EdgeInsets.all(16), + child: const ClerkAuthentication(), + ), + ), + ); +} +``` + +--- + +## Supported Authentication Strategies + +The widget automatically displays UI for strategies enabled in your Clerk Dashboard: + +**First-factor strategies:** +- Email + Password +- Email + Verification Code +- Phone + Verification Code +- Username + Password +- OAuth providers (Google, Apple, Facebook, GitHub, etc.) +- Passkeys (when supported) + +**Second-factor strategies:** +- SMS Code +- TOTP (Authenticator app) +- Backup codes + +--- + +## Customization + +### Theme Customization + +```dart +MaterialApp( + theme: ThemeData.light().copyWith( + extensions: [ + ClerkThemeExtension( + colors: ClerkThemeColors( + background: Colors.white, + altBackground: Colors.grey[100]!, + borderSide: Colors.grey[300]!, + text: Colors.black87, + icon: Colors.grey[600]!, + lightweightText: Colors.grey[500]!, + error: Colors.red, + accent: Colors.blue, // Primary button color + ), + ), + ], + ), + // ... rest of app +) +``` + +### Localization + +The widget uses your configured localizations: + +```dart +ClerkAuthConfig( + publishableKey: 'pk_test_...', + localizations: { + 'en': ClerkSdkLocalizationsEn(), + 'es': MySpanishLocalizations(), + }, +) +``` + +--- + +## Behavior + +### Sign-In Flow + +1. User enters identifier (email/phone/username) +2. User enters password or requests verification code +3. If verification code: user enters code +4. If 2FA enabled: user completes second factor +5. User is signed in + +### Sign-Up Flow + +1. User enters required fields (email, password, name, etc.) +2. User verifies email/phone with code +3. User is signed up and signed in + +### Mode Switching + +- "Don't have an account? Sign up" link switches to sign-up +- "Already have an account? Sign in" link switches to sign-in +- Automatic switching based on auth state + +--- + +## Best Practices + +1. **Use in signed-out state**: Wrap with `ClerkSignedOut` or use in `signedOutBuilder` +2. **Provide enough space**: The widget needs vertical space; use `SingleChildScrollView` if needed +3. **Configure in Dashboard**: Enable/disable strategies in your Clerk Dashboard +4. **Test all flows**: Test sign-in, sign-up, password reset, and 2FA +5. **Handle errors**: Use `ClerkErrorListener` to display authentication errors + +--- + +## Common Patterns + +### Modal Authentication + +```dart +class MyApp extends StatelessWidget { + const MyApp({super.key}); + + @override + Widget build(BuildContext context) { + return MaterialApp( + home: Scaffold( + body: ClerkAuthBuilder( + signedOutBuilder: (context, authState) { + return Center( + child: ElevatedButton( + onPressed: () { + showModalBottomSheet( + context: context, + isScrollControlled: true, + builder: (context) => DraggableScrollableSheet( + initialChildSize: 0.9, + builder: (context, scrollController) { + return const ClerkAuthentication(); + }, + ), + ); + }, + child: const Text('Sign In'), + ), + ); + }, + signedInBuilder: (context, authState) { + return Center( + child: Text('Welcome, ${authState.user?.fullName}!'), + ); + }, + ), + ), + ); + } +} +``` + +--- + +## Related Documentation + +- [ClerkAuth](clerk_auth.md) +- [ClerkAuthBuilder](clerk_auth_builder.md) +- [ClerkSignedOut](clerk_signed_out.md) +- [ClerkTheme](clerk_theme.md) +- [Auth (clerk_auth)](../clerk_auth/auth.md) + +--- + +*Generated for clerk_flutter version 0.0.14-beta* + diff --git a/docs/clerk_flutter/clerk_error_listener.md b/docs/clerk_flutter/clerk_error_listener.md new file mode 100644 index 00000000..652ebdc3 --- /dev/null +++ b/docs/clerk_flutter/clerk_error_listener.md @@ -0,0 +1,277 @@ +# ClerkErrorListener Widget Documentation + +The `ClerkErrorListener` widget listens to authentication errors and displays them to the user. + +## Overview + +`ClerkErrorListener` subscribes to the error stream from `ClerkAuthState` and automatically displays errors as SnackBars. You can also provide a custom error handler. + +## Class Definition + +```dart +class ClerkErrorListener extends StatefulWidget { + const ClerkErrorListener({ + super.key, + this.handler, + required this.child, + }); +} +``` + +--- + +## Parameters + +### `handler` + +**Type:** `ClerkErrorHandler?` (i.e., `FutureOr Function(BuildContext, ClerkError)`) + +Optional custom error handler. If not provided, errors are displayed as SnackBars. + +**Example:** +```dart +ClerkErrorListener( + handler: (context, error) { + showDialog( + context: context, + builder: (context) => AlertDialog( + title: const Text('Error'), + content: Text(error.message), + ), + ); + }, + child: myApp, +) +``` + +--- + +### `child` + +**Type:** `Widget` (required) + +The widget tree to wrap. + +--- + +## Behavior + +**Default behavior (no handler):** +- Listens to `ClerkAuth.errorStreamOf(context)` +- Displays errors as SnackBars using `ScaffoldMessenger` +- Requires a `Scaffold` or `ScaffoldMessenger` ancestor in the widget tree + +**With custom handler:** +- Calls your handler function for each error +- You have full control over error display + +--- + +## Complete Examples + +### Basic Usage (Default Handler) + +```dart +class MyApp extends StatelessWidget { + const MyApp({super.key}); + + @override + Widget build(BuildContext context) { + return MaterialApp( + builder: ClerkAuth.materialAppBuilder( + config: ClerkAuthConfig( + publishableKey: 'pk_test_...', + ), + ), + home: const HomePage(), + ); + } +} + +// Note: ClerkAuth.materialAppBuilder automatically includes ClerkErrorListener +``` + +### Manual Setup + +```dart +class MyApp extends StatelessWidget { + const MyApp({super.key}); + + @override + Widget build(BuildContext context) { + return MaterialApp( + builder: (context, child) { + return ClerkAuth( + config: ClerkAuthConfig( + publishableKey: 'pk_test_...', + ), + child: ClerkErrorListener(child: child!), + ); + }, + home: const HomePage(), + ); + } +} +``` + +### Custom Error Handler (Dialog) + +```dart +class MyApp extends StatelessWidget { + const MyApp({super.key}); + + @override + Widget build(BuildContext context) { + return MaterialApp( + builder: (context, child) { + return ClerkAuth( + config: ClerkAuthConfig( + publishableKey: 'pk_test_...', + ), + child: ClerkErrorListener( + handler: (context, error) { + showDialog( + context: context, + builder: (context) => AlertDialog( + title: const Text('Authentication Error'), + content: Text(error.message), + actions: [ + TextButton( + onPressed: () => Navigator.pop(context), + child: const Text('OK'), + ), + ], + ), + ); + }, + child: child!, + ), + ); + }, + home: const HomePage(), + ); + } +} +``` + +### Custom Error Handler (Toast) + +```dart +import 'package:fluttertoast/fluttertoast.dart'; + +ClerkErrorListener( + handler: (context, error) { + Fluttertoast.showToast( + msg: error.message, + toastLength: Toast.LENGTH_LONG, + gravity: ToastGravity.BOTTOM, + backgroundColor: Colors.red, + textColor: Colors.white, + ); + }, + child: myApp, +) +``` + +### Logging Errors + +```dart +import 'dart:developer' as developer; + +ClerkErrorListener( + handler: (context, error) async { + // Log to console + developer.log( + 'Clerk Error: ${error.message}', + name: 'clerk_flutter', + error: error, + ); + + // Also show to user + ScaffoldMessenger.of(context).showSnackBar( + SnackBar(content: Text(error.message)), + ); + }, + child: myApp, +) +``` + +### Error Analytics + +```dart +import 'package:firebase_crashlytics/firebase_crashlytics.dart'; + +ClerkErrorListener( + handler: (context, error) async { + // Report to Crashlytics + await FirebaseCrashlytics.instance.recordError( + error, + StackTrace.current, + reason: 'Clerk authentication error', + ); + + // Show to user + ScaffoldMessenger.of(context).showSnackBar( + SnackBar(content: Text(error.message)), + ); + }, + child: myApp, +) +``` + +--- + +## Error Types + +Errors are instances of `ClerkError` with the following properties: + +- `message`: Human-readable error message +- `code`: Error code (e.g., `'form_password_incorrect'`) +- `localizedMessage(localizations)`: Get localized error message + +--- + +## Best Practices + +1. **Place below ClerkAuth**: Always place `ClerkErrorListener` as a child of `ClerkAuth` +2. **Ensure Scaffold exists**: Default handler requires a `Scaffold` ancestor +3. **Use custom handler for special cases**: Implement custom handler for dialogs, logging, or analytics +4. **Don't throw in handler**: Handle errors gracefully in your custom handler +5. **Consider user experience**: Show clear, actionable error messages + +--- + +## Common Patterns + +### Conditional Error Display + +```dart +ClerkErrorListener( + handler: (context, error) { + // Only show certain errors to users + if (error.code == 'network_error') { + ScaffoldMessenger.of(context).showSnackBar( + const SnackBar( + content: Text('Network error. Please check your connection.'), + ), + ); + } else { + // Log other errors silently + print('Clerk error: ${error.message}'); + } + }, + child: myApp, +) +``` + +--- + +## Related Documentation + +- [ClerkAuth](clerk_auth.md) +- [ClerkError (clerk_auth)](../clerk_auth/auth.md#error-handling) + +--- + +*Generated for clerk_flutter version 0.0.14-beta* + diff --git a/docs/clerk_flutter/clerk_organization_list.md b/docs/clerk_flutter/clerk_organization_list.md new file mode 100644 index 00000000..5f13706d --- /dev/null +++ b/docs/clerk_flutter/clerk_organization_list.md @@ -0,0 +1,303 @@ +# ClerkOrganizationList Widget Documentation + +The `ClerkOrganizationList` widget renders a list of the user's organizations with management capabilities. + +## Overview + +`ClerkOrganizationList` displays: +- All organizations the user belongs to +- Organization invitations +- Ability to switch active organization +- Ability to create new organizations +- Ability to manage organization settings + +## Class Definition + +```dart +class ClerkOrganizationList extends StatefulWidget { + const ClerkOrganizationList({ + super.key, + this.actions, + }); +} +``` + +--- + +## Parameters + +### `actions` + +**Type:** `List?` + +Custom actions to display in the organization list. If null, uses default actions (Create Organization). + +**Example:** +```dart +ClerkOrganizationList( + actions: [ + ClerkUserAction( + icon: Icons.add, + label: 'Create Organization', + callback: (context, authState) async { + // Custom create logic + }, + ), + ], +) +``` + +--- + +## Complete Examples + +### Basic Usage + +```dart +class OrganizationsPage extends StatelessWidget { + const OrganizationsPage({super.key}); + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar( + title: const Text('Organizations'), + ), + body: const ClerkOrganizationList(), + ); + } +} +``` + +### In a Dialog + +```dart +void showOrganizationList(BuildContext context) { + showDialog( + context: context, + builder: (context) => Dialog( + child: Container( + constraints: const BoxConstraints(maxWidth: 500, maxHeight: 600), + child: const ClerkOrganizationList(), + ), + ), + ); +} +``` + +### With Custom Actions + +```dart +ClerkOrganizationList( + actions: [ + ClerkUserAction( + icon: Icons.add, + label: 'Create Organization', + callback: (context, authState) async { + // Show custom create dialog + showDialog( + context: context, + builder: (context) => const CreateOrgDialog(), + ); + }, + ), + ClerkUserAction( + icon: Icons.help, + label: 'Help', + callback: (context, authState) async { + // Show help + }, + ), + ], +) +``` + +### Conditional Display + +```dart +class HomePage extends StatelessWidget { + const HomePage({super.key}); + + @override + Widget build(BuildContext context) { + final authState = ClerkAuth.of(context); + + return Scaffold( + appBar: AppBar( + title: const Text('Home'), + actions: [ + if (authState.env.organization.isEnabled) + IconButton( + icon: const Icon(Icons.business), + onPressed: () { + Navigator.push( + context, + MaterialPageRoute( + builder: (context) => const Scaffold( + body: ClerkOrganizationList(), + ), + ), + ); + }, + ), + ], + ), + body: const Center(child: Text('Content')), + ); + } +} +``` + +--- + +## Features + +### Organization List + +- Displays all organizations the user is a member of +- Shows current active organization +- Allows switching between organizations +- Shows organization role (admin, member, etc.) + +### Invitations + +- Displays pending organization invitations +- Allows accepting invitations +- Shows invitation details (organization name, role) + +### Organization Creation + +- Create new organizations (if enabled) +- Set organization name and slug +- Upload organization logo + +### Organization Management + +- Access organization settings +- Manage members +- Manage domains +- Leave organization + +--- + +## Default Actions + +1. **Create Organization** - Opens create organization panel (if user has permission) + +--- + +## Best Practices + +1. **Check if enabled**: Verify organizations are enabled before showing +2. **Handle permissions**: Check if user can create organizations +3. **Provide navigation**: Make it easy to access from your app +4. **Test invitations**: Test accepting and declining invitations +5. **Consider mobile**: The widget adapts to mobile screens + +--- + +## Common Patterns + +### In Navigation Drawer + +```dart +class MyDrawer extends StatelessWidget { + const MyDrawer({super.key}); + + @override + Widget build(BuildContext context) { + final authState = ClerkAuth.of(context); + + return Drawer( + child: ListView( + children: [ + const DrawerHeader( + child: Text('Menu'), + ), + if (authState.env.organization.isEnabled) + ListTile( + leading: const Icon(Icons.business), + title: const Text('Organizations'), + onTap: () { + Navigator.pop(context); + Navigator.push( + context, + MaterialPageRoute( + builder: (context) => const Scaffold( + appBar: AppBar(title: Text('Organizations')), + body: ClerkOrganizationList(), + ), + ), + ); + }, + ), + ], + ), + ); + } +} +``` + +### With Current Organization Display + +```dart +class OrganizationHeader extends StatelessWidget { + const OrganizationHeader({super.key}); + + @override + Widget build(BuildContext context) { + final authState = ClerkAuth.of(context); + final org = authState.organization; + + return ListTile( + leading: const Icon(Icons.business), + title: Text(org?.name ?? 'No Organization'), + subtitle: const Text('Tap to switch'), + onTap: () { + Navigator.push( + context, + MaterialPageRoute( + builder: (context) => const Scaffold( + body: ClerkOrganizationList(), + ), + ), + ); + }, + ); + } +} +``` + +--- + +## Checking Organization Support + +```dart +final authState = ClerkAuth.of(context); + +// Check if organizations are enabled +if (authState.env.organization.isEnabled) { + // Show organization features +} + +// Check if user can create organizations +if (authState.user?.createOrganizationEnabled == true) { + // Show create button +} + +// Get current organization +final currentOrg = authState.organization; +``` + +--- + +## Related Documentation + +- [ClerkAuth](clerk_auth.md) +- [ClerkUserButton](clerk_user_button.md) +- [Auth - Organization Management](../clerk_auth/auth.md#organization-management) + +--- + +*Generated for clerk_flutter version 0.0.14-beta* + diff --git a/docs/clerk_flutter/clerk_signed_in.md b/docs/clerk_flutter/clerk_signed_in.md new file mode 100644 index 00000000..c8223bb6 --- /dev/null +++ b/docs/clerk_flutter/clerk_signed_in.md @@ -0,0 +1,231 @@ +# ClerkSignedIn Widget Documentation + +The `ClerkSignedIn` widget conditionally renders its child only when a user is signed in. + +## Overview + +`ClerkSignedIn` is a simple conditional widget that shows its child when `authState.user` is not null. It automatically rebuilds when authentication state changes. + +## Class Definition + +```dart +class ClerkSignedIn extends StatefulWidget { + const ClerkSignedIn({ + super.key, + required this.child, + }); +} +``` + +--- + +## Parameters + +### `child` + +**Type:** `Widget` (required) + +The widget to display when a user is signed in. + +**Example:** +```dart +ClerkSignedIn( + child: Text('You are signed in!'), +) +``` + +--- + +## Behavior + +- **When signed in** (user is not null): Renders the `child` widget +- **When signed out** (user is null): Renders an empty widget (nothing) + +--- + +## Complete Examples + +### Basic Usage + +```dart +class HomePage extends StatelessWidget { + const HomePage({super.key}); + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar(title: const Text('Home')), + body: Column( + children: [ + ClerkSignedIn( + child: Text('Welcome back!'), + ), + ClerkSignedOut( + child: Text('Please sign in'), + ), + ], + ), + ); + } +} +``` + +### Protected Content + +```dart +class ProtectedPage extends StatelessWidget { + const ProtectedPage({super.key}); + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar( + title: const Text('Protected Page'), + ), + body: ClerkSignedIn( + child: Center( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + const Text('This is protected content'), + const SizedBox(height: 20), + ElevatedButton( + onPressed: () { + final authState = ClerkAuth.of(context, listen: false); + authState.signOut(); + }, + child: const Text('Sign Out'), + ), + ], + ), + ), + ), + ); + } +} +``` + +### Conditional AppBar Actions + +```dart +class MyScaffold extends StatelessWidget { + const MyScaffold({super.key}); + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar( + title: const Text('My App'), + actions: [ + ClerkSignedIn( + child: const ClerkUserButton(), + ), + ], + ), + body: const Center(child: Text('Content')), + ); + } +} +``` + +### Navigation Drawer + +```dart +class MyDrawer extends StatelessWidget { + const MyDrawer({super.key}); + + @override + Widget build(BuildContext context) { + return Drawer( + child: ListView( + children: [ + const DrawerHeader( + child: Text('Menu'), + ), + ListTile( + leading: const Icon(Icons.home), + title: const Text('Home'), + onTap: () => Navigator.pop(context), + ), + ClerkSignedIn( + child: ListTile( + leading: const Icon(Icons.person), + title: const Text('Profile'), + onTap: () { + Navigator.pop(context); + Navigator.pushNamed(context, '/profile'); + }, + ), + ), + ClerkSignedIn( + child: ListTile( + leading: const Icon(Icons.settings), + title: const Text('Settings'), + onTap: () { + Navigator.pop(context); + Navigator.pushNamed(context, '/settings'); + }, + ), + ), + ], + ), + ); + } +} +``` + +### With User Data + +```dart +class UserGreeting extends StatelessWidget { + const UserGreeting({super.key}); + + @override + Widget build(BuildContext context) { + return ClerkSignedIn( + child: Builder( + builder: (context) { + final user = ClerkAuth.userOf(context); + return Text('Hello, ${user?.firstName ?? "User"}!'); + }, + ), + ); + } +} +``` + +--- + +## Best Practices + +1. **Use with ClerkSignedOut**: Pair with `ClerkSignedOut` to cover both states +2. **Keep it simple**: Use for simple conditional rendering; use `ClerkAuthBuilder` for complex logic +3. **Avoid nesting**: Don't nest multiple `ClerkSignedIn` widgets unnecessarily +4. **Consider loading state**: This widget doesn't handle loading state; use `ClerkAuthBuilder` if needed + +--- + +## Comparison with ClerkAuthBuilder + +**Use ClerkSignedIn when:** +- You only need to show/hide content based on sign-in state +- You want simple, declarative code +- You're building a list of conditional widgets + +**Use ClerkAuthBuilder when:** +- You need access to the full `authState` +- You want to handle both signed-in and signed-out states differently +- You need to handle loading or other intermediate states + +--- + +## Related Documentation + +- [ClerkSignedOut](clerk_signed_out.md) +- [ClerkAuthBuilder](clerk_auth_builder.md) +- [ClerkAuth](clerk_auth.md) + +--- + +*Generated for clerk_flutter version 0.0.14-beta* + diff --git a/docs/clerk_flutter/clerk_signed_out.md b/docs/clerk_flutter/clerk_signed_out.md new file mode 100644 index 00000000..0d322cce --- /dev/null +++ b/docs/clerk_flutter/clerk_signed_out.md @@ -0,0 +1,171 @@ +# ClerkSignedOut Widget Documentation + +The `ClerkSignedOut` widget conditionally renders its child only when no user is signed in. + +## Overview + +`ClerkSignedOut` is the complement to `ClerkSignedIn`. It shows its child when `authState.user` is null and automatically rebuilds when authentication state changes. + +## Class Definition + +```dart +class ClerkSignedOut extends StatefulWidget { + const ClerkSignedOut({ + super.key, + required this.child, + }); +} +``` + +--- + +## Parameters + +### `child` + +**Type:** `Widget` (required) + +The widget to display when no user is signed in. + +**Example:** +```dart +ClerkSignedOut( + child: Text('Please sign in'), +) +``` + +--- + +## Behavior + +- **When signed out** (user is null): Renders the `child` widget +- **When signed in** (user is not null): Renders an empty widget (nothing) + +--- + +## Complete Examples + +### Basic Usage + +```dart +class HomePage extends StatelessWidget { + const HomePage({super.key}); + + @override + Widget build(BuildContext context) { + return Scaffold( + body: Center( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + ClerkSignedOut( + child: const ClerkAuthentication(), + ), + ClerkSignedIn( + child: const Text('You are signed in!'), + ), + ], + ), + ), + ); + } +} +``` + +### Login Page + +```dart +class LoginPage extends StatelessWidget { + const LoginPage({super.key}); + + @override + Widget build(BuildContext context) { + return Scaffold( + body: ClerkSignedOut( + child: const Center( + child: ClerkAuthentication(), + ), + ), + ); + } +} +``` + +### Conditional Navigation + +```dart +class AuthGate extends StatelessWidget { + const AuthGate({super.key}); + + @override + Widget build(BuildContext context) { + return Column( + children: [ + ClerkSignedOut( + child: ElevatedButton( + onPressed: () => Navigator.pushNamed(context, '/login'), + child: const Text('Sign In'), + ), + ), + ClerkSignedIn( + child: ElevatedButton( + onPressed: () => Navigator.pushNamed(context, '/dashboard'), + child: const Text('Go to Dashboard'), + ), + ), + ], + ); + } +} +``` + +### AppBar Actions + +```dart +class MyAppBar extends StatelessWidget implements PreferredSizeWidget { + const MyAppBar({super.key}); + + @override + Size get preferredSize => const Size.fromHeight(kToolbarHeight); + + @override + Widget build(BuildContext context) { + return AppBar( + title: const Text('My App'), + actions: [ + ClerkSignedOut( + child: TextButton( + onPressed: () => Navigator.pushNamed(context, '/login'), + child: const Text('Sign In'), + ), + ), + ClerkSignedIn( + child: const ClerkUserButton(), + ), + ], + ); + } +} +``` + +--- + +## Best Practices + +1. **Use with ClerkSignedIn**: Pair with `ClerkSignedIn` to cover both states +2. **Show authentication UI**: Commonly used to display `ClerkAuthentication` widget +3. **Keep it simple**: Use for simple conditional rendering +4. **Consider user experience**: Provide clear calls-to-action for signing in + +--- + +## Related Documentation + +- [ClerkSignedIn](clerk_signed_in.md) +- [ClerkAuthBuilder](clerk_auth_builder.md) +- [ClerkAuthentication](clerk_authentication.md) + +--- + +*Generated for clerk_flutter version 0.0.14-beta* + diff --git a/docs/clerk_flutter/clerk_theme.md b/docs/clerk_flutter/clerk_theme.md new file mode 100644 index 00000000..7c422f7b --- /dev/null +++ b/docs/clerk_flutter/clerk_theme.md @@ -0,0 +1,267 @@ +# ClerkTheme Documentation + +The `ClerkThemeExtension` class provides theming capabilities for Clerk widgets in Flutter. + +## Overview + +`ClerkThemeExtension` is a Flutter `ThemeExtension` that allows you to customize the appearance of all Clerk widgets including colors, text styles, and component styles. + +## Class Definition + +```dart +class ClerkThemeExtension extends ThemeExtension { + const ClerkThemeExtension({ + required this.colors, + this.styles, + }); + + final ClerkThemeColors colors; + final ClerkThemeStyles? styles; +} +``` + +--- + +## ClerkThemeColors + +```dart +class ClerkThemeColors { + const ClerkThemeColors({ + required this.background, + required this.altBackground, + required this.borderSide, + required this.text, + required this.icon, + required this.lightweightText, + required this.error, + required this.accent, + }); + + final Color background; + final Color altBackground; + final Color borderSide; + final Color text; + final Color icon; + final Color lightweightText; + final Color error; + final Color accent; +} +``` + +--- + +## ClerkThemeStyles + +```dart +class ClerkThemeStyles { + const ClerkThemeStyles({ + this.heading, + this.subheading, + this.text, + this.lightweightText, + this.button, + }); + + final TextStyle? heading; + final TextStyle? subheading; + final TextStyle? text; + final TextStyle? lightweightText; + final TextStyle? button; +} +``` + +--- + +## Complete Examples + +### Light Theme + +```dart +MaterialApp( + theme: ThemeData.light().copyWith( + extensions: [ + ClerkThemeExtension( + colors: ClerkThemeColors( + background: Colors.white, + altBackground: Colors.grey[100]!, + borderSide: Colors.grey[300]!, + text: Colors.black87, + icon: Colors.grey[600]!, + lightweightText: Colors.grey[500]!, + error: Colors.red[700]!, + accent: Colors.blue[600]!, + ), + ), + ], + ), + // ... rest of app +) +``` + +### Dark Theme + +```dart +MaterialApp( + darkTheme: ThemeData.dark().copyWith( + extensions: [ + ClerkThemeExtension( + colors: ClerkThemeColors( + background: Colors.grey[900]!, + altBackground: Colors.grey[850]!, + borderSide: Colors.grey[700]!, + text: Colors.white, + icon: Colors.grey[400]!, + lightweightText: Colors.grey[500]!, + error: Colors.red[400]!, + accent: Colors.blue[400]!, + ), + ), + ], + ), + themeMode: ThemeMode.dark, + // ... rest of app +) +``` + +### Custom Brand Colors + +```dart +const brandPrimary = Color(0xFF6366F1); // Indigo +const brandSecondary = Color(0xFF8B5CF6); // Purple + +MaterialApp( + theme: ThemeData.light().copyWith( + extensions: [ + ClerkThemeExtension( + colors: ClerkThemeColors( + background: Colors.white, + altBackground: const Color(0xFFF9FAFB), + borderSide: const Color(0xFFE5E7EB), + text: const Color(0xFF111827), + icon: const Color(0xFF6B7280), + lightweightText: const Color(0xFF9CA3AF), + error: const Color(0xFFEF4444), + accent: brandPrimary, + ), + styles: ClerkThemeStyles( + heading: const TextStyle( + fontSize: 24, + fontWeight: FontWeight.bold, + color: Color(0xFF111827), + ), + subheading: const TextStyle( + fontSize: 18, + fontWeight: FontWeight.w600, + color: Color(0xFF374151), + ), + text: const TextStyle( + fontSize: 14, + color: Color(0xFF111827), + ), + button: const TextStyle( + fontSize: 14, + fontWeight: FontWeight.w600, + color: Colors.white, + ), + ), + ), + ], + ), +) +``` + +### With Custom Fonts + +```dart +ClerkThemeExtension( + colors: ClerkThemeColors( + // ... colors + ), + styles: ClerkThemeStyles( + heading: GoogleFonts.inter( + fontSize: 24, + fontWeight: FontWeight.bold, + ), + subheading: GoogleFonts.inter( + fontSize: 18, + fontWeight: FontWeight.w600, + ), + text: GoogleFonts.inter( + fontSize: 14, + ), + button: GoogleFonts.inter( + fontSize: 14, + fontWeight: FontWeight.w600, + ), + ), +) +``` + +--- + +## Accessing Theme in Widgets + +```dart +class MyWidget extends StatelessWidget { + const MyWidget({super.key}); + + @override + Widget build(BuildContext context) { + final theme = ClerkAuth.themeExtensionOf(context); + + return Container( + decoration: BoxDecoration( + color: theme.colors.background, + border: Border.all(color: theme.colors.borderSide), + ), + child: Column( + children: [ + Text( + 'Heading', + style: theme.styles.heading, + ), + Text( + 'Body text', + style: theme.styles.text, + ), + ], + ), + ); + } +} +``` + +--- + +## Color Properties + +- **background**: Main background color for panels and cards +- **altBackground**: Alternative background for sections +- **borderSide**: Border color for inputs and dividers +- **text**: Primary text color +- **icon**: Icon color +- **lightweightText**: Secondary/muted text color +- **error**: Error message and validation color +- **accent**: Primary action color (buttons, links) + +--- + +## Best Practices + +1. **Match your app theme**: Keep Clerk widgets consistent with your app +2. **Support dark mode**: Provide both light and dark themes +3. **Test contrast**: Ensure text is readable on backgrounds +4. **Use semantic colors**: Don't use accent color for errors +5. **Consider accessibility**: Follow WCAG guidelines for color contrast + +--- + +## Related Documentation + +- [ClerkAuth](clerk_auth.md) +- [ClerkAuthentication](clerk_authentication.md) + +--- + +*Generated for clerk_flutter version 0.0.14-beta* + diff --git a/docs/clerk_flutter/clerk_user_button.md b/docs/clerk_flutter/clerk_user_button.md new file mode 100644 index 00000000..85a7e435 --- /dev/null +++ b/docs/clerk_flutter/clerk_user_button.md @@ -0,0 +1,348 @@ +# ClerkUserButton Widget Documentation + +The `ClerkUserButton` widget renders a user profile button with multi-session management, profile access, and sign-out functionality. + +## Overview + +`ClerkUserButton` displays the current user's avatar and provides access to: +- User profile management +- Multi-session switching +- Organization management +- Sign-out functionality +- Custom actions + +## Class Definition + +```dart +class ClerkUserButton extends StatefulWidget { + const ClerkUserButton({ + super.key, + this.showName = true, + this.sessionActions, + this.additionalActions, + }); +} +``` + +--- + +## Parameters + +### `showName` + +**Type:** `bool` +**Default:** `true` + +Whether to display the user's name next to the avatar. + +**Example:** +```dart +ClerkUserButton(showName: false) // Avatar only +``` + +--- + +### `sessionActions` + +**Type:** `List?` + +Custom actions to display for each session. If null, uses default actions (Profile, Sign Out, Organizations). + +**Example:** +```dart +ClerkUserButton( + sessionActions: [ + ClerkUserAction( + icon: Icons.settings, + label: 'Settings', + callback: (context, authState) { + Navigator.pushNamed(context, '/settings'); + }, + ), + ], +) +``` + +--- + +### `additionalActions` + +**Type:** `List?` + +Additional actions to display in the user panel. If null, uses default action (Add Account). + +**Example:** +```dart +ClerkUserButton( + additionalActions: [ + ClerkUserAction( + icon: Icons.help, + label: 'Help', + callback: (context, authState) { + // Show help + }, + ), + ], +) +``` + +--- + +## ClerkUserAction + +```dart +class ClerkUserAction { + ClerkUserAction({ + this.icon, + this.asset, + required this.label, + required this.callback, + }); + + final IconData? icon; + final String? asset; + final String label; + final Future Function(BuildContext, ClerkAuthState) callback; +} +``` + +--- + +## Complete Examples + +### Basic Usage + +```dart +class MyAppBar extends StatelessWidget implements PreferredSizeWidget { + const MyAppBar({super.key}); + + @override + Size get preferredSize => const Size.fromHeight(kToolbarHeight); + + @override + Widget build(BuildContext context) { + return AppBar( + title: const Text('My App'), + actions: const [ + ClerkUserButton(), + ], + ); + } +} +``` + +### Avatar Only + +```dart +AppBar( + actions: const [ + ClerkUserButton(showName: false), + ], +) +``` + +### Custom Session Actions + +```dart +ClerkUserButton( + sessionActions: [ + ClerkUserAction( + icon: Icons.person, + label: 'Profile', + callback: (context, authState) async { + Navigator.pushNamed(context, '/profile'); + }, + ), + ClerkUserAction( + icon: Icons.settings, + label: 'Settings', + callback: (context, authState) async { + Navigator.pushNamed(context, '/settings'); + }, + ), + ClerkUserAction( + icon: Icons.logout, + label: 'Sign Out', + callback: (context, authState) async { + await authState.signOut(); + }, + ), + ], +) +``` + +### With Additional Actions + +```dart +ClerkUserButton( + additionalActions: [ + ClerkUserAction( + icon: Icons.add, + label: 'Add Account', + callback: (context, authState) async { + // Add account logic + }, + ), + ClerkUserAction( + icon: Icons.help, + label: 'Help & Support', + callback: (context, authState) async { + Navigator.pushNamed(context, '/help'); + }, + ), + ], +) +``` + +### In Drawer + +```dart +class MyDrawer extends StatelessWidget { + const MyDrawer({super.key}); + + @override + Widget build(BuildContext context) { + return Drawer( + child: ListView( + children: [ + DrawerHeader( + child: ClerkSignedIn( + child: const ClerkUserButton(), + ), + ), + ListTile( + leading: const Icon(Icons.home), + title: const Text('Home'), + onTap: () => Navigator.pop(context), + ), + // ... more items + ], + ), + ); + } +} +``` + +--- + +## Features + +### Multi-Session Support + +When multiple sessions exist, the button displays all sessions and allows switching between them: + +```dart +// Users can: +// 1. See all active sessions +// 2. Click to switch between sessions +// 3. Sign out of individual sessions +// 4. Add new accounts +``` + +### Profile Management + +Clicking the profile action opens the user profile panel where users can: +- Edit name and username +- Manage email addresses +- Manage phone numbers +- Add/remove passkeys +- Connect/disconnect OAuth accounts +- Update profile image + +### Organization Management + +If organizations are enabled, users can: +- View their organizations +- Switch active organization +- Create new organizations +- Accept invitations + +--- + +## Default Actions + +### Session Actions (default) + +1. **Profile** - Opens user profile panel +2. **Sign Out** - Signs out of the current session +3. **Organizations** - Opens organization list (if enabled) + +### Additional Actions (default) + +1. **Add Account** - Adds a new account (if multi-session enabled) + +--- + +## Best Practices + +1. **Use in signed-in areas**: Only show when user is signed in +2. **Place in AppBar**: Common pattern is to place in AppBar actions +3. **Customize for your app**: Add custom actions relevant to your app +4. **Test multi-session**: Test with multiple accounts if multi-session is enabled +5. **Consider mobile**: The button adapts to mobile screens + +--- + +## Common Patterns + +### Conditional Display + +```dart +AppBar( + actions: [ + ClerkSignedIn( + child: const ClerkUserButton(), + ), + ClerkSignedOut( + child: TextButton( + onPressed: () => Navigator.pushNamed(context, '/login'), + child: const Text('Sign In'), + ), + ), + ], +) +``` + +### With Navigation + +```dart +ClerkUserButton( + sessionActions: [ + ClerkUserAction( + icon: Icons.dashboard, + label: 'Dashboard', + callback: (context, authState) async { + Navigator.pushNamed(context, '/dashboard'); + }, + ), + ClerkUserAction( + icon: Icons.person, + label: 'Profile', + callback: (context, authState) async { + Navigator.pushNamed(context, '/profile'); + }, + ), + ClerkUserAction( + icon: Icons.logout, + label: 'Sign Out', + callback: (context, authState) async { + await authState.signOut(); + Navigator.pushReplacementNamed(context, '/'); + }, + ), + ], +) +``` + +--- + +## Related Documentation + +- [ClerkAuth](clerk_auth.md) +- [ClerkSignedIn](clerk_signed_in.md) +- [ClerkOrganizationList](clerk_organization_list.md) + +--- + +*Generated for clerk_flutter version 0.0.14-beta* + diff --git a/docs/getting-started.md b/docs/getting-started.md deleted file mode 100644 index 35c27fe5..00000000 --- a/docs/getting-started.md +++ /dev/null @@ -1,133 +0,0 @@ ---- -layout: default -title: Getting Started -nav_order: 2 -has_children: true -permalink: /getting-started ---- - -# Getting Started with Clerk Dart & Flutter SDKs - -This guide will help you integrate Clerk authentication into your Dart or Flutter application. - -## Choose Your SDK - -Clerk provides two SDKs for Dart and Flutter development: - -### clerk_auth (Dart SDK) - -The `clerk_auth` package is a pure Dart SDK suitable for: -- Backend applications -- CLI tools -- Server-side Dart applications -- Any Dart environment where you need authentication - -**[Get started with clerk_auth →]({{ '/getting-started/quickstart-dart' | relative_url }})** - -### clerk_flutter (Flutter SDK) - -The `clerk_flutter` package is designed for Flutter applications and includes: -- Pre-built UI components -- Material Design integration -- Cross-platform support (iOS, Android, Web, Desktop) -- Seamless integration with Flutter's widget tree - -**[Get started with clerk_flutter →]({{ '/getting-started/quickstart-flutter' | relative_url }})** - ---- - -## Prerequisites - -Before you begin, you'll need: - -1. **A Clerk Account** - [Sign up for free](https://dashboard.clerk.com/sign-up) -2. **A Clerk Application** - Create one in the [Clerk Dashboard](https://dashboard.clerk.com) -3. **Your Publishable Key** - Found in your application's API Keys section - -### Finding Your Publishable Key - -1. Navigate to the [Clerk Dashboard](https://dashboard.clerk.com) -2. Select your application -3. Go to **API Keys** in the sidebar -4. Copy your **Publishable Key** (starts with `pk_test_` or `pk_live_`) - -{: .warning } -> Never commit your secret keys to version control. Use environment variables or secure configuration management. - ---- - -## Installation - -### For Dart Projects (clerk_auth) - -Add `clerk_auth` to your `pubspec.yaml`: - -```yaml -dependencies: - clerk_auth: ^0.0.13-beta -``` - -Then run: - -```bash -dart pub get -``` - -### For Flutter Projects (clerk_flutter) - -Add `clerk_flutter` to your `pubspec.yaml`: - -```yaml -dependencies: - clerk_flutter: ^0.0.13-beta -``` - -Then run: - -```bash -flutter pub get -``` - -{: .note } -> The `clerk_flutter` package automatically includes `clerk_auth` as a dependency, so you don't need to add both. - ---- - -## Platform-Specific Setup - -### Android - -Add the following permission to your `android/app/src/main/AndroidManifest.xml`: - -```xml - -``` - -### iOS - -No additional setup required for basic functionality. - -### Web - -No additional setup required for basic functionality. - -### Desktop (Windows, macOS, Linux) - -No additional setup required for basic functionality. - ---- - -## Next Steps - -Choose your quickstart guide based on your project type: - -- **[Dart Quickstart]({{ '/getting-started/quickstart-dart' | relative_url }})** - For pure Dart applications -- **[Flutter Quickstart]({{ '/getting-started/quickstart-flutter' | relative_url }})** - For Flutter applications - -Or explore specific topics: - -- [Authentication Flows]({{ '/guides/authentication' | relative_url }}) -- [User Management]({{ '/guides/user-management' | relative_url }}) -- [Organizations]({{ '/guides/organizations' | relative_url }}) -- [Customization]({{ '/guides/customization' | relative_url }}) - diff --git a/docs/getting-started/quickstart-dart.md b/docs/getting-started/quickstart-dart.md deleted file mode 100644 index 3676e92d..00000000 --- a/docs/getting-started/quickstart-dart.md +++ /dev/null @@ -1,261 +0,0 @@ ---- -layout: default -title: Dart Quickstart -parent: Getting Started -nav_order: 1 ---- - -# Dart Quickstart -{: .no_toc } - -Get started with Clerk authentication in your Dart application in minutes. -{: .fs-6 .fw-300 } - -## Table of contents -{: .no_toc .text-delta } - -1. TOC -{:toc} - ---- - -## Install clerk_auth - -Add `clerk_auth` to your `pubspec.yaml`: - -```yaml -dependencies: - clerk_auth: ^0.0.13-beta -``` - -Install the package: - -```bash -dart pub get -``` - ---- - -## Set up your environment - -Create a `.env` file or set environment variables with your Clerk publishable key: - -```bash -CLERK_PUBLISHABLE_KEY=pk_test_xxxxxxxxxxxxx -``` - -{: .warning } -> Never commit your `.env` file to version control. Add it to your `.gitignore`. - ---- - -## Initialize Clerk Auth - -Create an `Auth` instance and initialize it: - -```dart -import 'dart:io'; -import 'package:clerk_auth/clerk_auth.dart'; - -Future main() async { - // Create the Auth instance - final auth = Auth( - config: AuthConfig( - publishableKey: Platform.environment['CLERK_PUBLISHABLE_KEY']!, - persistor: DefaultPersistor( - getCacheDirectory: () => Directory.current, - ), - ), - ); - - // Initialize the auth system - await auth.initialize(); - - // Your app logic here - - // Clean up when done - auth.terminate(); -} -``` - ---- - -## Sign in with email and password - -```dart -import 'dart:io'; -import 'package:clerk_auth/clerk_auth.dart'; - -Future main() async { - final auth = Auth( - config: AuthConfig( - publishableKey: Platform.environment['CLERK_PUBLISHABLE_KEY']!, - persistor: DefaultPersistor( - getCacheDirectory: () => Directory.current, - ), - ), - ); - - await auth.initialize(); - - try { - // Attempt to sign in - await auth.attemptSignIn( - strategy: Strategy.password, - identifier: 'user@example.com', - password: 'your-password', - ); - - print('✅ Signed in as: ${auth.user?.emailAddress}'); - print('User ID: ${auth.user?.id}'); - - // Access the current user - final user = auth.user; - if (user != null) { - print('Name: ${user.firstName} ${user.lastName}'); - print('Email: ${user.emailAddress}'); - } - - } on ClerkApiException catch (e) { - print('❌ Sign in failed: ${e.message}'); - } finally { - auth.terminate(); - } -} -``` - ---- - -## Sign up a new user - -```dart -import 'package:clerk_auth/clerk_auth.dart'; - -Future signUpUser(Auth auth) async { - try { - // Start the sign-up process - await auth.attemptSignUp( - strategy: Strategy.password, - identifier: 'newuser@example.com', - password: 'secure-password', - ); - - // If email verification is required - if (auth.signUp?.verification?.status == VerificationStatus.unverified) { - print('📧 Verification email sent'); - - // User enters the code from their email - final code = '123456'; // Get this from user input - - await auth.attemptSignUpVerification( - strategy: Strategy.emailCode, - code: code, - ); - } - - print('✅ Sign up successful!'); - print('User: ${auth.user?.emailAddress}'); - - } on ClerkApiException catch (e) { - print('❌ Sign up failed: ${e.message}'); - } -} -``` - ---- - -## Sign out - -```dart -Future signOutUser(Auth auth) async { - await auth.signOut(); - print('👋 Signed out successfully'); -} -``` - ---- - -## Access session tokens - -Session tokens are automatically managed and can be accessed via a stream: - -```dart -import 'dart:async'; - -Future listenToSessionTokens(Auth auth) async { - // Listen to session token updates - final subscription = auth.sessionTokenStream.listen((token) { - if (token != null) { - print('🔑 New session token: ${token.substring(0, 20)}...'); - // Use this token for authenticated API requests - } - }); - - // Remember to cancel the subscription when done - await Future.delayed(Duration(seconds: 30)); - await subscription.cancel(); -} -``` - ---- - -## Next steps - -Now that you have basic authentication working, explore more features: - -- [Authentication Strategies]({{ '/guides/authentication' | relative_url }}) - OAuth, passwordless, MFA -- [User Management]({{ '/guides/user-management' | relative_url }}) - Update profiles, manage sessions -- [Session Token Polling]({{ '/guides/session-tokens' | relative_url }}) - Configure token refresh behavior -- [Error Handling]({{ '/guides/error-handling' | relative_url }}) - Handle authentication errors gracefully - ---- - -## Complete example - -Here's a complete example with sign-up, sign-in, and sign-out: - -```dart -import 'dart:io'; -import 'package:clerk_auth/clerk_auth.dart'; - -Future main() async { - final auth = Auth( - config: AuthConfig( - publishableKey: Platform.environment['CLERK_PUBLISHABLE_KEY']!, - persistor: DefaultPersistor( - getCacheDirectory: () => Directory.current, - ), - ), - ); - - await auth.initialize(); - - try { - // Check if already signed in - if (auth.user != null) { - print('Already signed in as: ${auth.user?.emailAddress}'); - } else { - // Sign in - await auth.attemptSignIn( - strategy: Strategy.password, - identifier: 'user@example.com', - password: 'password', - ); - print('Signed in successfully!'); - } - - // Do authenticated work - print('User ID: ${auth.user?.id}'); - - // Sign out - await auth.signOut(); - print('Signed out'); - - } catch (e) { - print('Error: $e'); - } finally { - auth.terminate(); - } -} -``` - diff --git a/docs/getting-started/quickstart-flutter.md b/docs/getting-started/quickstart-flutter.md deleted file mode 100644 index f631906f..00000000 --- a/docs/getting-started/quickstart-flutter.md +++ /dev/null @@ -1,390 +0,0 @@ ---- -layout: default -title: Flutter Quickstart -parent: Getting Started -nav_order: 2 ---- - -# Flutter Quickstart -{: .no_toc } - -Get started with Clerk authentication in your Flutter application with pre-built UI components. -{: .fs-6 .fw-300 } - -## Table of contents -{: .no_toc .text-delta } - -1. TOC -{:toc} - ---- - -## Install clerk_flutter - -Add `clerk_flutter` to your `pubspec.yaml`: - -```yaml -dependencies: - clerk_flutter: ^0.0.13-beta -``` - -Install the package: - -```bash -flutter pub get -``` - ---- - -## Platform setup - -### Android - -Add the Internet permission to `android/app/src/main/AndroidManifest.xml`: - -```xml - - - - -``` - -### iOS, Web, Desktop - -No additional setup required. - ---- - -## Wrap your app with ClerkAuth - -Wrap your `MaterialApp` with the `ClerkAuth` widget: - -```dart -import 'package:flutter/material.dart'; -import 'package:clerk_flutter/clerk_flutter.dart'; - -void main() { - runApp(const MyApp()); -} - -class MyApp extends StatelessWidget { - const MyApp({super.key}); - - @override - Widget build(BuildContext context) { - return ClerkAuth( - config: ClerkAuthConfig( - publishableKey: 'pk_test_xxxxxxxxxxxxx', - ), - child: MaterialApp( - title: 'My Clerk App', - theme: ThemeData.light(), - darkTheme: ThemeData.dark(), - home: const HomePage(), - ), - ); - } -} -``` - -{: .warning } -> Replace `pk_test_xxxxxxxxxxxxx` with your actual publishable key from the [Clerk Dashboard](https://dashboard.clerk.com). - ---- - -## Build conditional UI with ClerkAuthBuilder - -Use `ClerkAuthBuilder` to show different UI based on authentication state: - -```dart -import 'package:flutter/material.dart'; -import 'package:clerk_flutter/clerk_flutter.dart'; - -class HomePage extends StatelessWidget { - const HomePage({super.key}); - - @override - Widget build(BuildContext context) { - return Scaffold( - body: SafeArea( - child: ClerkErrorListener( - child: ClerkAuthBuilder( - signedInBuilder: (context, authState) { - // User is signed in - return Center( - child: Column( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - Text('Welcome, ${authState.user?.firstName}!'), - const SizedBox(height: 20), - const ClerkUserButton(), - ], - ), - ); - }, - signedOutBuilder: (context, authState) { - // User is signed out - return const Center( - child: ClerkAuthentication(), - ); - }, - ), - ), - ), - ); - } -} -``` - ---- - -## Use pre-built authentication UI - -The `ClerkAuthentication` widget provides a complete sign-in/sign-up flow: - -```dart -import 'package:flutter/material.dart'; -import 'package:clerk_flutter/clerk_flutter.dart'; - -class SignInPage extends StatelessWidget { - const SignInPage({super.key}); - - @override - Widget build(BuildContext context) { - return Scaffold( - body: SafeArea( - child: Center( - child: ClerkAuthentication(), - ), - ), - ); - } -} -``` - -The `ClerkAuthentication` widget automatically handles: -- Email/password sign-in -- Email/password sign-up -- OAuth providers (Google, GitHub, etc.) -- Email verification -- Password reset -- Error states - ---- - -## Use the ClerkUserButton - -The `ClerkUserButton` provides a user menu with profile management and sign-out: - -```dart -import 'package:flutter/material.dart'; -import 'package:clerk_flutter/clerk_flutter.dart'; - -class ProfilePage extends StatelessWidget { - const ProfilePage({super.key}); - - @override - Widget build(BuildContext context) { - return Scaffold( - appBar: AppBar( - title: const Text('Profile'), - actions: const [ - ClerkUserButton(), - ], - ), - body: ClerkSignedIn( - child: Center( - child: Text('Protected content'), - ), - ), - ); - } -} -``` - ---- - -## Conditional rendering with ClerkSignedIn/ClerkSignedOut - -Use these widgets to conditionally render content: - -```dart -import 'package:flutter/material.dart'; -import 'package:clerk_flutter/clerk_flutter.dart'; - -class ConditionalPage extends StatelessWidget { - const ConditionalPage({super.key}); - - @override - Widget build(BuildContext context) { - return Scaffold( - body: Column( - children: [ - ClerkSignedIn( - child: Text('This is only visible when signed in'), - ), - ClerkSignedOut( - child: Text('This is only visible when signed out'), - ), - ], - ), - ); - } -} -``` - ---- - -## Access the current user - -Access user information through the `ClerkAuth` inherited widget: - -```dart -import 'package:flutter/material.dart'; -import 'package:clerk_flutter/clerk_flutter.dart'; - -class UserInfoWidget extends StatelessWidget { - const UserInfoWidget({super.key}); - - @override - Widget build(BuildContext context) { - final authState = ClerkAuth.of(context); - final user = authState.user; - - if (user == null) { - return const Text('Not signed in'); - } - - return Column( - children: [ - Text('Name: ${user.firstName} ${user.lastName}'), - Text('Email: ${user.emailAddress}'), - Text('User ID: ${user.id}'), - if (user.imageUrl != null) - Image.network(user.imageUrl!), - ], - ); - } -} -``` - ---- - -## Handle deep links for OAuth - -For OAuth authentication, you need to handle deep links: - -```dart -import 'package:clerk_flutter/clerk_flutter.dart'; -import 'package:app_links/app_links.dart'; - -ClerkAuth( - config: ClerkAuthConfig( - publishableKey: 'pk_test_xxxxxxxxxxxxx', - redirectionGenerator: (url) { - // Generate your app's deep link - return Uri.parse('myapp://oauth-callback'); - }, - deepLinkStream: AppLinks().allUriLinkStream.map((uri) { - // Handle incoming deep links - return ClerkDeepLink(uri: uri); - }), - ), - child: MaterialApp( - // ... - ), -) -``` - -Add the `app_links` package to handle deep links: - -```yaml -dependencies: - app_links: ^3.5.1 -``` - ---- - -## Next steps - -Explore more features and customization options: - -- [Widget Reference]({{ '/api/widgets' | relative_url }}) - All available widgets and their properties -- [Customization]({{ '/guides/customization' | relative_url }}) - Theme and style your components -- [Authentication Flows]({{ '/guides/authentication' | relative_url }}) - OAuth, passwordless, MFA -- [User Management]({{ '/guides/user-management' | relative_url }}) - Profile updates, session management -- [Organizations]({{ '/guides/organizations' | relative_url }}) - Multi-tenant support - ---- - -## Complete example - -Here's a complete Flutter app with Clerk authentication: - -```dart -import 'package:flutter/material.dart'; -import 'package:clerk_flutter/clerk_flutter.dart'; - -void main() { - runApp(const MyApp()); -} - -class MyApp extends StatelessWidget { - const MyApp({super.key}); - - @override - Widget build(BuildContext context) { - return ClerkAuth( - config: ClerkAuthConfig( - publishableKey: 'pk_test_xxxxxxxxxxxxx', - ), - child: MaterialApp( - title: 'Clerk Flutter Demo', - theme: ThemeData.light(), - darkTheme: ThemeData.dark(), - home: const HomePage(), - ), - ); - } -} - -class HomePage extends StatelessWidget { - const HomePage({super.key}); - - @override - Widget build(BuildContext context) { - return Scaffold( - appBar: AppBar( - title: const Text('Clerk Flutter Demo'), - ), - body: SafeArea( - child: ClerkErrorListener( - child: ClerkAuthBuilder( - signedInBuilder: (context, authState) { - return Center( - child: Column( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - Text( - 'Welcome, ${authState.user?.firstName ?? "User"}!', - style: Theme.of(context).textTheme.headlineMedium, - ), - const SizedBox(height: 20), - const ClerkUserButton(showName: true), - ], - ), - ); - }, - signedOutBuilder: (context, authState) { - return const Center( - child: ClerkAuthentication(), - ); - }, - ), - ), - ), - ); - } -} -``` - diff --git a/docs/guides/_index.md b/docs/guides/_index.md deleted file mode 100644 index af6f5bb9..00000000 --- a/docs/guides/_index.md +++ /dev/null @@ -1,43 +0,0 @@ ---- -layout: default -title: Guides -nav_order: 3 -has_children: true -permalink: /guides ---- - -# Guides - -Comprehensive guides for implementing authentication and user management with Clerk's Dart and Flutter SDKs. - -## Available Guides - -### [Authentication]({{ '/guides/authentication' | relative_url }}) -Learn how to implement various authentication strategies including email/password, passwordless, OAuth, and multi-factor authentication. - -### [User Management]({{ '/guides/user-management' | relative_url }}) -Manage user profiles, update user information, handle email addresses and phone numbers, and work with user metadata. - -### [Organizations]({{ '/guides/organizations' | relative_url }}) -Implement multi-tenant functionality with organizations, roles, and permissions. - -### [Customization]({{ '/guides/customization' | relative_url }}) -Customize the appearance and behavior of Clerk's UI components to match your brand. - -### [Session Tokens]({{ '/guides/session-tokens' | relative_url }}) -Work with JWT session tokens for authenticated API requests. - -### [Error Handling]({{ '/guides/error-handling' | relative_url }}) -Handle authentication errors and provide great user experiences. - -### [Backend Integration]({{ '/guides/backend-integration' | relative_url }}) -Integrate Clerk with your backend services and APIs. - ---- - -## Quick Navigation - -- **Getting Started?** Check out the [Dart Quickstart]({{ '/getting-started/quickstart-dart' | relative_url }}) or [Flutter Quickstart]({{ '/getting-started/quickstart-flutter' | relative_url }}) -- **Need API Reference?** See the [API Documentation]({{ '/api' | relative_url }}) -- **Looking for Examples?** Browse the [GitHub Examples](https://github.com/clerk/clerk-sdk-flutter/tree/main/packages/clerk_flutter/example) - diff --git a/docs/guides/authentication.md b/docs/guides/authentication.md deleted file mode 100644 index e8185c32..00000000 --- a/docs/guides/authentication.md +++ /dev/null @@ -1,370 +0,0 @@ ---- -layout: default -title: Authentication -parent: Guides -nav_order: 1 ---- - -# Authentication Flows -{: .no_toc } - -Learn how to implement various authentication strategies with Clerk's Dart and Flutter SDKs. -{: .fs-6 .fw-300 } - -## Table of contents -{: .no_toc .text-delta } - -1. TOC -{:toc} - ---- - -## Overview - -Clerk supports multiple authentication strategies to meet your application's needs: - -- **Email & Password** - Traditional username/password authentication -- **Email Code (Passwordless)** - One-time codes sent via email -- **Phone Code (Passwordless)** - One-time codes sent via SMS -- **OAuth** - Social login with Google, GitHub, Facebook, and more -- **Multi-Factor Authentication (MFA)** - Additional security layer - ---- - -## Email and Password Authentication - -### Sign In - -```dart -import 'package:clerk_auth/clerk_auth.dart'; - -Future signInWithPassword(Auth auth, String email, String password) async { - try { - await auth.attemptSignIn( - strategy: Strategy.password, - identifier: email, - password: password, - ); - - print('✅ Signed in as: ${auth.user?.emailAddress}'); - } on ClerkApiException catch (e) { - print('❌ Sign in failed: ${e.message}'); - // Handle specific error codes - if (e.errors.any((err) => err.code == 'form_password_incorrect')) { - print('Incorrect password'); - } - } -} -``` - -### Sign Up - -```dart -Future signUpWithPassword( - Auth auth, - String email, - String password, - {String? firstName, String? lastName} -) async { - try { - await auth.attemptSignUp( - strategy: Strategy.password, - identifier: email, - password: password, - firstName: firstName, - lastName: lastName, - ); - - // Check if email verification is required - if (auth.signUp?.verification?.status == VerificationStatus.unverified) { - print('📧 Verification email sent to $email'); - // Proceed to verification step - } else { - print('✅ Sign up successful!'); - } - } on ClerkApiException catch (e) { - print('❌ Sign up failed: ${e.message}'); - } -} -``` - -### Email Verification - -```dart -Future verifyEmail(Auth auth, String code) async { - try { - await auth.attemptSignUpVerification( - strategy: Strategy.emailCode, - code: code, - ); - - print('✅ Email verified! User is now signed in.'); - } on ClerkApiException catch (e) { - print('❌ Verification failed: ${e.message}'); - } -} -``` - ---- - -## Passwordless Authentication - -### Email Code (Magic Link) - -```dart -Future signInWithEmailCode(Auth auth, String email) async { - try { - // Step 1: Request the code - await auth.attemptSignIn( - strategy: Strategy.emailCode, - identifier: email, - ); - - print('📧 Verification code sent to $email'); - - // Step 2: User enters the code from their email - final code = '123456'; // Get from user input - - await auth.attemptSignInVerification( - strategy: Strategy.emailCode, - code: code, - ); - - print('✅ Signed in successfully!'); - } on ClerkApiException catch (e) { - print('❌ Error: ${e.message}'); - } -} -``` - -### Phone Code (SMS) - -```dart -Future signInWithPhoneCode(Auth auth, String phoneNumber) async { - try { - // Step 1: Request the SMS code - await auth.attemptSignIn( - strategy: Strategy.phoneCode, - identifier: phoneNumber, // Format: +1234567890 - ); - - print('📱 SMS code sent to $phoneNumber'); - - // Step 2: User enters the code from SMS - final code = '123456'; // Get from user input - - await auth.attemptSignInVerification( - strategy: Strategy.phoneCode, - code: code, - ); - - print('✅ Signed in successfully!'); - } on ClerkApiException catch (e) { - print('❌ Error: ${e.message}'); - } -} -``` - ---- - -## OAuth Authentication - -### Flutter OAuth Flow - -For Flutter applications, use the built-in OAuth support: - -```dart -import 'package:flutter/material.dart'; -import 'package:clerk_flutter/clerk_flutter.dart'; - -class OAuthSignInPage extends StatelessWidget { - const OAuthSignInPage({super.key}); - - @override - Widget build(BuildContext context) { - return Scaffold( - body: Center( - child: ClerkAuthentication(), - ), - ); - } -} -``` - -The `ClerkAuthentication` widget automatically displays configured OAuth providers. - -### Custom OAuth Button - -```dart -import 'package:clerk_flutter/clerk_flutter.dart'; - -class CustomOAuthButton extends StatelessWidget { - const CustomOAuthButton({super.key}); - - @override - Widget build(BuildContext context) { - final authState = ClerkAuth.of(context); - - return ElevatedButton.icon( - icon: Icon(Icons.login), - label: Text('Sign in with Google'), - onPressed: () async { - try { - await authState.attemptSignIn( - strategy: Strategy.oauth(OAuthProvider.google), - ); - } catch (e) { - print('OAuth error: $e'); - } - }, - ); - } -} -``` - -### Supported OAuth Providers - -Clerk supports the following OAuth providers: - -- Google (`OAuthProvider.google`) -- GitHub (`OAuthProvider.github`) -- Facebook (`OAuthProvider.facebook`) -- Microsoft (`OAuthProvider.microsoft`) -- Apple (`OAuthProvider.apple`) -- Discord (`OAuthProvider.discord`) -- Twitter/X (`OAuthProvider.twitter`) -- LinkedIn (`OAuthProvider.linkedin`) -- And many more... - ---- - -## Multi-Factor Authentication (MFA) - -### Enable MFA for a User - -```dart -Future enableMFA(Auth auth) async { - final user = auth.user; - if (user == null) return; - - try { - // Generate TOTP secret - final totp = await user.createTOTP(); - - print('Scan this QR code: ${totp.qrCodeUrl}'); - print('Or enter this secret: ${totp.secret}'); - - // User scans QR code and enters verification code - final code = '123456'; // Get from user input - - await totp.verify(code: code); - - print('✅ MFA enabled successfully!'); - } catch (e) { - print('❌ MFA setup failed: $e'); - } -} -``` - -### Sign In with MFA - -```dart -Future signInWithMFA(Auth auth, String email, String password) async { - try { - // Step 1: Initial sign-in - await auth.attemptSignIn( - strategy: Strategy.password, - identifier: email, - password: password, - ); - - // Step 2: Check if MFA is required - if (auth.signIn?.secondFactor?.status == VerificationStatus.unverified) { - print('🔐 MFA code required'); - - // User enters their TOTP code - final mfaCode = '123456'; // Get from user input - - await auth.attemptSignInVerification( - strategy: Strategy.totp, - code: mfaCode, - ); - } - - print('✅ Signed in with MFA!'); - } catch (e) { - print('❌ Sign in failed: $e'); - } -} -``` - ---- - -## Password Reset - -```dart -Future resetPassword(Auth auth, String email) async { - try { - // Step 1: Request password reset - await auth.resetPassword(email: email); - - print('📧 Password reset email sent to $email'); - - // Step 2: User clicks link in email and enters new password - // This typically happens in a web view or separate flow - - } catch (e) { - print('❌ Password reset failed: $e'); - } -} -``` - ---- - -## Session Management - -### Check Authentication Status - -```dart -bool isSignedIn(Auth auth) { - return auth.user != null; -} -``` - -### Get Current Session - -```dart -import 'package:clerk_auth/clerk_auth.dart'; - -Session? getCurrentSession(Auth auth) { - return auth.client.activeSession; -} -``` - -### Sign Out - -```dart -Future signOut(Auth auth) async { - await auth.signOut(); - print('👋 Signed out successfully'); -} -``` - -### Sign Out of Specific Session - -```dart -Future signOutOfSession(Auth auth, Session session) async { - await auth.signOutOf(session); - print('👋 Signed out of session: ${session.id}'); -} -``` - ---- - -## Next Steps - -- [User Management]({{ '/guides/user-management' | relative_url }}) - Manage user profiles and data -- [Session Tokens]({{ '/guides/session-tokens' | relative_url }}) - Work with JWT tokens -- [Error Handling]({{ '/guides/error-handling' | relative_url }}) - Handle authentication errors -- [Organizations]({{ '/guides/organizations' | relative_url }}) - Multi-tenant authentication - diff --git a/docs/guides/backend-integration.md b/docs/guides/backend-integration.md deleted file mode 100644 index 572e646b..00000000 --- a/docs/guides/backend-integration.md +++ /dev/null @@ -1,567 +0,0 @@ ---- -layout: default -title: Backend Integration -parent: Guides -nav_order: 7 ---- - -# Backend Integration -{: .no_toc } - -Learn how to integrate Clerk authentication with your backend services and verify session tokens. -{: .fs-6 .fw-300 } - -## Table of contents -{: .no_toc .text-delta } - -1. TOC -{:toc} - ---- - -## Overview - -When building applications with Clerk, you'll often need to: - -- Verify session tokens on your backend -- Access user information server-side -- Implement protected API endpoints -- Manage users from your backend - ---- - -## Verifying Session Tokens - -Session tokens are JWTs that can be verified using Clerk's public keys. - -### Using Clerk Backend SDK - -The recommended approach is to use Clerk's backend SDK for your server language: - -**Node.js/Express:** -```javascript -import { ClerkExpressRequireAuth } from '@clerk/clerk-sdk-node'; - -app.get('/api/protected', - ClerkExpressRequireAuth(), - (req, res) => { - const userId = req.auth.userId; - res.json({ message: 'Protected data', userId }); - } -); -``` - -**Python/Flask:** -```python -from clerk_backend_api import Clerk - -clerk = Clerk(bearer_auth="sk_live_xxxxx") - -@app.route('/api/protected') -def protected(): - token = request.headers.get('Authorization').replace('Bearer ', '') - - try: - session = clerk.sessions.verify_token(token) - return {'message': 'Protected data', 'userId': session.user_id} - except Exception as e: - return {'error': 'Unauthorized'}, 401 -``` - ---- - -## Manual JWT Verification - -If you need to manually verify JWTs: - -### 1. Get Clerk's Public Keys - -Clerk's public keys are available at: -``` -https://[your-frontend-api]/.well-known/jwks.json -``` - -Example: -``` -https://clerk.example.com/.well-known/jwks.json -``` - -### 2. Verify the JWT - -**Node.js Example:** -```javascript -import jwt from 'jsonwebtoken'; -import jwksClient from 'jwks-rsa'; - -const client = jwksClient({ - jwksUri: 'https://clerk.example.com/.well-known/jwks.json' -}); - -function getKey(header, callback) { - client.getSigningKey(header.kid, (err, key) => { - const signingKey = key.publicKey || key.rsaPublicKey; - callback(null, signingKey); - }); -} - -function verifyToken(token) { - return new Promise((resolve, reject) => { - jwt.verify(token, getKey, { - algorithms: ['RS256'] - }, (err, decoded) => { - if (err) reject(err); - else resolve(decoded); - }); - }); -} - -// Usage -app.get('/api/protected', async (req, res) => { - const token = req.headers.authorization?.replace('Bearer ', ''); - - try { - const payload = await verifyToken(token); - res.json({ userId: payload.sub }); - } catch (err) { - res.status(401).json({ error: 'Unauthorized' }); - } -}); -``` - ---- - -## Token Claims - -A verified Clerk session token contains these claims: - -```json -{ - "sub": "user_2abc123", // User ID - "sid": "sess_xyz789", // Session ID - "iat": 1616239022, // Issued at - "exp": 1616242622, // Expires at - "azp": "https://example.com", // Authorized party - "org_id": "org_456", // Organization ID (if applicable) - "org_role": "admin", // Organization role (if applicable) - "org_permissions": ["read", "write"] // Permissions (if applicable) -} -``` - ---- - - ---- - -## Protecting API Endpoints - -### Example: Express.js Middleware - -```javascript -import { ClerkExpressRequireAuth } from '@clerk/clerk-sdk-node'; - -// Protect a single route -app.get('/api/user/profile', - ClerkExpressRequireAuth(), - (req, res) => { - const userId = req.auth.userId; - res.json({ userId, message: 'Protected data' }); - } -); - -// Protect multiple routes -app.use('/api/admin', ClerkExpressRequireAuth()); - -app.get('/api/admin/users', (req, res) => { - // Only accessible with valid session token - res.json({ users: [] }); -}); -``` - -### Example: Python/FastAPI - -```python -from fastapi import FastAPI, Depends, HTTPException -from fastapi.security import HTTPBearer, HTTPAuthorizationCredentials -from clerk_backend_api import Clerk - -app = FastAPI() -security = HTTPBearer() -clerk = Clerk(bearer_auth="sk_live_xxxxx") - -async def verify_clerk_token( - credentials: HTTPAuthorizationCredentials = Depends(security) -): - try: - session = clerk.sessions.verify_token(credentials.credentials) - return session - except Exception: - raise HTTPException(status_code=401, detail="Unauthorized") - -@app.get("/api/protected") -async def protected_route(session = Depends(verify_clerk_token)): - return {"userId": session.user_id, "message": "Protected data"} -``` - ---- - -## Organization-Based Access Control - -Verify organization membership and roles: - -### Node.js Example - -```javascript -import { clerkClient } from '@clerk/clerk-sdk-node'; - -app.get('/api/org/:orgId/data', - ClerkExpressRequireAuth(), - async (req, res) => { - const { userId, orgId, orgRole } = req.auth; - const requestedOrgId = req.params.orgId; - - // Verify user belongs to the organization - if (orgId !== requestedOrgId) { - return res.status(403).json({ error: 'Forbidden' }); - } - - // Check role-based permissions - if (orgRole === 'admin') { - // Admin-only data - return res.json({ data: 'Admin data' }); - } else { - // Member data - return res.json({ data: 'Member data' }); - } - } -); -``` - -### Dart Backend Example - -```dart -import 'package:clerk_backend_api/clerk_backend_api.dart'; -import 'package:shelf/shelf.dart'; - -Future handleOrgRequest(Request request) async { - final token = request.headers['authorization']?.replaceFirst('Bearer ', ''); - - if (token == null) { - return Response.forbidden('No token provided'); - } - - try { - // Verify token and get claims - final session = await clerk.sessions.verifyToken(token); - final orgId = session.organizationId; - final orgRole = session.organizationRole; - - // Check organization access - if (orgId != request.params['orgId']) { - return Response.forbidden('Access denied'); - } - - // Role-based logic - if (orgRole == 'admin') { - return Response.ok('Admin data'); - } else { - return Response.ok('Member data'); - } - - } catch (e) { - return Response.forbidden('Invalid token'); - } -} -``` - ---- - -## Managing Users from Backend - -Use the Clerk Backend API to manage users server-side: - -### Create User - -```dart -import 'package:clerk_backend_api/clerk_backend_api.dart'; - -final clerk = Clerk( - security: Security(bearerAuth: 'sk_live_xxxxxxxxxxxxx'), -); - -// Create a new user -final user = await clerk.users.createUser( - request: CreateUserRequestBody( - emailAddress: ['user@example.com'], - password: 'securePassword123', - firstName: 'John', - lastName: 'Doe', - ), -); - -print('Created user: ${user.id}'); -``` - -### Update User - -```dart -// Update user metadata -final updatedUser = await clerk.users.updateUser( - userId: 'user_123', - request: UpdateUserRequestBody( - publicMetadata: { - 'plan': 'premium', - 'credits': 100, - }, - ), -); -``` - -### Delete User - -```dart -// Delete a user -await clerk.users.deleteUser(userId: 'user_123'); -``` - ---- - -## Session Management - -### List User Sessions - -```dart -// Get all sessions for a user -final sessions = await clerk.sessions.getSessionList( - userId: 'user_123', -); - -for (final session in sessions.data) { - print('Session: ${session.id}'); - print('Last active: ${session.lastActiveAt}'); -} -``` - -### Revoke Session - -```dart -// Revoke a specific session -await clerk.sessions.revokeSession(sessionId: 'sess_xyz789'); - -// This will sign the user out of that session -``` - ---- - -## Webhooks - -Clerk can send webhooks to your backend when events occur: - -### Common Webhook Events - -- `user.created` - New user signed up -- `user.updated` - User profile updated -- `user.deleted` - User deleted -- `session.created` - New session started -- `session.ended` - Session ended -- `organization.created` - Organization created -- `organizationMembership.created` - User joined organization - -### Verifying Webhooks - -```javascript -import { Webhook } from 'svix'; - -app.post('/api/webhooks/clerk', async (req, res) => { - const webhookSecret = process.env.CLERK_WEBHOOK_SECRET; - const headers = req.headers; - const payload = req.body; - - const wh = new Webhook(webhookSecret); - - try { - const evt = wh.verify(JSON.stringify(payload), { - 'svix-id': headers['svix-id'], - 'svix-timestamp': headers['svix-timestamp'], - 'svix-signature': headers['svix-signature'], - }); - - // Handle the event - switch (evt.type) { - case 'user.created': - console.log('New user:', evt.data.id); - break; - case 'user.updated': - console.log('User updated:', evt.data.id); - break; - } - - res.json({ received: true }); - } catch (err) { - res.status(400).json({ error: 'Invalid signature' }); - } -}); -``` - ---- - -## Best Practices - -### 1. Always Verify Tokens Server-Side - -```dart -// ❌ DON'T: Trust client-provided user IDs -app.get('/api/user/:userId/data', (req, res) => { - const userId = req.params.userId; // Can be forged! - // ... -}); - -// ✅ DO: Get user ID from verified token -app.get('/api/user/data', - ClerkExpressRequireAuth(), - (req, res) => { - const userId = req.auth.userId; // From verified JWT - // ... - } -); -``` - -### 2. Use Secret Keys Securely - -```dart -// ✅ DO: Use environment variables -final clerk = Clerk( - security: Security( - bearerAuth: Platform.environment['CLERK_SECRET_KEY']!, - ), -); - -// ❌ DON'T: Hardcode secret keys -final clerk = Clerk( - security: Security(bearerAuth: 'sk_live_xxxxx'), // Never do this! -); -``` - -### 3. Implement Rate Limiting - -```javascript -import rateLimit from 'express-rate-limit'; - -const limiter = rateLimit({ - windowMs: 15 * 60 * 1000, // 15 minutes - max: 100 // limit each IP to 100 requests per windowMs -}); - -app.use('/api/', limiter); -``` - -### 4. Log Security Events - -```dart -void logSecurityEvent(String event, Map data) { - print('[SECURITY] $event: ${json.encode(data)}'); - // Send to logging service (e.g., Sentry, CloudWatch) -} - -// Usage -try { - final session = await clerk.sessions.verifyToken(token); - logSecurityEvent('token_verified', {'userId': session.userId}); -} catch (e) { - logSecurityEvent('token_verification_failed', {'error': e.toString()}); -} -``` - ---- - -## Next Steps - -- [Session Tokens]({{ '/guides/session-tokens' | relative_url }}) - Understanding JWT tokens -- [Organizations]({{ '/guides/organizations' | relative_url }}) - Multi-tenant access control -- [User Management]({{ '/guides/user-management' | relative_url }}) - Client-side user operations -- [Error Handling]({{ '/guides/error-handling' | relative_url }}) - Handle backend errors gracefully - - -## Sending Tokens from Client - -### From Dart (clerk_auth) - -```dart -import 'package:http/http.dart' as http; -import 'package:clerk_auth/clerk_auth.dart'; - -Future callProtectedApi(Auth auth) async { - final token = await auth.sessionTokenStream.first; - - if (token == null) { - throw Exception('Not authenticated'); - } - - final response = await http.get( - Uri.parse('https://api.example.com/protected'), - headers: { - 'Authorization': 'Bearer $token', - }, - ); - - print('Response: ${response.body}'); -} -``` - -### From Flutter (clerk_flutter) - -```dart -import 'package:clerk_flutter/clerk_flutter.dart'; - -class ApiService { - Future fetchData(BuildContext context) async { - final authState = ClerkAuth.of(context); - final token = await authState.sessionTokenStream.first; - - if (token == null) { - throw Exception('Not authenticated'); - } - - final response = await http.get( - Uri.parse('https://api.example.com/protected'), - headers: { - 'Authorization': 'Bearer $token', - }, - ); - } -} -``` - ---- - -## Using Clerk Backend API - -For advanced backend operations, use the Clerk Backend API: - -### Available Operations - -- **Users**: Create, update, delete users -- **Sessions**: List, revoke sessions -- **Organizations**: Manage organizations and memberships -- **Invitations**: Send and manage invitations - -### Dart Backend API Client - -```dart -import 'package:clerk_backend_api/clerk_backend_api.dart'; - -final clerk = Clerk( - security: Security(bearerAuth: 'sk_live_xxxxxxxxxxxxx'), -); - -// Get user by ID -final user = await clerk.users.getUser(userId: 'user_123'); -print('User: ${user.emailAddress}'); - -// List all users -final users = await clerk.users.getUserList(); -for (final user in users.data) { - print('User: ${user.emailAddress}'); -} -``` - - diff --git a/docs/guides/customization.md b/docs/guides/customization.md deleted file mode 100644 index 379fadd6..00000000 --- a/docs/guides/customization.md +++ /dev/null @@ -1,461 +0,0 @@ ---- -layout: default -title: Customization -parent: Guides -nav_order: 4 ---- - -# Customization -{: .no_toc } - -Learn how to customize the appearance and behavior of Clerk's Flutter UI components to match your brand. -{: .fs-6 .fw-300 } - -## Table of contents -{: .no_toc .text-delta } - -1. TOC -{:toc} - ---- - -## Overview - -Clerk's Flutter SDK provides extensive customization options through: - -- **ClerkTheme** - Theme extension for colors, typography, and spacing -- **Widget Properties** - Customize individual widget behavior -- **Custom Builders** - Build your own authentication UI - ---- - -## Theming with ClerkTheme - -### Basic Theme Setup - -Apply a custom theme to your Clerk components: - -```dart -import 'package:flutter/material.dart'; -import 'package:clerk_flutter/clerk_flutter.dart'; - -class MyApp extends StatelessWidget { - const MyApp({super.key}); - - @override - Widget build(BuildContext context) { - return ClerkAuth( - config: ClerkAuthConfig( - publishableKey: 'pk_test_xxxxxxxxxxxxx', - ), - child: MaterialApp( - title: 'My App', - theme: ThemeData.light().copyWith( - extensions: [ - ClerkTheme( - colors: ClerkColors.light( - primary: Colors.blue, - background: Colors.white, - text: Colors.black87, - ), - ), - ], - ), - darkTheme: ThemeData.dark().copyWith( - extensions: [ - ClerkTheme( - colors: ClerkColors.dark( - primary: Colors.blueAccent, - background: Colors.grey[900]!, - text: Colors.white, - ), - ), - ], - ), - home: const HomePage(), - ), - ); - } -} -``` - -### Custom Colors - -Define a complete custom color scheme: - -```dart -ClerkTheme( - colors: ClerkColors( - primary: Color(0xFF6366F1), // Primary brand color - secondary: Color(0xFF8B5CF6), // Secondary accent - background: Color(0xFFFFFFFF), // Background color - altBackground: Color(0xFFF9FAFB), // Alternative background - text: Color(0xFF111827), // Primary text - textSecondary: Color(0xFF6B7280), // Secondary text - border: Color(0xFFE5E7EB), // Border color - error: Color(0xFFEF4444), // Error color - success: Color(0xFF10B981), // Success color - warning: Color(0xFFF59E0B), // Warning color - ), -) -``` - -### Typography Customization - -Customize text styles: - -```dart -ClerkTheme( - typography: ClerkTypography( - headlineLarge: TextStyle( - fontSize: 32, - fontWeight: FontWeight.bold, - fontFamily: 'YourCustomFont', - ), - headlineMedium: TextStyle( - fontSize: 24, - fontWeight: FontWeight.w600, - ), - bodyLarge: TextStyle( - fontSize: 16, - fontWeight: FontWeight.normal, - ), - bodyMedium: TextStyle( - fontSize: 14, - ), - labelLarge: TextStyle( - fontSize: 14, - fontWeight: FontWeight.w500, - ), - ), -) -``` - ---- - -## Customizing Individual Widgets - -### ClerkAuthentication Widget - -Customize the authentication flow: - -```dart -import 'package:clerk_flutter/clerk_flutter.dart'; - -ClerkAuthentication( - // Customize which strategies are shown - // (Note: This depends on your Clerk Dashboard configuration) - - // Add custom styling - // The widget respects your ClerkTheme configuration -) -``` - -### ClerkUserButton Widget - -Customize the user button: - -```dart -import 'package:clerk_flutter/clerk_flutter.dart'; - -ClerkUserButton( - showName: true, // Show user's name next to avatar - - // Add custom actions - sessionActions: [ - ClerkUserAction( - label: 'Custom Action', - icon: Icons.star, - onPressed: (context, authState) { - // Handle custom action - print('Custom action pressed'); - }, - ), - ], - - // Add additional menu items - additionalActions: [ - ClerkUserAction( - label: 'Settings', - icon: Icons.settings, - onPressed: (context, authState) { - Navigator.push( - context, - MaterialPageRoute(builder: (_) => SettingsPage()), - ); - }, - ), - ClerkUserAction( - label: 'Help', - icon: Icons.help, - onPressed: (context, authState) { - // Show help dialog - }, - ), - ], -) -``` - ---- - -## Building Custom Authentication UI - -### Custom Sign-In Form - -Build your own sign-in UI using the Auth API: - -```dart -import 'package:flutter/material.dart'; -import 'package:clerk_flutter/clerk_flutter.dart'; - -class CustomSignInForm extends StatefulWidget { - const CustomSignInForm({super.key}); - - @override - State createState() => _CustomSignInFormState(); -} - -class _CustomSignInFormState extends State { - final _emailController = TextEditingController(); - final _passwordController = TextEditingController(); - bool _isLoading = false; - String? _errorMessage; - - @override - void dispose() { - _emailController.dispose(); - _passwordController.dispose(); - super.dispose(); - } - - Future _signIn() async { - setState(() { - _isLoading = true; - _errorMessage = null; - }); - - final authState = ClerkAuth.of(context, listen: false); - - try { - await authState.attemptSignIn( - strategy: Strategy.password, - identifier: _emailController.text, - password: _passwordController.text, - ); - - // Success - navigation handled by ClerkAuthBuilder - } catch (e) { - setState(() { - _errorMessage = e.toString(); - }); - } finally { - setState(() { - _isLoading = false; - }); - } - } - - @override - Widget build(BuildContext context) { - return Card( - margin: const EdgeInsets.all(16), - child: Padding( - padding: const EdgeInsets.all(24), - child: Column( - mainAxisSize: MainAxisSize.min, - crossAxisAlignment: CrossAxisAlignment.stretch, - children: [ - Text( - 'Sign In', - style: Theme.of(context).textTheme.headlineMedium, - textAlign: TextAlign.center, - ), - const SizedBox(height: 24), - - // Email Field - TextField( - controller: _emailController, - decoration: const InputDecoration( - labelText: 'Email', - border: OutlineInputBorder(), - prefixIcon: Icon(Icons.email), - ), - keyboardType: TextInputType.emailAddress, - enabled: !_isLoading, - ), - const SizedBox(height: 16), - - // Password Field - TextField( - controller: _passwordController, - decoration: const InputDecoration( - labelText: 'Password', - border: OutlineInputBorder(), - prefixIcon: Icon(Icons.lock), - ), - obscureText: true, - enabled: !_isLoading, - ), - const SizedBox(height: 24), - - // Error Message - if (_errorMessage != null) - Padding( - padding: const EdgeInsets.only(bottom: 16), - child: Text( - _errorMessage!, - style: TextStyle(color: Theme.of(context).colorScheme.error), - textAlign: TextAlign.center, - ), - ), - - // Sign In Button - ElevatedButton( - onPressed: _isLoading ? null : _signIn, - child: _isLoading - ? const SizedBox( - height: 20, - width: 20, - child: CircularProgressIndicator(strokeWidth: 2), - ) - : const Text('Sign In'), - ), - ], - ), - ), - ); - } -} -``` - -### Custom OAuth Buttons - -Create custom-styled OAuth buttons: - -```dart -import 'package:flutter/material.dart'; -import 'package:clerk_flutter/clerk_flutter.dart'; - -class CustomOAuthButtons extends StatelessWidget { - const CustomOAuthButtons({super.key}); - - Widget _buildOAuthButton({ - required BuildContext context, - required String label, - required IconData icon, - required Color color, - required OAuthProvider provider, - }) { - final authState = ClerkAuth.of(context, listen: false); - - return ElevatedButton.icon( - icon: Icon(icon), - label: Text(label), - style: ElevatedButton.styleFrom( - backgroundColor: color, - foregroundColor: Colors.white, - minimumSize: const Size(double.infinity, 48), - ), - onPressed: () async { - try { - await authState.attemptSignIn( - strategy: Strategy.oauth(provider), - ); - } catch (e) { - if (context.mounted) { - ScaffoldMessenger.of(context).showSnackBar( - SnackBar(content: Text('Sign in failed: $e')), - ); - } - } - }, - ); - } - - @override - Widget build(BuildContext context) { - return Column( - crossAxisAlignment: CrossAxisAlignment.stretch, - children: [ - _buildOAuthButton( - context: context, - label: 'Continue with Google', - icon: Icons.g_mobiledata, - color: Colors.red, - provider: OAuthProvider.google, - ), - const SizedBox(height: 12), - _buildOAuthButton( - context: context, - label: 'Continue with GitHub', - icon: Icons.code, - color: Colors.black, - provider: OAuthProvider.github, - ), - ], - ); - } -} -``` - ---- - -## Localization - -Clerk supports localization through Flutter's localization system: - -```dart -import 'package:flutter/material.dart'; -import 'package:clerk_flutter/clerk_flutter.dart'; -import 'package:flutter_localizations/flutter_localizations.dart'; - -MaterialApp( - localizationsDelegates: const [ - ClerkSdkLocalizations.delegate, - GlobalMaterialLocalizations.delegate, - GlobalWidgetsLocalizations.delegate, - GlobalCupertinoLocalizations.delegate, - ], - supportedLocales: const [ - Locale('en', ''), // English - // Add more locales as needed - ], - // ... -) -``` - ---- - -## Responsive Design - -Make your authentication UI responsive: - -```dart -import 'package:flutter/material.dart'; -import 'package:clerk_flutter/clerk_flutter.dart'; - -class ResponsiveAuthPage extends StatelessWidget { - const ResponsiveAuthPage({super.key}); - - @override - Widget build(BuildContext context) { - return Scaffold( - body: Center( - child: ConstrainedBox( - constraints: const BoxConstraints(maxWidth: 400), - child: const ClerkAuthentication(), - ), - ), - ); - } -} -``` - ---- - -## Next Steps - -- [Widget Reference]({{ '/api/widgets' | relative_url }}) - Complete widget API documentation -- [ClerkTheme API]({{ '/api/clerk-theme' | relative_url }}) - Detailed theming options -- [Examples](https://github.com/clerk/clerk-sdk-flutter/tree/main/packages/clerk_flutter/example) - See complete examples - diff --git a/docs/guides/error-handling.md b/docs/guides/error-handling.md deleted file mode 100644 index 06faef59..00000000 --- a/docs/guides/error-handling.md +++ /dev/null @@ -1,481 +0,0 @@ ---- -layout: default -title: Error Handling -parent: Guides -nav_order: 6 ---- - -# Error Handling -{: .no_toc } - -Learn how to handle authentication errors and provide great user experiences in your Dart and Flutter applications. -{: .fs-6 .fw-300 } - -## Table of contents -{: .no_toc .text-delta } - -1. TOC -{:toc} - ---- - -## Overview - -Clerk provides structured error handling through the `ClerkApiException` class. All authentication operations can throw this exception when errors occur. - ---- - -## ClerkApiException - -The main exception class for Clerk API errors: - -```dart -import 'package:clerk_auth/clerk_auth.dart'; - -try { - await auth.attemptSignIn( - strategy: Strategy.password, - identifier: 'user@example.com', - password: 'wrong-password', - ); -} on ClerkApiException catch (e) { - print('Error: ${e.message}'); - print('Status Code: ${e.status}'); - - // Access individual errors - for (final error in e.errors) { - print('Code: ${error.code}'); - print('Message: ${error.message}'); - print('Field: ${error.meta?.paramName}'); - } -} -``` - -**Properties:** - -| Property | Type | Description | -|----------|------|-------------| -| `message` | `String` | Human-readable error message | -| `status` | `int?` | HTTP status code | -| `errors` | `List` | Detailed error information | - ---- - -## ClerkApiError - -Individual error details: - -```dart -class ClerkApiError { - final String code; // Error code (e.g., 'form_password_incorrect') - final String message; // Error message - final String? longMessage; // Detailed explanation - final ErrorMeta? meta; // Additional metadata -} -``` - ---- - -## Common Error Codes - -### Authentication Errors - -| Code | Description | Solution | -|------|-------------|----------| -| `form_identifier_not_found` | Email/username not found | User needs to sign up | -| `form_password_incorrect` | Wrong password | Prompt user to retry or reset password | -| `form_code_incorrect` | Wrong verification code | Ask user to re-enter code | -| `verification_expired` | Verification code expired | Send new verification code | - -### Validation Errors - -| Code | Description | Solution | -|------|-------------|----------| -| `form_param_format_invalid` | Invalid format (e.g., email) | Validate input before submission | -| `form_password_pwned` | Password found in breach database | Require stronger password | -| `form_password_length_too_short` | Password too short | Show password requirements | - -### Rate Limiting - -| Code | Description | Solution | -|------|-------------|----------| -| `rate_limit_exceeded` | Too many requests | Show cooldown message | - ---- - -## Handling Errors in Dart - -### Basic Error Handling - -```dart -import 'package:clerk_auth/clerk_auth.dart'; - -Future signIn(String email, String password) async { - try { - await auth.attemptSignIn( - strategy: Strategy.password, - identifier: email, - password: password, - ); - - print('✅ Sign in successful'); - - } on ClerkApiException catch (e) { - // Handle Clerk-specific errors - print('❌ Sign in failed: ${e.message}'); - - for (final error in e.errors) { - if (error.code == 'form_password_incorrect') { - print('Wrong password. Please try again.'); - } else if (error.code == 'form_identifier_not_found') { - print('Account not found. Please sign up.'); - } - } - - } catch (e) { - // Handle other errors - print('❌ Unexpected error: $e'); - } -} -``` - -### Field-Specific Errors - -```dart -Map getFieldErrors(ClerkApiException e) { - final fieldErrors = {}; - - for (final error in e.errors) { - final fieldName = error.meta?.paramName; - if (fieldName != null) { - - -### Custom Error Display - -```dart -import 'package:flutter/material.dart'; -import 'package:clerk_flutter/clerk_flutter.dart'; - -class SignInForm extends StatefulWidget { - @override - _SignInFormState createState() => _SignInFormState(); -} - -class _SignInFormState extends State { - final _emailController = TextEditingController(); - final _passwordController = TextEditingController(); - String? _errorMessage; - Map _fieldErrors = {}; - - Future _handleSignIn() async { - setState(() { - _errorMessage = null; - _fieldErrors = {}; - }); - - try { - final authState = ClerkAuth.of(context); - - await authState.attemptSignIn( - strategy: Strategy.password, - identifier: _emailController.text, - password: _passwordController.text, - ); - - } on ClerkApiException catch (e) { - setState(() { - _errorMessage = e.message; - - // Extract field-specific errors - for (final error in e.errors) { - final field = error.meta?.paramName; - if (field != null) { - _fieldErrors[field] = error.message; - } - } - }); - } - } - - @override - Widget build(BuildContext context) { - return Column( - children: [ - if (_errorMessage != null) - Container( - padding: EdgeInsets.all(12), - color: Colors.red.shade100, - child: Text( - _errorMessage!, - style: TextStyle(color: Colors.red.shade900), - ), - ), - - TextField( - controller: _emailController, - decoration: InputDecoration( - labelText: 'Email', - errorText: _fieldErrors['identifier'], - ), - ), - - TextField( - controller: _passwordController, - obscureText: true, - decoration: InputDecoration( - labelText: 'Password', - errorText: _fieldErrors['password'], - ), - ), - - ElevatedButton( - onPressed: _handleSignIn, - child: Text('Sign In'), - ), - ], - ); - } -} -``` - ---- - -## Error Recovery Strategies - -### Password Reset Flow - -```dart -Future handlePasswordError(ClerkApiException e, String identifier) async { - for (final error in e.errors) { - if (error.code == 'form_password_incorrect') { - // Offer password reset - print('Forgot your password?'); - - try { - await auth.resetPassword(identifier: identifier); - print('Password reset email sent!'); - } catch (e) { - print('Failed to send reset email: $e'); - } - } - } -} -``` - -### Retry with Exponential Backoff - -```dart -Future signInWithRetry( - String email, - String password, { - int maxRetries = 3, -}) async { - int retries = 0; - - while (retries < maxRetries) { - try { - await auth.attemptSignIn( - strategy: Strategy.password, - identifier: email, - password: password, - ); - return; // Success - - } on ClerkApiException catch (e) { - // Check if error is retryable - final isRateLimit = e.errors.any((err) => - err.code == 'rate_limit_exceeded' - ); - - if (isRateLimit && retries < maxRetries - 1) { - retries++; - final delay = Duration(seconds: pow(2, retries).toInt()); - print('Rate limited. Retrying in ${delay.inSeconds}s...'); - await Future.delayed(delay); - } else { - rethrow; // Not retryable or max retries reached - } - } - } -} -``` - ---- - -## Logging Errors - -### Development Logging - -```dart -void logClerkError(ClerkApiException e) { - print('=== Clerk API Error ==='); - print('Message: ${e.message}'); - print('Status: ${e.status}'); - print('Errors:'); - - for (final error in e.errors) { - print(' - Code: ${error.code}'); - print(' Message: ${error.message}'); - print(' Field: ${error.meta?.paramName ?? 'N/A'}'); - } - - print('====================='); -} - -// Usage -try { - await auth.attemptSignIn(/* ... */); -} on ClerkApiException catch (e) { - logClerkError(e); -} -``` - -### Production Error Tracking - -```dart -import 'package:sentry_flutter/sentry_flutter.dart'; - -try { - await auth.attemptSignIn(/* ... */); -} on ClerkApiException catch (e, stackTrace) { - // Log to error tracking service - await Sentry.captureException( - e, - stackTrace: stackTrace, - hint: Hint.withMap({ - 'error_codes': e.errors.map((err) => err.code).toList(), - 'status': e.status, - }), - ); - - // Show user-friendly message - showErrorDialog(context, 'Sign in failed. Please try again.'); -} -``` - ---- - -## Best Practices - -### 1. Show User-Friendly Messages - -```dart -String getUserFriendlyMessage(ClerkApiException e) { - for (final error in e.errors) { - switch (error.code) { - case 'form_password_incorrect': - return 'Incorrect password. Please try again.'; - case 'form_identifier_not_found': - return 'Account not found. Please sign up.'; - case 'rate_limit_exceeded': - return 'Too many attempts. Please try again later.'; - default: - return 'Something went wrong. Please try again.'; - } - } - return e.message; -} -``` - -### 2. Validate Before Submission - -```dart -String? validateEmail(String email) { - if (email.isEmpty) return 'Email is required'; - if (!email.contains('@')) return 'Invalid email format'; - return null; -} - -String? validatePassword(String password) { - if (password.isEmpty) return 'Password is required'; - if (password.length < 8) return 'Password must be at least 8 characters'; - return null; -} -``` - -### 3. Provide Clear Next Steps - -```dart -void handleSignInError(ClerkApiException e, BuildContext context) { - for (final error in e.errors) { - if (error.code == 'form_identifier_not_found') { - showDialog( - context: context, - builder: (context) => AlertDialog( - title: Text('Account Not Found'), - content: Text('Would you like to create an account?'), - actions: [ - TextButton( - onPressed: () => Navigator.pop(context), - child: Text('Cancel'), - ), - ElevatedButton( - onPressed: () { - Navigator.pop(context); - Navigator.pushNamed(context, '/sign-up'); - }, - child: Text('Sign Up'), - ), - ], - ), - ); - } - } -} -``` - ---- - -## Next Steps - -- [Authentication]({{ '/guides/authentication' | relative_url }}) - Implement authentication flows -- [User Management]({{ '/guides/user-management' | relative_url }}) - Handle user-related errors -- [Session Tokens]({{ '/guides/session-tokens' | relative_url }}) - Handle token errors -- [Backend Integration]({{ '/guides/backend-integration' | relative_url }}) - Server-side error handling - - fieldErrors[fieldName] = error.message; - } - } - - return fieldErrors; -} - -// Usage -try { - await auth.attemptSignUp(/* ... */); -} on ClerkApiException catch (e) { - final errors = getFieldErrors(e); - - if (errors.containsKey('email_address')) { - print('Email error: ${errors['email_address']}'); - } - if (errors.containsKey('password')) { - print('Password error: ${errors['password']}'); - } -} -``` - ---- - -## Handling Errors in Flutter - -### Using ClerkErrorListener - -The `ClerkErrorListener` widget automatically displays errors: - -```dart -import 'package:clerk_flutter/clerk_flutter.dart'; - -class MyApp extends StatelessWidget { - @override - Widget build(BuildContext context) { - return Scaffold( - body: ClerkErrorListener( - child: YourWidget(), - ), - ); - } -} -``` - - diff --git a/docs/guides/organizations.md b/docs/guides/organizations.md deleted file mode 100644 index 229fb766..00000000 --- a/docs/guides/organizations.md +++ /dev/null @@ -1,411 +0,0 @@ ---- -layout: default -title: Organizations -parent: Guides -nav_order: 3 ---- - -# Organizations -{: .no_toc } - -Learn how to implement multi-tenant functionality with organizations in your Dart and Flutter applications. -{: .fs-6 .fw-300 } - -## Table of contents -{: .no_toc .text-delta } - -1. TOC -{:toc} - ---- - -## Overview - -Organizations in Clerk enable you to build multi-tenant applications where users can belong to one or more organizations. Each organization can have: - -- Multiple members with different roles -- Custom permissions and access control -- Organization-specific metadata -- Invitations and membership management - ---- - -## Prerequisites - -Before using organizations, you need to: - -1. **Enable Organizations** in your [Clerk Dashboard](https://dashboard.clerk.com) - - Go to your application settings - - Navigate to **Organizations** - - Enable the Organizations feature - -2. **Configure Roles** (optional) - - Define custom roles for your organization members - - Set up permissions for each role - ---- - -## Accessing Organizations - -### In Dart (clerk_auth) - -```dart -import 'package:clerk_auth/clerk_auth.dart'; - -void listUserOrganizations(Auth auth) { - final user = auth.user; - - if (user == null) { - print('No user signed in'); - return; - } - - // Get all organizations the user belongs to - final memberships = user.organizationMemberships; - - for (final membership in memberships) { - print('Organization: ${membership.organization.name}'); - print('Role: ${membership.role}'); - } -} -``` - -### In Flutter (clerk_flutter) - -```dart -import 'package:flutter/material.dart'; -import 'package:clerk_flutter/clerk_flutter.dart'; - -class OrganizationsPage extends StatelessWidget { - @override - Widget build(BuildContext context) { - return Scaffold( - appBar: AppBar(title: const Text('My Organizations')), - body: const ClerkOrganizationList(), - ); - } -} -``` - ---- - -## Organization Object - -The `Organization` object contains information about an organization: - -```dart -final organization = membership.organization; - -print('ID: ${organization.id}'); -print('Name: ${organization.name}'); -print('Slug: ${organization.slug}'); -print('Logo: ${organization.imageUrl}'); -print('Created: ${organization.createdAt}'); -``` - -**Key Properties:** - -| Property | Type | Description | -|----------|------|-------------| -| `id` | `String` | Unique organization identifier | -| `name` | `String` | Organization name | -| `slug` | `String` | URL-friendly identifier | -| `imageUrl` | `String?` | Organization logo URL | -| `createdAt` | `DateTime` | Creation timestamp | -| `publicMetadata` | `Map` | Public metadata | - ---- - -## Organization Membership - -The `OrganizationMembership` object represents a user's membership in an organization: - -```dart -final membership = user.organizationMemberships.first; - -print('Organization: ${membership.organization.name}'); -print('Role: ${membership.role}'); -print('Joined: ${membership.createdAt}'); -``` - -**Key Properties:** - -| Property | Type | Description | -|----------|------|-------------| -| `id` | `String` | Membership identifier | -| `organization` | `Organization` | The organization | -| `role` | `String` | User's role in the organization | -| `createdAt` | `DateTime` | When user joined | -| `publicMetadata` | `Map` | Membership metadata | - ---- - -## Common Organization Roles - -Clerk provides default roles for organizations: - -- **`admin`** - Full access to organization settings and members -- **`member`** - Standard member access - -You can also define custom roles in your Clerk Dashboard with specific permissions. - - ---- - -## Switching Active Organization - -Users can belong to multiple organizations. You can switch the active organization context: - -### In Dart (clerk_auth) - -```dart -import 'package:clerk_auth/clerk_auth.dart'; - -Future switchOrganization(Auth auth, String organizationId) async { - try { - // Set the active organization - await auth.setActiveOrganization(organizationId); - - print('Switched to organization: $organizationId'); - } catch (e) { - print('Failed to switch organization: $e'); - } -} -``` - -### In Flutter (clerk_flutter) - -The `ClerkOrganizationList` widget handles organization switching automatically. When a user selects an organization, it becomes the active organization. - ---- - -## Checking Organization Membership - -You can check if a user belongs to a specific organization: - -```dart -bool isUserInOrganization(User user, String organizationId) { - return user.organizationMemberships.any( - (membership) => membership.organization.id == organizationId, - ); -} -``` - -Check if user has a specific role: - -```dart -bool isUserAdmin(User user, String organizationId) { - final membership = user.organizationMemberships.firstWhere( - (m) => m.organization.id == organizationId, - orElse: () => null, - ); - - return membership?.role == 'admin'; -} -``` - ---- - -## Organization Invitations - -Organizations can invite new members via email. - -{: .note } -> Organization invitations are managed through the Clerk Dashboard or Backend API. The Dart/Flutter SDKs currently support viewing and accepting invitations. - -### Viewing Pending Invitations - -```dart -import 'package:clerk_auth/clerk_auth.dart'; - -void listPendingInvitations(User user) { - // Check for pending organization invitations - final invitations = user.organizationInvitations ?? []; - - for (final invitation in invitations) { - print('Invited to: ${invitation.organization.name}'); - print('Role: ${invitation.role}'); - print('Invited by: ${invitation.inviterEmail}'); - } -} -``` - ---- - -## Multi-Tenant Data Isolation - -When building multi-tenant applications, you'll want to scope data to the active organization: - -```dart -import 'package:clerk_auth/clerk_auth.dart'; - -class ApiService { - final Auth auth; - - ApiService(this.auth); - - Future fetchOrganizationData() async { - final user = auth.user; - if (user == null) return; - - // Get the active organization - final activeOrgId = user.activeOrganizationId; - - if (activeOrgId == null) { - print('No active organization'); - return; - } - - // Fetch data scoped to this organization - final token = await auth.sessionTokenStream.first; - - // Make API request with organization context - // The session token will include organization claims - final response = await http.get( - Uri.parse('https://api.example.com/org/$activeOrgId/data'), - headers: { - 'Authorization': 'Bearer $token', - }, - ); - } -} -``` - ---- - -## Session Tokens with Organization Claims - -When a user has an active organization, the session token includes organization-specific claims: - -```dart -import 'dart:convert'; -import 'package:clerk_auth/clerk_auth.dart'; - -void printOrganizationClaims(Auth auth) { - auth.sessionTokenStream.listen((token) { - if (token == null) return; - - // Decode JWT (for demonstration - use a proper JWT library) - final parts = token.split('.'); - if (parts.length != 3) return; - - final payload = json.decode( - utf8.decode(base64Url.decode(base64Url.normalize(parts[1]))), - ); - - print('Organization ID: ${payload['org_id']}'); - print('Organization Role: ${payload['org_role']}'); - print('Organization Permissions: ${payload['org_permissions']}'); - }); -} -``` - ---- - -## Best Practices - -### 1. Always Check Active Organization - -```dart -Future performOrganizationAction(Auth auth) async { - final user = auth.user; - - if (user?.activeOrganizationId == null) { - throw Exception('No active organization selected'); - } - - // Proceed with organization-scoped action -} -``` - -### 2. Handle Organization Switching - -```dart -class OrganizationAwareWidget extends StatefulWidget { - @override - _OrganizationAwareWidgetState createState() => - _OrganizationAwareWidgetState(); -} - -class _OrganizationAwareWidgetState extends State { - String? _currentOrgId; - - @override - void initState() { - super.initState(); - _loadOrganizationData(); - } - - void _loadOrganizationData() { - final authState = ClerkAuth.of(context); - final newOrgId = authState.user?.activeOrganizationId; - - if (newOrgId != _currentOrgId) { - setState(() { - _currentOrgId = newOrgId; - }); - // Reload data for new organization - } - } - - @override - Widget build(BuildContext context) { - return Container( - child: Text('Current Org: $_currentOrgId'), - ); - } -} -``` - -### 3. Validate Permissions - -Always validate organization permissions on your backend, not just in the client: - -```dart -// Client-side check (for UI only) -bool canEditSettings = user.organizationMemberships - .firstWhere((m) => m.organization.id == activeOrgId) - .role == 'admin'; - -// Always verify on backend before performing sensitive operations -``` - ---- - -## Next Steps - -- [Authentication]({{ '/guides/authentication' | relative_url }}) - Implement authentication flows -- [User Management]({{ '/guides/user-management' | relative_url }}) - Manage user profiles -- [Session Tokens]({{ '/guides/session-tokens' | relative_url }}) - Work with JWT tokens -- [Backend Integration]({{ '/guides/backend-integration' | relative_url }}) - Verify organization claims on your backend - - ---- - -## Using ClerkOrganizationList Widget - -The `ClerkOrganizationList` widget provides a pre-built UI for managing organizations: - -```dart -import 'package:flutter/material.dart'; -import 'package:clerk_flutter/clerk_flutter.dart'; - -class MyApp extends StatelessWidget { - @override - Widget build(BuildContext context) { - return MaterialApp( - home: Scaffold( - appBar: AppBar(title: const Text('Organizations')), - body: const ClerkOrganizationList(), - ), - ); - } -} -``` - -Features: -- List all organizations the user belongs to -- Switch between organizations -- Create new organizations (if enabled) -- Leave organizations - diff --git a/docs/guides/session-tokens.md b/docs/guides/session-tokens.md deleted file mode 100644 index 59ed0109..00000000 --- a/docs/guides/session-tokens.md +++ /dev/null @@ -1,331 +0,0 @@ ---- -layout: default -title: Session Tokens -parent: Guides -nav_order: 5 ---- - -# Session Tokens -{: .no_toc } - -Learn how to work with JWT session tokens for authenticated API requests in your Dart and Flutter applications. -{: .fs-6 .fw-300 } - -## Table of contents -{: .no_toc .text-delta } - -1. TOC -{:toc} - ---- - -## Overview - -Clerk uses JSON Web Tokens (JWTs) for session management. These tokens: - -- Are automatically generated when a user signs in -- Contain user and session information -- Are cryptographically signed by Clerk -- Can be verified on your backend -- Automatically refresh to stay valid - ---- - -## Accessing Session Tokens - -### In Dart (clerk_auth) - -The `clerk_auth` package provides a stream of session tokens: - -```dart -import 'package:clerk_auth/clerk_auth.dart'; - -final auth = Auth( - config: AuthConfig( - publishableKey: 'pk_test_xxxxxxxxxxxxx', - persistor: DefaultPersistor( - getCacheDirectory: () => Directory.current, - ), - ), -); - -await auth.initialize(); - -// Listen to session token updates -auth.sessionTokenStream.listen((token) { - if (token != null) { - print('Session token: $token'); - // Use for authenticated API requests - } else { - print('No active session'); - } -}); -``` - -### In Flutter (clerk_flutter) - -```dart -import 'package:clerk_flutter/clerk_flutter.dart'; - -class MyWidget extends StatelessWidget { - @override - Widget build(BuildContext context) { - final authState = ClerkAuth.of(context); - - // Access the session token stream - return StreamBuilder( - stream: authState.sessionTokenStream, - builder: (context, snapshot) { - final token = snapshot.data; - - if (token == null) { - return Text('No session token'); - } - - return Text('Token available'); - }, - ); - } -} -``` - ---- - -## Making Authenticated API Requests - -Use the session token to authenticate requests to your backend: - -```dart -import 'package:http/http.dart' as http; -import 'package:clerk_auth/clerk_auth.dart'; - -class ApiClient { - final Auth auth; - - ApiClient(this.auth); - - Future fetchUserData() async { - // Get the current session token - final token = await auth.sessionTokenStream.first; - - if (token == null) { - throw Exception('No active session'); - } - - // Make authenticated request - final response = await http.get( - Uri.parse('https://api.example.com/user/profile'), - headers: { - 'Authorization': 'Bearer $token', - 'Content-Type': 'application/json', - }, - ); - - return response; - } -} -``` - ---- - -## Session Token Polling - -By default, `clerk_auth` automatically polls for fresh session tokens to ensure they remain valid. - -### Enable/Disable Polling - -```dart -final auth = Auth( - config: AuthConfig( - publishableKey: 'pk_test_xxxxxxxxxxxxx', - sessionTokenPolling: true, // Enable (default) - persistor: DefaultPersistor( - getCacheDirectory: () => Directory.current, - ), - ), -); -``` - -### Why Polling? - - - ---- - -## Token Claims - -Common claims in a Clerk session token: - -| Claim | Description | -|-------|-------------| -| `sub` | User ID (subject) | -| `sid` | Session ID | -| `iat` | Issued at timestamp | -| `exp` | Expiration timestamp | -| `azp` | Authorized party (your app URL) | -| `org_id` | Organization ID (if user has active org) | -| `org_role` | User's role in the organization | -| `org_permissions` | Array of permissions | - -### Decoding Token Claims (Client-Side) - -{: .warning } -> **Security Note:** Never trust client-side token decoding for authorization. Always verify tokens on your backend. - -```dart -import 'dart:convert'; - -Map decodeJwtPayload(String token) { - final parts = token.split('.'); - if (parts.length != 3) { - throw Exception('Invalid JWT'); - } - - // Decode the payload (second part) - final payload = parts[1]; - final normalized = base64Url.normalize(payload); - final decoded = utf8.decode(base64Url.decode(normalized)); - - return json.decode(decoded); -} - -// Usage -final token = await auth.sessionTokenStream.first; -if (token != null) { - final claims = decodeJwtPayload(token); - print('User ID: ${claims['sub']}'); - print('Expires: ${DateTime.fromMillisecondsSinceEpoch(claims['exp'] * 1000)}'); -} -``` - ---- - -## Verifying Tokens on Backend - -Always verify session tokens on your backend before trusting them. See the [Backend Integration](/guides/backend-integration) guide for details. - -**Quick Example (Node.js):** - -```javascript -import { verifyToken } from '@clerk/backend'; - -app.get('/api/protected', async (req, res) => { - const token = req.headers.authorization?.replace('Bearer ', ''); - - try { - const payload = await verifyToken(token, { - secretKey: process.env.CLERK_SECRET_KEY - }); - - res.json({ userId: payload.sub }); - } catch (err) { - res.status(401).json({ error: 'Unauthorized' }); - } -}); -``` - ---- - -## Token Expiration - -Session tokens expire after a period of time (default: 1 hour). Clerk automatically handles token refresh through polling. - -### Handling Expired Tokens - -```dart -import 'package:clerk_auth/clerk_auth.dart'; - -Future getValidToken(Auth auth) async { - final token = await auth.sessionTokenStream.first; - - if (token == null) { - throw Exception('No active session'); - } - - // Token is automatically refreshed by Clerk - // Just use the latest token from the stream - return token; -} -``` - ---- - -## Best Practices - -### 1. Always Use HTTPS - -Session tokens should only be transmitted over HTTPS to prevent interception. - -### 2. Store Tokens Securely - -The `clerk_auth` package handles token storage securely. Don't store tokens in: -- Local storage (web) -- Shared preferences without encryption -- Plain text files - -### 3. Verify on Backend - -Never trust client-side token validation. Always verify tokens on your backend: - -```dart -// ❌ DON'T: Trust client-side checks -if (decodeJwtPayload(token)['exp'] > DateTime.now()) { - // Allow access -} - -// ✅ DO: Verify on backend -final response = await http.get( - Uri.parse('https://api.example.com/protected'), - headers: {'Authorization': 'Bearer $token'}, -); -``` - -### 4. Handle Token Refresh - -Let Clerk handle token refresh automatically: - -```dart -// ✅ DO: Use the stream -auth.sessionTokenStream.listen((token) { - // Always get the latest valid token - if (token != null) { - makeApiRequest(token); - } -}); - -// ❌ DON'T: Cache tokens manually -String? cachedToken; -final token = await auth.sessionTokenStream.first; -cachedToken = token; // Token might expire! -``` - ---- - -## Next Steps - -- [Backend Integration]({{ '/guides/backend-integration' | relative_url }}) - Verify tokens on your backend -- [User Management]({{ '/guides/user-management' | relative_url }}) - Access user data from tokens -- [Organizations]({{ '/guides/organizations' | relative_url }}) - Work with organization claims -- [Error Handling]({{ '/guides/error-handling' | relative_url }}) - Handle token-related errors - -- Session tokens expire after a period of time -- Polling ensures you always have a valid token -- Clerk automatically refreshes tokens before they expire -- No manual token refresh logic needed - ---- - -## Token Structure - -A Clerk session token is a JWT with three parts: - -``` -eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJ1c2VyXzEyMyIsImlhdCI6MTYxNjIzOTAyMn0.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c -``` - -**Parts:** -1. **Header** - Algorithm and token type -2. **Payload** - Claims (user data, session info) -3. **Signature** - Cryptographic signature - - diff --git a/docs/guides/user-management.md b/docs/guides/user-management.md deleted file mode 100644 index cf2b7d37..00000000 --- a/docs/guides/user-management.md +++ /dev/null @@ -1,456 +0,0 @@ ---- -layout: default -title: User Management -parent: Guides -nav_order: 2 ---- - -# User Management -{: .no_toc } - -Learn how to manage user profiles, update user information, and handle user data with Clerk's Dart and Flutter SDKs. -{: .fs-6 .fw-300 } - -## Table of contents -{: .no_toc .text-delta } - -1. TOC -{:toc} - ---- - -## Overview - -The `User` object contains all account information for a user in your application. Clerk provides comprehensive APIs to: - -- Access user profile data -- Update user information -- Manage email addresses and phone numbers -- Upload profile images -- Handle user metadata - ---- - -## Accessing the Current User - -### In Dart (clerk_auth) - -```dart -import 'package:clerk_auth/clerk_auth.dart'; - -void printUserInfo(Auth auth) { - final user = auth.user; - - if (user == null) { - print('No user signed in'); - return; - } - - print('User ID: ${user.id}'); - print('Email: ${user.emailAddress}'); - print('Name: ${user.firstName} ${user.lastName}'); - print('Username: ${user.username}'); - print('Profile Image: ${user.imageUrl}'); - print('Created: ${user.createdAt}'); - print('Updated: ${user.updatedAt}'); -} -``` - -### In Flutter (clerk_flutter) - -```dart -import 'package:flutter/material.dart'; -import 'package:clerk_flutter/clerk_flutter.dart'; - -class UserProfileWidget extends StatelessWidget { - const UserProfileWidget({super.key}); - - @override - Widget build(BuildContext context) { - final authState = ClerkAuth.of(context); - final user = authState.user; - - if (user == null) { - return const Text('Not signed in'); - } - - return Column( - children: [ - if (user.imageUrl != null) - CircleAvatar( - radius: 50, - backgroundImage: NetworkImage(user.imageUrl!), - ), - Text('${user.firstName} ${user.lastName}'), - Text(user.emailAddress ?? 'No email'), - Text('User ID: ${user.id}'), - ], - ); - } -} -``` - ---- - -## User Properties - -The `User` object includes the following properties: - -| Property | Type | Description | -|----------|------|-------------| -| `id` | `String` | Unique identifier for the user | -| `firstName` | `String?` | User's first name | -| `lastName` | `String?` | User's last name | -| `username` | `String?` | User's username | -| `emailAddress` | `String?` | Primary email address | -| `phoneNumber` | `String?` | Primary phone number | -| `imageUrl` | `String?` | URL to profile image | -| `hasImage` | `bool` | Whether user has uploaded an image | -| `primaryEmailAddressId` | `String?` | ID of primary email | -| `primaryPhoneNumberId` | `String?` | ID of primary phone | -| `emailAddresses` | `List` | All email addresses | -| `phoneNumbers` | `List` | All phone numbers | -| `externalAccounts` | `List` | Connected OAuth accounts | -| `publicMetadata` | `Map` | Public metadata | -| `privateMetadata` | `Map` | Private metadata (backend only) | -| `unsafeMetadata` | `Map` | Unsafe metadata | -| `createdAt` | `DateTime` | Account creation timestamp | -| `updatedAt` | `DateTime` | Last update timestamp | - ---- - -## Updating User Profile - -### Update Basic Information - -```dart -import 'package:clerk_auth/clerk_auth.dart'; - -Future updateUserProfile(Auth auth) async { - final user = auth.user; - if (user == null) return; - - try { - await user.update( - firstName: 'John', - lastName: 'Doe', - username: 'johndoe', - ); - - print('✅ Profile updated successfully'); - } catch (e) { - print('❌ Update failed: $e'); - } -} -``` - -### Update Profile Image - -```dart -import 'dart:io'; -import 'package:clerk_auth/clerk_auth.dart'; - -Future updateProfileImage(Auth auth, File imageFile) async { - final user = auth.user; - if (user == null) return; - - try { - await user.setProfileImage(imageFile); - print('✅ Profile image updated'); - } catch (e) { - print('❌ Image upload failed: $e'); - } -} -``` - -### In Flutter with Image Picker - -```dart -import 'package:flutter/material.dart'; -import 'package:clerk_flutter/clerk_flutter.dart'; -import 'package:image_picker/image_picker.dart'; -import 'dart:io'; - -class ProfileImageUploader extends StatelessWidget { - const ProfileImageUploader({super.key}); - - Future _pickAndUploadImage(BuildContext context) async { - final authState = ClerkAuth.of(context, listen: false); - final user = authState.user; - if (user == null) return; - - final picker = ImagePicker(); - final pickedFile = await picker.pickImage(source: ImageSource.gallery); - - if (pickedFile != null) { - try { - await user.setProfileImage(File(pickedFile.path)); - if (context.mounted) { - ScaffoldMessenger.of(context).showSnackBar( - const SnackBar(content: Text('Profile image updated!')), - ); - } - } catch (e) { - if (context.mounted) { - ScaffoldMessenger.of(context).showSnackBar( - SnackBar(content: Text('Upload failed: $e')), - ); - } - } - } - } - - @override - Widget build(BuildContext context) { - return ElevatedButton.icon( - icon: const Icon(Icons.upload), - label: const Text('Upload Profile Image'), - onPressed: () => _pickAndUploadImage(context), - ); - } -} -``` - ---- - -## Managing Email Addresses - -### Add Email Address - -```dart -Future addEmailAddress(Auth auth, String email) async { - final user = auth.user; - if (user == null) return; - - try { - final emailAddress = await user.createEmailAddress(email); - - print('📧 Verification email sent to $email'); - - // User enters verification code - final code = '123456'; // Get from user input - - await emailAddress.attemptVerification(code: code); - - print('✅ Email address verified and added'); - } catch (e) { - print('❌ Failed to add email: $e'); - } -} -``` - -### Set Primary Email - -```dart -Future setPrimaryEmail(Auth auth, String emailAddressId) async { - final user = auth.user; - if (user == null) return; - - try { - await user.update(primaryEmailAddressId: emailAddressId); - print('✅ Primary email updated'); - } catch (e) { - print('❌ Failed to update primary email: $e'); - } -} -``` - -### Remove Email Address - -```dart -Future removeEmailAddress(Auth auth, String emailAddressId) async { - final user = auth.user; - if (user == null) return; - - try { - final emailAddress = user.emailAddresses - .firstWhere((e) => e.id == emailAddressId); - - await emailAddress.destroy(); - print('✅ Email address removed'); - } catch (e) { - print('❌ Failed to remove email: $e'); - } -} -``` - ---- - -## Managing Phone Numbers - -### Add Phone Number - -```dart -Future addPhoneNumber(Auth auth, String phoneNumber) async { - final user = auth.user; - if (user == null) return; - - try { - final phone = await user.createPhoneNumber(phoneNumber); - - print('📱 SMS verification sent to $phoneNumber'); - - // User enters verification code - final code = '123456'; // Get from user input - - await phone.attemptVerification(code: code); - - print('✅ Phone number verified and added'); - } catch (e) { - print('❌ Failed to add phone: $e'); - } -} -``` - ---- - -## User Metadata - -Clerk provides three types of metadata: - -### Public Metadata -Visible to the frontend and included in session tokens. - -```dart -Future updatePublicMetadata(Auth auth) async { - final user = auth.user; - if (user == null) return; - - await user.update( - publicMetadata: { - 'theme': 'dark', - 'language': 'en', - 'notifications': true, - }, - ); -} -``` - -### Unsafe Metadata -Editable by the user, visible to the frontend. - -```dart -Future updateUnsafeMetadata(Auth auth) async { - final user = auth.user; - if (user == null) return; - - await user.update( - unsafeMetadata: { - 'preferences': { - 'newsletter': true, - 'marketing': false, - }, - }, - ); -} -``` - -### Private Metadata -Only accessible from the backend (requires Backend API). - -{: .note } -> Private metadata can only be set using the Backend API, not from client SDKs. - ---- - -## User Profile Component (Flutter) - -Here's a complete user profile component: - -```dart -import 'package:flutter/material.dart'; -import 'package:clerk_flutter/clerk_flutter.dart'; - -class UserProfilePage extends StatelessWidget { - const UserProfilePage({super.key}); - - @override - Widget build(BuildContext context) { - final authState = ClerkAuth.of(context); - final user = authState.user; - - if (user == null) { - return const Scaffold( - body: Center(child: Text('Not signed in')), - ); - } - - return Scaffold( - appBar: AppBar( - title: const Text('Profile'), - actions: const [ClerkUserButton()], - ), - body: ListView( - padding: const EdgeInsets.all(16), - children: [ - // Profile Image - Center( - child: CircleAvatar( - radius: 60, - backgroundImage: user.imageUrl != null - ? NetworkImage(user.imageUrl!) - : null, - child: user.imageUrl == null - ? Text( - user.firstName?.substring(0, 1).toUpperCase() ?? 'U', - style: const TextStyle(fontSize: 40), - ) - : null, - ), - ), - const SizedBox(height: 24), - - // Name - ListTile( - leading: const Icon(Icons.person), - title: const Text('Name'), - subtitle: Text('${user.firstName ?? ''} ${user.lastName ?? ''}'), - ), - - // Email - ListTile( - leading: const Icon(Icons.email), - title: const Text('Email'), - subtitle: Text(user.emailAddress ?? 'No email'), - ), - - // Username - if (user.username != null) - ListTile( - leading: const Icon(Icons.alternate_email), - title: const Text('Username'), - subtitle: Text(user.username!), - ), - - // Phone - if (user.phoneNumber != null) - ListTile( - leading: const Icon(Icons.phone), - title: const Text('Phone'), - subtitle: Text(user.phoneNumber!), - ), - - // Account Created - ListTile( - leading: const Icon(Icons.calendar_today), - title: const Text('Member Since'), - subtitle: Text( - '${user.createdAt.year}-${user.createdAt.month}-${user.createdAt.day}', - ), - ), - ], - ), - ); - } -} -``` - ---- - -## Next Steps - -- [Organizations]({{ '/guides/organizations' | relative_url }}) - Multi-tenant user management -- [Session Tokens]({{ '/guides/session-tokens' | relative_url }}) - Access user data in tokens -- [Customization]({{ '/guides/customization' | relative_url }}) - Customize user profile UI -- [Backend Integration]({{ '/guides/backend-integration' | relative_url }}) - Manage users from your backend - diff --git a/docs/index.md b/docs/index.md deleted file mode 100644 index 362e1ed4..00000000 --- a/docs/index.md +++ /dev/null @@ -1,96 +0,0 @@ ---- -layout: default -title: Home -nav_order: 1 -description: "Official documentation for Clerk's Dart and Flutter SDKs - Add complete user management to your Dart and Flutter applications." -permalink: / ---- - -# Clerk Dart & Flutter SDK Documentation -{: .fs-9 } - -Add complete user management to your Dart and Flutter applications in minutes. -{: .fs-6 .fw-300 } - -[Get Started]({{ '/getting-started' | relative_url }}){: .btn .btn-primary .fs-5 .mb-4 .mb-md-0 .mr-2 } -[View on GitHub](https://github.com/clerk/clerk-sdk-flutter){: .btn .fs-5 .mb-4 .mb-md-0 } - ---- - -## Beta Notice - -{: .warning } -> ⚠️ **The Clerk Dart and Flutter SDKs are in Beta** -> -> Breaking changes should be expected until the first stable release (1.0.0). We recommend pinning to specific versions and reviewing changelogs before upgrading. - ---- - -## Overview - -Clerk provides streamlined user experiences for your users to sign up, sign in, and manage their profiles from your Dart and Flutter applications. Our SDKs offer: - -- 🔐 **Complete Authentication** - Email, phone, OAuth, passwordless, and multi-factor authentication -- 👤 **User Management** - Pre-built UI components and headless APIs for user profiles -- 🏢 **Organizations** - Multi-tenant support with role-based access control -- 🎨 **Customizable UI** - Beautiful, themeable components that match your brand -- 📱 **Cross-Platform** - Works on iOS, Android, Web, Windows, macOS, and Linux -- 🚀 **Production Ready** - Battle-tested infrastructure handling millions of users - ---- - -## Quick Links - -### 🚀 Getting Started - -- [Installation & Setup]({{ '/getting-started' | relative_url }}) -- [Dart Quickstart]({{ '/getting-started/quickstart-dart' | relative_url }}) -- [Flutter Quickstart]({{ '/getting-started/quickstart-flutter' | relative_url }}) - -### 📚 Guides - -- [Authentication]({{ '/guides/authentication' | relative_url }}) - Authentication flows, OAuth, passwordless, MFA -- [User Management]({{ '/guides/user-management' | relative_url }}) - Managing user profiles and data -- [Organizations]({{ '/guides/organizations' | relative_url }}) - Multi-tenant functionality -- [Customization]({{ '/guides/customization' | relative_url }}) - Theming and styling -- [Session Tokens]({{ '/guides/session-tokens' | relative_url }}) - Working with JWTs -- [Error Handling]({{ '/guides/error-handling' | relative_url }}) - Handling errors gracefully -- [Backend Integration]({{ '/guides/backend-integration' | relative_url }}) - Server-side verification - -### 📦 Packages - -- [clerk_auth]({{ '/packages/clerk-auth' | relative_url }}) - Dart SDK for backend and CLI applications -- [clerk_flutter]({{ '/packages/clerk-flutter' | relative_url }}) - Flutter SDK with pre-built UI components - -### 🔧 API Reference - -- [Widget Reference]({{ '/api/widgets' | relative_url }}) - Complete Flutter widget documentation -- [clerk_auth API](https://pub.dev/documentation/clerk_auth/latest/) - Dart API documentation -- [clerk_flutter API](https://pub.dev/documentation/clerk_flutter/latest/) - Flutter API documentation - ---- - -## Requirements - -### Dart SDK (clerk_auth) -- Dart >= 3.6.2 - -### Flutter SDK (clerk_flutter) -- Flutter >= 3.27.4 -- Dart >= 3.6.2 - ---- - -## Community & Support - -- 💬 [Join our Discord](https://clerk.com/discord) - Chat with the community and Clerk team -- 📖 [Main Clerk Documentation](https://clerk.com/docs) - Comprehensive guides and references -- 🐛 [Report Issues](https://github.com/clerk/clerk-sdk-flutter/issues) - Bug reports and feature requests -- 🐦 [Follow us on Twitter](https://twitter.com/ClerkDev) - Latest updates and announcements - ---- - -## License - -These SDKs are licensed under the MIT license. See the [LICENSE](https://github.com/clerk/clerk-sdk-flutter/blob/main/LICENSE) file for details. - diff --git a/docs/packages/_index.md b/docs/packages/_index.md deleted file mode 100644 index 2c63f99e..00000000 --- a/docs/packages/_index.md +++ /dev/null @@ -1,64 +0,0 @@ ---- -layout: default -title: Packages -nav_order: 4 -has_children: true -permalink: /packages ---- - -# Packages - -Official Clerk packages for Dart and Flutter development. - -## Available Packages - -### [clerk_auth]({{ '/packages/clerk-auth' | relative_url }}) -Pure Dart SDK for backend applications, CLI tools, and server-side Dart applications. - -**Features:** -- Complete authentication API -- Session management -- User management -- Organization support -- No UI dependencies - -**Install:** -```yaml -dependencies: - clerk_auth: ^0.0.13-beta -``` - ---- - -### [clerk_flutter]({{ '/packages/clerk-flutter' | relative_url }}) -Flutter SDK with pre-built UI components for mobile, web, and desktop applications. - -**Features:** -- Pre-built authentication widgets -- Material Design integration -- Customizable themes -- Cross-platform support -- Includes clerk_auth - -**Install:** -```yaml -dependencies: - clerk_flutter: ^0.0.13-beta -``` - ---- - -## Quick Links - -- [Getting Started]({{ '/getting-started' | relative_url }}) - Installation and setup guides -- [Guides]({{ '/guides' | relative_url }}) - Implementation guides -- [API Reference]({{ '/api' | relative_url }}) - Detailed API documentation - ---- - -## External Resources - -- [clerk_auth on pub.dev](https://pub.dev/packages/clerk_auth) -- [clerk_flutter on pub.dev](https://pub.dev/packages/clerk_flutter) -- [GitHub Repository](https://github.com/clerk/clerk-sdk-flutter) - diff --git a/docs/packages/clerk-auth.md b/docs/packages/clerk-auth.md deleted file mode 100644 index 4c5a327a..00000000 --- a/docs/packages/clerk-auth.md +++ /dev/null @@ -1,354 +0,0 @@ ---- -layout: default -title: clerk_auth -parent: Packages -nav_order: 1 ---- - -# clerk_auth Package -{: .no_toc } - -The official Clerk Dart SDK for backend and CLI applications. -{: .fs-6 .fw-300 } - -## Table of contents -{: .no_toc .text-delta } - -1. TOC -{:toc} - ---- - -## Overview - -The `clerk_auth` package is a pure Dart SDK that provides authentication and user management capabilities for: - -- Backend Dart applications -- CLI tools -- Server-side applications -- Any Dart environment - -**Package:** [clerk_auth on pub.dev](https://pub.dev/packages/clerk_auth) -**Version:** 0.0.13-beta -**Repository:** [GitHub](https://github.com/clerk/clerk-sdk-flutter/tree/main/packages/clerk_auth) - ---- - -## Installation - -Add to your `pubspec.yaml`: - -```yaml -dependencies: - clerk_auth: ^0.0.13-beta -``` - -Then run: - -```bash -dart pub get -``` - ---- - -## Requirements - -- Dart >= 3.6.2 - ---- - -## Core Classes - -### Auth - -The main class for managing authentication state. - -```dart -import 'package:clerk_auth/clerk_auth.dart'; - -final auth = Auth( - config: AuthConfig( - publishableKey: 'pk_test_xxxxxxxxxxxxx', - persistor: DefaultPersistor( - getCacheDirectory: () => Directory.current, - ), - ), -); - -await auth.initialize(); -``` - -**Key Methods:** - -| Method | Description | -|--------|-------------| -| `initialize()` | Initialize the auth system | -| `attemptSignIn()` | Start a sign-in attempt | -| `attemptSignUp()` | Start a sign-up attempt | -| `attemptSignInVerification()` | Verify a sign-in attempt | -| `attemptSignUpVerification()` | Verify a sign-up attempt | -| `signOut()` | Sign out the current user | -| `signOutOf(Session)` | Sign out of a specific session | -| `resetPassword()` | Request a password reset | -| `terminate()` | Clean up resources | - -**Key Properties:** - -| Property | Type | Description | -|----------|------|-------------| -| `user` | `User?` | Current signed-in user | -| `client` | `Client` | Client object with sessions | -| `env` | `Environment` | Environment configuration | -| `sessionTokenStream` | `Stream` | Stream of session tokens | - ---- - -### AuthConfig - -Configuration for the Auth instance. - -```dart -AuthConfig( - publishableKey: 'pk_test_xxxxxxxxxxxxx', - persistor: DefaultPersistor( - getCacheDirectory: () => Directory.current, - ), - sessionTokenPolling: true, // Enable automatic token refresh - httpService: null, // Optional custom HTTP service -) -``` - -**Properties:** - -| Property | Type | Default | Description | -|----------|------|---------|-------------| -| `publishableKey` | `String` | Required | Your Clerk publishable key | -| `persistor` | `Persistor` | Required | Storage mechanism for auth state | -| `sessionTokenPolling` | `bool` | `true` | Enable automatic token refresh | -| `httpService` | `HttpService?` | `null` | Custom HTTP service | - ---- - -### User - -Represents a user in your application. - -```dart -final user = auth.user; - -if (user != null) { - print('User ID: ${user.id}'); - print('Email: ${user.emailAddress}'); - print('Name: ${user.firstName} ${user.lastName}'); -} -``` - -**Key Properties:** - -| Property | Type | Description | -|----------|------|-------------| -| `id` | `String` | Unique user identifier | -| `firstName` | `String?` | First name | -| `lastName` | `String?` | Last name | -| `username` | `String?` | Username | -| `emailAddress` | `String?` | Primary email | -| `phoneNumber` | `String?` | Primary phone | -| `imageUrl` | `String?` | Profile image URL | -| `emailAddresses` | `List` | All email addresses | -| `phoneNumbers` | `List` | All phone numbers | -| `publicMetadata` | `Map` | Public metadata | -| `unsafeMetadata` | `Map` | Unsafe metadata | - -**Key Methods:** - -| Method | Description | -|--------|-------------| -| `update()` | Update user profile | -| `setProfileImage()` | Upload profile image | -| `createEmailAddress()` | Add new email address | -| `createPhoneNumber()` | Add new phone number | -| `createTOTP()` | Enable MFA | - ---- - -### Strategy - -Authentication strategies supported by Clerk. - -```dart -// Password authentication -Strategy.password - -// Email code (passwordless) -Strategy.emailCode - -// Phone code (SMS) -Strategy.phoneCode - -// OAuth -Strategy.oauth(OAuthProvider.google) -Strategy.oauth(OAuthProvider.github) - -// TOTP (MFA) -Strategy.totp -``` - ---- - -### Persistor - -Interface for persisting authentication state. - -**Built-in Persistors:** - -```dart -// Default file-based persistor -DefaultPersistor( - getCacheDirectory: () => Directory.current, -) - -// No persistence (in-memory only) -Persistor.none -``` - -**Custom Persistor:** - -```dart -class MyCustomPersistor implements Persistor { - @override - Future write(String key, T value) async { - // Your storage logic - } - - @override - Future read(String key) async { - // Your retrieval logic - } - - @override - Future delete(String key) async { - // Your deletion logic - } -} -``` - ---- - -## Session Token Polling - -By default, `clerk_auth` automatically polls for session tokens and provides them via the `sessionTokenStream`. - -### Enable/Disable Polling - -```dart -final auth = Auth( - config: AuthConfig( - publishableKey: 'pk_test_xxxxxxxxxxxxx', - sessionTokenPolling: true, // Enable (default) - // sessionTokenPolling: false, // Disable - ), -); -``` - -### Listen to Session Tokens - -```dart -auth.sessionTokenStream.listen((token) { - if (token != null) { - print('New session token: $token'); - // Use for authenticated API requests - } -}); -``` - ---- - -## Error Handling - -All authentication methods can throw `ClerkApiException`: - -```dart -import 'package:clerk_auth/clerk_auth.dart'; - -try { - await auth.attemptSignIn( - strategy: Strategy.password, - identifier: 'user@example.com', - password: 'password', - ); -} on ClerkApiException catch (e) { - print('Error: ${e.message}'); - print('Status: ${e.status}'); - - // Check specific error codes - for (final error in e.errors) { - print('Code: ${error.code}'); - print('Message: ${error.message}'); - } -} -``` - ---- - -## Examples - -### Complete Sign-In Flow - -```dart -import 'dart:io'; -import 'package:clerk_auth/clerk_auth.dart'; - -Future main() async { - final auth = Auth( - config: AuthConfig( - publishableKey: Platform.environment['CLERK_PUBLISHABLE_KEY']!, - persistor: DefaultPersistor( - getCacheDirectory: () => Directory.current, - ), - ), - ); - - await auth.initialize(); - - try { - await auth.attemptSignIn( - strategy: Strategy.password, - identifier: 'user@example.com', - password: 'password', - ); - - print('✅ Signed in as: ${auth.user?.emailAddress}'); - - // Use session token for API requests - auth.sessionTokenStream.listen((token) { - if (token != null) { - // Make authenticated requests - } - }); - - } catch (e) { - print('❌ Sign in failed: $e'); - } finally { - auth.terminate(); - } -} -``` - ---- - -## API Documentation - -For complete API documentation, see: - -- [pub.dev Documentation](https://pub.dev/documentation/clerk_auth/latest/) -- [GitHub Repository](https://github.com/clerk/clerk-sdk-flutter/tree/main/packages/clerk_auth) - ---- - -## Next Steps - -- [Dart Quickstart]({{ '/getting-started/quickstart-dart' | relative_url }}) -- [Authentication Guide]({{ '/guides/authentication' | relative_url }}) -- [User Management]({{ '/guides/user-management' | relative_url }}) -- [Session Tokens]({{ '/guides/session-tokens' | relative_url }}) - diff --git a/docs/packages/clerk-flutter.md b/docs/packages/clerk-flutter.md deleted file mode 100644 index bc4998f6..00000000 --- a/docs/packages/clerk-flutter.md +++ /dev/null @@ -1,434 +0,0 @@ ---- -layout: default -title: clerk_flutter -parent: Packages -nav_order: 2 ---- - -# clerk_flutter Package -{: .no_toc } - -The official Clerk Flutter SDK with pre-built UI components. -{: .fs-6 .fw-300 } - -## Table of contents -{: .no_toc .text-delta } - -1. TOC -{:toc} - ---- - -## Overview - -The `clerk_flutter` package provides a complete authentication solution for Flutter applications with: - -- Pre-built UI components -- Material Design integration -- Cross-platform support (iOS, Android, Web, Desktop) -- Seamless Flutter integration - -**Package:** [clerk_flutter on pub.dev](https://pub.dev/packages/clerk_flutter) -**Version:** 0.0.13-beta -**Repository:** [GitHub](https://github.com/clerk/clerk-sdk-flutter/tree/main/packages/clerk_flutter) - ---- - -## Installation - -Add to your `pubspec.yaml`: - -```yaml -dependencies: - clerk_flutter: ^0.0.13-beta -``` - -Then run: - -```bash -flutter pub get -``` - ---- - -## Requirements - -- Flutter >= 3.27.4 -- Dart >= 3.6.2 - ---- - -## Core Widgets - -### ClerkAuth - -The root widget that initializes Clerk authentication. - -```dart -import 'package:clerk_flutter/clerk_flutter.dart'; - -ClerkAuth( - config: ClerkAuthConfig( - publishableKey: 'pk_test_xxxxxxxxxxxxx', - ), - child: MaterialApp( - // Your app - ), -) -``` - -**Properties:** - -| Property | Type | Description | -|----------|------|-------------| -| `config` | `ClerkAuthConfig` | Configuration object | -| `child` | `Widget` | Your app widget | -| `persistor` | `Persistor?` | Custom storage mechanism | -| `httpService` | `HttpService?` | Custom HTTP service | - ---- - -### ClerkAuthConfig - -Configuration for ClerkAuth. - -```dart -ClerkAuthConfig( - publishableKey: 'pk_test_xxxxxxxxxxxxx', - redirectionGenerator: (url) => Uri.parse('myapp://oauth'), - deepLinkStream: AppLinks().allUriLinkStream.map( - (uri) => ClerkDeepLink(uri: uri), - ), -) -``` - -**Properties:** - -| Property | Type | Description | -|----------|------|-------------| -| `publishableKey` | `String` | Your Clerk publishable key | -| `redirectionGenerator` | `Function?` | Generate OAuth redirect URLs | -| `deepLinkStream` | `Stream?` | Handle OAuth callbacks | -| `loading` | `Widget?` | Widget shown during initialization | - ---- - -### ClerkAuthBuilder - -Build different UI based on authentication state. - -```dart -ClerkAuthBuilder( - signedInBuilder: (context, authState) { - return Text('Welcome, ${authState.user?.firstName}!'); - }, - signedOutBuilder: (context, authState) { - return const ClerkAuthentication(); - }, -) -``` - -**Properties:** - -| Property | Type | Description | -|----------|------|-------------| -| `signedInBuilder` | `AuthWidgetBuilder?` | Builder when user is signed in | -| `signedOutBuilder` | `AuthWidgetBuilder?` | Builder when user is signed out | -| `builder` | `AuthWidgetBuilder?` | Fallback builder | - ---- - -### ClerkAuthentication - -Pre-built authentication UI with sign-in and sign-up. - -```dart -const ClerkAuthentication() -``` - -Features: -- Email/password sign-in and sign-up -- OAuth provider buttons -- Email verification -- Password reset -- Automatic error handling - ---- - -### ClerkUserButton - -User menu with profile and sign-out options. - -```dart -ClerkUserButton( - showName: true, - sessionActions: [ - ClerkUserAction( - label: 'Settings', - icon: Icons.settings, - onPressed: (context, authState) { - // Handle action - }, - ), - ], -) -``` - -**Properties:** - -| Property | Type | Description | -|----------|------|-------------| -| `showName` | `bool` | Show user's name | -| `sessionActions` | `List?` | Custom session actions | -| `additionalActions` | `List?` | Additional menu items | - ---- - -### ClerkSignedIn / ClerkSignedOut - -Conditional rendering based on auth state. - -```dart -ClerkSignedIn( - child: Text('Only visible when signed in'), -) - -ClerkSignedOut( - child: Text('Only visible when signed out'), -) -``` - ---- - -### ClerkErrorListener - -Listens for authentication errors and displays them. - -```dart -ClerkErrorListener( - child: YourWidget(), -) -``` - ---- - -### ClerkOrganizationList - -Display and manage organizations (if enabled). - -```dart -const ClerkOrganizationList() -``` - ---- - -## Accessing Auth State - -### Using ClerkAuth.of() - -```dart -import 'package:clerk_flutter/clerk_flutter.dart'; - -class MyWidget extends StatelessWidget { - @override - Widget build(BuildContext context) { - final authState = ClerkAuth.of(context); - final user = authState.user; - - return Text('Hello, ${user?.firstName}!'); - } -} -``` - -### ClerkAuthState Properties - -| Property | Type | Description | -|----------|------|-------------| -| `user` | `User?` | Current user | -| `client` | `Client` | Client with sessions | -| `env` | `Environment` | Environment config | -| `isSignedIn` | `bool` | Whether user is signed in | -| `sessionTokenStream` | `Stream` | Session token stream | - -### ClerkAuthState Methods - -| Method | Description | -|--------|-------------| -| `attemptSignIn()` | Start sign-in | -| `attemptSignUp()` | Start sign-up | -| `signOut()` | Sign out current user | -| `signOutOf(Session)` | Sign out of specific session | - ---- - -## Theming - -### Apply Custom Theme - -```dart -import 'package:clerk_flutter/clerk_flutter.dart'; - -MaterialApp( - theme: ThemeData.light().copyWith( - extensions: [ - ClerkTheme( - colors: ClerkColors.light( - primary: Colors.blue, - background: Colors.white, - ), - ), - ], - ), -) -``` - -### ClerkTheme Properties - -| Property | Type | Description | -|----------|------|-------------| -| `colors` | `ClerkColors` | Color scheme | -| `typography` | `ClerkTypography?` | Text styles | - ---- - -## Deep Links for OAuth - -### Setup Deep Links - -1. Add `app_links` package: - -```yaml -dependencies: - app_links: ^3.5.1 -``` - -2. Configure ClerkAuth: - -```dart -import 'package:app_links/app_links.dart'; - -ClerkAuth( - config: ClerkAuthConfig( - publishableKey: 'pk_test_xxxxxxxxxxxxx', - redirectionGenerator: (url) { - return Uri.parse('myapp://oauth-callback'); - }, - deepLinkStream: AppLinks().allUriLinkStream.map((uri) { - return ClerkDeepLink(uri: uri); - }), - ), - child: MaterialApp(/* ... */), -) -``` - -3. Configure platform-specific deep links: - -**Android** (`android/app/src/main/AndroidManifest.xml`): -```xml - - - - - - -``` - -**iOS** (`ios/Runner/Info.plist`): -```xml -CFBundleURLTypes - - - CFBundleURLSchemes - - myapp - - - -``` - ---- - -## Examples - -### Complete App Example - -```dart -import 'package:flutter/material.dart'; -import 'package:clerk_flutter/clerk_flutter.dart'; - -void main() { - runApp(const MyApp()); -} - -class MyApp extends StatelessWidget { - const MyApp({super.key}); - - @override - Widget build(BuildContext context) { - return ClerkAuth( - config: ClerkAuthConfig( - publishableKey: 'pk_test_xxxxxxxxxxxxx', - ), - child: MaterialApp( - title: 'Clerk Flutter Demo', - theme: ThemeData.light(), - home: const HomePage(), - ), - ); - } -} - -class HomePage extends StatelessWidget { - const HomePage({super.key}); - - @override - Widget build(BuildContext context) { - return Scaffold( - appBar: AppBar(title: const Text('My App')), - body: SafeArea( - child: ClerkErrorListener( - child: ClerkAuthBuilder( - signedInBuilder: (context, authState) { - return Center( - child: Column( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - Text('Welcome, ${authState.user?.firstName}!'), - const SizedBox(height: 20), - const ClerkUserButton(showName: true), - ], - ), - ); - }, - signedOutBuilder: (context, authState) { - return const Center( - child: ClerkAuthentication(), - ); - }, - ), - ), - ), - ); - } -} -``` - ---- - -## API Documentation - -For complete API documentation, see: - -- [pub.dev Documentation](https://pub.dev/documentation/clerk_flutter/latest/) -- [GitHub Repository](https://github.com/clerk/clerk-sdk-flutter/tree/main/packages/clerk_flutter) -- [Example App](https://github.com/clerk/clerk-sdk-flutter/tree/main/packages/clerk_flutter/example) - ---- - -## Next Steps - -- [Flutter Quickstart]({{ '/getting-started/quickstart-flutter' | relative_url }}) -- [Widget Reference]({{ '/api/widgets' | relative_url }}) -- [Customization Guide]({{ '/guides/customization' | relative_url }}) -- [Authentication Flows]({{ '/guides/authentication' | relative_url }}) - From 56e5aabe173988205684261f17a645a91cd9a1e3 Mon Sep 17 00:00:00 2001 From: Nic Ford Date: Mon, 16 Mar 2026 17:09:11 +0000 Subject: [PATCH 03/27] fix: remove doc deployment workflow [#341] --- .github/workflows/deploy-docs.yml | 63 ------------------------------- 1 file changed, 63 deletions(-) delete mode 100644 .github/workflows/deploy-docs.yml diff --git a/.github/workflows/deploy-docs.yml b/.github/workflows/deploy-docs.yml deleted file mode 100644 index cb1d469e..00000000 --- a/.github/workflows/deploy-docs.yml +++ /dev/null @@ -1,63 +0,0 @@ -name: Deploy Documentation to GitHub Pages - -on: - push: - branches: - - main - - 'feat/341-docs' - paths: - - 'docs/**' - - '.github/workflows/deploy-docs.yml' - workflow_dispatch: - -permissions: - contents: read - pages: write - id-token: write - -concurrency: - group: "pages" - cancel-in-progress: false - -jobs: - build: - runs-on: ubuntu-latest - steps: - - name: Checkout - uses: actions/checkout@v4 - - - name: Setup Ruby - uses: ruby/setup-ruby@v1 - with: - ruby-version: '3.1' - bundler-cache: true - working-directory: docs - - - name: Setup Pages - id: pages - uses: actions/configure-pages@v4 - - - name: Build with Jekyll - working-directory: docs - run: | - bundle install - bundle exec jekyll build --baseurl "${{ steps.pages.outputs.base_path }}" --config _config.yml,_config_production.yml - env: - JEKYLL_ENV: production - - - name: Upload artifact - uses: actions/upload-pages-artifact@v3 - with: - path: docs/_site - - deploy: - environment: - name: github-pages - url: ${{ steps.deployment.outputs.page_url }} - runs-on: ubuntu-latest - needs: build - steps: - - name: Deploy to GitHub Pages - id: deployment - uses: actions/deploy-pages@v4 - From 39c8131502b127de2de52d3ca6e724a2ae33e74a Mon Sep 17 00:00:00 2001 From: Mark O'Sullivan <6950843+MarkOSullivan94@users.noreply.github.com> Date: Tue, 31 Mar 2026 11:26:35 +0200 Subject: [PATCH 04/27] docs: badges for packages updated from v0.0.1 to v0.0.14-beta --- README.md | 12 ++++-------- packages/clerk_auth/README.md | 2 +- packages/clerk_flutter/README.md | 2 +- 3 files changed, 6 insertions(+), 10 deletions(-) diff --git a/README.md b/README.md index 407045a2..9f2bda6f 100644 --- a/README.md +++ b/README.md @@ -10,8 +10,8 @@
-[![Pub Version](https://img.shields.io/pub/v/clerk_flutter?color=blueviolet&label=clerk_flutter)](https://pub.dev/packages/clerk_flutter) -[![Pub Version](https://img.shields.io/pub/v/clerk_auth?color=blueviolet&label=clerk_auth)](https://pub.dev/packages/clerk_auth) +[![Pub Version](https://img.shields.io/badge/clerk__flutter-v0.0.14--beta-blueviolet)](https://pub.dev/packages/clerk_flutter) +[![Pub Version](https://img.shields.io/badge/clerk__auth-v0.0.14--beta-blueviolet)](https://pub.dev/packages/clerk_auth) [![chat on Discord](https://img.shields.io/discord/856971667393609759.svg?logo=discord)](https://discord.com/invite/b5rXHjAg7A) [![documentation](https://img.shields.io/badge/documentation-clerk-green.svg)](https://clerk.com/docs) @@ -46,9 +46,7 @@ Our comprehensive documentation includes: ### [clerk_auth](./packages/clerk_auth) - Dart SDK -Pure Dart SDK for backend and CLI applications. - -[![Pub Version](https://img.shields.io/pub/v/clerk_auth?color=blueviolet)](https://pub.dev/packages/clerk_auth) +[![Pub Version](https://img.shields.io/badge/pub-v0.0.14--beta-blueviolet)](https://pub.dev/packages/clerk_auth) [![Pub Points](https://img.shields.io/pub/points/clerk_auth?label=pub%20points)](https://pub.dev/packages/clerk_auth/score) ```yaml @@ -60,9 +58,7 @@ dependencies: ### [clerk_flutter](./packages/clerk_flutter) - Flutter SDK -Flutter SDK with pre-built UI components for iOS, Android, Web, and Desktop. - -[![Pub Version](https://img.shields.io/pub/v/clerk_flutter?color=blueviolet)](https://pub.dev/packages/clerk_flutter) +[![Pub Version](https://img.shields.io/badge/pub-v0.0.14--beta-blueviolet)](https://pub.dev/packages/clerk_flutter) [![Pub Points](https://img.shields.io/pub/points/clerk_flutter?label=pub%20points)](https://pub.dev/packages/clerk_flutter/score) ```yaml diff --git a/packages/clerk_auth/README.md b/packages/clerk_auth/README.md index c1678cb4..0c14ffed 100644 --- a/packages/clerk_auth/README.md +++ b/packages/clerk_auth/README.md @@ -4,7 +4,7 @@ ## Official [Clerk](https://clerk.com) Dart SDK (Beta) -[![Pub Version](https://img.shields.io/pub/v/clerk_auth?color=blueviolet)](https://pub.dev/packages/clerk_auth) +[![Pub Version](https://img.shields.io/badge/pub-v0.0.14--beta-blueviolet)](https://pub.dev/packages/clerk_auth) [![Pub Points](https://img.shields.io/pub/points/clerk_auth?label=pub%20points)](https://pub.dev/packages/clerk_auth/score) [![chat on Discord](https://img.shields.io/discord/856971667393609759.svg?logo=discord)](https://discord.com/invite/b5rXHjAg7A) [![documentation](https://img.shields.io/badge/documentation-clerk-green.svg)](https://clerk.com/docs) diff --git a/packages/clerk_flutter/README.md b/packages/clerk_flutter/README.md index 64d25269..95e0c1e2 100644 --- a/packages/clerk_flutter/README.md +++ b/packages/clerk_flutter/README.md @@ -4,7 +4,7 @@ ## Official [Clerk](https://clerk.com) Flutter SDK (Beta) -[![Pub Version](https://img.shields.io/pub/v/clerk_flutter?color=blueviolet)](https://pub.dev/packages/clerk_flutter) +[![Pub Version](https://img.shields.io/badge/pub-v0.0.14--beta-blueviolet)](https://pub.dev/packages/clerk_flutter) [![Pub Points](https://img.shields.io/pub/points/clerk_flutter?label=pub%20points)](https://pub.dev/packages/clerk_flutter/score) [![chat on Discord](https://img.shields.io/discord/856971667393609759.svg?logo=discord)](https://discord.com/invite/b5rXHjAg7A) [![documentation](https://img.shields.io/badge/documentation-clerk-green.svg)](https://clerk.com/docs) From f3d018ec488bcc261dc76a74370d16fed47d0294 Mon Sep 17 00:00:00 2001 From: Mark O'Sullivan <6950843+MarkOSullivan94@users.noreply.github.com> Date: Tue, 31 Mar 2026 11:30:35 +0200 Subject: [PATCH 05/27] docs: updated mentions of v0.0.13-beta to latest version --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 9f2bda6f..d4b6a189 100644 --- a/README.md +++ b/README.md @@ -51,7 +51,7 @@ Our comprehensive documentation includes: ```yaml dependencies: - clerk_auth: ^0.0.13-beta + clerk_auth: ^0.0.14-beta ``` **[View clerk_auth Documentation →](https://clerk.github.io/clerk-sdk-flutter/packages/clerk-auth)** @@ -63,7 +63,7 @@ dependencies: ```yaml dependencies: - clerk_flutter: ^0.0.13-beta + clerk_flutter: ^0.0.14-beta ``` **[View clerk_flutter Documentation →](https://clerk.github.io/clerk-sdk-flutter/packages/clerk-flutter)** From 2d5761f8ea30a6d63879025391eb07a2bb12c17c Mon Sep 17 00:00:00 2001 From: Mark O'Sullivan <6950843+MarkOSullivan94@users.noreply.github.com> Date: Tue, 31 Mar 2026 11:31:54 +0200 Subject: [PATCH 06/27] docs: fixed broken twitter link --- README.md | 2 +- packages/clerk_auth/README.md | 2 +- packages/clerk_flutter/README.md | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index d4b6a189..d0756105 100644 --- a/README.md +++ b/README.md @@ -160,7 +160,7 @@ class MyApp extends StatelessWidget { - 💬 [Discord Community](https://clerk.com/discord) - Chat with the community and Clerk team - 📖 [Main Clerk Docs](https://clerk.com/docs) - Comprehensive guides and references - 🐛 [GitHub Issues](https://github.com/clerk/clerk-sdk-flutter/issues) - Report bugs and request features -- 🐦 [Twitter](https://twitter.com/ClerkDev) - Latest updates and announcements +- 🐦 [Twitter](https://twitter.com/Clerk) - Latest updates and announcements --- diff --git a/packages/clerk_auth/README.md b/packages/clerk_auth/README.md index 0c14ffed..fbb42422 100644 --- a/packages/clerk_auth/README.md +++ b/packages/clerk_auth/README.md @@ -8,7 +8,7 @@ [![Pub Points](https://img.shields.io/pub/points/clerk_auth?label=pub%20points)](https://pub.dev/packages/clerk_auth/score) [![chat on Discord](https://img.shields.io/discord/856971667393609759.svg?logo=discord)](https://discord.com/invite/b5rXHjAg7A) [![documentation](https://img.shields.io/badge/documentation-clerk-green.svg)](https://clerk.com/docs) -[![twitter](https://img.shields.io/twitter/follow/ClerkDev?style=social)](https://twitter.com/intent/follow?screen_name=ClerkDev) +[![twitter](https://img.shields.io/twitter/follow/Clerk?style=social)](https://twitter.com/intent/follow?screen_name=Clerk) > ### ⚠️ The Clerk Dart SDK is in Beta ⚠️ > ❗️ Breaking changes should be expected until the first stable release (1.0.0) ❗️ diff --git a/packages/clerk_flutter/README.md b/packages/clerk_flutter/README.md index 95e0c1e2..b9bc8a0d 100644 --- a/packages/clerk_flutter/README.md +++ b/packages/clerk_flutter/README.md @@ -8,7 +8,7 @@ [![Pub Points](https://img.shields.io/pub/points/clerk_flutter?label=pub%20points)](https://pub.dev/packages/clerk_flutter/score) [![chat on Discord](https://img.shields.io/discord/856971667393609759.svg?logo=discord)](https://discord.com/invite/b5rXHjAg7A) [![documentation](https://img.shields.io/badge/documentation-clerk-green.svg)](https://clerk.com/docs) -[![twitter](https://img.shields.io/twitter/follow/ClerkDev?style=social)](https://twitter.com/intent/follow?screen_name=ClerkDev) +[![twitter](https://img.shields.io/twitter/follow/Clerk?style=social)](https://twitter.com/intent/follow?screen_name=Clerk) > ### ⚠️ The Clerk Flutter SDK is in Beta ⚠️ > ❗️ Breaking changes should be expected until the first stable release (1.0.0) ❗️ From d6782b4ca82491aae2517a2d5b804c5b0cfe4e78 Mon Sep 17 00:00:00 2001 From: Mark O'Sullivan <6950843+MarkOSullivan94@users.noreply.github.com> Date: Wed, 1 Apr 2026 09:23:49 +0200 Subject: [PATCH 07/27] docs: updated discord link --- README.md | 2 +- packages/clerk_auth/README.md | 2 +- packages/clerk_flutter/README.md | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index d0756105..f7f4ef03 100644 --- a/README.md +++ b/README.md @@ -12,7 +12,7 @@ [![Pub Version](https://img.shields.io/badge/clerk__flutter-v0.0.14--beta-blueviolet)](https://pub.dev/packages/clerk_flutter) [![Pub Version](https://img.shields.io/badge/clerk__auth-v0.0.14--beta-blueviolet)](https://pub.dev/packages/clerk_auth) -[![chat on Discord](https://img.shields.io/discord/856971667393609759.svg?logo=discord)](https://discord.com/invite/b5rXHjAg7A) +[![chat on Discord](https://img.shields.io/discord/856971667393609759.svg?logo=discord)](https://clerk.com/discord) [![documentation](https://img.shields.io/badge/documentation-clerk-green.svg)](https://clerk.com/docs)
diff --git a/packages/clerk_auth/README.md b/packages/clerk_auth/README.md index fbb42422..af2e3146 100644 --- a/packages/clerk_auth/README.md +++ b/packages/clerk_auth/README.md @@ -6,7 +6,7 @@ [![Pub Version](https://img.shields.io/badge/pub-v0.0.14--beta-blueviolet)](https://pub.dev/packages/clerk_auth) [![Pub Points](https://img.shields.io/pub/points/clerk_auth?label=pub%20points)](https://pub.dev/packages/clerk_auth/score) -[![chat on Discord](https://img.shields.io/discord/856971667393609759.svg?logo=discord)](https://discord.com/invite/b5rXHjAg7A) +[![chat on Discord](https://img.shields.io/discord/856971667393609759.svg?logo=discord)](https://clerk.com/discord) [![documentation](https://img.shields.io/badge/documentation-clerk-green.svg)](https://clerk.com/docs) [![twitter](https://img.shields.io/twitter/follow/Clerk?style=social)](https://twitter.com/intent/follow?screen_name=Clerk) diff --git a/packages/clerk_flutter/README.md b/packages/clerk_flutter/README.md index b9bc8a0d..6f2d0398 100644 --- a/packages/clerk_flutter/README.md +++ b/packages/clerk_flutter/README.md @@ -6,7 +6,7 @@ [![Pub Version](https://img.shields.io/badge/pub-v0.0.14--beta-blueviolet)](https://pub.dev/packages/clerk_flutter) [![Pub Points](https://img.shields.io/pub/points/clerk_flutter?label=pub%20points)](https://pub.dev/packages/clerk_flutter/score) -[![chat on Discord](https://img.shields.io/discord/856971667393609759.svg?logo=discord)](https://discord.com/invite/b5rXHjAg7A) +[![chat on Discord](https://img.shields.io/discord/856971667393609759.svg?logo=discord)](https://clerk.com/discord) [![documentation](https://img.shields.io/badge/documentation-clerk-green.svg)](https://clerk.com/docs) [![twitter](https://img.shields.io/twitter/follow/Clerk?style=social)](https://twitter.com/intent/follow?screen_name=Clerk) From d5af957615d109f5fe3e39325af17d0669798d9e Mon Sep 17 00:00:00 2001 From: Mark O'Sullivan <6950843+MarkOSullivan94@users.noreply.github.com> Date: Wed, 1 Apr 2026 09:26:06 +0200 Subject: [PATCH 08/27] docs: trimmed down repo README --- README.md | 48 ++++---------------------------- packages/clerk_flutter/README.md | 20 +++++-------- 2 files changed, 12 insertions(+), 56 deletions(-) diff --git a/README.md b/README.md index f7f4ef03..02050625 100644 --- a/README.md +++ b/README.md @@ -17,9 +17,7 @@ -# Clerk Dart and Flutter SDKs - -The official [Clerk](https://clerk.com) Dart and Flutter SDKs for authentication and user management. +# Clerk Flutter and Dart SDKs **Clerk helps developers build user management. We provide streamlined user experiences for your users to sign up, sign in, and manage their profiles.** @@ -28,17 +26,11 @@ The official [Clerk](https://clerk.com) Dart and Flutter SDKs for authentication --- -## 📚 Documentation - -**[View Full Documentation →](https://clerk.github.io/clerk-sdk-flutter/)** +## 🚀 Get Started With Clerk -Our comprehensive documentation includes: - -- 🚀 [Getting Started Guides](https://clerk.github.io/clerk-sdk-flutter/getting-started) -- 📖 [API Reference](https://clerk.github.io/clerk-sdk-flutter/packages/clerk-auth) -- 🎯 [Authentication Flows](https://clerk.github.io/clerk-sdk-flutter/guides/authentication) -- 👤 [User Management](https://clerk.github.io/clerk-sdk-flutter/guides/user-management) -- 🎨 [Customization](https://clerk.github.io/clerk-sdk-flutter/guides/customization) +1. [Sign up for an account](https://dashboard.clerk.com/sign-up?utm_source=github&utm_medium=clerk_flutter_repo_readme) +2. Create an application in your Clerk dashboard +3. Follow the guides for our SDKs: [clerk_flutter](./packages/clerk_flutter/README.md) or [clerk_auth](./packages/clerk_auth/README.md) --- @@ -72,36 +64,6 @@ dependencies: ## 🚀 Quick Start -### Dart (clerk_auth) - -```dart -import 'dart:io'; -import 'package:clerk_auth/clerk_auth.dart'; - -Future main() async { - final auth = Auth( - config: AuthConfig( - publishableKey: 'pk_test_xxxxxxxxxxxxx', - persistor: DefaultPersistor( - getCacheDirectory: () => Directory.current, - ), - ), - ); - - await auth.initialize(); - - await auth.attemptSignIn( - strategy: Strategy.password, - identifier: 'user@example.com', - password: 'password', - ); - - print('Signed in as ${auth.user?.emailAddress}'); -} -``` - -**[View Dart Quickstart →](https://clerk.github.io/clerk-sdk-flutter/getting-started/quickstart-dart)** - ### Flutter (clerk_flutter) ```dart diff --git a/packages/clerk_flutter/README.md b/packages/clerk_flutter/README.md index 6f2d0398..ab254beb 100644 --- a/packages/clerk_flutter/README.md +++ b/packages/clerk_flutter/README.md @@ -35,10 +35,6 @@ for your users to sign up, sign in, and manage their profile from your Flutter c * Flutter >= 3.27.4 * Dart >= 3.6.2 -## In Development - -* Organization support - ## Example Usage To use this package you will need to go to your [Clerk Dashboard](https://dashboard.clerk.com/) @@ -66,15 +62,13 @@ class ExampleApp extends StatelessWidget { debugShowCheckedModeBanner: false, home: Scaffold( body: SafeArea( - child: ClerkErrorListener( - child: ClerkAuthBuilder( - signedInBuilder: (context, authState) { - return const ClerkUserButton(); - }, - signedOutBuilder: (context, authState) { - return const ClerkAuthentication(); - }, - ), + child: ClerkAuthBuilder( + signedInBuilder: (context, authState) { + return const ClerkUserButton(); + }, + signedOutBuilder: (context, authState) { + return const ClerkAuthentication(); + }, ), ), ), From 85c84da038d00b3eb1fca5b1dc7ccd26bc1caa58 Mon Sep 17 00:00:00 2001 From: Mark O'Sullivan <6950843+MarkOSullivan94@users.noreply.github.com> Date: Tue, 7 Apr 2026 12:29:04 +0200 Subject: [PATCH 09/27] docs: added screenshots --- README.md | 116 +++++---------------------------------- assets/example-dark.png | Bin 0 -> 235598 bytes assets/example-light.png | Bin 0 -> 235304 bytes 3 files changed, 14 insertions(+), 102 deletions(-) create mode 100644 assets/example-dark.png create mode 100644 assets/example-light.png diff --git a/README.md b/README.md index 02050625..386e5df1 100644 --- a/README.md +++ b/README.md @@ -10,10 +10,10 @@
-[![Pub Version](https://img.shields.io/badge/clerk__flutter-v0.0.14--beta-blueviolet)](https://pub.dev/packages/clerk_flutter) -[![Pub Version](https://img.shields.io/badge/clerk__auth-v0.0.14--beta-blueviolet)](https://pub.dev/packages/clerk_auth) + [![chat on Discord](https://img.shields.io/discord/856971667393609759.svg?logo=discord)](https://clerk.com/discord) [![documentation](https://img.shields.io/badge/documentation-clerk-green.svg)](https://clerk.com/docs) +[![twitter](https://img.shields.io/twitter/follow/Clerk?style=social)](https://twitter.com/intent/follow?screen_name=Clerk)
@@ -24,114 +24,26 @@ > ### ⚠️ Beta Notice > These SDKs are currently in Beta. Breaking changes should be expected until the first stable release (1.0.0). ---- - -## 🚀 Get Started With Clerk - -1. [Sign up for an account](https://dashboard.clerk.com/sign-up?utm_source=github&utm_medium=clerk_flutter_repo_readme) -2. Create an application in your Clerk dashboard -3. Follow the guides for our SDKs: [clerk_flutter](./packages/clerk_flutter/README.md) or [clerk_auth](./packages/clerk_auth/README.md) +

+ + +
+ The clerk_flutter example app +

--- ## 📦 Packages -### [clerk_auth](./packages/clerk_auth) - Dart SDK - -[![Pub Version](https://img.shields.io/badge/pub-v0.0.14--beta-blueviolet)](https://pub.dev/packages/clerk_auth) -[![Pub Points](https://img.shields.io/pub/points/clerk_auth?label=pub%20points)](https://pub.dev/packages/clerk_auth/score) +| Package | Description | Pub | +|---------|-------------|-----| +| [clerk_auth](./packages/clerk_auth) | Dart SDK for Clerk authentication | [![pub package](https://img.shields.io/badge/pub-v0.0.14--beta-blueviolet)](https://pub.dev/packages/clerk_auth) | +| [clerk_flutter](./packages/clerk_flutter) | Flutter UI components for Clerk authentication | [![pub package](https://img.shields.io/badge/pub-v0.0.14--beta-blueviolet)](https://pub.dev/packages/clerk_flutter) | -```yaml -dependencies: - clerk_auth: ^0.0.14-beta -``` - -**[View clerk_auth Documentation →](https://clerk.github.io/clerk-sdk-flutter/packages/clerk-auth)** - -### [clerk_flutter](./packages/clerk_flutter) - Flutter SDK - -[![Pub Version](https://img.shields.io/badge/pub-v0.0.14--beta-blueviolet)](https://pub.dev/packages/clerk_flutter) -[![Pub Points](https://img.shields.io/pub/points/clerk_flutter?label=pub%20points)](https://pub.dev/packages/clerk_flutter/score) - -```yaml -dependencies: - clerk_flutter: ^0.0.14-beta -``` - -**[View clerk_flutter Documentation →](https://clerk.github.io/clerk-sdk-flutter/packages/clerk-flutter)** - ---- -## 🚀 Quick Start - -### Flutter (clerk_flutter) - -```dart -import 'package:flutter/material.dart'; -import 'package:clerk_flutter/clerk_flutter.dart'; - -void main() => runApp(const MyApp()); - -class MyApp extends StatelessWidget { - const MyApp({super.key}); - - @override - Widget build(BuildContext context) { - return ClerkAuth( - config: ClerkAuthConfig( - publishableKey: 'pk_test_xxxxxxxxxxxxx', - ), - child: MaterialApp( - home: Scaffold( - body: ClerkAuthBuilder( - signedInBuilder: (context, authState) { - return Center( - child: ClerkUserButton(showName: true), - ); - }, - signedOutBuilder: (context, authState) { - return const Center( - child: ClerkAuthentication(), - ); - }, - ), - ), - ), - ); - } -} -``` - -**[View Flutter Quickstart →](https://clerk.github.io/clerk-sdk-flutter/getting-started/quickstart-flutter)** - ---- - -## ✨ Features - -- 🔐 **Complete Authentication** - Email, phone, OAuth, passwordless, and MFA -- 👤 **User Management** - Pre-built UI components and headless APIs -- 🏢 **Organizations** - Multi-tenant support with RBAC -- 🎨 **Customizable UI** - Themeable components that match your brand -- 📱 **Cross-Platform** - iOS, Android, Web, Windows, macOS, Linux -- 🚀 **Production Ready** - Battle-tested infrastructure - ---- - -## 🤝 Community & Support - -- 💬 [Discord Community](https://clerk.com/discord) - Chat with the community and Clerk team -- 📖 [Main Clerk Docs](https://clerk.com/docs) - Comprehensive guides and references -- 🐛 [GitHub Issues](https://github.com/clerk/clerk-sdk-flutter/issues) - Report bugs and request features -- 🐦 [Twitter](https://twitter.com/Clerk) - Latest updates and announcements - ---- ## 📄 License -These SDKs are licensed under the MIT license found in the [LICENSE](./LICENSE) file. +This project is licensed under the MIT license. ---- - -
-

Made with ❤️ by Clerk

-
+See [LICENSE](./LICENSE) for more information. diff --git a/assets/example-dark.png b/assets/example-dark.png new file mode 100644 index 0000000000000000000000000000000000000000..b8f9c1e10d099a187e049dde82da7532a3463297 GIT binary patch literal 235598 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 z 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 qt2*Lo$ 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ʿjoYP1ClV#2$9ODh@lCt6Nljpu1VIroxGCRm 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≀G8_d>|19 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 z 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 literal 0 HcmV?d00001 diff --git a/assets/example-light.png b/assets/example-light.png new file mode 100644 index 0000000000000000000000000000000000000000..8de270644661dc34bfa87bbefce6f88e2e17c650 GIT binary patch literal 235304 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%0DGV+LmJU1t)7GRYMV$NKn~^V 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⪻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%0D(rAlBp=)N$3S^J@q_dJ<1O~ZtPGtzL8YE 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 F{{_f7xSaq1 literal 0 HcmV?d00001 From 069206b86075c0c1489608de7a8ad003c09cd004 Mon Sep 17 00:00:00 2001 From: Mark O'Sullivan <6950843+MarkOSullivan94@users.noreply.github.com> Date: Tue, 7 Apr 2026 12:35:48 +0200 Subject: [PATCH 10/27] docs(clerk_flutter): trimmed down readme --- packages/clerk_flutter/README.md | 65 ++++++++++++-------------------- 1 file changed, 24 insertions(+), 41 deletions(-) diff --git a/packages/clerk_flutter/README.md b/packages/clerk_flutter/README.md index ab254beb..95fdf25c 100644 --- a/packages/clerk_flutter/README.md +++ b/packages/clerk_flutter/README.md @@ -16,33 +16,35 @@ **Clerk helps developers build user management. We provide streamlined user experiences for your users to sign up, sign in, and manage their profile from your Flutter code.** ---- - -## 📚 Documentation - -**[View Full Documentation →](https://clerk.github.io/clerk-sdk-flutter/packages/clerk-flutter)** - -- 🚀 [Flutter Quickstart](https://clerk.github.io/clerk-sdk-flutter/getting-started/quickstart-flutter) -- 📖 [Widget Reference](https://clerk.github.io/clerk-sdk-flutter/api/widgets) -- 🎨 [Customization Guide](https://clerk.github.io/clerk-sdk-flutter/guides/customization) -- 🎯 [Authentication Flows](https://clerk.github.io/clerk-sdk-flutter/guides/authentication) -- 👤 [User Management](https://clerk.github.io/clerk-sdk-flutter/guides/user-management) +

    + + +
    + The clerk_flutter example app +

    --- ## Requirements -* Flutter >= 3.27.4 -* Dart >= 3.6.2 +| Flutter | Dart | +|---------|------| +| 3.27.4+ | 3.6.2+ | + +## 🚀 Getting Started -## Example Usage +1. [Sign up for an account](https://dashboard.clerk.com/sign-up?utm_source=github&utm_medium=clerk_flutter_repo_readme) +2. Create an application in your Clerk dashboard +3. Copy the publishable key from the dashboard +4. Add `clerk_flutter` to your `pubspec.yaml` -To use this package you will need to go to your [Clerk Dashboard](https://dashboard.clerk.com/) -create an application and copy the public and publishable API keys into your project. +```yaml +dependencies: + clerk_flutter: ^0.0.14-beta +``` + +5. You can now make use of clerk_flutter widgets adding authentication to your application -The bundled example app requires one, possibly two, variables to be set up in your environment: -- `publishable_key`: your Clerk publishable key, usually starting `pk_` -- `google_client_id`: the ID of your GCP web project, if you are using Google token oauth ```dart /// Example App @@ -78,27 +80,8 @@ class ExampleApp extends StatelessWidget { } ``` -## Installation (Android) - -Add the following line to your `android/app/src/main/AndroidManifest.xml` file: - -```xml - -``` - ---- - -## 📖 Learn More - -- [Full Documentation](https://clerk.github.io/clerk-sdk-flutter/) -- [Flutter Quickstart](https://clerk.github.io/clerk-sdk-flutter/getting-started/quickstart-flutter) -- [Widget Reference](https://clerk.github.io/clerk-sdk-flutter/api/widgets) -- [Customization Guide](https://clerk.github.io/clerk-sdk-flutter/guides/customization) -- [Authentication Flows](https://clerk.github.io/clerk-sdk-flutter/guides/authentication) -- [Example App](https://github.com/clerk/clerk-sdk-flutter/tree/main/packages/clerk_flutter/example) - ---- - ## License -This SDK is licensed under the MIT license found in the [LICENSE](./LICENSE) file. +This project is licensed under the MIT license. + +See [LICENSE](./LICENSE) for more information. From ed2f6ed73b870e159e520062c2dd0831ea1dc7f3 Mon Sep 17 00:00:00 2001 From: Mark O'Sullivan <6950843+MarkOSullivan94@users.noreply.github.com> Date: Tue, 7 Apr 2026 15:39:53 +0200 Subject: [PATCH 11/27] docs(clerk_auth): requirements table --- packages/clerk_auth/README.md | 19 ++++++------------- 1 file changed, 6 insertions(+), 13 deletions(-) diff --git a/packages/clerk_auth/README.md b/packages/clerk_auth/README.md index af2e3146..28c398dc 100644 --- a/packages/clerk_auth/README.md +++ b/packages/clerk_auth/README.md @@ -18,20 +18,11 @@ for your users to sign up, sign in, and manage their profile from your Dart code --- -## 📚 Documentation - -**[View Full Documentation →](https://clerk.github.io/clerk-sdk-flutter/packages/clerk-auth)** - -- 🚀 [Dart Quickstart](https://clerk.github.io/clerk-sdk-flutter/getting-started/quickstart-dart) -- 📖 [API Reference](https://pub.dev/documentation/clerk_auth/latest/) -- 🎯 [Authentication Guide](https://clerk.github.io/clerk-sdk-flutter/guides/authentication) -- 👤 [User Management](https://clerk.github.io/clerk-sdk-flutter/guides/user-management) - ---- - ## Requirements -* Dart >= 3.6.2 +| Dart | +|------| +| 3.6.2+ | ## Example Usage @@ -126,4 +117,6 @@ the system defaulting to no polling, please review your code. ## License -This SDK is licensed under the MIT license found in the [LICENSE](./LICENSE) file. +This project is licensed under the MIT license. + +See [LICENSE](./LICENSE) for more information. From d01b3253dcfc88ebff09f613e480a74f4af29a4d Mon Sep 17 00:00:00 2001 From: Mark O'Sullivan <6950843+MarkOSullivan94@users.noreply.github.com> Date: Tue, 7 Apr 2026 15:40:44 +0200 Subject: [PATCH 12/27] docs: updated clerk_flutter --- packages/clerk_flutter/README.md | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/packages/clerk_flutter/README.md b/packages/clerk_flutter/README.md index 95fdf25c..001bfe13 100644 --- a/packages/clerk_flutter/README.md +++ b/packages/clerk_flutter/README.md @@ -2,7 +2,7 @@

    -## Official [Clerk](https://clerk.com) Flutter SDK (Beta) +## Official Clerk Flutter SDK (Beta) [![Pub Version](https://img.shields.io/badge/pub-v0.0.14--beta-blueviolet)](https://pub.dev/packages/clerk_flutter) [![Pub Points](https://img.shields.io/pub/points/clerk_flutter?label=pub%20points)](https://pub.dev/packages/clerk_flutter/score) @@ -36,14 +36,15 @@ for your users to sign up, sign in, and manage their profile from your Flutter c 1. [Sign up for an account](https://dashboard.clerk.com/sign-up?utm_source=github&utm_medium=clerk_flutter_repo_readme) 2. Create an application in your Clerk dashboard 3. Copy the publishable key from the dashboard -4. Add `clerk_flutter` to your `pubspec.yaml` +4. Add `clerk_flutter` and `clerk_auth` to your `pubspec.yaml` ```yaml dependencies: + clerk_auth: ^0.0.14-beta clerk_flutter: ^0.0.14-beta ``` -5. You can now make use of clerk_flutter widgets adding authentication to your application +5. You can now make use of Clerk Flutter widgets adding authentication to your application ```dart From aa973e59d69a89cc88263040c34891d71ebf38f3 Mon Sep 17 00:00:00 2001 From: Mark O'Sullivan <6950843+MarkOSullivan94@users.noreply.github.com> Date: Tue, 7 Apr 2026 15:40:52 +0200 Subject: [PATCH 13/27] docs: melos badge --- README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/README.md b/README.md index 386e5df1..bfd57bb5 100644 --- a/README.md +++ b/README.md @@ -14,6 +14,7 @@ [![chat on Discord](https://img.shields.io/discord/856971667393609759.svg?logo=discord)](https://clerk.com/discord) [![documentation](https://img.shields.io/badge/documentation-clerk-green.svg)](https://clerk.com/docs) [![twitter](https://img.shields.io/twitter/follow/Clerk?style=social)](https://twitter.com/intent/follow?screen_name=Clerk) +[![melos](https://img.shields.io/badge/maintained%20with-melos-f700ff.svg?style=flat-square)](https://github.com/invertase/melos) From e705db1837f1e1c3b0e6293717fe5f1ecf7dbc12 Mon Sep 17 00:00:00 2001 From: Mark O'Sullivan <6950843+MarkOSullivan94@users.noreply.github.com> Date: Wed, 8 Apr 2026 16:09:07 +0200 Subject: [PATCH 14/27] docs: added link to issues within beta notice --- README.md | 2 +- packages/clerk_auth/README.md | 2 +- packages/clerk_flutter/README.md | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index bfd57bb5..ef8ab4bf 100644 --- a/README.md +++ b/README.md @@ -23,7 +23,7 @@ **Clerk helps developers build user management. We provide streamlined user experiences for your users to sign up, sign in, and manage their profiles.** > ### ⚠️ Beta Notice -> These SDKs are currently in Beta. Breaking changes should be expected until the first stable release (1.0.0). +> These SDKs are currently in Beta. Breaking changes should be expected until the first stable release (1.0.0). Please [file any issues you encounter](https://github.com/clerk/clerk-sdk-flutter/issues).

    diff --git a/packages/clerk_auth/README.md b/packages/clerk_auth/README.md index 28c398dc..a9db2360 100644 --- a/packages/clerk_auth/README.md +++ b/packages/clerk_auth/README.md @@ -11,7 +11,7 @@ [![twitter](https://img.shields.io/twitter/follow/Clerk?style=social)](https://twitter.com/intent/follow?screen_name=Clerk) > ### ⚠️ The Clerk Dart SDK is in Beta ⚠️ -> ❗️ Breaking changes should be expected until the first stable release (1.0.0) ❗️ +> ❗️ Breaking changes should be expected until the first stable release (1.0.0). Please [file any issues you encounter](https://github.com/clerk/clerk-sdk-flutter/issues). ❗️ **Clerk helps developers build user management. We provide streamlined user experiences for your users to sign up, sign in, and manage their profile from your Dart code.** diff --git a/packages/clerk_flutter/README.md b/packages/clerk_flutter/README.md index 001bfe13..a261b131 100644 --- a/packages/clerk_flutter/README.md +++ b/packages/clerk_flutter/README.md @@ -11,7 +11,7 @@ [![twitter](https://img.shields.io/twitter/follow/Clerk?style=social)](https://twitter.com/intent/follow?screen_name=Clerk) > ### ⚠️ The Clerk Flutter SDK is in Beta ⚠️ -> ❗️ Breaking changes should be expected until the first stable release (1.0.0) ❗️ +> ❗️ Breaking changes should be expected until the first stable release (1.0.0). Please [file any issues you encounter](https://github.com/clerk/clerk-sdk-flutter/issues). ❗️ **Clerk helps developers build user management. We provide streamlined user experiences for your users to sign up, sign in, and manage their profile from your Flutter code.** From 59b434283831177b836d38fbce6aecd868986c7d Mon Sep 17 00:00:00 2001 From: Mark O'Sullivan <6950843+MarkOSullivan94@users.noreply.github.com> Date: Wed, 8 Apr 2026 19:16:21 +0200 Subject: [PATCH 15/27] docs(clerk_flutter): matching clerk doc website structure --- packages/clerk_flutter/doc/clerk-ui/authentication-views.md | 0 packages/clerk_flutter/doc/clerk-ui/customization.md | 0 packages/clerk_flutter/doc/clerk-ui/overview.md | 0 packages/clerk_flutter/doc/clerk-ui/user-views.md | 0 packages/clerk_flutter/doc/getting-started/clerk-auth.md | 0 packages/clerk_flutter/doc/getting-started/configure-the-sdk.md | 0 packages/clerk_flutter/doc/getting-started/install-the-sdk.md | 0 packages/clerk_flutter/doc/getting-started/quickstart.md | 0 packages/clerk_flutter/doc/guides/deploy-to-production.md | 0 packages/clerk_flutter/doc/guides/sign-in-with-apple.md | 0 packages/clerk_flutter/doc/guides/sign-in-with-google.md | 0 packages/clerk_flutter/doc/sdk-reference/authentication-flows.md | 0 .../clerk_flutter/doc/sdk-reference/organization-management.md | 0 packages/clerk_flutter/doc/sdk-reference/user-management.md | 0 14 files changed, 0 insertions(+), 0 deletions(-) create mode 100644 packages/clerk_flutter/doc/clerk-ui/authentication-views.md create mode 100644 packages/clerk_flutter/doc/clerk-ui/customization.md create mode 100644 packages/clerk_flutter/doc/clerk-ui/overview.md create mode 100644 packages/clerk_flutter/doc/clerk-ui/user-views.md create mode 100644 packages/clerk_flutter/doc/getting-started/clerk-auth.md create mode 100644 packages/clerk_flutter/doc/getting-started/configure-the-sdk.md create mode 100644 packages/clerk_flutter/doc/getting-started/install-the-sdk.md create mode 100644 packages/clerk_flutter/doc/getting-started/quickstart.md create mode 100644 packages/clerk_flutter/doc/guides/deploy-to-production.md create mode 100644 packages/clerk_flutter/doc/guides/sign-in-with-apple.md create mode 100644 packages/clerk_flutter/doc/guides/sign-in-with-google.md create mode 100644 packages/clerk_flutter/doc/sdk-reference/authentication-flows.md create mode 100644 packages/clerk_flutter/doc/sdk-reference/organization-management.md create mode 100644 packages/clerk_flutter/doc/sdk-reference/user-management.md diff --git a/packages/clerk_flutter/doc/clerk-ui/authentication-views.md b/packages/clerk_flutter/doc/clerk-ui/authentication-views.md new file mode 100644 index 00000000..e69de29b diff --git a/packages/clerk_flutter/doc/clerk-ui/customization.md b/packages/clerk_flutter/doc/clerk-ui/customization.md new file mode 100644 index 00000000..e69de29b diff --git a/packages/clerk_flutter/doc/clerk-ui/overview.md b/packages/clerk_flutter/doc/clerk-ui/overview.md new file mode 100644 index 00000000..e69de29b diff --git a/packages/clerk_flutter/doc/clerk-ui/user-views.md b/packages/clerk_flutter/doc/clerk-ui/user-views.md new file mode 100644 index 00000000..e69de29b diff --git a/packages/clerk_flutter/doc/getting-started/clerk-auth.md b/packages/clerk_flutter/doc/getting-started/clerk-auth.md new file mode 100644 index 00000000..e69de29b diff --git a/packages/clerk_flutter/doc/getting-started/configure-the-sdk.md b/packages/clerk_flutter/doc/getting-started/configure-the-sdk.md new file mode 100644 index 00000000..e69de29b diff --git a/packages/clerk_flutter/doc/getting-started/install-the-sdk.md b/packages/clerk_flutter/doc/getting-started/install-the-sdk.md new file mode 100644 index 00000000..e69de29b diff --git a/packages/clerk_flutter/doc/getting-started/quickstart.md b/packages/clerk_flutter/doc/getting-started/quickstart.md new file mode 100644 index 00000000..e69de29b diff --git a/packages/clerk_flutter/doc/guides/deploy-to-production.md b/packages/clerk_flutter/doc/guides/deploy-to-production.md new file mode 100644 index 00000000..e69de29b diff --git a/packages/clerk_flutter/doc/guides/sign-in-with-apple.md b/packages/clerk_flutter/doc/guides/sign-in-with-apple.md new file mode 100644 index 00000000..e69de29b diff --git a/packages/clerk_flutter/doc/guides/sign-in-with-google.md b/packages/clerk_flutter/doc/guides/sign-in-with-google.md new file mode 100644 index 00000000..e69de29b diff --git a/packages/clerk_flutter/doc/sdk-reference/authentication-flows.md b/packages/clerk_flutter/doc/sdk-reference/authentication-flows.md new file mode 100644 index 00000000..e69de29b diff --git a/packages/clerk_flutter/doc/sdk-reference/organization-management.md b/packages/clerk_flutter/doc/sdk-reference/organization-management.md new file mode 100644 index 00000000..e69de29b diff --git a/packages/clerk_flutter/doc/sdk-reference/user-management.md b/packages/clerk_flutter/doc/sdk-reference/user-management.md new file mode 100644 index 00000000..e69de29b From 304ea8db39498662a70644871b638ba5944d0f16 Mon Sep 17 00:00:00 2001 From: Mark O'Sullivan <6950843+MarkOSullivan94@users.noreply.github.com> Date: Mon, 13 Apr 2026 17:39:08 +0200 Subject: [PATCH 16/27] docs(clerk_flutter): quickstart --- ...tion-views.md => authentication-views.mdx} | 0 .../{customization.md => customization.mdx} | 0 .../clerk-ui/{overview.md => overview.mdx} | 0 .../{user-views.md => user-views.mdx} | 0 .../{clerk-auth.md => clerk-auth.mdx} | 0 ...igure-the-sdk.md => configure-the-sdk.mdx} | 0 ...install-the-sdk.md => install-the-sdk.mdx} | 0 .../doc/getting-started/quickstart.mdx | 90 +++++++++++++++++++ .../deploy-to-production.mdx} | 0 ...o-production.md => sign-in-with-apple.mdx} | 0 ...-with-apple.md => sign-in-with-google.mdx} | 0 .../authentication-flows.mdx} | 0 ...n-flows.md => organization-management.mdx} | 0 .../doc/sdk-reference/user-management.md | 0 ...tion-management.md => user-management.mdx} | 0 15 files changed, 90 insertions(+) rename packages/clerk_flutter/doc/clerk-ui/{authentication-views.md => authentication-views.mdx} (100%) rename packages/clerk_flutter/doc/clerk-ui/{customization.md => customization.mdx} (100%) rename packages/clerk_flutter/doc/clerk-ui/{overview.md => overview.mdx} (100%) rename packages/clerk_flutter/doc/clerk-ui/{user-views.md => user-views.mdx} (100%) rename packages/clerk_flutter/doc/getting-started/{clerk-auth.md => clerk-auth.mdx} (100%) rename packages/clerk_flutter/doc/getting-started/{configure-the-sdk.md => configure-the-sdk.mdx} (100%) rename packages/clerk_flutter/doc/getting-started/{install-the-sdk.md => install-the-sdk.mdx} (100%) create mode 100644 packages/clerk_flutter/doc/getting-started/quickstart.mdx rename packages/clerk_flutter/doc/{getting-started/quickstart.md => guides/deploy-to-production.mdx} (100%) rename packages/clerk_flutter/doc/guides/{deploy-to-production.md => sign-in-with-apple.mdx} (100%) rename packages/clerk_flutter/doc/guides/{sign-in-with-apple.md => sign-in-with-google.mdx} (100%) rename packages/clerk_flutter/doc/{guides/sign-in-with-google.md => sdk-reference/authentication-flows.mdx} (100%) rename packages/clerk_flutter/doc/sdk-reference/{authentication-flows.md => organization-management.mdx} (100%) delete mode 100644 packages/clerk_flutter/doc/sdk-reference/user-management.md rename packages/clerk_flutter/doc/sdk-reference/{organization-management.md => user-management.mdx} (100%) diff --git a/packages/clerk_flutter/doc/clerk-ui/authentication-views.md b/packages/clerk_flutter/doc/clerk-ui/authentication-views.mdx similarity index 100% rename from packages/clerk_flutter/doc/clerk-ui/authentication-views.md rename to packages/clerk_flutter/doc/clerk-ui/authentication-views.mdx diff --git a/packages/clerk_flutter/doc/clerk-ui/customization.md b/packages/clerk_flutter/doc/clerk-ui/customization.mdx similarity index 100% rename from packages/clerk_flutter/doc/clerk-ui/customization.md rename to packages/clerk_flutter/doc/clerk-ui/customization.mdx diff --git a/packages/clerk_flutter/doc/clerk-ui/overview.md b/packages/clerk_flutter/doc/clerk-ui/overview.mdx similarity index 100% rename from packages/clerk_flutter/doc/clerk-ui/overview.md rename to packages/clerk_flutter/doc/clerk-ui/overview.mdx diff --git a/packages/clerk_flutter/doc/clerk-ui/user-views.md b/packages/clerk_flutter/doc/clerk-ui/user-views.mdx similarity index 100% rename from packages/clerk_flutter/doc/clerk-ui/user-views.md rename to packages/clerk_flutter/doc/clerk-ui/user-views.mdx diff --git a/packages/clerk_flutter/doc/getting-started/clerk-auth.md b/packages/clerk_flutter/doc/getting-started/clerk-auth.mdx similarity index 100% rename from packages/clerk_flutter/doc/getting-started/clerk-auth.md rename to packages/clerk_flutter/doc/getting-started/clerk-auth.mdx diff --git a/packages/clerk_flutter/doc/getting-started/configure-the-sdk.md b/packages/clerk_flutter/doc/getting-started/configure-the-sdk.mdx similarity index 100% rename from packages/clerk_flutter/doc/getting-started/configure-the-sdk.md rename to packages/clerk_flutter/doc/getting-started/configure-the-sdk.mdx diff --git a/packages/clerk_flutter/doc/getting-started/install-the-sdk.md b/packages/clerk_flutter/doc/getting-started/install-the-sdk.mdx similarity index 100% rename from packages/clerk_flutter/doc/getting-started/install-the-sdk.md rename to packages/clerk_flutter/doc/getting-started/install-the-sdk.mdx diff --git a/packages/clerk_flutter/doc/getting-started/quickstart.mdx b/packages/clerk_flutter/doc/getting-started/quickstart.mdx new file mode 100644 index 00000000..20d9caa1 --- /dev/null +++ b/packages/clerk_flutter/doc/getting-started/quickstart.mdx @@ -0,0 +1,90 @@ +--- +title: Flutter Quickstart +description: Add authentication and user management to your Flutter app with Clerk. +sdk: flutter +--- + + + + + ## Enable Native API + + + + ## Create a new Flutter app + + 1. If you don't already have a Flutter app, first follow the [Create a new Flutter + app](https://docs.flutter.dev/reference/create-new-app) + guide in the Flutter Docs. This will guide you step-by-step on how to create a Flutter app in your preferred + development environment. + + 1. Add `clerk_flutter` and `clerk_auth` to your Flutter app. + + ```sh + flutter pub add clerk_flutter clerk_auth + ``` + + 1. Install the Clerk packages + + ```sh + flutter pub get + ``` + + 1. The Clerk packages can now be imported into your Flutter app. + + ## Configure the `AndroidManifest.xml` file + + Note: This is only necessary if you want to make your Flutter app run on Android devices. If you will not release + an Android build of your Flutter app then ignore this section. + + In `android/app/src/main/AndroidManifest.xml`, inside the root ` + ` tag, add the following line to enable + internet permission when running your Flutter app on Android. + + ```xml + + + ... + + ``` + + ## Use Clerk's prebuilt widgets + + Clerk provides prebuilt Flutter widgets that handle authentication flows and user management, removing the need + to + build custom widgets. + + `ClerkAuth` is the root control widget that initializes the Clerk authentication system for your Flutter app. It + must be placed at the root of your widget tree (or near it) to provide authentication state to all + descendant widgets. It manages the ClerkAuthState object and makes it available throughout your app via + InheritedWidget. + + To see how to use `ClerkAuth` check out the [clerk_flutter example + app](https://github.com/clerk/clerk-sdk-flutter/blob/main/packages/clerk_flutter/example/lib/main.dart#L110). + + ## Run your application + + Now run your Flutter app. + + ## Create your first user + + Once the app launches successfully, the `ClerkAuth` widget will appear, allowing you to sign up or + sign in to create your first user. + + + diff --git a/packages/clerk_flutter/doc/getting-started/quickstart.md b/packages/clerk_flutter/doc/guides/deploy-to-production.mdx similarity index 100% rename from packages/clerk_flutter/doc/getting-started/quickstart.md rename to packages/clerk_flutter/doc/guides/deploy-to-production.mdx diff --git a/packages/clerk_flutter/doc/guides/deploy-to-production.md b/packages/clerk_flutter/doc/guides/sign-in-with-apple.mdx similarity index 100% rename from packages/clerk_flutter/doc/guides/deploy-to-production.md rename to packages/clerk_flutter/doc/guides/sign-in-with-apple.mdx diff --git a/packages/clerk_flutter/doc/guides/sign-in-with-apple.md b/packages/clerk_flutter/doc/guides/sign-in-with-google.mdx similarity index 100% rename from packages/clerk_flutter/doc/guides/sign-in-with-apple.md rename to packages/clerk_flutter/doc/guides/sign-in-with-google.mdx diff --git a/packages/clerk_flutter/doc/guides/sign-in-with-google.md b/packages/clerk_flutter/doc/sdk-reference/authentication-flows.mdx similarity index 100% rename from packages/clerk_flutter/doc/guides/sign-in-with-google.md rename to packages/clerk_flutter/doc/sdk-reference/authentication-flows.mdx diff --git a/packages/clerk_flutter/doc/sdk-reference/authentication-flows.md b/packages/clerk_flutter/doc/sdk-reference/organization-management.mdx similarity index 100% rename from packages/clerk_flutter/doc/sdk-reference/authentication-flows.md rename to packages/clerk_flutter/doc/sdk-reference/organization-management.mdx diff --git a/packages/clerk_flutter/doc/sdk-reference/user-management.md b/packages/clerk_flutter/doc/sdk-reference/user-management.md deleted file mode 100644 index e69de29b..00000000 diff --git a/packages/clerk_flutter/doc/sdk-reference/organization-management.md b/packages/clerk_flutter/doc/sdk-reference/user-management.mdx similarity index 100% rename from packages/clerk_flutter/doc/sdk-reference/organization-management.md rename to packages/clerk_flutter/doc/sdk-reference/user-management.mdx From 0d892831747b75b518a676fde2ba6e7cb7e4e3b5 Mon Sep 17 00:00:00 2001 From: Mark O'Sullivan <6950843+MarkOSullivan94@users.noreply.github.com> Date: Mon, 13 Apr 2026 18:06:12 +0200 Subject: [PATCH 17/27] docs(clerk_flutter): install the sdk --- .../doc/getting-started/install-the-sdk.mdx | 37 +++++++++++++++++++ 1 file changed, 37 insertions(+) diff --git a/packages/clerk_flutter/doc/getting-started/install-the-sdk.mdx b/packages/clerk_flutter/doc/getting-started/install-the-sdk.mdx index e69de29b..5610fd9b 100644 --- a/packages/clerk_flutter/doc/getting-started/install-the-sdk.mdx +++ b/packages/clerk_flutter/doc/getting-started/install-the-sdk.mdx @@ -0,0 +1,37 @@ +--- +title: Install the SDK +description: Learn how to install and configure the Clerk Flutter SDK in your Flutter app. +sdk: flutter +--- + + + ## Install the packages + + There are two ways to add the Clerk packages to your Flutter app. + + **Using the Flutter CLI (recommended)** + + ```sh + flutter pub add clerk_flutter clerk_auth + ``` + + **Manually via `pubspec.yaml`** + + In your `pubspec.yaml`, add `clerk_flutter` and `clerk_auth` under `dependencies`, then run `flutter pub get`. + + ```yaml + dependencies: + clerk_flutter: ^0.0.14-beta + clerk_auth: ^0.0.14-beta + ``` + + ## Import the packages + + You can now import the Clerk packages into your Flutter app. + + ```dart + import 'package:clerk_flutter/clerk_flutter.dart'; + import 'package:clerk_auth/clerk_auth.dart'; + ``` + + From eab2249b0b987e1a1812f1a3719c790589f5e635 Mon Sep 17 00:00:00 2001 From: Mark O'Sullivan <6950843+MarkOSullivan94@users.noreply.github.com> Date: Tue, 14 Apr 2026 15:35:21 +0200 Subject: [PATCH 18/27] docs(clerk_flutter): configure the sdk --- .../doc/getting-started/configure-the-sdk.mdx | 274 ++++++++++++++++++ 1 file changed, 274 insertions(+) diff --git a/packages/clerk_flutter/doc/getting-started/configure-the-sdk.mdx b/packages/clerk_flutter/doc/getting-started/configure-the-sdk.mdx index e69de29b..24e05939 100644 --- a/packages/clerk_flutter/doc/getting-started/configure-the-sdk.mdx +++ b/packages/clerk_flutter/doc/getting-started/configure-the-sdk.mdx @@ -0,0 +1,274 @@ +--- +title: Configure the SDK +description: Configure the Clerk Flutter SDK with your publishable key and options. +sdk: flutter +--- + +## Configuration options + +`ClerkAuthConfig` is the primary way to configure the Clerk Flutter SDK. It extends the core `AuthConfig` from +`clerk_auth` and adds Flutter-specific options. Pass it to `ClerkAuth` either directly or via +`ClerkAuth.materialAppBuilder `. + +You can use it to configure: + +- **Authentication** — your publishable key, test mode, and session token behaviour +- **Networking** — custom HTTP service, connection timeouts, and retry options +- **Persistence** — custom persistor for storing session state +- **Loading UI** — a widget shown while the SDK initialises +- **Localizations** — translated strings for each supported locale +- **OAuth / SSO** — redirect URI generation and deep link handling +- **File caching** — caching of remote assets such as avatars and logos +- **Telemetry** — endpoint, send period, and opt-out + +--- + +## Class definition + +```dart +class ClerkAuthConfig extends clerk.AuthConfig { + ClerkAuthConfig({ + // From AuthConfig (required) + required String publishableKey, + + // From AuthConfig (optional) + bool sessionTokenPolling, + bool isTestMode, + String? telemetryEndpoint, + Duration? telemetryPeriod, + Duration? clientRefreshPeriod, + HttpService? httpService, + Duration? httpConnectionTimeout, + RetryOptions retryOptions, + String? defaultSessionTokenTemplate, + Persistor? persistor, + ClerkSdkFlags flags, + + // Flutter-specific + Widget? loading, + ClerkRedirectUriGenerator? redirectionGenerator, + Stream? deepLinkStream, + LaunchMode defaultLaunchMode, + bool supportsHardwareSecurityKeys, + ClerkFileCache? fileCache, + ClerkSdkLocalizationsCollection? localizations, + ClerkSdkLocalizations? fallbackLocalization, + ClerkSdkGrammarCollection? grammars, + ClerkSdkGrammar? fallbackGrammar, + }); +} +``` + +--- + +## Parameters + +### `ClerkAuthConfig` + + + - `publishableKey` + - `String` + + Your Clerk publishable key from the Clerk Dashboard. + + --- + + - `loading` + - `Widget?` + + Widget displayed while the SDK is initialising. Pass `null` to defer the first frame entirely (the OS splash screen + stays visible) rather than showing a spinner. + + Default: A built-in loading spinner. + + --- + + - `sessionTokenPolling` + - `bool` + + Whether the SDK should regularly poll for a refreshed session token in the background. Disable this if you want to + manage token refresh yourself. + + Default: `true`. + + --- + + - `defaultSessionTokenTemplate` + - `String?` + + The name of a JWT template to use by default when fetching session tokens. When `null`, the default Clerk session + token is used. + + Default: `null`. + + --- + + - `persistor` + - `Persistor?` + + Override the default persistor used to store session state between app launches. Implement `clerk.Persistor` to + provide your own storage backend. + + Default: A file-based caching persistor. + + --- + + - `httpService` + - `HttpService?` + + Override the HTTP client used for all Clerk API calls. Useful for injecting mock clients in tests or adding custom + interceptors. + + Default: `DefaultHttpService`. + + --- + + - `httpConnectionTimeout` + - `Duration?` + + How long the SDK waits for an HTTP connection before timing out during a connectivity check. + + Default: `Duration(milliseconds: 500)`. + + --- + + - `retryOptions` + - `RetryOptions` + + Controls retry behaviour for failed API requests — number of attempts, delays, and back-off strategy. + + Default: `RetryOptions()` (from the `retry` package). + + --- + + - `clientRefreshPeriod` + - `Duration?` + + How often the SDK polls to refresh the client object. Set to `Duration.zero` to disable polling entirely. + + Default: `Duration(milliseconds: 9700)` (~10 s). + + --- + + - `telemetryEndpoint` + - `String?` + + The URL telemetry data is sent to. + + Default: `'https://clerk-telemetry.com/v1/event'`. + + --- + + - `telemetryPeriod` + - `Duration?` + + How often telemetry data is sent. Set to `Duration.zero` to disable telemetry sending. + + Default: `Duration(milliseconds: 29300)` (~30 s). + + --- + + - `isTestMode` + - `bool?` + + Enables test mode. Usually inferred automatically from the publishable key prefix. + + Default: `false`. + + --- + + - `flags` + - `ClerkSdkFlags` + + Advanced feature flags that affect SDK behaviour. Not needed for typical usage. + + Default: `ClerkSdkFlags()`. + + --- + + - `redirectionGenerator` + - `ClerkRedirectUriGenerator?` + + A function (`Uri? Function(BuildContext, clerk.Strategy)`) that returns the redirect URI the SDK should use after an + OAuth / SSO flow completes. Called with the current `BuildContext` and the strategy being used. + + Default: `null`. + + --- + + - `deepLinkStream` + - `Stream?` + + A stream of incoming deep links. The SDK listens to this stream to detect OAuth / SSO callbacks directed back into + the app. + + Default: `null`. + + --- + + - `defaultLaunchMode` + - `LaunchMode` + + The `url_launcher` launch mode used when opening OAuth / SSO URLs. + + Default: `LaunchMode.externalApplication`. + + --- + + - `supportsHardwareSecurityKeys` + - `bool` + + Whether the device supports hardware security keys (passkeys). Set to `false` when running on an iOS simulator, + which does not support the platform API. + + Default: `true`. + + --- + + - `fileCache` + - `ClerkFileCache?` + + Override the cache used to fetch and store remote assets such as avatars and organisation logos. + + Default: A caching implementation backed by the app documents directory. + + --- + + - `localizations` + - `ClerkSdkLocalizationsCollection?` + + A map (`Map`) of IETF language tags to localizations objects. The SDK picks the best + match for the device locale, falling back to `fallbackLocalization`. + + Default: `{'en': ClerkSdkLocalizationsEn()}`. + + --- + + - `fallbackLocalization` + - `ClerkSdkLocalizations?` + + The localization used when no entry in `localizations` matches the device locale. + + Default: `ClerkSdkLocalizationsEn()`. + + --- + + - `grammars` + - `ClerkSdkGrammarCollection?` + + A map (`Map`) of language codes to grammar helpers used for locale-aware string formatting + (e.g. pluralisation). + + Default: `{'en': ClerkSdkGrammarEn()}`. + + --- + + - `fallbackGrammar` + - `ClerkSdkGrammar?` + + The grammar used when no entry in `grammars` matches the device locale. + + Default: `ClerkSdkGrammarEn()`. + + --- + From 8f86a158dcf343304a6abbb7fec0f9c13fb7be57 Mon Sep 17 00:00:00 2001 From: Mark O'Sullivan <6950843+MarkOSullivan94@users.noreply.github.com> Date: Tue, 14 Apr 2026 16:35:17 +0200 Subject: [PATCH 19/27] docs(clerk_flutter): clerk auth --- .../doc/getting-started/clerk-auth.mdx | 62 +++++++++++++++++++ 1 file changed, 62 insertions(+) diff --git a/packages/clerk_flutter/doc/getting-started/clerk-auth.mdx b/packages/clerk_flutter/doc/getting-started/clerk-auth.mdx index e69de29b..97f8b553 100644 --- a/packages/clerk_flutter/doc/getting-started/clerk-auth.mdx +++ b/packages/clerk_flutter/doc/getting-started/clerk-auth.mdx @@ -0,0 +1,62 @@ +--- +title: ClerkAuth +description: Access sign-in methods and user state with ClerkAuth in your Flutter app +sdk: flutter +--- + +`ClerkAuth` is the root control widget that initialises the Clerk auth system and makes auth state available to the +widget tree beneath it. All other Clerk widgets and helpers depend on a `ClerkAuth` ancestor being present. + +--- + +## Access the ClerkAuthState state + +Call `ClerkAuth.of(context)` anywhere below a `ClerkAuth` widget to get the `ClerkAuthState` object. By default this +registers a rebuild dependency — the widget rebuilds whenever auth state changes. Pass `listen: false` to read the +state without subscribing to changes (useful outside of `build`). + +```dart +final auth = ClerkAuth.of(context); +``` + +Sign-in methods are available directly on the `ClerkAuthState` object returned by `ClerkAuth.of(context)`. + +```dart +final auth = ClerkAuth.of(context, listen: false); + +// Email / password sign-in +await auth.attemptSignIn( + strategy: Strategy.password, + identifier: 'user@example.com', + password: 'secret', +); + +// OAuth sign-in (opens the provider in a browser) +await auth.oauthSignIn(strategy: Strategy.oauth_google); + +// Sign out the current user +await auth.signOut(); + +// Sign out of a specific session +await auth.signOutOf(session); +``` + +--- + +## Access user state + +Use the static convenience methods or read directly from the auth state object. + +```dart +// Static helpers (listen = true by default) +final user = ClerkAuth.userOf(context); // clerk.User? +final session = ClerkAuth.sessionOf(context); // clerk.Session? + +// Or via the auth state object +final auth = ClerkAuth.of(context); +final user = auth.user; // clerk.User? +final session = auth.session; // clerk.Session? +final isSigningIn = auth.isSigningIn; // bool — true while a sign-in is in progress +``` + +--- From 3177ef2e925064ce9cce0523ca0ae384d1f59397 Mon Sep 17 00:00:00 2001 From: Mark O'Sullivan <6950843+MarkOSullivan94@users.noreply.github.com> Date: Tue, 14 Apr 2026 19:52:15 +0200 Subject: [PATCH 20/27] docs(clerk_flutter): authentication flow --- .../sdk-reference/authentication-flows.mdx | 410 ++++++++++++++++++ 1 file changed, 410 insertions(+) diff --git a/packages/clerk_flutter/doc/sdk-reference/authentication-flows.mdx b/packages/clerk_flutter/doc/sdk-reference/authentication-flows.mdx index e69de29b..6f87a739 100644 --- a/packages/clerk_flutter/doc/sdk-reference/authentication-flows.mdx +++ b/packages/clerk_flutter/doc/sdk-reference/authentication-flows.mdx @@ -0,0 +1,410 @@ +--- +title: Authentication flows +description: Learn how to handle sign-in, sign-up, MFA, password reset, and session management with the Clerk Flutter SDK. +sdk: flutter +--- + +The Flutter SDK provides pre-built widgets that handle authentication flows automatically, as well as a lower-level API for building custom UI. + +## Pre-built authentication UI + +The `ClerkAuthentication` widget handles sign-in and sign-up flows, including OAuth, MFA, and password reset, with no additional code required: + +```dart +ClerkAuthentication() +``` + +Place it anywhere in your widget tree when the user is signed out. Wrap your app with `ClerkAuth` to provide the auth state: + +```dart +ClerkAuth( + publishableKey: 'pk_...', + child: ClerkAuthBuilder( + signedInBuilder: (context, auth) => const HomeScreen(), + signedOutBuilder: (context, auth) => const ClerkAuthentication(), + ), +) +``` + +Other pre-built widgets: + +- `ClerkSignInPanel` — sign-in form only +- `ClerkSignUpPanel` — sign-up form only +- `ClerkOAuthPanel` — OAuth provider buttons only +- `ClerkForgottenPasswordPanel` — password reset flow + +--- + +For custom UI, access the auth state with `ClerkAuth.of(context)` and use the methods below. + +## Sign in + +The `attemptSignIn` method is progressive — call it repeatedly with updated parameters as the user moves through each step of the flow. + +### Sign in with password + +Sign in with an identifier (email, phone, or username) and password: + +```dart +final auth = ClerkAuth.of(context); + +await auth.attemptSignIn( + strategy: Strategy.password, + identifier: 'user@email.com', + password: 'secretpassword', +); +``` + +### Sign in with OTP (email) + +Send an OTP to the user's email, then verify the code: + +```dart +final auth = ClerkAuth.of(context); + +// Send the code +await auth.attemptSignIn( + strategy: Strategy.emailCode, + identifier: 'user@email.com', +); + +// Verify the code +await auth.attemptSignIn( + strategy: Strategy.emailCode, + code: '123456', +); +``` + +### Sign in with OTP (phone) + +Send an OTP to the user's phone number, then verify the code: + +```dart +final auth = ClerkAuth.of(context); + +// Send the code +await auth.attemptSignIn( + strategy: Strategy.phoneCode, + identifier: '+1234567890', +); + +// Verify the code +await auth.attemptSignIn( + strategy: Strategy.phoneCode, + code: '123456', +); +``` + +### Sign in with email link + +Send a magic link to the user's email. The link will be polled automatically: + +```dart +final auth = ClerkAuth.of(context); + +await auth.attemptSignIn( + strategy: Strategy.emailLink, + identifier: 'user@email.com', + redirectUrl: 'yourapp://clerk/callback', +); +``` + +### Sign in with OAuth + +Sign in using an OAuth provider (e.g., Google, GitHub): + +```dart +final auth = ClerkAuth.of(context); + +await auth.ssoSignIn(context, Strategy.oauthGoogle); +``` + +The SDK handles the OAuth redirect flow in-app using a WebView by default. To handle the redirect externally (e.g., via a deep link), configure a `redirectionGenerator` in `ClerkAuthConfig`. + +### Sign in with ID token + +Sign in with an ID token from an identity provider (e.g., Apple): + +```dart +final auth = ClerkAuth.of(context); + +await auth.idTokenSignIn( + provider: IdTokenProvider.apple, + token: credential.identityToken, +); + +// If the user doesn't exist, transfer to sign-up +if (auth.signIn?.isTransferable == true) { + await auth.transfer(); +} +``` + +### Sign in with passkey + +Sign in using a passkey: + +```dart +final auth = ClerkAuth.of(context); + +await auth.attemptSignIn(strategy: Strategy.passkey); +``` + +### Sign in with Enterprise SSO + +Sign in using Enterprise SSO: + +```dart +final auth = ClerkAuth.of(context); + +await auth.ssoSignIn(context, Strategy.enterpriseSSO); +``` + +### Multi-factor authentication + +After the first factor is verified, if MFA is required, call `attemptSignIn` again with the second factor strategy. + +#### MFA with phone code + +```dart +// Send the SMS code +await auth.attemptSignIn(strategy: Strategy.phoneCode); + +// Verify the code +await auth.attemptSignIn( + strategy: Strategy.phoneCode, + code: '123456', +); +``` + +#### MFA with email code + +```dart +// Send the email code +await auth.attemptSignIn(strategy: Strategy.emailCode); + +// Verify the code +await auth.attemptSignIn( + strategy: Strategy.emailCode, + code: '123456', +); +``` + +#### MFA with TOTP (authenticator app) + +```dart +await auth.attemptSignIn( + strategy: Strategy.totp, + code: '123456', +); +``` + +#### MFA with backup code + +```dart +await auth.attemptSignIn( + strategy: Strategy.backupCode, + code: 'backup-code', +); +``` + +#### Resend a code + +```dart +await auth.resendCode(Strategy.phoneCode); +``` + +### Password reset + +#### Password reset with email + +```dart +final auth = ClerkAuth.of(context); + +// Initiate reset and send code +await auth.initiatePasswordReset( + identifier: 'user@email.com', + strategy: Strategy.resetPasswordEmailCode, +); + +// Verify the code and set the new password +await auth.attemptSignIn( + strategy: Strategy.resetPasswordEmailCode, + code: '123456', + password: 'newpassword', +); +``` + +#### Password reset with phone + +```dart +final auth = ClerkAuth.of(context); + +// Initiate reset and send code +await auth.initiatePasswordReset( + identifier: '+1234567890', + strategy: Strategy.resetPasswordPhoneCode, +); + +// Verify the code and set the new password +await auth.attemptSignIn( + strategy: Strategy.resetPasswordPhoneCode, + code: '123456', + password: 'newpassword', +); +``` + +## Sign up + +The `attemptSignUp` method is progressive — call it repeatedly with updated parameters until the user is fully signed up. + +### Create a new sign-up + +```dart +final auth = ClerkAuth.of(context); + +await auth.attemptSignUp( + emailAddress: 'newuser@email.com', + password: 'secretpassword', + firstName: 'John', + lastName: 'Doe', + username: 'johndoe', + phoneNumber: '+1234567890', +); +``` + +### Verify email via OTP + +After submitting a sign-up with an email address, verify it with a code: + +```dart +// Send the verification code (triggered automatically by attemptSignUp, +// or call again with the emailCode strategy) +await auth.attemptSignUp(strategy: Strategy.emailCode); + +// Verify the code +await auth.attemptSignUp( + strategy: Strategy.emailCode, + code: '123456', +); +``` + +### Verify phone via OTP + +```dart +// Send the verification code +await auth.attemptSignUp(strategy: Strategy.phoneCode); + +// Verify the code +await auth.attemptSignUp( + strategy: Strategy.phoneCode, + code: '123456', +); +``` + +### Sign up with OAuth + +```dart +final auth = ClerkAuth.of(context); + +await auth.ssoSignUp(context, Strategy.oauthGoogle); +``` + +### Sign up with ID token + +```dart +final auth = ClerkAuth.of(context); + +await auth.idTokenSignUp( + provider: IdTokenProvider.apple, + idToken: credential.identityToken!, + firstName: credential.givenName, + lastName: credential.familyName, +); + +// If the user already exists, transfer to sign-in +if (auth.signUp?.isTransferable == true) { + await auth.transfer(); +} +``` + +### Sign up with Enterprise SSO + +```dart +final auth = ClerkAuth.of(context); + +await auth.ssoSignUp(context, Strategy.enterpriseSSO); +``` + +## Current sign-in/sign-up + +Access the in-progress sign-in or sign-up for multi-step flows. + +### Current sign-in + +```dart +final signIn = ClerkAuth.of(context).signIn; +``` + +### Current sign-up + +```dart +final signUp = ClerkAuth.of(context).signUp; +``` + +## Session management + +### Sessions + +Retrieve all sessions for the current client: + +```dart +final sessions = ClerkAuth.of(context).client.sessions; +``` + +### Get session token + +Retrieve the current session token to authenticate requests to your backend: + +```dart +final auth = ClerkAuth.of(context); +final tokenObj = await auth.sessionToken(); +final token = tokenObj.jwt; +``` + +You can also listen to the `sessionTokenStream` for token renewals: + +```dart +auth.sessionTokenStream.listen((token) { + // Use token.jwt to authenticate requests +}); +``` + +### Set active session + +Switch to a different session: + +```dart +final auth = ClerkAuth.of(context); +await auth.activate(session); +``` + +### Set active organization + +Set the active organization for the current session: + +```dart +final auth = ClerkAuth.of(context); +await auth.setActiveOrganization(organization); +``` + +### Sign out + +```dart +final auth = ClerkAuth.of(context); + +// Sign out from all sessions +await auth.signOut(); + +// Sign out from a specific session +await auth.signOutOf(session); +``` From 7dafaf90c492198191db71637c1f0f6a1025c401 Mon Sep 17 00:00:00 2001 From: Mark O'Sullivan <6950843+MarkOSullivan94@users.noreply.github.com> Date: Wed, 15 Apr 2026 17:40:12 +0200 Subject: [PATCH 21/27] docs(clerk_flutter): user management --- .../doc/sdk-reference/user-management.mdx | 207 ++++++++++++++++++ 1 file changed, 207 insertions(+) diff --git a/packages/clerk_flutter/doc/sdk-reference/user-management.mdx b/packages/clerk_flutter/doc/sdk-reference/user-management.mdx index e69de29b..4997aca2 100644 --- a/packages/clerk_flutter/doc/sdk-reference/user-management.mdx +++ b/packages/clerk_flutter/doc/sdk-reference/user-management.mdx @@ -0,0 +1,207 @@ +--- +title: User management +description: Manage the signed-in user with the Clerk Flutter SDK. +sdk: flutter +--- + +Use `ClerkAuthState` methods to manage the current user's account. Access the auth state with `ClerkAuth.of(context)` and the current user with `ClerkAuth.of(context).user`. + +## Access the current user + +```dart +final auth = ClerkAuth.of(context); +final user = auth.user; // User? (null if not signed in) +``` + +--- + +## Update user + +Update the user's profile fields: + +```dart +final auth = ClerkAuth.of(context); + +await auth.updateUser( + username: 'janedoe', + firstName: 'Jane', + lastName: 'Doe', +); +``` + +### Set profile image + +```dart +final auth = ClerkAuth.of(context); + +await auth.updateUserImage(imageFile); // imageFile is a dart:io File +``` + +### Delete profile image + +```dart +final auth = ClerkAuth.of(context); + +await auth.deleteUserImage(); +``` + +--- + +## Email addresses + +### Add email address + +```dart +final auth = ClerkAuth.of(context); + +await auth.addIdentifyingData( + 'jane@example.com', + IdentifierType.emailAddress, +); +``` + +### Verify email address + +After adding an email address, verify it with the code sent to the user: + +```dart +final auth = ClerkAuth.of(context); + +// Get the unverified email from the user +final email = auth.user?.emailAddresses + ?.firstWhere((e) => e.identifier == 'jane@example.com'); + +// Verify the code +await auth.verifyIdentifyingData(email!, '123456'); +``` + +### Remove email address + +```dart +final auth = ClerkAuth.of(context); + +final email = auth.user?.emailAddresses + ?.firstWhere((e) => e.identifier == 'jane@example.com'); + +await auth.deleteIdentifyingData(email!); +``` + +--- + +## Phone numbers + +### Add phone number + +```dart +final auth = ClerkAuth.of(context); + +await auth.addIdentifyingData( + '+15551234567', + IdentifierType.phoneNumber, +); +``` + +### Verify phone number + +After adding a phone number, verify it with the code sent via SMS: + +```dart +final auth = ClerkAuth.of(context); + +final phone = auth.user?.phoneNumbers + ?.firstWhere((p) => p.identifier == '+15551234567'); + +await auth.verifyIdentifyingData(phone!, '123456'); +``` + +### Remove phone number + +```dart +final auth = ClerkAuth.of(context); + +final phone = auth.user?.phoneNumbers + ?.firstWhere((p) => p.identifier == '+15551234567'); + +await auth.deleteIdentifyingData(phone!); +``` + +--- + +## Passkeys + +### Create passkey + +```dart +final auth = ClerkAuth.of(context); + +await auth.createPasskey(); +``` + +### Remove passkey + +```dart +final auth = ClerkAuth.of(context); + +final passkey = auth.user?.passkeys?.first; + +await auth.deleteIdentifyingData(passkey!); +``` + +--- + +## External accounts + +### Remove external account + +```dart +final auth = ClerkAuth.of(context); + +final account = auth.user?.externalAccounts?.first; + +await auth.deleteExternalAccount(account: account!); +``` + +--- + +## Passwords + +### Update password + +```dart +final auth = ClerkAuth.of(context); + +await auth.updateUserPassword( + 'old-password', + 'new-password', +); +``` + +To keep other sessions active after the password change, pass `signOut: false`: + +```dart +await auth.updateUserPassword( + 'old-password', + 'new-password', + signOut: false, +); +``` + +### Delete password + +```dart +final auth = ClerkAuth.of(context); + +await auth.deleteUserPassword('current-password'); +``` + +--- + +## Delete user + +Permanently deletes the current user's account. Only available if your Clerk instance has self-deletion enabled. + +```dart +final auth = ClerkAuth.of(context); + +await auth.deleteUser(); +``` From 045b3c00db0978bee31313f5627ca6c4ad1f9ea8 Mon Sep 17 00:00:00 2001 From: Mark O'Sullivan <6950843+MarkOSullivan94@users.noreply.github.com> Date: Wed, 15 Apr 2026 18:05:19 +0200 Subject: [PATCH 22/27] docs(clerk_flutter): organization management --- .../sdk-reference/organization-management.mdx | 173 ++++++++++++++++++ 1 file changed, 173 insertions(+) diff --git a/packages/clerk_flutter/doc/sdk-reference/organization-management.mdx b/packages/clerk_flutter/doc/sdk-reference/organization-management.mdx index e69de29b..116197d2 100644 --- a/packages/clerk_flutter/doc/sdk-reference/organization-management.mdx +++ b/packages/clerk_flutter/doc/sdk-reference/organization-management.mdx @@ -0,0 +1,173 @@ +--- +title: Organization management +description: Manage Organizations, Memberships, invitations, and domains with the Clerk Flutter SDK. +sdk: flutter +--- + +Use Clerk's Organization resources to manage members, invitations, and Organization settings. Access the auth state with `ClerkAuth.of(context)`. + +## Organization + +`Organization` represents a Clerk Organization. You can access the active organization via `ClerkAuth.of(context).organization`, or from an `OrganizationMembership` via `membership.organization`. + +### Access the current organization + +```dart +final auth = ClerkAuth.of(context); +final organization = auth.organization; // Organization? (null if no active org) +``` + +### Set active organization + +```dart +final auth = ClerkAuth.of(context); + +await auth.setActiveOrganization(organization); +``` + +### Create organization + +```dart +final auth = ClerkAuth.of(context); + +await auth.createOrganization( + name: 'Acme', + slug: 'acme', +); +``` + +To set a logo at creation time, pass a `dart:io` `File`: + +```dart +await auth.createOrganization( + name: 'Acme', + slug: 'acme', + logo: logoFile, +); +``` + +### Update organization + +```dart +final auth = ClerkAuth.of(context); + +await auth.updateOrganization( + organization: organization, + name: 'Acme Corp', +); +``` + +To update the logo, pass a `dart:io` `File`: + +```dart +await auth.updateOrganization( + organization: organization, + logo: logoFile, +); +``` + +### Leave organization + +```dart +final auth = ClerkAuth.of(context); + +await auth.leaveOrganization(organization: organization); +``` + +--- + +## Organization membership + +`OrganizationMembership` represents the current user's membership within an Organization. You can access memberships from `ClerkAuth.of(context).user?.organizationMemberships`. + +### List memberships + +```dart +final auth = ClerkAuth.of(context); +final memberships = auth.user?.organizationMemberships ?? []; +``` + +### Current membership + +```dart +final auth = ClerkAuth.of(context); +final membership = auth.user?.currentOrganization; +final organization = membership?.organization; +``` + +### Check a permission + +Use `membership.hasPermission` to check if the current user has a specific permission within the organization: + +```dart +final membership = auth.user?.currentOrganization; + +if (membership?.hasPermission(Permission.membershipsManage) == true) { + // User can manage members +} +``` + +Predefined permissions: + +| Permission | Key | +|---|---| +| `Permission.profileManage` | `org:sys_profile:manage` | +| `Permission.profileDelete` | `org:sys_profile:delete` | +| `Permission.membershipsRead` | `org:sys_memberships:read` | +| `Permission.membershipsManage` | `org:sys_memberships:manage` | +| `Permission.domainsRead` | `org:sys_domains:read` | +| `Permission.domainsManage` | `org:sys_domains:manage` | + +--- + +## Organization invitations + +`OrganizationInvitation` represents an invitation sent to the current user to join an Organization. + +### List pending invitations + +```dart +final auth = ClerkAuth.of(context); +final invitations = await auth.fetchOrganizationInvitations(); +``` + +### Accept invitation + +```dart +final auth = ClerkAuth.of(context); + +await auth.acceptOrganizationInvitation(invitation); +``` + +--- + +## Organization domains + +`OrganizationDomain` represents a verified email domain for an Organization. + +### List domains + +```dart +final auth = ClerkAuth.of(context); +final domains = await auth.fetchOrganizationDomains(organization: organization); +``` + +### Create domain + +```dart +final auth = ClerkAuth.of(context); + +await auth.createDomain( + organization: organization, + name: 'acme.com', + mode: EnrollmentMode.automaticInvitation, +); +``` + +Available enrollment modes: + +| Mode | Description | +|---|---| +| `EnrollmentMode.automaticSuggestion` | Users with a matching email domain are suggested to join | +| `EnrollmentMode.automaticInvitation` | Users with a matching email domain are automatically invited | +| `EnrollmentMode.manualInvitation` | Users must be manually invited | From ab0d741fb427c9850b2080b0e9f1413905f72f99 Mon Sep 17 00:00:00 2001 From: Mark O'Sullivan <6950843+MarkOSullivan94@users.noreply.github.com> Date: Wed, 15 Apr 2026 18:57:47 +0200 Subject: [PATCH 23/27] docs(clerk_flutter): overview --- .../clerk_flutter/doc/clerk-ui/overview.mdx | 38 +++++++++++++++++++ 1 file changed, 38 insertions(+) diff --git a/packages/clerk_flutter/doc/clerk-ui/overview.mdx b/packages/clerk_flutter/doc/clerk-ui/overview.mdx index e69de29b..1387c26c 100644 --- a/packages/clerk_flutter/doc/clerk-ui/overview.mdx +++ b/packages/clerk_flutter/doc/clerk-ui/overview.mdx @@ -0,0 +1,38 @@ +--- +title: Clerk UI overview +description: A list of Clerk's pre-built Flutter widgets designed to seamlessly integrate authentication and multi-tenancy into your application. +sdk: flutter +--- + +Clerk offers prebuilt Flutter widgets designed to seamlessly integrate authentication and multi-tenancy into your application. +With Clerk's UI widgets you can customise the appearance of authentication screens, manage the entire authentication flow to suit your specific needs, and build robust SaaS applications. + +## Clerk widgets + +- `ClerkAuthentication` — Renders a full authentication interface, supporting multiple sign-up and sign-in methods, +multi-factor authentication (MFA), and password recovery flows. +- `ClerkUserButton` — Displays the signed-in user's profile image along with controls to manage the account and sign +out. +- `ClerkUserProfile` — Renders a complete user profile interface with personal information, security settings, +account switching, and sign-out options. +- `ClerkOrganizationList` — Lists all organizations the signed-in user belongs to and provides controls to switch +between them or create new ones. + +## Customization + +To learn how to customise the Clerk widgets, see the dedicated guide. + +If the prebuilt widgets do not meet your specific needs or if you require more control over the logic, you can rebuild +the existing Clerk flows using `clerk_auth`. + + +### Secured by Clerk branding + + + +By default, Clerk displays a **Secured by Clerk** badge on Clerk widgets. You can remove this branding by following these steps: + +1. In the Clerk Dashboard, navigate to your application's [**Settings**](https://dashboard.clerk.com/~/settings). +1. Under **Branding**, toggle on the **Remove "Secured by Clerk" branding** option. + + From 3379ae16874bdb8a6a770d3b2cb8d00dd81f126c Mon Sep 17 00:00:00 2001 From: Mark O'Sullivan <6950843+MarkOSullivan94@users.noreply.github.com> Date: Mon, 20 Apr 2026 11:41:43 +0200 Subject: [PATCH 24/27] docs(clerk_flutter): clerk authentication --- .../doc/clerk-ui/authentication-views.mdx | 0 .../doc/clerk-ui/clerk-authentication.mdx | 78 +++++++++++++++++++ 2 files changed, 78 insertions(+) delete mode 100644 packages/clerk_flutter/doc/clerk-ui/authentication-views.mdx create mode 100644 packages/clerk_flutter/doc/clerk-ui/clerk-authentication.mdx diff --git a/packages/clerk_flutter/doc/clerk-ui/authentication-views.mdx b/packages/clerk_flutter/doc/clerk-ui/authentication-views.mdx deleted file mode 100644 index e69de29b..00000000 diff --git a/packages/clerk_flutter/doc/clerk-ui/clerk-authentication.mdx b/packages/clerk_flutter/doc/clerk-ui/clerk-authentication.mdx new file mode 100644 index 00000000..2ea725ed --- /dev/null +++ b/packages/clerk_flutter/doc/clerk-ui/clerk-authentication.mdx @@ -0,0 +1,78 @@ +--- +title: '`ClerkAuthentication`' +description: Clerk's ClerkAuthentication widget renders a UI for signing in and signing up users in your Flutter app. +sdk: flutter +--- + +![The ClerkAuthentication widget renders a comprehensive authentication interface that handles both user sign-in and sign-up flows.](/docs/images/ui-components/flutter-clerk-authentication.png){{ style: { maxWidth: '460px' } }} + +The `ClerkAuthentication` widget renders a comprehensive authentication interface with support for multiple sign-up flows and sign-in methods, multi-factor authentication, password reset, and account recovery. The functionality of the `ClerkAuthentication` widget is controlled by the instance settings you specify in the [Clerk Dashboard](https://dashboard.clerk.com), such as [sign-in and sign-up options](/docs/guides/configure/auth-strategies/sign-up-sign-in-options) and [social connections](/docs/guides/configure/auth-strategies/social-connections/overview). + +By default, `ClerkAuthentication` automatically determines whether to sign users in or sign them up based on whether they already have an account, providing a seamless authentication experience without requiring users to choose between the two flows. + +`ClerkAuthentication` must be placed somewhere below a [`ClerkAuth`](/docs/flutter/getting-started/clerk-auth) widget in the widget tree. + +## Properties + +`ClerkAuthentication` has no additional properties beyond the standard Flutter `key`. + +## Usage + +### Basic usage + +The following example shows how to embed `ClerkAuthentication` in a screen that conditionally shows the authentication widget or the signed-in content. + +```dart +import 'package:clerk_flutter/clerk_flutter.dart'; +import 'package:flutter/material.dart'; + +class HomeScreen extends StatelessWidget { + const HomeScreen({super.key}); + + @override + Widget build(BuildContext context) { + final user = ClerkAuth.userOf(context); + + if (user != null) { + return const Text('You are signed in!'); + } + + return const ClerkAuthentication(); + } +} +``` + +### Present as a modal sheet + +The following example shows how to present `ClerkAuthentication` as a modal bottom sheet when the user taps a button. + +```dart +import 'package:clerk_flutter/clerk_flutter.dart'; +import 'package:flutter/material.dart'; + +class HomeScreen extends StatelessWidget { + const HomeScreen({super.key}); + + @override + Widget build(BuildContext context) { + return Scaffold( + body: Center( + child: ElevatedButton( + onPressed: () { + showModalBottomSheet( + context: context, + isScrollControlled: true, + builder: (_) => const ClerkAuthentication(), + ); + }, + child: const Text('Sign in'), + ), + ), + ); + } +} +``` + +## Customization + + From 3be2006d0b13aa7827fac3352279dda629d343d7 Mon Sep 17 00:00:00 2001 From: Mark O'Sullivan <6950843+MarkOSullivan94@users.noreply.github.com> Date: Mon, 20 Apr 2026 18:29:17 +0200 Subject: [PATCH 25/27] docs(clerk_flutter): user views --- .../doc/clerk-ui/clerk-authentication.mdx | 6 +- .../clerk_flutter/doc/clerk-ui/user-views.mdx | 0 .../doc/clerk-ui/user-views/user-button.mdx | 173 ++++++++++++++++++ .../user-views/user-profile-views.mdx | 81 ++++++++ 4 files changed, 257 insertions(+), 3 deletions(-) delete mode 100644 packages/clerk_flutter/doc/clerk-ui/user-views.mdx create mode 100644 packages/clerk_flutter/doc/clerk-ui/user-views/user-button.mdx create mode 100644 packages/clerk_flutter/doc/clerk-ui/user-views/user-profile-views.mdx diff --git a/packages/clerk_flutter/doc/clerk-ui/clerk-authentication.mdx b/packages/clerk_flutter/doc/clerk-ui/clerk-authentication.mdx index 2ea725ed..797a2683 100644 --- a/packages/clerk_flutter/doc/clerk-ui/clerk-authentication.mdx +++ b/packages/clerk_flutter/doc/clerk-ui/clerk-authentication.mdx @@ -4,7 +4,7 @@ description: Clerk's ClerkAuthentication widget renders a UI for signing in and sdk: flutter --- -![The ClerkAuthentication widget renders a comprehensive authentication interface that handles both user sign-in and sign-up flows.](/docs/images/ui-components/flutter-clerk-authentication.png){{ style: { maxWidth: '460px' } }} +[//]: # (![The ClerkAuthentication widget renders a comprehensive authentication interface that handles both user sign-in and sign-up flows.](/docs/images/ui-components/flutter-clerk-authentication.png){{ style: { maxWidth: '460px' } }}) The `ClerkAuthentication` widget renders a comprehensive authentication interface with support for multiple sign-up flows and sign-in methods, multi-factor authentication, password reset, and account recovery. The functionality of the `ClerkAuthentication` widget is controlled by the instance settings you specify in the [Clerk Dashboard](https://dashboard.clerk.com), such as [sign-in and sign-up options](/docs/guides/configure/auth-strategies/sign-up-sign-in-options) and [social connections](/docs/guides/configure/auth-strategies/social-connections/overview). @@ -12,9 +12,9 @@ By default, `ClerkAuthentication` automatically determines whether to sign users `ClerkAuthentication` must be placed somewhere below a [`ClerkAuth`](/docs/flutter/getting-started/clerk-auth) widget in the widget tree. -## Properties +## Parameters -`ClerkAuthentication` has no additional properties beyond the standard Flutter `key`. +`ClerkAuthentication` has no additional parameters beyond the standard Flutter `key`. ## Usage diff --git a/packages/clerk_flutter/doc/clerk-ui/user-views.mdx b/packages/clerk_flutter/doc/clerk-ui/user-views.mdx deleted file mode 100644 index e69de29b..00000000 diff --git a/packages/clerk_flutter/doc/clerk-ui/user-views/user-button.mdx b/packages/clerk_flutter/doc/clerk-ui/user-views/user-button.mdx new file mode 100644 index 00000000..576a3b74 --- /dev/null +++ b/packages/clerk_flutter/doc/clerk-ui/user-views/user-button.mdx @@ -0,0 +1,173 @@ +--- +title: '`ClerkUserButton`' +description: Clerk's ClerkUserButton renders a panel that displays the signed-in user's profile and provides account management controls. +sdk: flutter +--- + +[//]: # (![The ClerkUserButton renders a panel displaying the signed-in user's profile image, name, and account management options.](/docs/images/ui-components/flutter-clerk-user-button.png){{ style: { maxWidth: '460px' } }}) + +The `ClerkUserButton` renders a panel that lists all currently signed-in sessions and provides controls to manage +accounts and sign out. When a session row is selected, action buttons appear for managing the profile, signing out, +and accessing organizations (if enabled). When multiple sessions are active, a **Sign out of all accounts** button +is shown at the bottom. + +> [!IMPORTANT] +> The `ClerkUserButton` is intended for use when a user is signed in. Place it in a part of your widget tree that +> is only reachable after authentication. + +`ClerkUserButton` must be placed somewhere below a [`ClerkAuth`](/docs/flutter/getting-started/clerk-auth) widget in the widget tree. + +## Parameters + + + - `showName` + - `bool` + + Whether to display the user's name alongside their avatar in each session row. Defaults to `true`. + + --- + + - `sessionActions` + - `List?` + + A list of actions rendered as buttons within the selected session row. When not provided, defaults to **Profile**, **Sign out**, and (if organizations are enabled) **Organizations**. + + --- + + - `additionalActions` + - `List?` + + A list of actions rendered as rows below the session list. When not provided, defaults to **Add account** (only in multi-session mode). + + +### `ClerkUserAction` parameters + +Each entry in `sessionActions` and `additionalActions` is a `ClerkUserAction`: + + + - `label` + - `String` + + The text label displayed for the action. Required. + + --- + + - `callback` + - `FutureOr Function(BuildContext, ClerkAuthState)` + + The function invoked when the action is tapped. Receives the current `BuildContext` and `ClerkAuthState`. Required. + + --- + + - `icon` + - `IconData?` + + A Material icon to display alongside the label. + + --- + + - `asset` + - `String?` + + An SVG asset path to display as the action's icon. Takes precedence over `icon` when both are provided. + + +## Usage + +### Basic usage + +The following example shows how to render `ClerkUserButton` when a user is signed in. + +```dart +import 'package:clerk_flutter/clerk_flutter.dart'; +import 'package:flutter/material.dart'; + +class HomeScreen extends StatelessWidget { + const HomeScreen({super.key}); + + @override + Widget build(BuildContext context) { + final user = ClerkAuth.userOf(context); + + if (user == null) { + return const SizedBox.shrink(); + } + + return const ClerkUserButton(); + } +} +``` + +### In an app bar + +The following example shows how to place `ClerkUserButton` in an `AppBar` action. + +```dart +import 'package:clerk_flutter/clerk_flutter.dart'; +import 'package:flutter/material.dart'; + +class HomeScreen extends StatelessWidget { + const HomeScreen({super.key}); + + @override + Widget build(BuildContext context) { + final user = ClerkAuth.userOf(context); + + return Scaffold( + appBar: AppBar( + title: const Text('Home'), + actions: [ + if (user != null) const ClerkUserButton(), + ], + ), + body: const Center(child: Text('Welcome!')), + ); + } +} +``` + +### With custom actions + +The following example shows how to add a custom action to the session row and an additional action below the session list. + +```dart +import 'package:clerk_flutter/clerk_flutter.dart'; +import 'package:flutter/material.dart'; + +class HomeScreen extends StatelessWidget { + const HomeScreen({super.key}); + + @override + Widget build(BuildContext context) { + return ClerkUserButton( + sessionActions: [ + ClerkUserAction( + icon: Icons.settings, + label: 'Settings', + callback: (context, authState) async { + // Navigate to settings + }, + ), + ClerkUserAction( + icon: Icons.logout, + label: 'Sign out', + callback: (context, authState) => authState.signOut(), + ), + ], + additionalActions: [ + ClerkUserAction( + icon: Icons.help_outline, + label: 'Help', + callback: (context, authState) async { + // Open help page + }, + ), + ], + ); + } +} +``` + +## Customization + + diff --git a/packages/clerk_flutter/doc/clerk-ui/user-views/user-profile-views.mdx b/packages/clerk_flutter/doc/clerk-ui/user-views/user-profile-views.mdx new file mode 100644 index 00000000..19c46089 --- /dev/null +++ b/packages/clerk_flutter/doc/clerk-ui/user-views/user-profile-views.mdx @@ -0,0 +1,81 @@ +--- +title: '`ClerkUserProfile`' +description: Clerk's ClerkUserProfile widget renders a full-featured account management UI that allows users to manage their profile and security settings. +sdk: flutter +--- + +[//]: # (![The ClerkUserProfile widget renders a comprehensive user profile interface that displays user information and provides account management options.](/docs/images/ui-components/flutter-clerk-user-profile.png){{ style: { maxWidth: '460px' } }}) + +The `ClerkUserProfile` widget renders a comprehensive user profile interface that displays user information and provides account management options. It includes personal information, security settings, connected accounts, and passkey management. + +> [!IMPORTANT] +> The `ClerkUserProfile` widget only renders when there is a current user. Place it in a part of your widget tree +that is only reachable when a user is signed in, or guard it with a signed-in check. + +`ClerkUserProfile` must be placed somewhere below a [`ClerkAuth`](/docs/flutter/getting-started/clerk-auth) widget +in the widget tree. + +## Parameters + +`ClerkUserProfile` has no additional parameters beyond the standard Flutter `key`. + +## Usage + +### Basic usage + +The following example shows how to render `ClerkUserProfile` on a dedicated screen that is only shown when a user is signed in. + +```dart +import 'package:clerk_flutter/clerk_flutter.dart'; +import 'package:flutter/material.dart'; + +class ProfileScreen extends StatelessWidget { + const ProfileScreen({super.key}); + + @override + Widget build(BuildContext context) { + final user = ClerkAuth.userOf(context); + + if (user == null) { + return const SizedBox.shrink(); + } + + return const ClerkUserProfile(); + } +} +``` + +### Present as a modal sheet + +The following example shows how to present `ClerkUserProfile` as a modal bottom sheet when the user taps a button. + +```dart +import 'package:clerk_flutter/clerk_flutter.dart'; +import 'package:flutter/material.dart'; + +class HomeScreen extends StatelessWidget { + const HomeScreen({super.key}); + + @override + Widget build(BuildContext context) { + return Scaffold( + body: Center( + child: ElevatedButton( + onPressed: () { + showModalBottomSheet( + context: context, + isScrollControlled: true, + builder: (_) => const ClerkUserProfile(), + ); + }, + child: const Text('Manage profile'), + ), + ), + ); + } +} +``` + +## Customization + + From afaf631b018674a3842da35b5c2a735f46ef96b1 Mon Sep 17 00:00:00 2001 From: Mark O'Sullivan <6950843+MarkOSullivan94@users.noreply.github.com> Date: Wed, 22 Apr 2026 07:23:45 +0200 Subject: [PATCH 26/27] docs(clerk_flutter): customization --- .../doc/clerk-ui/customization.mdx | 310 ++++++++++++++++++ 1 file changed, 310 insertions(+) diff --git a/packages/clerk_flutter/doc/clerk-ui/customization.mdx b/packages/clerk_flutter/doc/clerk-ui/customization.mdx index e69de29b..8cf68f92 100644 --- a/packages/clerk_flutter/doc/clerk-ui/customization.mdx +++ b/packages/clerk_flutter/doc/clerk-ui/customization.mdx @@ -0,0 +1,310 @@ +--- +title: '`ClerkThemeExtension`' +description: Utilize Clerk's ClerkThemeExtension to adjust the general styles of the Clerk widgets, like colors and text styles. +sdk: flutter +--- + +The `ClerkThemeExtension` is used to customize the appearance of Clerk widgets by adjusting colors and text styles. It integrates with Flutter's `ThemeData` extension system to provide a consistent visual experience across all Clerk widgets. + +## Structure + +`ClerkThemeExtension` consists of two main properties: + +- **`colors`** - Color properties for various UI elements. +- **`stylesBuilder`** - A builder function that produces text styles from the color palette. Defaults to `ClerkThemeStyles.defaultStylesBuilder`. + +## Colors + +The `colors` property is a `ClerkThemeColors` object containing the following color options: + + + - `background` + - `Color` + + The main background color of Clerk widgets. Light theme default: `Colors.white`. Dark theme default: `Colors.black`. + + --- + + - `altBackground` + - `Color` + + An alternative background color used for secondary surfaces. Light theme default: `Color(0xFFdddddd)`. Dark theme default: `Color(0xFF333333)`. + + --- + + - `borderSide` + - `Color` + + The color used for borders and dividers. Light theme default: `Color(0xFFdddddd)`. Dark theme default: `Color(0xFF333333)`. + + --- + + - `text` + - `Color` + + The primary text color. Light theme default: `Color(0xFF3c3c3d)`. Dark theme default: `Colors.white`. + + --- + + - `icon` + - `Color` + + The color used for icons. Light theme default: `Color(0xFFaaaaaa)`. Dark theme default: `Colors.white`. + + --- + + - `lightweightText` + - `Color` + + The color used for secondary or hint text. Light theme default: `Color(0xFFaaaaaa)`. Dark theme default: `Color(0xFF555555)`. + + --- + + - `error` + - `Color` + + The color used for error messages and validation states. Default: `Color(0xFFff3333)` (both themes). + + --- + + - `accent` + - `Color` + + The color used for links, buttons, and interactive elements. Default: `Color(0xFF6c47ff)` (both themes). + + +## Text styles + +The computed `styles` property is a `ClerkThemeStyles` object containing the following text styles, derived automatically from `colors`: + + + - `heading` + - `TextStyle` + + Used for main headings. Font size: `18.0`, weight: `w500`, color: `colors.text`. + + --- + + - `subheading` + - `TextStyle` + + Used for secondary headings. Font size: `14.0`, weight: `w400`, color: `colors.text`. + + --- + + - `inputText` + - `TextStyle` + + Used for form field labels and hints. Font size: `14.0`, color: `colors.lightweightText`. + + --- + + - `error` + - `TextStyle` + + Used for validation error messages. Font size: `14.0`, color: `colors.error`. + + --- + + - `text` + - `TextStyle` + + Used for regular body text. Font size: `12.0`, color: `colors.text`. + + --- + + - `subtext` + - `TextStyle` + + Used for small helper text. Font size: `10.0`, color: `colors.lightweightText`. + + --- + + - `clickableText` + - `TextStyle` + + Used for links and tappable text. Font size: `12.0`, color: `colors.accent`. + + --- + + - `button` + - `TextStyle` + + Used for button labels. Font size: `12.0`, weight: `w500`, color: `colors.text`. + + --- + + - `avatarInitials` + - `TextStyle` + + Used for user avatar initials. Font size: `14.0`, color: `colors.background`. + + +## Usage + +Clerk widgets automatically inherit the `ClerkThemeExtension` from the nearest `ThemeData` in the widget tree. If no extension is provided, Clerk falls back to a built-in light or dark preset based on the current `ThemeData.brightness`. + +### Apply a custom theme to all Clerk widgets + +To customize all Clerk widgets in your app, add a `ClerkThemeExtension` to your root `MaterialApp`'s theme extensions. + +```dart +import 'package:clerk_flutter/clerk_flutter.dart'; +import 'package:flutter/material.dart'; + +void main() { + runApp(const MyApp()); +} + +class MyApp extends StatelessWidget { + const MyApp({super.key}); + + @override + Widget build(BuildContext context) { + return MaterialApp( + theme: ThemeData.light().copyWith( + extensions: [ + ClerkThemeExtension( + colors: const ClerkThemeColors( + background: Colors.white, + altBackground: Color(0xFFf5f5f5), + borderSide: Color(0xFFe0e0e0), + text: Color(0xFF1a1a1a), + icon: Color(0xFF757575), + lightweightText: Color(0xFF9e9e9e), + error: Color(0xFFd32f2f), + accent: Color(0xFF0057FF), + ), + ), + ], + ), + home: const RootPage(), + ); + } +} +``` + +### Apply a theme to specific widgets + +You can scope a `ClerkThemeExtension` to a subtree by wrapping it in a `Theme` widget. This is useful when different screens require different Clerk styles. + +```dart +import 'package:clerk_flutter/clerk_flutter.dart'; +import 'package:flutter/material.dart'; + +class SignInPage extends StatelessWidget { + const SignInPage({super.key}); + + @override + Widget build(BuildContext context) { + return Theme( + data: Theme.of(context).copyWith( + extensions: [ + ClerkThemeExtension( + colors: const ClerkThemeColors( + background: Colors.white, + altBackground: Color(0xFFf5f5f5), + borderSide: Color(0xFFe0e0e0), + text: Color(0xFF1a1a1a), + icon: Color(0xFF757575), + lightweightText: Color(0xFF9e9e9e), + error: Color(0xFFd32f2f), + accent: Colors.purple, + ), + ), + ], + ), + child: const ClerkAuthentication(), + ); + } +} +``` + +### Custom text styles + +You can override the default text styles by providing a custom `stylesBuilder`. This is useful when you want to use a custom font family or adjust specific text properties. + +```dart +import 'package:clerk_flutter/clerk_flutter.dart'; +import 'package:flutter/material.dart'; + +class CustomStylesApp extends StatelessWidget { + const CustomStylesApp({super.key}); + + @override + Widget build(BuildContext context) { + return MaterialApp( + theme: ThemeData.light().copyWith( + extensions: [ + ClerkThemeExtension( + colors: const ClerkThemeColors( + background: Colors.white, + altBackground: Color(0xFFf5f5f5), + borderSide: Color(0xFFe0e0e0), + text: Color(0xFF1a1a1a), + icon: Color(0xFF757575), + lightweightText: Color(0xFF9e9e9e), + error: Color(0xFFd32f2f), + accent: Color(0xFF0057FF), + ), + stylesBuilder: (colors) => ClerkThemeStyles(colors: colors), + ), + ], + ), + home: const RootPage(), + ); + } +} +``` + +## Light and dark mode support + +Clerk widgets automatically support both light and dark mode, adapting to the active `ThemeData.brightness`. If your app provides separate `theme` and `darkTheme` values to `MaterialApp`, add a `ClerkThemeExtension` to each. + +```dart +import 'package:clerk_flutter/clerk_flutter.dart'; +import 'package:flutter/material.dart'; + +const _lightClerkTheme = ClerkThemeExtension( + colors: ClerkThemeColors( + background: Colors.white, + altBackground: Color(0xFFf5f5f5), + borderSide: Color(0xFFe0e0e0), + text: Color(0xFF1a1a1a), + icon: Color(0xFF757575), + lightweightText: Color(0xFF9e9e9e), + error: Color(0xFFd32f2f), + accent: Color(0xFF0057FF), + ), +); + +const _darkClerkTheme = ClerkThemeExtension( + colors: ClerkThemeColors( + background: Color(0xFF121212), + altBackground: Color(0xFF1e1e1e), + borderSide: Color(0xFF2c2c2c), + text: Colors.white, + icon: Color(0xFFbbbbbb), + lightweightText: Color(0xFF757575), + error: Color(0xFFef5350), + accent: Color(0xFF4d8fff), + ), +); + +class MyApp extends StatelessWidget { + const MyApp({super.key}); + + @override + Widget build(BuildContext context) { + return MaterialApp( + theme: ThemeData.light().copyWith( + extensions: [_lightClerkTheme], + ), + darkTheme: ThemeData.dark().copyWith( + extensions: [_darkClerkTheme], + ), + home: const RootPage(), + ); + } +} +``` From 1d6c6eb915548f4cd43d17a218b74569b0438152 Mon Sep 17 00:00:00 2001 From: Mark O'Sullivan <6950843+MarkOSullivan94@users.noreply.github.com> Date: Wed, 22 Apr 2026 11:20:47 +0200 Subject: [PATCH 27/27] docs(clerk_flutter): sign in with google & apple and deploy to production --- .../doc/guides/deploy-to-production.mdx | 140 ++++++++++++ .../doc/guides/sign-in-with-apple.flutter.mdx | 117 ++++++++++ .../doc/guides/sign-in-with-apple.mdx | 0 .../guides/sign-in-with-google.flutter.mdx | 204 ++++++++++++++++++ .../doc/guides/sign-in-with-google.mdx | 0 5 files changed, 461 insertions(+) create mode 100644 packages/clerk_flutter/doc/guides/sign-in-with-apple.flutter.mdx delete mode 100644 packages/clerk_flutter/doc/guides/sign-in-with-apple.mdx create mode 100644 packages/clerk_flutter/doc/guides/sign-in-with-google.flutter.mdx delete mode 100644 packages/clerk_flutter/doc/guides/sign-in-with-google.mdx diff --git a/packages/clerk_flutter/doc/guides/deploy-to-production.mdx b/packages/clerk_flutter/doc/guides/deploy-to-production.mdx index e69de29b..808b3fce 100644 --- a/packages/clerk_flutter/doc/guides/deploy-to-production.mdx +++ b/packages/clerk_flutter/doc/guides/deploy-to-production.mdx @@ -0,0 +1,140 @@ +--- +title: Deploy your Flutter app to production +description: Learn how to deploy your Flutter app to production. +sdk: flutter +--- + +Before you begin: + +1. You will need to have a domain you own. +1. You will need social sign-in (OAuth) credentials for any providers you want to use in production. Each [OAuth provider](/docs/guides/configure/auth-strategies/social-connections/overview) has a dedicated guide on how to set up OAuth credentials for Clerk production apps. +1. You will need your app registered under [**Native Applications**](https://dashboard.clerk.com/~/native-applications) in the Clerk Dashboard. + +## Create your production instance + +1. Navigate to the [Clerk Dashboard](https://dashboard.clerk.com). +1. At the top of the Dashboard, select the **Development** button to reveal the instance selection dropdown. Select **Create production instance**. +1. You will be prompted with a modal to either clone your development instance settings to your production instance or create your production instance with Clerk's default settings. +1. The homepage of the dashboard will show you what is still required to deploy your production instance. + +> [!WARNING] +> For security reasons, [**SSO connections**](https://dashboard.clerk.com/~/user-authentication/sso-connections), [**Integrations**](https://dashboard.clerk.com/~/integrations), and [**Paths**](https://dashboard.clerk.com/~/paths) settings do not copy over. You will need to set these values again. + +## Update your publishable key + +A common mistake when deploying to production is **forgetting to change your publishable key to your production instance's key.** The best way to handle this is to use environment variables or build-time configuration so the correct key is set for each environment. + +Your publishable key is formatted as `pk_test_` in development and `pk_live_` in production. Pass it to `ClerkAuth` during initialization: + +```dart +ClerkAuth( + config: ClerkAuthConfig( + publishableKey: const String.fromEnvironment('CLERK_PUBLISHABLE_KEY'), + ), + child: MaterialApp(...), +) +``` + +Run your Flutter app with the production key: + +```sh +flutter run --dart-define=CLERK_PUBLISHABLE_KEY=pk_live_... +``` + +Or use a configuration file: + +```sh +flutter run --dart-define-from-file=config.production.json +``` + +> [!TIP] +> The SDK automatically infers whether it is in test or production mode from the publishable key prefix, so no additional configuration is required. + +## OAuth credentials + +In development, Clerk provides shared OAuth credentials for most social providers. + +In production, these shared credentials are not secure and you must provide your own. Each [OAuth provider](/docs/guides/configure/auth-strategies/social-connections/overview) has a dedicated guide on how to set up OAuth credentials for Clerk production apps. + +## Deep link redirect URLs + +If your app uses deep links for OAuth or email link callbacks, you must register the redirect URLs for your production app in the Clerk Dashboard. + +1. Navigate to [**Redirect URLs**](https://dashboard.clerk.com/~/redirect-urls) in the Clerk Dashboard. +1. Add your production deep link scheme, for example `myapp://clerk/oauth` and `myapp://clerk/email-link`. + +> [!NOTE] +> Deep link URL schemes are registered per instance. Ensure the schemes configured in your production app's `Info.plist` (iOS) or `AndroidManifest.xml` (Android) match what you have registered in the Dashboard. + +## iOS setup + +### Confirm your iOS app is registered in production + +Ensure your iOS app exists in the production instance under [**Native Applications**](https://dashboard.clerk.com/~/native-applications). You will need your **App ID Prefix** (Team ID) and **Bundle ID**. + +### Update associated domains + +In Xcode, update the **Associated Domains** entitlement to use your production [Frontend API URL](!frontend-api-url). + +Under **Signing & Capabilities**, add or update the entry to: + +``` +webcredentials: +``` + +Replace `` with the value found on the [**Domains**](https://dashboard.clerk.com/~/domains) page in the Clerk Dashboard. + +## Android setup + +### Confirm your Android app is registered in production + +Ensure your Android app exists in the production instance under [**Native Applications**](https://dashboard.clerk.com/~/native-applications). You will need your **Package Name** and **SHA-256 certificate fingerprint** from your production keystore. + +To get the SHA-256 fingerprint from your production keystore, run: + +```sh +keytool -keystore path-to-production-keystore -list -v +``` + +> [!IMPORTANT] +> Your development and production keystores have different SHA-256 fingerprints. Make sure you register the fingerprint from your **production** keystore, not the debug keystore. + +## Deploy certificates + +The Clerk Dashboard home page shows what steps are still required to deploy your production instance. Once you have completed all necessary steps, a **Deploy certificates** button will appear. Selecting this button deploys your production instance. + +## Test on a physical device + +Build a release build using your production keys and verify the authentication flow on a physical device before releasing to users: + +```sh +# iOS — build a release archive +flutter build ipa --dart-define=CLERK_PUBLISHABLE_KEY=pk_live_... + +# Android — build a signed release APK +flutter build apk --dart-define=CLERK_PUBLISHABLE_KEY=pk_live_... +``` + +## Troubleshooting + +### DNS records not propagating + +Clerk uses a DNS check to validate CNAME records. If the subdomain is reverse proxied behind a service that points to generic hostnames (such as Cloudflare), the DNS check will fail. Set the DNS record for the subdomain to a **DNS only** mode on your host to prevent proxying. + +### Deployment stuck in certificate issuance + +If your instance is stuck during TLS certificate issuance, this may be caused by [CAA DNS records](https://en.wikipedia.org/wiki/DNS_Certification_Authority_Authorization) on your primary domain. Clerk provisions certificates using either [LetsEncrypt](https://letsencrypt.org/) or [Google Trust Services](https://pki.goog/). Ensure your primary domain does not have CAA records that prevent either authority from issuing certificates. + +To check your CAA records, run: + +```sh +dig example.com +short CAA +``` + +If the command returns an empty response, your domain is correctly configured. If it returns any text, remove the restrictive CAA records from your primary domain. + +### Associated domains not working on iOS + +- Ensure the `webcredentials` entry in Xcode uses your **production** Frontend API URL, not the development URL. +- Allow up to 48 hours for DNS changes to propagate. +- Verify the app bundle ID and Team ID registered in the Clerk Dashboard match your Xcode project. diff --git a/packages/clerk_flutter/doc/guides/sign-in-with-apple.flutter.mdx b/packages/clerk_flutter/doc/guides/sign-in-with-apple.flutter.mdx new file mode 100644 index 00000000..5ed062fb --- /dev/null +++ b/packages/clerk_flutter/doc/guides/sign-in-with-apple.flutter.mdx @@ -0,0 +1,117 @@ +--- +title: Add "Sign in with Apple" to your Flutter app +description: Learn how to add native Sign in with Apple to your Clerk Flutter app on iOS. +sdk: flutter +--- + + + +> [!NOTE] +> Native Sign in with Apple is only available on **iOS**. On Android, use the browser-based OAuth flow instead — call `ssoSignIn(context, Strategy.oauthApple)` as described in the [authentication flows reference](/docs/sdk-reference/authentication-flows#sign-in-with-oauth). + +This guide explains how to add native Sign in with Apple to your Flutter app using Clerk. The native approach uses Apple's platform SDK to obtain an ID token directly, which is then passed to Clerk for authentication. + +If you only need browser-based OAuth (a WebView flow that doesn't require additional packages), you can call `ssoSignIn(context, Strategy.oauthApple)` directly — see the [authentication flows reference](/docs/sdk-reference/authentication-flows#sign-in-with-oauth). The steps below cover the native ID token approach. + + + ## Enable Apple as a social connection + + 1. Navigate to the [Clerk Dashboard](https://dashboard.clerk.com/~/user-authentication/sso-connections). + 1. In the left sidebar, select **User & Authentication**, then select **SSO Connections**. + 1. Select **Add connection** and select **For all users**. + 1. In the **Choose provider** dropdown, select **Apple**. + 1. Toggle on **Enable for sign-up and sign-in** and select **Save**. + + ## Add the Sign in with Apple capability in Xcode + + 1. Open your Flutter project's iOS workspace in Xcode (`ios/Runner.xcworkspace`). + 1. Select the **Runner** target, then open the **Signing & Capabilities** tab. + 1. Select **+ Capability** and add **Sign In with Apple**. + + > [!NOTE] + > Sign in with Apple works on both iOS Simulators and physical devices. However, simulators have limited support — physical devices provide full functionality including biometric authentication (Face ID / Touch ID). + + ## Install dependencies + + Add the `sign_in_with_apple` and `uuid` packages to your `pubspec.yaml`: + + ```sh + flutter pub add sign_in_with_apple uuid + ``` + + ## Implement Sign in with Apple + + ### Using Clerk's pre-built UI + + If you're using `ClerkAuthentication` or `ClerkOAuthPanel`, no additional code is required. Once Apple is enabled as a social connection in the Clerk Dashboard, the **Sign in with Apple** button is rendered automatically. + + ```dart + ClerkAuthentication() + ``` + + ### Using a custom flow + + For custom UI, use `sign_in_with_apple` to obtain an ID token and pass it to Clerk using `idTokenSignIn`. + + ```dart + import 'package:clerk_auth/clerk_auth.dart' as clerk; + import 'package:clerk_flutter/clerk_flutter.dart'; + import 'package:sign_in_with_apple/sign_in_with_apple.dart'; + import 'package:uuid/uuid.dart'; + + Future signInWithApple(BuildContext context) async { + final authState = ClerkAuth.of(context); + + // Request Apple ID credentials with a secure nonce + final credential = await SignInWithApple.getAppleIDCredential( + nonce: const Uuid().v4(), + scopes: [ + AppleIDAuthorizationScopes.email, + AppleIDAuthorizationScopes.fullName, + ], + ); + + final token = credential.identityToken; + if (token == null) return; + + // Pass the ID token to Clerk + await authState.idTokenSignIn( + provider: clerk.IdTokenProvider.apple, + token: token, + ); + + // If this is a new user, a sign-up object may be returned with missing fields + if (authState.signUp case clerk.SignUp signUp + when signUp.missingFields.isNotEmpty) { + await authState.attemptSignUp( + firstName: signUp.missing(clerk.Field.firstName) + ? credential.givenName + : null, + lastName: signUp.missing(clerk.Field.lastName) + ? credential.familyName + : null, + ); + } + } + ``` + + > [!IMPORTANT] + > Apple only provides the user's full name (`givenName` and `familyName`) during their **first** app authorisation. On subsequent sign-ins, this data is not returned. Store the name when you first receive it and treat it as optional thereafter. + + +## Hide My Email + +Apple's **Hide My Email** feature lets users share a private relay email address instead of their real one. Clerk supports this out of the box, but you may need to configure [email communication through Apple's private relay](https://developer.apple.com/documentation/sign_in_with_apple/communicating_using_the_private_relay_service) in your Apple Developer account if your app sends transactional emails. diff --git a/packages/clerk_flutter/doc/guides/sign-in-with-apple.mdx b/packages/clerk_flutter/doc/guides/sign-in-with-apple.mdx deleted file mode 100644 index e69de29b..00000000 diff --git a/packages/clerk_flutter/doc/guides/sign-in-with-google.flutter.mdx b/packages/clerk_flutter/doc/guides/sign-in-with-google.flutter.mdx new file mode 100644 index 00000000..d5b67883 --- /dev/null +++ b/packages/clerk_flutter/doc/guides/sign-in-with-google.flutter.mdx @@ -0,0 +1,204 @@ +--- +title: Add "Sign in with Google" to your Flutter app +description: Learn how to add native Sign in with Google to your Clerk Flutter app on iOS and Android. +sdk: flutter +--- + + + +This guide explains how to add native Sign in with Google to your Flutter app using Clerk. The native approach uses Google's platform SDK to obtain an ID token directly, which is then passed to Clerk for authentication. + +> [!WARNING] +> On **Android**, Google [does not allow sign-in via in-app browsers](https://developers.googleblog.com/en/modernizing-oauth-interactions-in-native-apps-for-better-usability-and-security). The native ID token approach described in this guide is required for Android. On iOS, the in-app WebView OAuth flow (via `ssoSignIn`) is also supported. + +To make the setup process easier, it's recommended to keep two browser tabs open — one for the [Clerk Dashboard](https://dashboard.clerk.com/~/user-authentication/sso-connections) and one for the [Google Cloud Console](https://console.cloud.google.com/). + + + ## Enable Google as a social connection + + 1. Navigate to the [Clerk Dashboard](https://dashboard.clerk.com/~/user-authentication/sso-connections). + 1. In the left sidebar, select **User & Authentication**, then select **SSO Connections**. + 1. Select **Add connection** and select **For all users**. + 1. In the **Choose provider** dropdown, select **Google**. + 1. Save the **Authorized Redirect URI** displayed — you will need it in the next step. + 1. Toggle on **Enable for sign-up and sign-in**. + + ## Create Google OAuth credentials + + You need three OAuth clients in Google Cloud Console: one for Android, one for iOS, and one Web client. Clerk uses the Web client for server-side token verification — **the Web client is required even for native apps**. + + 1. Navigate to the [Google Cloud Console](https://console.cloud.google.com/). + 1. Select an existing project or [create a new one](https://console.cloud.google.com/projectcreate). + 1. In the top-left, select the menu icon (**≡**) and select **APIs & Services**, then **Credentials**. + + ### Create the Android client + + 1. Next to **Credentials**, select **Create Credentials**, then **OAuth client ID**. + 1. For **Application type**, select **Android**. + 1. Complete the required fields: + - **Package name**: Your app's package name (e.g. `com.example.myapp`), found in `android/app/build.gradle`. + - **SHA-1 certificate fingerprint**: Run the following command, replacing the path with your debug or production keystore: + ```sh + keytool -keystore path-to-debug-or-production-keystore -list -v + ``` + > [!IMPORTANT] + > By default, the debug keystore is at `~/.android/debug.keystore`. The keystore password is `android`. You may need to install [OpenJDK](https://openjdk.org/) to run `keytool`. + 1. Select **Create**. + + ### Create the iOS client + + 1. Select **Create Credentials**, then **OAuth client ID**. + 1. For **Application type**, select **iOS**. + 1. Enter your **Bundle ID** (e.g. `com.example.myapp`), found in your Xcode project settings. + 1. Select **Create**. + + ### Create the Web client + + 1. Select **Create Credentials**, then **OAuth client ID**. + 1. For **Application type**, select **Web application**. + 1. Under **Authorized redirect URIs**, paste the **Authorized Redirect URI** you saved from the Clerk Dashboard. + 1. Select **Create**. A modal will display your **Client ID** and **Client Secret** — save these securely. + + ## Set the Client ID and Secret in the Clerk Dashboard + + 1. Return to the [Clerk Dashboard SSO connections page](https://dashboard.clerk.com/~/user-authentication/sso-connections). + 1. Select the Google connection you created earlier. + 1. Enter the **Client ID** and **Client Secret** from the Web client you created in the previous step. + 1. Select **Save**. + + ## Install dependencies + + Add the `google_sign_in` and `uuid` packages to your `pubspec.yaml`: + + ```sh + flutter pub add google_sign_in uuid + ``` + + ## Configure your platforms + + ### Android + + No additional `AndroidManifest.xml` changes are required for the `google_sign_in` package beyond the `INTERNET` permission already present in the quickstart. + + ### iOS + + The `google_sign_in` package requires a URL scheme for the OAuth callback. Add the following to your `ios/Runner/Info.plist`, replacing the value with the reversed form of your **iOS Client ID** from Google Cloud Console: + + ```xml + CFBundleURLTypes + + + CFBundleTypeRole + Editor + CFBundleURLSchemes + + + com.googleusercontent.apps.YOUR_IOS_CLIENT_ID + + + + ``` + + ## Implement Sign in with Google + + ### Using Clerk's pre-built UI (iOS only) + + On iOS, if you're using `ClerkAuthentication` or `ClerkOAuthPanel`, the **Sign in with Google** button is rendered automatically once Google is enabled in the Clerk Dashboard. This uses the in-app WebView OAuth flow and does not require the `google_sign_in` package. + + ```dart + ClerkAuthentication() + ``` + + ### Using a custom flow (iOS and Android) + + For custom UI, or for Android where the in-app browser is not permitted, use `google_sign_in` to obtain an ID token natively and pass it to Clerk. + + > [!IMPORTANT] + > The `serverClientId` must be the **Web client ID** from Google Cloud Console, not the Android or iOS client ID. Clerk requires this to verify the token server-side. + + ```dart + import 'package:clerk_auth/clerk_auth.dart' as clerk; + import 'package:clerk_flutter/clerk_flutter.dart'; + import 'package:google_sign_in/google_sign_in.dart'; + import 'package:uuid/uuid.dart'; + + Future signInWithGoogle(BuildContext context) async { + final authState = ClerkAuth.of(context); + + // Reset the Clerk client before initialising Google Sign-In + await authState.resetClient(); + + final google = GoogleSignIn.instance; + await google.initialize( + // Use your Web client ID from Google Cloud Console + serverClientId: 'YOUR_WEB_CLIENT_ID.apps.googleusercontent.com', + nonce: const Uuid().v4(), + ); + + final account = await google.authenticate( + scopeHint: const ['openid', 'email', 'profile'], + ); + + final token = account.authentication.idToken; + if (token == null) return; + + // Pass the ID token to Clerk + await authState.idTokenSignIn( + provider: clerk.IdTokenProvider.google, + token: token, + ); + + // If this is a new user, a sign-up object may be returned with missing fields + if (authState.signUp case clerk.SignUp signUp + when signUp.missingFields.isNotEmpty) { + final nameParts = account.displayName?.split(' ') ?? []; + await authState.attemptSignUp( + firstName: signUp.missing(clerk.Field.firstName) + ? nameParts.firstOrNull + : null, + lastName: signUp.missing(clerk.Field.lastName) + ? (nameParts.length > 1 ? nameParts.last : null) + : null, + ); + } + } + ``` + + To conditionally show the native Google Sign-In button only when the strategy is available in your Clerk environment: + + ```dart + if (authState.env.config.firstFactors.contains(clerk.Strategy.oauthTokenGoogle)) + ElevatedButton( + onPressed: () => signInWithGoogle(context), + child: const Text('Sign in with Google'), + ), + ``` + + +## Troubleshooting + +### `PlatformException: sign_in_failed` on Android + +Ensure the SHA-1 fingerprint registered in Google Cloud Console matches the keystore you are using to sign your build. Debug and release builds use different keystores with different fingerprints — both may need to be registered during development. + +### Token verification fails + +Verify that the `serverClientId` passed to `GoogleSignIn.instance.initialize()` is the **Web client ID**, not the Android or iOS client ID. Clerk uses the Web client for server-side verification. diff --git a/packages/clerk_flutter/doc/guides/sign-in-with-google.mdx b/packages/clerk_flutter/doc/guides/sign-in-with-google.mdx deleted file mode 100644 index e69de29b..00000000