├── tests ├── __init__.py └── test_demo.py ├── clerk_api_demo ├── clerk_api_demo │ ├── __init__.py │ └── clerk_api_demo.py ├── requirements.txt ├── .gitignore ├── rxconfig.py ├── assets │ └── favicon.ico └── Dockerfile ├── docs ├── full_docs │ ├── pages.md │ ├── clerk_provider.md │ ├── user_components.md │ ├── control_components.md │ ├── unstyled_components.md │ └── authentication_components.md ├── migrating.md ├── about.md ├── getting_started.md └── features.md ├── custom_components └── reflex_clerk_api │ ├── base.py │ ├── authentication_components.py │ ├── __init__.py │ ├── pages.py │ ├── user_components.py │ ├── unstyled_components.py │ ├── models.py │ ├── control_components.py │ ├── organization_components.py │ └── clerk_provider.py ├── .gitignore ├── .github ├── actions │ ├── basic-checks │ │ └── action.yml │ ├── setup-python-env │ │ └── action.yml │ └── full-checks │ │ └── action.yml └── workflows │ ├── ci.yml │ ├── ci-forks.yml │ ├── publish.yml │ ├── _reusable-ci.yml │ ├── full-ci-manual.yml │ ├── full-ci-comment.yml │ └── deploy.yml ├── .pre-commit-config.yaml ├── mkdocs.yml ├── Taskfile.yml ├── pyproject.toml └── README.md /tests/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /clerk_api_demo/clerk_api_demo/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /docs/full_docs/pages.md: -------------------------------------------------------------------------------- 1 | ::: reflex_clerk_api.pages 2 | -------------------------------------------------------------------------------- /docs/full_docs/clerk_provider.md: -------------------------------------------------------------------------------- 1 | ::: reflex_clerk_api.clerk_provider 2 | -------------------------------------------------------------------------------- /docs/full_docs/user_components.md: -------------------------------------------------------------------------------- 1 | ::: reflex_clerk_api.user_components 2 | -------------------------------------------------------------------------------- /docs/full_docs/control_components.md: -------------------------------------------------------------------------------- 1 | ::: reflex_clerk_api.control_components 2 | -------------------------------------------------------------------------------- /docs/full_docs/unstyled_components.md: -------------------------------------------------------------------------------- 1 | ::: reflex_clerk_api.unstyled_components 2 | -------------------------------------------------------------------------------- /clerk_api_demo/requirements.txt: -------------------------------------------------------------------------------- 1 | reflex==0.7.3 2 | reflex-clerk-api 3 | dotenv==0.9.9 4 | -------------------------------------------------------------------------------- /docs/full_docs/authentication_components.md: -------------------------------------------------------------------------------- 1 | ::: reflex_clerk_api.authentication_components 2 | -------------------------------------------------------------------------------- /clerk_api_demo/.gitignore: -------------------------------------------------------------------------------- 1 | __pycache__/ 2 | *.py[cod] 3 | .states 4 | *.db 5 | .web 6 | assets/external/ 7 | -------------------------------------------------------------------------------- /clerk_api_demo/rxconfig.py: -------------------------------------------------------------------------------- 1 | import reflex as rx 2 | 3 | config = rx.Config( 4 | app_name="clerk_api_demo", 5 | ) 6 | -------------------------------------------------------------------------------- /clerk_api_demo/assets/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TimChild/reflex-clerk-api/HEAD/clerk_api_demo/assets/favicon.ico -------------------------------------------------------------------------------- /custom_components/reflex_clerk_api/base.py: -------------------------------------------------------------------------------- 1 | import reflex as rx 2 | 3 | 4 | class ClerkBase(rx.Component): 5 | # The React library to wrap. 6 | library = "@clerk/clerk-react" 7 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | __pycache__/ 2 | dist/ 3 | *.egg-info/ 4 | *.py[cod] 5 | .env 6 | site/ 7 | .states/ 8 | playwright_test_error.png 9 | .idea 10 | custom_components/reflex_clerk_api/*.pyi 11 | -------------------------------------------------------------------------------- /custom_components/reflex_clerk_api/authentication_components.py: -------------------------------------------------------------------------------- 1 | from reflex_clerk_api.base import ClerkBase 2 | 3 | 4 | class SignIn(ClerkBase): 5 | tag = "SignIn" 6 | 7 | path: str 8 | 9 | 10 | class SignUp(ClerkBase): 11 | tag = "SignUp" 12 | 13 | path: str 14 | 15 | 16 | sign_in = SignIn.create 17 | sign_up = SignUp.create 18 | -------------------------------------------------------------------------------- /.github/actions/basic-checks/action.yml: -------------------------------------------------------------------------------- 1 | name: 'Basic CI Checks' 2 | description: 'Runs basic CI checks (lint and typecheck) without secrets' 3 | 4 | runs: 5 | using: 'composite' 6 | steps: 7 | - name: Run lint 8 | run: task lint 9 | shell: bash 10 | 11 | - name: Run typecheck 12 | run: task typecheck 13 | shell: bash 14 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: 4 | push: 5 | tags: 6 | - "v*.*.*" 7 | # branches: [main] 8 | pull_request: 9 | branches: [main] 10 | 11 | concurrency: 12 | group: ${{ github.workflow }}-${{ github.ref }} 13 | cancel-in-progress: true 14 | 15 | jobs: 16 | ci: 17 | uses: ./.github/workflows/_reusable-ci.yml 18 | with: 19 | check_type: 'full' 20 | environment: 'demo' 21 | secrets: inherit 22 | -------------------------------------------------------------------------------- /.github/workflows/ci-forks.yml: -------------------------------------------------------------------------------- 1 | name: CI for Fork PRs 2 | 3 | on: 4 | pull_request_target: 5 | # Note: Repo must be set to require approval before running workflows from forks 6 | # This only runs basic checks without secrets for safety 7 | branches: [main] 8 | 9 | concurrency: 10 | group: ${{ github.workflow }}-${{ github.ref }} 11 | cancel-in-progress: true 12 | 13 | jobs: 14 | basic-checks: 15 | uses: ./.github/workflows/_reusable-ci.yml 16 | with: 17 | checkout_ref: ${{ github.event.pull_request.head.sha }} 18 | checkout_repository: ${{ github.event.pull_request.head.repo.full_name }} 19 | check_type: 'basic' 20 | secrets: inherit 21 | -------------------------------------------------------------------------------- /.pre-commit-config.yaml: -------------------------------------------------------------------------------- 1 | repos: 2 | - repo: https://github.com/pre-commit/pre-commit-hooks 3 | rev: v5.0.0 4 | hooks: 5 | - id: trailing-whitespace 6 | - id: end-of-file-fixer 7 | - id: check-yaml 8 | - id: check-added-large-files 9 | 10 | - repo: https://github.com/astral-sh/ruff-pre-commit 11 | rev: v0.11.2 12 | hooks: 13 | - id: ruff 14 | args: [--fix] 15 | - id: ruff-format 16 | 17 | - repo: https://github.com/RobertCraigie/pyright-python 18 | rev: v1.1.397 19 | hooks: 20 | - id: pyright 21 | 22 | - repo: https://github.com/astral-sh/uv-pre-commit 23 | # uv version. 24 | rev: 0.6.9 25 | hooks: 26 | - id: uv-lock 27 | -------------------------------------------------------------------------------- /.github/actions/setup-python-env/action.yml: -------------------------------------------------------------------------------- 1 | name: 'Setup Python Environment' 2 | description: 'Sets up Python, uv, and Task for CI' 3 | inputs: 4 | python-version: 5 | description: 'Python version to setup' 6 | required: true 7 | github-token: 8 | description: 'GitHub token for Task installation' 9 | required: true 10 | 11 | runs: 12 | using: 'composite' 13 | steps: 14 | - name: Set up Python 15 | uses: actions/setup-python@v5 16 | with: 17 | python-version: ${{ inputs.python-version }} 18 | 19 | - name: Install uv 20 | uses: astral-sh/setup-uv@v5 21 | with: 22 | version: "0.6.5" 23 | 24 | - name: Install Task 25 | uses: arduino/setup-task@v2 26 | with: 27 | version: 3.x 28 | repo-token: ${{ inputs.github-token }} 29 | -------------------------------------------------------------------------------- /custom_components/reflex_clerk_api/__init__.py: -------------------------------------------------------------------------------- 1 | __version__ = "1.1.3" 2 | 3 | from .authentication_components import sign_in, sign_up 4 | from .clerk_provider import ( 5 | ClerkState, 6 | ClerkUser, 7 | clerk_provider, 8 | on_load, 9 | register_on_auth_change_handler, 10 | wrap_app, 11 | ) 12 | from .control_components import ( 13 | clerk_loaded, 14 | clerk_loading, 15 | protect, 16 | redirect_to_user_profile, 17 | signed_in, 18 | signed_out, 19 | ) 20 | from .pages import add_sign_in_page, add_sign_up_page 21 | from .unstyled_components import ( 22 | SignInButton, 23 | sign_in_button, 24 | sign_out_button, 25 | sign_up_button, 26 | ) 27 | from .user_components import user_button, user_profile 28 | 29 | __all__ = [ 30 | "ClerkState", 31 | "ClerkUser", 32 | "SignInButton", 33 | "add_sign_in_page", 34 | "add_sign_up_page", 35 | "clerk_loaded", 36 | "clerk_loading", 37 | "clerk_provider", 38 | "on_load", 39 | "protect", 40 | "redirect_to_user_profile", 41 | "register_on_auth_change_handler", 42 | "sign_in", 43 | "sign_in_button", 44 | "sign_out_button", 45 | "sign_up", 46 | "sign_up_button", 47 | "signed_in", 48 | "signed_out", 49 | "user_button", 50 | "user_profile", 51 | "wrap_app", 52 | ] 53 | -------------------------------------------------------------------------------- /clerk_api_demo/Dockerfile: -------------------------------------------------------------------------------- 1 | ## https://github.com/astral-sh/uv-docker-example/blob/main/standalone.Dockerfile 2 | 3 | # Using uv image with explicitly managed python 4 | FROM ghcr.io/astral-sh/uv:bookworm-slim AS builder 5 | ENV UV_COMPILE_BYTECODE=1 UV_LINK_MODE=copy 6 | 7 | # Configure the Python directory so it is consistent 8 | ENV UV_PYTHON_INSTALL_DIR /python 9 | 10 | # Only use the managed Python version 11 | ENV UV_PYTHON_PREFERENCE=only-managed 12 | 13 | # Install Python before the project for caching 14 | RUN uv python install 3.13 15 | 16 | WORKDIR /app 17 | 18 | RUN uv venv 19 | RUN --mount=type=cache,target=/root/.cache/uv \ 20 | --mount=type=bind,source=requirements.txt,target=requirements.txt \ 21 | uv pip install -r requirements.txt 22 | 23 | COPY assets assets 24 | COPY clerk_api_demo clerk_api_demo 25 | COPY rxconfig.py . 26 | 27 | # Then, use a final image without uv (note this also doesn't include python) 28 | FROM debian:bookworm-slim 29 | 30 | # Copy the Python installed in the builder 31 | COPY --from=builder --chown=python:python /python /python 32 | 33 | # Copy the application from the builder 34 | COPY --from=builder --chown=app:app /app /app 35 | 36 | # Place executables in the environment at the front of the path 37 | ENV PATH="/app/.venv/bin:$PATH" 38 | 39 | ########## 40 | WORKDIR /app 41 | CMD ["reflex", "run", "--env", "prod", "--backend-only", "--loglevel", "info"] 42 | -------------------------------------------------------------------------------- /.github/workflows/publish.yml: -------------------------------------------------------------------------------- 1 | name: Publish 2 | 3 | on: 4 | workflow_run: 5 | workflows: ["CI"] 6 | types: 7 | - completed 8 | branches: [v*.*.*] 9 | 10 | concurrency: 11 | group: ${{ github.workflow }}-${{ github.ref }} 12 | cancel-in-progress: true 13 | 14 | jobs: 15 | deploy: 16 | runs-on: ubuntu-latest 17 | # If CI was successful and the push was on the main branch 18 | if: ${{ github.event.workflow_run.conclusion == 'success' && github.repository == 'TimChild/reflex-clerk-api' }} 19 | environment: deploy 20 | permissions: 21 | contents: write 22 | 23 | steps: 24 | - uses: actions/checkout@v4 25 | 26 | - name: Set up Python 27 | uses: actions/setup-python@v5 28 | with: 29 | python-version: "3.13" 30 | 31 | - name: Install uv 32 | uses: astral-sh/setup-uv@v5 33 | with: 34 | version: "0.6.5" 35 | 36 | - name: Install Task 37 | uses: arduino/setup-task@v2 38 | with: 39 | version: 3.x 40 | repo-token: ${{ secrets.GITHUB_TOKEN }} 41 | 42 | - name: Check PYPI_TOKEN 43 | run: | 44 | if [ -z "${{ secrets.PYPI_TOKEN }}" ]; then 45 | echo "PYPI_TOKEN is not set" 46 | exit 1 47 | fi 48 | 49 | - name: Create .env file 50 | run: echo "PYPI_TOKEN=${{ secrets.PYPI_TOKEN }}" > .env 51 | 52 | - name: Publish 53 | run: task manual-publish 54 | 55 | - name: Publish docs 56 | run: task manual-publish-docs 57 | -------------------------------------------------------------------------------- /mkdocs.yml: -------------------------------------------------------------------------------- 1 | # yaml-language-server: $schema=https://squidfunk.github.io/mkdocs-material/schema.json 2 | 3 | site_name: Reflex Clerk API Docs 4 | site_url: https://timchild.github.io/reflex-clerk-api 5 | repo_url: https://github.com/TimChild/reflex-clerk-api 6 | site_description: Documentation reflex-clerk-api 7 | site_author: Tim Child 8 | copyright: Copyright 2025 9 | 10 | nav: 11 | - About: about.md 12 | - Getting Started: getting_started.md 13 | - Reference: 14 | - full_docs/clerk_provider.md 15 | - full_docs/control_components.md 16 | - full_docs/unstyled_components.md 17 | - full_docs/pages.md 18 | - full_docs/user_components.md 19 | - full_docs/authentication_components.md 20 | 21 | - Features: features.md 22 | - Migrating: migrating.md 23 | 24 | theme: 25 | name: material 26 | features: 27 | - content.action.edit 28 | - navigation.instant 29 | palette: 30 | - scheme: slate 31 | media: "(prefers-color-scheme: dark)" 32 | toggle: 33 | icon: material/eye 34 | name: Switch to light mode 35 | - scheme: default 36 | media: "(prefers-color-scheme: light)" 37 | toggle: 38 | icon: material/eye-outline 39 | name: Switch to dark mode 40 | 41 | markdown_extensions: 42 | - admonition 43 | - pymdownx.details 44 | - pymdownx.superfences 45 | 46 | plugins: 47 | - search 48 | # - sitemap 49 | - mkdocstrings: 50 | default_handler: python 51 | handlers: 52 | python: 53 | options: 54 | members_order: source 55 | show_symbol_type_heading: true 56 | -------------------------------------------------------------------------------- /.github/actions/full-checks/action.yml: -------------------------------------------------------------------------------- 1 | name: 'Full CI Checks' 2 | description: 'Runs full CI including integration tests with secrets' 3 | inputs: 4 | clerk-publishable-key: 5 | description: 'Clerk publishable key' 6 | required: true 7 | clerk-secret-key: 8 | description: 'Clerk secret key' 9 | required: true 10 | 11 | runs: 12 | using: 'composite' 13 | steps: 14 | - name: Check env vars set 15 | run: | 16 | if [ -z "${{ inputs.clerk-publishable-key }}" ]; then 17 | echo "CLERK_PUBLISHABLE_KEY is not set" 18 | exit 1 19 | fi 20 | if [ -z "${{ inputs.clerk-secret-key }}" ]; then 21 | echo "CLERK_SECRET_KEY is not set" 22 | exit 1 23 | fi 24 | shell: bash 25 | 26 | - name: Create .env 27 | run: | 28 | echo "CLERK_PUBLISHABLE_KEY=${{ inputs.clerk-publishable-key }}" >> .env 29 | echo "CLERK_SECRET_KEY=${{ inputs.clerk-secret-key }}" >> .env 30 | shell: bash 31 | 32 | - name: DEBUG - check python version 33 | run: uv sync && uv run python --version 34 | shell: bash 35 | 36 | - name: Run lint 37 | run: task lint 38 | shell: bash 39 | 40 | - name: Run typecheck 41 | run: task typecheck 42 | shell: bash 43 | 44 | - name: Initialize Reflex 45 | run: uv run reflex init 46 | working-directory: clerk_api_demo 47 | shell: bash 48 | 49 | - name: Install playwright 50 | run: uv run playwright install chromium 51 | shell: bash 52 | 53 | - name: Run tests 54 | run: task test 55 | shell: bash 56 | -------------------------------------------------------------------------------- /custom_components/reflex_clerk_api/pages.py: -------------------------------------------------------------------------------- 1 | """ 2 | This provides some example code for adding dedicated sign-in and sign-up pages to your app. 3 | """ 4 | 5 | import os 6 | 7 | import reflex as rx 8 | 9 | import reflex_clerk_api as clerk 10 | 11 | 12 | def add_sign_in_page( 13 | app: rx.App, publishable_key: str | None = None, route: str = "/sign-in" 14 | ) -> None: 15 | """ 16 | Adds a sign-in page that is customizable via the Clerk dashboard. 17 | """ 18 | assert route.startswith("/") 19 | publishable_key = publishable_key or os.environ["CLERK_PUBLISHABLE_KEY"] 20 | 21 | sign_in_page = clerk.clerk_provider( 22 | rx.center( 23 | rx.vstack( 24 | clerk.sign_in(path=route), 25 | align="center", 26 | spacing="7", 27 | ), 28 | height="100vh", 29 | ), 30 | publishable_key=publishable_key, 31 | ) 32 | app.add_page(sign_in_page, route=route + "/[[...splat]]") 33 | 34 | 35 | def add_sign_up_page( 36 | app: rx.App, publishable_key: str | None = None, route: str = "/sign-up" 37 | ) -> None: 38 | """ 39 | Adds a sign-up page that is customizable via the Clerk dashboard. 40 | """ 41 | assert route.startswith("/") 42 | publishable_key = publishable_key or os.environ["CLERK_PUBLISHABLE_KEY"] 43 | 44 | sign_up_page = clerk.clerk_provider( 45 | rx.center( 46 | rx.vstack( 47 | clerk.sign_up(path=route), 48 | align="center", 49 | spacing="7", 50 | ), 51 | height="100vh", 52 | ), 53 | publishable_key=publishable_key, 54 | ) 55 | app.add_page(sign_up_page, route=route + "/[[...splat]]") 56 | -------------------------------------------------------------------------------- /.github/workflows/_reusable-ci.yml: -------------------------------------------------------------------------------- 1 | name: Reusable CI 2 | 3 | on: 4 | workflow_call: 5 | inputs: 6 | checkout_ref: 7 | description: 'Git ref to checkout' 8 | required: false 9 | type: string 10 | default: '' 11 | checkout_repository: 12 | description: 'Repository to checkout (for forks)' 13 | required: false 14 | type: string 15 | default: '' 16 | check_type: 17 | description: 'Type of checks to run: basic or full' 18 | required: true 19 | type: string 20 | environment: 21 | description: 'Environment to use for secrets (only for full checks)' 22 | required: false 23 | type: string 24 | default: '' 25 | 26 | jobs: 27 | ci: 28 | runs-on: ubuntu-latest 29 | strategy: 30 | matrix: 31 | python-versions: ["3.11", "3.12", "3.13"] 32 | environment: ${{ inputs.environment || null }} 33 | 34 | steps: 35 | - uses: actions/checkout@v4 36 | with: 37 | ref: ${{ inputs.checkout_ref || github.sha }} 38 | repository: ${{ inputs.checkout_repository || github.repository }} 39 | 40 | - name: Setup Python Environment 41 | uses: ./.github/actions/setup-python-env 42 | with: 43 | python-version: ${{ matrix.python-versions }} 44 | github-token: ${{ secrets.GITHUB_TOKEN }} 45 | 46 | - name: Run Basic Checks 47 | if: inputs.check_type == 'basic' 48 | uses: ./.github/actions/basic-checks 49 | 50 | - name: Run Full Checks 51 | if: inputs.check_type == 'full' 52 | uses: ./.github/actions/full-checks 53 | with: 54 | clerk-publishable-key: ${{ vars.CLERK_PUBLISHABLE_KEY }} 55 | clerk-secret-key: ${{ secrets.CLERK_SECRET_KEY }} 56 | -------------------------------------------------------------------------------- /.github/workflows/full-ci-manual.yml: -------------------------------------------------------------------------------- 1 | name: Full CI (Manual) 2 | 3 | on: 4 | workflow_dispatch: 5 | inputs: 6 | pr_number: 7 | description: 'PR number to run full CI against' 8 | required: true 9 | type: string 10 | 11 | jobs: 12 | get-pr-info: 13 | runs-on: ubuntu-latest 14 | outputs: 15 | head_sha: ${{ steps.pr-info.outputs.head_sha }} 16 | head_repo: ${{ steps.pr-info.outputs.head_repo }} 17 | steps: 18 | - name: Get PR details 19 | id: pr-info 20 | uses: actions/github-script@v7 21 | with: 22 | script: | 23 | const pr = await github.rest.pulls.get({ 24 | owner: context.repo.owner, 25 | repo: context.repo.repo, 26 | pull_number: ${{ inputs.pr_number }} 27 | }); 28 | core.setOutput('head_sha', pr.data.head.sha); 29 | core.setOutput('head_repo', pr.data.head.repo.full_name); 30 | 31 | full-ci: 32 | needs: get-pr-info 33 | uses: ./.github/workflows/_reusable-ci.yml 34 | with: 35 | checkout_ref: ${{ needs.get-pr-info.outputs.head_sha }} 36 | checkout_repository: ${{ needs.get-pr-info.outputs.head_repo }} 37 | check_type: 'full' 38 | environment: 'demo' 39 | secrets: inherit 40 | 41 | comment-result: 42 | needs: [get-pr-info, full-ci] 43 | if: always() 44 | runs-on: ubuntu-latest 45 | steps: 46 | - name: Comment on PR 47 | uses: actions/github-script@v7 48 | with: 49 | script: | 50 | const status = '${{ needs.full-ci.result }}' === 'success' ? '✅ PASSED' : '❌ FAILED'; 51 | await github.rest.issues.createComment({ 52 | owner: context.repo.owner, 53 | repo: context.repo.repo, 54 | issue_number: ${{ inputs.pr_number }}, 55 | body: `Full CI ${status} for commit ${{ needs.get-pr-info.outputs.head_sha }} (manually triggered)` 56 | }); 57 | -------------------------------------------------------------------------------- /docs/migrating.md: -------------------------------------------------------------------------------- 1 | # Migration Guide 2 | 3 | This guide provides information on migrating from the `Kroo/reflex-clerk` package to the `reflex-clerk-api`. It's mostly a direct drop-in replacement, but there are a few small changes to be aware of. Also some improvements that fill some gaps in the previous package. 4 | 5 | ## Key Changes 6 | 7 | 1. **Import Path Update**: 8 | Update your imports to use `reflex_clerk_api` instead of `reflex_clerk`. 9 | 10 | 2. **Page Installation**: 11 | Use `clerk.add_sign_in_page(app)` and `clerk.add_sign_up_page(app)` instead of `clerk.install_pages(app)`. 12 | 13 | 3. **User Information Retrieval**: 14 | For full user info, use `await clerk.get_user()` inside event handlers instead of `clerk_state.user`. This makes the user data retrieval occur explicitly when needed. You can choose to cache the information however you like. 15 | 16 | 4. **ClerkUser**: 17 | If you just want basic user information, you can enable the `ClerkUser` state by setting `register_user_state=True` when calling `clerk.clerk_provider(...)`. 18 | 19 | 5. **On load Event Handling**: 20 | Wrap `on_load` events that depend on the user authentication state with `clerk.on_load()` to ensure the `ClerkState` is updated before other `on_load` events. This ensures that `is_signed_in` will be accurate. (This was not handled with the previous package). 21 | 22 | 6. **On Auth Change Handlers**: 23 | Use `clerk.register_on_auth_change_handler()` to register event handlers that are called on authentication changes (login/logout). (This was not handled with the previous package). 24 | 25 | 7. **Backend API**: 26 | Note that you can also import and directly use the `clerk_backend_api` if desired, as it is a dependency of this plugin. The `client` used by the `ClerkState` is available as a property `clerk_state.client`. 27 | 28 | !!! note 29 | 30 | The lower case `clerk_state` implies using `clerk_state = await self.get_state(clerk.ClerkState)` within an event handler method. 31 | -------------------------------------------------------------------------------- /.github/workflows/full-ci-comment.yml: -------------------------------------------------------------------------------- 1 | name: Full CI (Comment Triggered) 2 | 3 | on: 4 | issue_comment: 5 | types: [created] 6 | 7 | jobs: 8 | trigger-check: 9 | if: | 10 | github.event.issue.pull_request && 11 | contains(github.event.comment.body, '/run-full-ci') && 12 | github.event.comment.author_association == 'OWNER' 13 | runs-on: ubuntu-latest 14 | outputs: 15 | pr_head_sha: ${{ steps.pr.outputs.pr_head_sha }} 16 | pr_head_repo: ${{ steps.pr.outputs.pr_head_repo }} 17 | 18 | steps: 19 | - name: Get PR details 20 | id: pr 21 | uses: actions/github-script@v7 22 | with: 23 | script: | 24 | const pr = await github.rest.pulls.get({ 25 | owner: context.repo.owner, 26 | repo: context.repo.repo, 27 | pull_number: context.issue.number 28 | }); 29 | core.setOutput('pr_head_sha', pr.data.head.sha); 30 | core.setOutput('pr_head_repo', pr.data.head.repo.full_name); 31 | 32 | full-ci: 33 | needs: trigger-check 34 | uses: ./.github/workflows/_reusable-ci.yml 35 | with: 36 | checkout_ref: ${{ needs.trigger-check.outputs.pr_head_sha }} 37 | checkout_repository: ${{ needs.trigger-check.outputs.pr_head_repo }} 38 | check_type: 'full' 39 | environment: 'demo' 40 | secrets: inherit 41 | 42 | comment-result: 43 | needs: [trigger-check, full-ci] 44 | runs-on: ubuntu-latest 45 | # if the trigger-check job ran (i.e., the comment was valid) 46 | if: needs.trigger-check.result != 'skipped' 47 | steps: 48 | - name: Comment on PR 49 | uses: actions/github-script@v7 50 | with: 51 | script: | 52 | const status = '${{ needs.full-ci.result }}' === 'success' ? '✅ PASSED' : '❌ FAILED'; 53 | await github.rest.issues.createComment({ 54 | owner: context.repo.owner, 55 | repo: context.repo.repo, 56 | issue_number: context.issue.number, 57 | body: `Full CI ${status} for commit ${{ needs.trigger-check.outputs.pr_head_sha }}\n\nTriggered by: @${{ github.event.comment.user.login }}` 58 | }); 59 | -------------------------------------------------------------------------------- /Taskfile.yml: -------------------------------------------------------------------------------- 1 | version: "3" 2 | 3 | dotenv: [".env"] 4 | 5 | tasks: 6 | install: 7 | cmds: 8 | - uv sync --dev 9 | - uv run pre-commit install 10 | 11 | run: 12 | dir: clerk_api_demo 13 | cmds: 14 | - uv run reflex run 15 | 16 | run-docs: 17 | cmds: 18 | # Use non-default port to not clash with reflex backend 19 | - uv run mkdocs serve -a localhost:9000 20 | 21 | test: 22 | deps: 23 | - task: lint 24 | - task: typecheck 25 | cmds: 26 | - uv sync --dev 27 | - uv run playwright install chromium 28 | - uv run pytest 29 | 30 | lint: 31 | cmds: 32 | - uv run ruff check . 33 | 34 | typecheck: 35 | cmds: 36 | - uv run pyright 37 | 38 | bump-patch: 39 | cmds: 40 | - uvx hatch version patch 41 | - uv lock 42 | 43 | bump-minor: 44 | cmds: 45 | - uvx hatch version minor 46 | - uv lock 47 | 48 | bump-major: 49 | cmds: 50 | - uvx hatch version major 51 | - uv lock 52 | 53 | publish: 54 | preconditions: 55 | - sh: git status 56 | cmds: 57 | - git diff --quiet --staged || (echo "There are staged changes. They must be committed first" && exit 1) 58 | - git diff --quiet || (echo "There are unstaged changes. They must be committed first." && exit 1) 59 | 60 | - uvx hatch version | xargs -I {} git tag -a v{} -m "Release v{}" 61 | - git push --follow-tags 62 | 63 | pre-commit-all: 64 | cmds: 65 | - uv run pre-commit run --all-files 66 | 67 | pre-commit-update: 68 | cmds: 69 | - uv run pre-commit autoupdate 70 | 71 | manual-publish: 72 | preconditions: 73 | # Check environment variable 74 | - sh: test -n "$PYPI_TOKEN" 75 | msg: "PYPI_TOKEN is not set -- Add PYPI_TOKEN=... to .env file" 76 | cmds: 77 | - echo "Publishing to PyPI" 78 | # - uv run reflex component build # Until https://github.com/reflex-dev/reflex/issues/5114 is resolved 79 | - uvx reflex==0.7.5 component build 80 | - uv publish --token $PYPI_TOKEN 81 | 82 | manual-publish-docs: 83 | cmds: 84 | - uv run mkdocs gh-deploy --force 85 | 86 | share: 87 | desc: Share the component via the reflex library 88 | cmds: 89 | - uv run reflex component share 90 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [build-system] 2 | requires = ["setuptools", "wheel", "hatchling"] 3 | build-backend = "setuptools.build_meta" 4 | 5 | [project.optional-dependencies] 6 | dev = ["build", "twine"] 7 | 8 | [tool.setuptools.packages.find] 9 | where = ["custom_components"] 10 | 11 | [tool.setuptools.dynamic] 12 | version = { attr = "reflex_clerk_api.__version__" } 13 | 14 | [tool.hatch.version] 15 | path = "custom_components/reflex_clerk_api/__init__.py" 16 | 17 | [project] 18 | name = "reflex-clerk-api" 19 | description = "Reflex custom component wrapping @clerk/clerk-react and integrating the clerk-backend-api" 20 | readme = "README.md" 21 | license = "Apache-2.0" 22 | requires-python = ">=3.11" 23 | authors = [{ name = "Tim Child", email = "timjchild@gmail.com" }] 24 | keywords = ["reflex","reflex-custom-components", "clerk", "clerk-backend-api"] 25 | dynamic = ["version"] 26 | 27 | dependencies = [ 28 | "authlib>=1.5.1,<2.0.0", 29 | "clerk-backend-api>=2.0.0,<3.0.0", 30 | "reflex>=0.8.0", 31 | ] 32 | 33 | classifiers = [ 34 | "Development Status :: 4 - Beta", 35 | "Typing :: Typed", 36 | "Programming Language :: Python :: 3.11", 37 | "Programming Language :: Python :: 3.12", 38 | "Programming Language :: Python :: 3.13", 39 | # "Framework :: Reflex", # Should be added once this is accepted 40 | ] 41 | 42 | [project.urls] 43 | documentation = "https://timchild.github.io/reflex-clerk-api/about/" 44 | repository = "https://github.com/TimChild/reflex-clerk-api" 45 | issues = "https://github.com/TimChild/reflex-clerk-api/issues" 46 | homepage = "https://reflex-clerk-api-demo.adventuresoftim.com" 47 | 48 | [tool.pytest.ini_options] 49 | # addopts = "--headed" 50 | 51 | [tool.pyright] 52 | venvPath = "." 53 | venv = ".venv" 54 | 55 | [tool.ruff.lint] 56 | extend-select = ["I", "RUF", "T20"] 57 | ignore = ["RUF012"] 58 | 59 | [dependency-groups] 60 | dev = [ 61 | "coverage>=7.6.12", 62 | "dotenv>=0.9.9", 63 | "mkdocs-material>=9.6.9", 64 | "mkdocstrings-python>=1.16.8", 65 | "pre-commit>=4.1.0", 66 | "pyright>=1.1.396", 67 | "pytest>=8.3.5", 68 | "pytest-playwright>=0.7.0", 69 | "pytest-pretty>=1.2.0", 70 | "ruff>=0.9.10", 71 | "semver>=3.0.4", 72 | "reflex>=0.8.0", 73 | "hatchling>=1.27.0", 74 | "uvicorn>=0.38.0", 75 | "psutil>=7.0.0", 76 | ] 77 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ![Test Status](https://github.com/TimChild/reflex-clerk-api/actions/workflows/ci.yml/badge.svg?branch=v1.0.0) 2 | ![PyPi publish Status](https://github.com/TimChild/reflex-clerk-api/actions/workflows/publish.yml/badge.svg) 3 | ![Demo Deploy Status](https://github.com/TimChild/reflex-clerk-api/actions/workflows/deploy.yml/badge.svg) 4 | 5 | # reflex-clerk-api 6 | 7 | A Reflex custom component for integrating Clerk authentication into a Reflex application. 8 | 9 | See a [Demo](https://reflex-clerk-api-demo.adventuresoftim.com). 10 | 11 | See the [Docs](https://timchild.github.io/reflex-clerk-api/about/) 12 | 13 | ## Installation 14 | 15 | Any of: 16 | 17 | ```bash 18 | uv add reflex-clerk-api 19 | 20 | pip install reflex-clerk-api 21 | 22 | poetry add reflex-clerk-api 23 | ``` 24 | 25 | ## Usage 26 | 27 | ```python 28 | import reflex_clerk_api as clerk 29 | 30 | def index() -> rx.Component: 31 | return clerk.clerk_provider( 32 | rx.container( 33 | clerk.clerk_loaded( 34 | clerk.signed_in( 35 | clerk.sign_on( 36 | rx.button("Sign out"), 37 | ), 38 | ), 39 | clerk.signed_out( 40 | rx.button("Sign in"), 41 | ), 42 | ), 43 | ), 44 | publishable_key=os.environ["CLERK_PUBLISHABLE_KEY"], 45 | secret_key=os.environ["CLERK_SECRET_KEY"], 46 | register_user_state=True, 47 | ) 48 | ``` 49 | 50 | ## Contributing 51 | 52 | Feel free to open issues or make PRs. 53 | 54 | Usual process for contributing: 55 | 56 | - Fork the repo 57 | - Make changes on a feature branch 58 | - Ideally, add tests for any changes (this will mean your changes don't get broken in the future too). 59 | - Submit a PR 60 | 61 | I use [Taskfile](https://taskfile.dev/) (similar to `makefile`) to make common tasks easier. If you have that installed, you can run: 62 | 63 | - `task install` -- Install dev dependencies and pre-commit. 64 | - `task run` -- Run the demo locally 65 | - `task run-docs` -- Run the docs locally 66 | - `task test` -- Run tests 67 | - `task bump-patch/minor/major` -- Bump the version (`patch` for a bug fix, `minor` for an added feature). 68 | 69 | 70 | ## TODO: 71 | 72 | - How should the `condition` and `fallback` props be defined on `Protect`? They are supposed to be `Javascript` and `JSX` respectively, but are just `str` for now... Is `Javascript` `rx.Script`? And `JSX` `rx.Component`? 73 | -------------------------------------------------------------------------------- /custom_components/reflex_clerk_api/user_components.py: -------------------------------------------------------------------------------- 1 | import reflex as rx 2 | 3 | from reflex_clerk_api.base import ClerkBase 4 | from reflex_clerk_api.models import Appearance 5 | 6 | 7 | class UserButton(ClerkBase): 8 | tag = "UserButton" 9 | 10 | after_switch_session_url: str | None = None 11 | "The full URL or path to navigate to after a successful account change in a multi-session app." 12 | 13 | # NOTE: `apperance.base_theme` does not work yet. 14 | appearance: Appearance | None = None 15 | """Optional object to style your components. Will only affect Clerk components.""" 16 | 17 | default_open: bool | None = None 18 | "Controls whether the should open by default during the first render." 19 | 20 | show_name: bool | None = None 21 | "Controls if the user name is displayed next to the user image button." 22 | 23 | sign_in_url: str | None = None 24 | "The full URL or path to navigate to when the Add another account button is clicked. It's recommended to use the environment variable instead." 25 | 26 | user_profile_mode: str | None = None 27 | "Controls whether selecting the Manage your account button will cause the component to open as a modal, or if the browser will navigate to the userProfileUrl where is mounted as a page. Defaults to: 'modal'." 28 | 29 | user_profile_props: dict | None = None 30 | "Specify options for the underlying component. For example: {additionalOAuthScopes: {google: ['foo', 'bar'], github: ['qux']}}." 31 | 32 | user_profile_url: str | None = None 33 | "The full URL or path leading to the user management interface." 34 | 35 | fallback: rx.Component | None = None 36 | "An optional element to be rendered while the component is mounting." 37 | 38 | 39 | class UserProfile(ClerkBase): 40 | tag = "UserProfile" 41 | 42 | appearance: Appearance | None = None 43 | """Optional object to style your components. Will only affect Clerk components.""" 44 | 45 | routing: str | None = None 46 | "The routing strategy for your pages. Defaults to 'path' for frameworks that handle routing, such as Next.js and Remix. Defaults to hash for all other SDK's, such as React." 47 | 48 | path: str | None = None 49 | "The path where the component is mounted on when routing is set to path. It is ignored in hash-based routing. For example: /user-profile." 50 | 51 | additional_oauth_scopes: dict | None = None 52 | "Specify additional scopes per OAuth provider that your users would like to provide if not already approved. For example: {google: ['foo', 'bar'], github: ['qux']}." 53 | 54 | fallback: rx.Component | None = None 55 | "An optional element to be rendered while the component is mounting." 56 | 57 | 58 | user_button = UserButton.create 59 | user_profile = UserProfile.create 60 | -------------------------------------------------------------------------------- /custom_components/reflex_clerk_api/unstyled_components.py: -------------------------------------------------------------------------------- 1 | from reflex_clerk_api.base import ClerkBase 2 | 3 | 4 | class SignUpButton(ClerkBase): 5 | tag = "SignUpButton" 6 | 7 | force_redirect_url: str | None = None 8 | "If provided, this URL will always be redirected to after the user signs up. It's recommended to use the environment variable instead." 9 | 10 | fallback_redirect_url: str | None = None 11 | "The fallback URL to redirect to after the user signs up, if there's no redirect_url in the path already. Defaults to /. It's recommended to use the environment variable instead." 12 | 13 | sign_in_force_redirect_url: str | None = None 14 | "If provided, this URL will always be redirected to after the user signs in. It's recommended to use the environment variable instead." 15 | 16 | sign_in_fallback_redirect_url: str | None = None 17 | "The fallback URL to redirect to after the user signs in, if there's no redirect_url in the path already. Defaults to /. It's recommended to use the environment variable instead." 18 | 19 | mode: str | None = None 20 | "Determines what happens when a user clicks on the . Setting this to 'redirect' will redirect the user to the sign-up route. Setting this to 'modal' will open a modal on the current route. Defaults to 'redirect'." 21 | 22 | 23 | class SignInButton(ClerkBase): 24 | tag = "SignInButton" 25 | 26 | force_redirect_url: str | None = None 27 | "If provided, this URL will always be redirected to after the user signs in. It's recommended to use the environment variable instead." 28 | 29 | fallback_redirect_url: str | None = None 30 | "The fallback URL to redirect to after the user signs in, if there's no redirect_url in the path already. Defaults to /. It's recommended to use the environment variable instead." 31 | 32 | sign_up_force_redirect_url: str | None = None 33 | "If provided, this URL will always be redirected to after the user signs up. It's recommended to use the environment variable instead." 34 | 35 | sign_up_fallback_redirect_url: str | None = None 36 | "The fallback URL to redirect to after the user signs up, if there's no redirect_url in the path already. Defaults to /. It's recommended to use the environment variable instead." 37 | 38 | mode: str | None = None 39 | "Determines what happens when a user clicks on the . Setting this to 'redirect' will redirect the user to the sign-in route. Setting this to 'modal' will open a modal on the current route. Defaults to 'redirect'." 40 | 41 | 42 | class SignOutButton(ClerkBase): 43 | tag = "SignOutButton" 44 | 45 | redirect_url: str | None = None 46 | "The full URL or path to navigate after successful sign-out." 47 | 48 | 49 | sign_up_button = SignUpButton.create 50 | sign_in_button = SignInButton.create 51 | sign_out_button = SignOutButton.create 52 | -------------------------------------------------------------------------------- /docs/about.md: -------------------------------------------------------------------------------- 1 | # Reflex Clerk API Documentation 2 | 3 | Welcome to the Reflex Clerk API documentation! This package provides integration with the [Clerk](https://clerk.com) authentication service, allowing you to easily manage user authentication within within your [Reflex](https://reflex.dev) app. 4 | 5 | ## Overview 6 | 7 | Primarily, this package wraps the [@clerk/clerk-react](https://www.npmjs.com/package/@clerk/clerk-react) library, using the Clerk maintained [clerk-backend-api](https://pypi.org/project/clerk-backend-api/") python package to synchronize the reflex FastAPI backend with the Clerk frontend components. 8 | 9 | ### Wrapped Components 10 | 11 | An overview of some of the clerk-react components that are wrapped here: 12 | 13 | - **ClerkProvider**: A component that wraps your app/page to handle Clerk authentication. 14 | - **Control Components**: Components such as `clerk_loaded`, `protect`, and `signed_in`, etc. 15 | - **Authentication Components**: Components for `sign_in` and `sign_up` that redirect the user to Clerk's authentication pages. 16 | - **Wrapper Components**: Button wrappers for `sign_in_button`, `sign_out_button`, and `user_button`, that you can wrap regular reflex components with. 17 | 18 | See more in the [features](features.md) section. 19 | 20 | ### Backend synchronization 21 | 22 | The `ClerkProvider` state is set up so that the backend states are synchronized with the Clerk authentication that happens in the frontend. The two main reflex backend states are: 23 | 24 | - **ClerkState**: Manages the authentication state of the user. 25 | - **ClerkUser**: Optional state for accessing additional user information. 26 | 27 | Additionally, you can keep your own states up to date by registering event handlers to be called on authentication changes with e.g. `clerk.register_on_auth_change_handler(State.some_handler)`. 28 | 29 | ### Additional Notes 30 | 31 | This packages: 32 | 33 | - Is fully asynchronous, using `async/await` for all requests to the Clerk backend. 34 | - Supports Reflex 0.7.x. 35 | - Provides helper functions for handling `on_load` events that require knowledge of user authentication status. 36 | - Allows registration of event handlers to be called on authentication changes (login/logout). 37 | - Is fully typed 38 | - Is tested against python versions 3.11 - 3.13 39 | 40 | ## Demo 41 | 42 | See a demo of `reflex-clerk-api` [here](https://reflex-clerk-api-demo.adventuresoftim.com). 43 | 44 | The demo uses a development Clerk account so you can try out sign-up/sign-in etc. 45 | 46 | ## Quick Links 47 | 48 | Additionally, you can find the following resources in the documentation: 49 | 50 | - [Getting Started](getting_started.md): Learn how to install and set up the Reflex Clerk API. 51 | - [Migration Guide](migrating.md): Notes on migrating from the `Kroo/reflex-clerk` package. 52 | - [Features](features.md): More details on the additional features provided. 53 | 54 | ## Not yet implemented 55 | 56 | Some of these things are minimally implemented, but not at all tested, and likely without the additional props. 57 | 58 | - **GoogleOneTap** 59 | - **Waitlist** 60 | - **Organization Components** 61 | -------------------------------------------------------------------------------- /.github/workflows/deploy.yml: -------------------------------------------------------------------------------- 1 | name: Deploy Demo to VPS 2 | 3 | on: 4 | workflow_run: 5 | workflows: ["Publish"] 6 | types: 7 | - completed 8 | workflow_dispatch: 9 | 10 | concurrency: 11 | group: ${{ github.workflow }}-${{ github.ref }} 12 | cancel-in-progress: true 13 | 14 | jobs: 15 | deploy-frontend: 16 | runs-on: ubuntu-latest 17 | # If previous workflow was successful or this was triggered manually 18 | if: ${{ github.event.workflow_run.conclusion == 'success' && github.repository == 'TimChild/reflex-clerk-api' }} || github.event_name == 'workflow_dispatch' 19 | environment: 20 | name: demo 21 | permissions: 22 | contents: read 23 | 24 | steps: 25 | - name: Checkout code 26 | uses: actions/checkout@v4 27 | 28 | - name: Check env vars set 29 | run: | 30 | if [ -z "${{ vars.CLERK_PUBLISHABLE_KEY }}" ]; then 31 | echo "CLERK_PUBLISHABLE_KEY is not set" 32 | exit 1 33 | fi 34 | 35 | - name: Create .env 36 | run: | 37 | echo "CLERK_PUBLISHABLE_KEY=${{ vars.CLERK_PUBLISHABLE_KEY }}" >> .env 38 | # echo "CLERK_SECRET_KEY=${{ secrets.CLERK_SECRET_KEY }}" >> .env 39 | 40 | - name: Deploy frontend 41 | uses: TimChild/webserver-template/actions/deploy-reflex-frontend@main 42 | with: 43 | vps-ip: ${{ vars.VPS_IP }} 44 | site-name: ${{ vars.SITE_NAME }} 45 | ssh-private-key: ${{ secrets.SSH_PRIVATE_KEY }} 46 | ssh-user: "webadmin" 47 | working-directory: clerk_api_demo 48 | 49 | - name: Remove .env 50 | if: always() 51 | run: rm .env 52 | 53 | deploy-backend: 54 | runs-on: ubuntu-latest 55 | if: ${{ github.event.workflow_run.conclusion == 'success' && github.repository == 'TimChild/reflex-clerk-api' }} || github.event_name == 'workflow_dispatch' 56 | environment: 57 | name: demo 58 | permissions: 59 | contents: read 60 | packages: write 61 | 62 | steps: 63 | - name: Checkout code 64 | uses: actions/checkout@v4 65 | 66 | - name: Check env vars set 67 | run: | 68 | if [ -z "${{ vars.CLERK_PUBLISHABLE_KEY }}" ]; then 69 | echo "CLERK_PUBLISHABLE_KEY is not set" 70 | exit 1 71 | fi 72 | if [ -z "${{ secrets.CLERK_SECRET_KEY }}" ]; then 73 | echo "CLERK_SECRET_KEY is not set" 74 | exit 1 75 | fi 76 | 77 | - name: Create .env 78 | run: | 79 | echo "CLERK_PUBLISHABLE_KEY=${{ vars.CLERK_PUBLISHABLE_KEY }}" >> .env 80 | echo "CLERK_SECRET_KEY=${{ secrets.CLERK_SECRET_KEY }}" >> .env 81 | 82 | - name: Deploy backend 83 | uses: TimChild/webserver-template/actions/deploy-reflex-backend@main 84 | with: 85 | vps-ip: ${{ vars.VPS_IP }} 86 | site-name: ${{ vars.SITE_NAME }} 87 | dotenv-path: .env 88 | ssh-private-key: ${{ secrets.SSH_PRIVATE_KEY }} 89 | ssh-user: "webadmin" 90 | working-directory: clerk_api_demo 91 | 92 | - name: Remove .env 93 | if: always() 94 | run: rm .env 95 | -------------------------------------------------------------------------------- /custom_components/reflex_clerk_api/models.py: -------------------------------------------------------------------------------- 1 | from typing import Any, Literal 2 | 3 | from reflex.components.props import PropsBase 4 | 5 | LiteralBaseTheme = Literal["default", "dark", "neobrutalism", "shadesOfPurple"] 6 | 7 | 8 | class Layout(PropsBase): 9 | animations: bool = True 10 | """Whether to enable animations inside the components. Defaults to true.""" 11 | 12 | help_page_url: str = "" 13 | """The URL to your help page.""" 14 | 15 | logo_image_url: str = "" 16 | """The URL to your logo image.""" 17 | 18 | logo_link_url: str = "" 19 | """Controls where the browser will redirect to after the user clicks the application logo.""" 20 | 21 | logo_placement: str = "inside" 22 | """The placement of your logo. Defaults to 'inside'.""" 23 | 24 | privacy_page_url: str = "" 25 | """The URL to your privacy page.""" 26 | 27 | shimmer: bool = True 28 | """Enables the shimmer animation for avatars. Defaults to true.""" 29 | 30 | show_optional_fields: bool = True 31 | """Whether to show optional fields on the sign in and sign up forms. Defaults to true.""" 32 | 33 | social_buttons_placement: str = "top" 34 | """The placement of your social buttons. Defaults to 'top'.""" 35 | 36 | social_buttons_variant: str = "auto" 37 | """The variant of your social buttons.""" 38 | 39 | terms_page_url: str = "" 40 | """The URL to your terms page.""" 41 | 42 | unsafe_disable_development_mode_warnings: bool = False 43 | """Whether development warnings show up in development mode.""" 44 | 45 | 46 | class Variables(PropsBase): 47 | color_primary: str = "" 48 | color_danger: str = "" 49 | color_success: str = "" 50 | color_warning: str = "" 51 | color_neutral: str = "" 52 | color_text: str = "" 53 | color_text_on_primary_background: str = "" 54 | color_text_secondary: str = "" 55 | color_background: str = "" 56 | color_input_text: str = "" 57 | color_input_background: str = "" 58 | color_shimmer: str = "" 59 | font_family: str = "inherit" 60 | font_family_buttons: str = "inherit" 61 | font_size: str = "0.8125rem" 62 | font_weight: dict[str, int] = { 63 | "normal": 400, 64 | "medium": 500, 65 | "semibold": 600, 66 | "bold": 700, 67 | } 68 | border_radius: str = "0.375rem" 69 | spacing_unit: str = "1rem" 70 | 71 | 72 | class Captcha(PropsBase): 73 | theme: str = "auto" 74 | """The CAPTCHA widget theme. Defaults to auto.""" 75 | 76 | size: str = "normal" 77 | """The CAPTCHA widget size. Defaults to normal.""" 78 | 79 | language: str = "" 80 | """The CAPTCHA widget language/locale.""" 81 | 82 | 83 | class Appearance(PropsBase): 84 | # TODO: This needs to reference the actual theme object, not a string -- don't know how to do that yet. 85 | # base_theme: LiteralBaseTheme = "default" 86 | # """A theme used as the base theme for the components.""" 87 | 88 | layout: Layout | None = None 89 | """Configuration options that affect the layout of the components.""" 90 | 91 | variables: Variables | None = None 92 | """General theme overrides.""" 93 | 94 | elements: dict[str, Any] | None = None 95 | """Fine-grained theme overrides.""" 96 | 97 | captcha: Captcha | None = None 98 | """Configuration options that affect the appearance of the CAPTCHA widget.""" 99 | -------------------------------------------------------------------------------- /custom_components/reflex_clerk_api/control_components.py: -------------------------------------------------------------------------------- 1 | from reflex_clerk_api.base import ClerkBase 2 | 3 | Javascript = str 4 | JSX = str 5 | SignInInitialValues = dict[str, str] 6 | SignUpInitialValues = dict[str, str] 7 | 8 | 9 | class ClerkLoaded(ClerkBase): 10 | """Only renders children after authentication has been checked.""" 11 | 12 | tag = "ClerkLoaded" 13 | 14 | 15 | class ClerkLoading(ClerkBase): 16 | """Only renders childen while Clerk authenticates the user.""" 17 | 18 | tag = "ClerkLoading" 19 | 20 | 21 | class Protect(ClerkBase): 22 | tag = "Protect" 23 | 24 | condition: Javascript | None = None 25 | "Optional conditional logic that renders the children if it returns true" 26 | fallback: JSX | None = None 27 | "An optional snippet of JSX to show when a user doesn't have the role or permission to access the protected content." 28 | permission: str | None = None 29 | "Optional string corresponding to a Role's Permission in the format org::" 30 | role: str | None = None 31 | "Optional string corresponding to an Organization's Role in the format org:" 32 | 33 | 34 | class RedirectToSignIn(ClerkBase): 35 | """Immediately redirects the user to the sign in page when rendered.""" 36 | 37 | tag = "RedirectToSignIn" 38 | 39 | sign_in_fallback_redirect_url: str | None = None 40 | "The fallback URL to redirect to after the user signs in, if there's no redirect_url in the path already. Defaults to /." 41 | sign_in_force_redirect_url: str | None = None 42 | "If provided, this URL will always be redirected to after the user signs in." 43 | initial_values: SignInInitialValues | None = None 44 | "The values used to prefill the sign-in fields with." 45 | 46 | 47 | class RedirectToSignUp(ClerkBase): 48 | """Immediately redirects the user to the sign up page when rendered.""" 49 | 50 | tag = "RedirectToSignUp" 51 | 52 | sign_up_fallback_redirect_url: str | None = None 53 | "The fallback URL to redirect to after the user signs up, if there's no redirect_url in the path already. Defaults to /." 54 | sign_up_force_redirect_url: str | None = None 55 | "If provided, this URL will always be redirected to after the user signs up." 56 | initial_values: SignUpInitialValues | None = None 57 | "The values used to prefill the sign-up fields with." 58 | 59 | 60 | class RedirectToUserProfile(ClerkBase): 61 | """Immediately redirects the user to their profile page when rendered.""" 62 | 63 | tag = "RedirectToUserProfile" 64 | 65 | 66 | class RedirectToOrganizationProfile(ClerkBase): 67 | tag = "RedirectToOrganizationProfile" 68 | 69 | 70 | class RedirectToCreateOrganization(ClerkBase): 71 | tag = "RedirectToCreateOrganization" 72 | 73 | 74 | class SignedIn(ClerkBase): 75 | """Only renders children when the user is signed in.""" 76 | 77 | tag = "SignedIn" 78 | 79 | 80 | class SignedOut(ClerkBase): 81 | """Only renders children when the user is signed out.""" 82 | 83 | tag = "SignedOut" 84 | 85 | 86 | clerk_loaded = ClerkLoaded.create 87 | clerk_loading = ClerkLoading.create 88 | protect = Protect.create 89 | redirect_to_sign_in = RedirectToSignIn.create 90 | redirect_to_sign_up = RedirectToSignUp.create 91 | redirect_to_user_profile = RedirectToUserProfile.create 92 | redirect_to_organization_profile = RedirectToOrganizationProfile.create 93 | redirect_to_create_organization = RedirectToCreateOrganization.create 94 | signed_in = SignedIn.create 95 | signed_out = SignedOut.create 96 | -------------------------------------------------------------------------------- /docs/getting_started.md: -------------------------------------------------------------------------------- 1 | # Getting Started with Reflex Clerk API 2 | 3 | Welcome to the getting started guide for the `reflex-clerk-api`! 4 | 5 | You can add `Clerk` user authentication to your app for free (up to 10,000 users) ([Clerk Pricing](https://clerk.com/pricing)). 6 | 7 | This covers the basics to add authentication to your Reflex app. 8 | 9 | ## Clerk API 10 | 11 | Sign up for a Clerk account [here](https://dashboard.clerk.com/sign-up). 12 | 13 | ## Installation 14 | 15 | Installation is the same an any other python package: 16 | 17 | ### Using pip 18 | 19 | ```bash 20 | pip install reflex-clerk-api 21 | ``` 22 | 23 | ### Using a package manager 24 | 25 | ```bash 26 | uv add reflex-clerk-api 27 | ``` 28 | 29 | or 30 | 31 | ```bash 32 | poetry add reflex-clerk-api 33 | ``` 34 | 35 | etc. 36 | 37 | ## Basic Setup 38 | 39 | After installation, you can start integrating the Clerk components into your Reflex application. 40 | 41 | ### Import the Package 42 | 43 | To use the Reflex Clerk API in your app, start by importing the package: 44 | 45 | ```python 46 | import reflex_clerk_api as clerk 47 | ``` 48 | 49 | All examples here assume the package is imported as `clerk`. 50 | 51 | ### Setting Up ClerkProvider 52 | 53 | Typically, you'll wrap whole pages with the `ClerkProvider` component. This is required for clerk components within to work. This is a minimal example: 54 | 55 | ```python 56 | import os 57 | 58 | import reflex as rx 59 | import reflex_clerk_api as clerk 60 | 61 | def index() -> rx.Component: 62 | return clerk.clerk_provider( 63 | clerk.clerk_loading( 64 | rx.spinner(), 65 | ), 66 | clerk.clerk_loaded( 67 | clerk.signed_in( 68 | clerk.sign_out_button(rx.button("Sign out")) 69 | ), 70 | clerk.signed_out( 71 | clerk.sign_in_button(rx.button("Sign in")) 72 | ), 73 | ), 74 | publishable_key=os.environ["CLERK_PUBLISHABLE_KEY"], 75 | secret_key=os.environ["CLERK_SECRET_KEY"], 76 | register_user_state=True, 77 | ) 78 | 79 | app = rx.App() 80 | app.add_page(index) 81 | ``` 82 | 83 | While Clerk is loading (checking user authentication), the spinner will be displayed. Then either the sign-in or sign-out button will be displayed based on the user's authentication status. 84 | 85 | The `publishable_key` and `secret_key` can be obtained from your [Clerk dashboard](https://dashboard.clerk.com) (Configure/API keys). Read more [here](https://clerk.com/glossary/api-key) 86 | 87 | !!! note 88 | 89 | The `register_user_state` parameter is optional. Setting this to `True` enables the `clerk.ClerkUser` state which can be used to access or display basic user information. 90 | 91 | **alternatively** 92 | 93 | Wrap the entire app (all pages) via: 94 | 95 | ```python 96 | 97 | clerk.wrap_app(app, publishable_key=...) 98 | ``` 99 | 100 | Taking the same arguments as `clerk.clerk_provider`. 101 | 102 | ### Environment Variables 103 | 104 | A good way to provide the keys is via environment variables (to avoid accidentally sharing them). You can do this by creating a `.env` file in the root of your project with: 105 | 106 | ``` 107 | CLERK_PUBLISHABLE_KEY=your_publishable_key 108 | CLERK_SECRET_KEY=your_secret_key 109 | ``` 110 | 111 | Then you can use the `python-dotenv` package to load the variables: 112 | 113 | ```bash 114 | pip install python-dotenv 115 | ``` 116 | 117 | ```python 118 | from dotenv import load_dotenv 119 | load_dotenv() 120 | ``` 121 | 122 | This will load the environment variables from the `.env` file into the `os.environ` dictionary. 123 | 124 | !!! warning 125 | 126 | Make sure to add the `.env` file to your `.gitignore` file to avoid accidentally sharing your keys. 127 | 128 | ## Adding Sign-In and Sign-Up Pages 129 | 130 | You can additionally add some pages for signing in and signing up. By default they will be available at `/sign-in` and `/sign-up` respectively. 131 | 132 | ```python 133 | app = rx.App() 134 | app.add_page(index) 135 | clerk.add_sign_in_page(app) 136 | clerk.add_sign_up_page(app) 137 | ``` 138 | 139 | ## Next Steps 140 | 141 | - Explore more features of the Reflex Clerk API in the [Features](features.md) section. 142 | - Visit the [Demo](https://reflex-clerk-api-demo.adventuresoftim.com) to see the Reflex Clerk API in action. 143 | - Check out the [Migration Guide](migrating.md) if you're migrating from the `Kroo/reflex-clerk` package. 144 | -------------------------------------------------------------------------------- /docs/features.md: -------------------------------------------------------------------------------- 1 | # Features of Reflex Clerk API 2 | 3 | This documentation outlines the key components and functionalities available in the package. 4 | 5 | See the [Reference](full_docs/clerk_provider.md) for more detailed documentation. This is intended to be more of an overview. 6 | 7 | ## Reflex Synchronization 8 | 9 | `reflex-clerk-api` ensures that the backend states are synchronized with the Clerk authentication happening in the frontend. The main backend states include: 10 | 11 | - **ClerkState**: Manages the authentication state of the user. You'll mostly want the `is_signed_in` and `user_id` attributes. Access them from your own event handlers like so: 12 | 13 | ```python 14 | @rx.event 15 | async def some_handler(self): 16 | clerk_state = await self.get_state(clerk.ClerkState) 17 | user_id = clerk_state.user_id 18 | ... 19 | ``` 20 | 21 | - **ClerkUser**: Provides access to additional user information like `image_url` and `email`. 22 | 23 | !!! note 24 | 25 | To enable the `ClerkUser` state, set `clerk_provider(..., register_user_state=True)` when wrapping your page. 26 | 27 | This is not enabled by default since you may want to get the information you need yourself. 28 | 29 | There is also a helper method for getting more user info. 30 | 31 | ## Helper methods 32 | 33 | - **On Load Event Handling**: Use `clerk.on_load()` to ensure the `ClerkState` is updated before other `on_load` events. This ensures that `is_signed_in` will be accurate. 34 | 35 | - **On Auth Change Handlers**: Register event handlers that are called on authentication changes (login/logout) using `clerk.register_on_auth_change_handler()`. 36 | 37 | - **Get User Info**: Use `await clerk.get_user(self)` within an event handler to get the full Clerk `User` model. 38 | 39 | ## Clerk Components 40 | 41 | The components implementation largely follows that of the [Clerk react overview](https://clerk.com/docs/references/react/overview). 42 | 43 | The components are dscribed here with their `CamelCase` names as they are in the clerk react. To use them in your app, use the `snake_case` versions e.g. `clerk.clerk_provider(...)`. 44 | 45 | ### ClerkProvider 46 | 47 | The `ClerkProvider` component should wrap the contents of your app. 48 | It is required for the clerk frontend components, and is set up in a way that ensures the reflex backend is synchronized. 49 | 50 | !!! note 51 | 52 | This is additionally wrapped in a custom `ClerkSessionSynchronizer` component to facilitate the backend synchronization. 53 | 54 | ### Authentication Components 55 | 56 | I.e., **SignIn** and **SignUp** Components to create customizable sign-in and sign-up forms. 57 | 58 | ### User Components 59 | 60 | For displaying the currently signed in user's information via clerk components. 61 | 62 | - **UserButton**: A google style avatar button that opens a dropdown with user info. 63 | 64 | - **UserProfile**: Displays a user profile with additional information. 65 | 66 | ### Organization Components 67 | 68 | These are only minimally implemented, and not tested. If you would like to use these, I will happily accept pull requests. 69 | 70 | ### Waitlist Component 71 | 72 | This is only minimally implemented, and not tested. If you would like to use this, I will happily accept pull requests. 73 | 74 | ### Control Components 75 | 76 | These determine what content is displayed based on the user's authentication state. Use them to wrap parts of your app that should only be displayed under certain circumstances. 77 | 78 | - **ClerkLoaded**: Displays content once Clerk is fully loaded. 79 | 80 | - **ClerkLoading**: Displays content while Clerk is loading. 81 | 82 | - **Protect**: Protects specific content to ensure only authenticated users can access them. 83 | 84 | - **RedirectToSignIn**: Redirects users to the sign-in page if they are not authenticated. 85 | 86 | - **RedirectToSignUp**: Redirects users to the sign-up page if they are not authenticated. 87 | 88 | - **RedirectToUserProfile**: Redirects users to their profile page. 89 | 90 | - **RedirectToOrganizationProfile**: Redirects users to their organization profile page. 91 | 92 | - **RedirectToCreateOrganization**: Redirects users to create an organization. 93 | 94 | - **SignedIn** and **SignedOut**: Conditional rendering based on user authentication state. 95 | 96 | ### Unstyled Components 97 | 98 | Wrap regular reflex components with these to add the Clerk functionality. 99 | 100 | - **SignInButton**, **SignUpButton**, and **SignOutButton**: Button components to trigger sign-in and sign-out actions. 101 | 102 | E.g. `clerk.sign_in_button(rx.button("Sign in"))` 103 | 104 | - **SignInWithMetamaskButton**: Not yet implemented. 105 | 106 | ## Demos 107 | 108 | To see these features in action, visit the [demo](https://reflex-clerk-api-demo.adventuresoftim.com). 109 | -------------------------------------------------------------------------------- /custom_components/reflex_clerk_api/organization_components.py: -------------------------------------------------------------------------------- 1 | from typing import Optional 2 | 3 | from reflex_clerk_api.base import ClerkBase 4 | 5 | 6 | class CreateOrganization(ClerkBase): 7 | """ 8 | The CreateOrganization component provides a form for users to create new organizations. 9 | 10 | This component renders Clerk's React component, 11 | allowing users to set up new organizations with customizable appearance and routing. 12 | 13 | Props: 14 | appearance: Optional object to style your components. Will only affect Clerk components. 15 | path: The path where the component is mounted when routing is set to 'path'. 16 | routing: The routing strategy for your pages. Defaults to 'path' for frameworks 17 | that handle routing, or 'hash' for other SDKs. 18 | after_create_organization_url: The full URL or path to navigate to after creating an organization. 19 | fallback: An optional element to be rendered while the component is mounting. 20 | """ 21 | 22 | tag = "CreateOrganization" 23 | 24 | # Optional props that CreateOrganization supports 25 | appearance: Optional[str] = None 26 | path: Optional[str] = None 27 | routing: Optional[str] = None 28 | after_create_organization_url: Optional[str] = None 29 | fallback: Optional[str] = None 30 | 31 | 32 | class OrganizationProfile(ClerkBase): 33 | """ 34 | The OrganizationProfile component allows users to manage their organization membership and security settings. 35 | 36 | This component renders Clerk's React component, 37 | allowing users to manage organization information, members, billing, and security settings. 38 | 39 | Props: 40 | appearance: Optional object to style your components. Will only affect Clerk components. 41 | path: The path where the component is mounted when routing is set to 'path'. 42 | routing: The routing strategy for your pages. Defaults to 'path' for frameworks 43 | that handle routing, or 'hash' for other SDKs. 44 | after_leave_organization_url: The full URL or path to navigate to after leaving an organization. 45 | custom_pages: An array of custom pages to add to the organization profile. 46 | fallback: An optional element to be rendered while the component is mounting. 47 | """ 48 | 49 | tag = "OrganizationProfile" 50 | 51 | # Optional props that OrganizationProfile supports 52 | appearance: Optional[str] = None 53 | path: Optional[str] = None 54 | routing: Optional[str] = None 55 | after_leave_organization_url: Optional[str] = None 56 | custom_pages: Optional[str] = None 57 | fallback: Optional[str] = None 58 | 59 | 60 | class OrganizationSwitcher(ClerkBase): 61 | """ 62 | The OrganizationSwitcher component displays the currently active organization and allows users to switch between organizations. 63 | 64 | This component renders Clerk's React component, 65 | providing a dropdown interface for organization switching with customizable appearance. 66 | 67 | Props: 68 | appearance: Optional object to style your components. Will only affect Clerk components. 69 | organization_profile_mode: Controls whether selecting the organization opens as a modal or navigates to a page. 70 | organization_profile_url: The full URL or path leading to the organization management interface. 71 | create_organization_mode: Controls whether selecting create organization opens as a modal or navigates to a page. 72 | create_organization_url: The full URL or path leading to the create organization interface. 73 | after_leave_organization_url: The full URL or path to navigate to after leaving an organization. 74 | after_create_organization_url: The full URL or path to navigate to after creating an organization. 75 | after_select_organization_url: The full URL or path to navigate to after selecting an organization. 76 | default_open: Controls whether the OrganizationSwitcher should open by default during the first render. 77 | hide_personal_account: Controls whether the personal account option is hidden in the switcher. 78 | fallback: An optional element to be rendered while the component is mounting. 79 | """ 80 | 81 | tag = "OrganizationSwitcher" 82 | 83 | # Optional props that OrganizationSwitcher supports 84 | appearance: Optional[str] = None 85 | organization_profile_mode: Optional[str] = None 86 | organization_profile_url: Optional[str] = None 87 | create_organization_mode: Optional[str] = None 88 | create_organization_url: Optional[str] = None 89 | after_leave_organization_url: Optional[str] = None 90 | after_create_organization_url: Optional[str] = None 91 | after_select_organization_url: Optional[str] = None 92 | default_open: Optional[str] = None 93 | hide_personal_account: Optional[str] = None 94 | fallback: Optional[str] = None 95 | 96 | 97 | class OrganizationList(ClerkBase): 98 | """ 99 | The OrganizationList component displays a list of organizations that the user is a member of. 100 | 101 | This component renders Clerk's React component, 102 | providing an interface to view and manage organization memberships. 103 | 104 | Props: 105 | appearance: Optional object to style your components. Will only affect Clerk components. 106 | after_create_organization_url: The full URL or path to navigate to after creating an organization. 107 | after_select_organization_url: The full URL or path to navigate to after selecting an organization. 108 | after_select_personal_url: The full URL or path to navigate to after selecting the personal account. 109 | create_organization_mode: Controls whether selecting create organization opens as a modal or navigates to a page. 110 | create_organization_url: The full URL or path leading to the create organization interface. 111 | hide_personal_account: Controls whether the personal account option is hidden in the list. 112 | skip_invitation_screen: Controls whether to skip the invitation screen when creating an organization. 113 | fallback: An optional element to be rendered while the component is mounting. 114 | """ 115 | 116 | tag = "OrganizationList" 117 | 118 | # Optional props that OrganizationList supports 119 | appearance: Optional[str] = None 120 | after_create_organization_url: Optional[str] = None 121 | after_select_organization_url: Optional[str] = None 122 | after_select_personal_url: Optional[str] = None 123 | create_organization_mode: Optional[str] = None 124 | create_organization_url: Optional[str] = None 125 | hide_personal_account: Optional[str] = None 126 | skip_invitation_screen: Optional[str] = None 127 | fallback: Optional[str] = None 128 | 129 | 130 | create_organization = CreateOrganization.create 131 | organization_profile = OrganizationProfile.create 132 | organization_switcher = OrganizationSwitcher.create 133 | organization_list = OrganizationList.create 134 | -------------------------------------------------------------------------------- /tests/test_demo.py: -------------------------------------------------------------------------------- 1 | import logging 2 | import os 3 | import uuid 4 | from pathlib import Path 5 | from typing import Iterator 6 | 7 | import clerk_backend_api 8 | import pytest 9 | from clerk_backend_api import Clerk, User 10 | from dotenv import load_dotenv 11 | from playwright.sync_api import Page, expect 12 | from reflex.testing import AppHarness 13 | 14 | load_dotenv() 15 | 16 | TEST_EMAIL = "ci-test+clerk_test@gmail.com" 17 | TEST_PASSWORD = "test-clerk-password" 18 | 19 | PAGE_LOAD_TIMEOUT = 10000 if not os.getenv("CI") else 30000 20 | INTERACTION_TIMEOUT = 2000 if not os.getenv("CI") else 20000 21 | 22 | 23 | @pytest.fixture(scope="session") 24 | def demo_app(): 25 | app_root = Path(__file__).parent.parent / "clerk_api_demo" 26 | with AppHarness.create(root=app_root) as harness: 27 | yield harness 28 | 29 | 30 | @pytest.fixture(scope="function") 31 | def page( 32 | request: pytest.FixtureRequest, demo_app: AppHarness, page: Page 33 | ) -> Iterator[Page]: 34 | """Load the demo app main page.""" 35 | page.set_default_timeout(PAGE_LOAD_TIMEOUT) 36 | assert demo_app.frontend_url is not None 37 | page.goto(demo_app.frontend_url) 38 | page.set_default_timeout(INTERACTION_TIMEOUT) 39 | yield page 40 | if request.session.testsfailed: 41 | logging.error("Test failed. Saving screenshot as playwright_test_error.png") 42 | page.screenshot(path="playwright_test_error.png") 43 | 44 | 45 | @pytest.fixture() 46 | def clerk_client() -> Iterator[Clerk]: 47 | """Create clerk backend api client.""" 48 | secret_key = os.environ["CLERK_SECRET_KEY"] 49 | with clerk_backend_api.Clerk(bearer_auth=secret_key) as client: 50 | yield client 51 | 52 | 53 | def test_clerk_client(clerk_client: Clerk): 54 | """Really basic test that the clerk client is working.""" 55 | # NOTE: Getting a deprecation warning here, but not sure why... 56 | # Warning message is not helpful. 57 | existing = clerk_client.users.list(request={"email_address": [TEST_EMAIL]}) 58 | assert existing is not None 59 | 60 | 61 | @pytest.fixture 62 | def create_test_user(clerk_client: Clerk) -> User: 63 | """Creates (or checks already exists) a test clerk user. 64 | 65 | This can then be used to sign in during tests. 66 | """ 67 | existing = clerk_client.users.list(request={"email_address": [TEST_EMAIL]}) 68 | if existing is not None and len(existing) > 0: 69 | if len(existing) != 1: 70 | raise SetupError( 71 | "Multiple test users found with same email... This should not happen." 72 | ) 73 | logging.error("Test user already exists.") 74 | return existing[0] 75 | 76 | logging.error("Creating test user...") 77 | res = clerk_client.users.create( 78 | request={ 79 | "external_id": "ext-id-" + uuid.uuid4().hex[:5], 80 | "first_name": "John", 81 | "last_name": "Doe", 82 | "email_address": [ 83 | TEST_EMAIL, 84 | ], 85 | "username": "fake_username_" + uuid.uuid4().hex[:5], 86 | "password": TEST_PASSWORD, 87 | "skip_password_checks": False, 88 | "skip_password_requirement": False, 89 | "public_metadata": { 90 | "role": "user", 91 | }, 92 | "private_metadata": { 93 | "internal_id": "789", 94 | }, 95 | "unsafe_metadata": { 96 | "preferences": { 97 | "theme": "dark", 98 | }, 99 | }, 100 | "delete_self_enabled": True, 101 | "skip_legal_checks": False, 102 | "create_organization_enabled": True, 103 | "create_organizations_limit": 134365, 104 | "created_at": "2023-03-15T07:15:20.902Z", 105 | } 106 | ) 107 | assert res is not None 108 | return res 109 | 110 | 111 | def test_test_user(create_test_user: User): 112 | """Check the test user was either created or found correctly.""" 113 | user = create_test_user 114 | assert user.email_addresses is not None 115 | assert user.email_addresses[0].email_address == TEST_EMAIL 116 | 117 | 118 | def test_render(page: Page): 119 | """Check that the demo renders correctly. 120 | 121 | I.e. Check components are visible. 122 | """ 123 | page.pause() 124 | expect(page.get_by_role("heading", name="reflex-clerk-api demo")).to_contain_text( 125 | "reflex-clerk-api demo" 126 | ) 127 | expect(page.get_by_role("heading", name="Getting Started")).to_contain_text( 128 | "Getting Started" 129 | ) 130 | 131 | expect(page.get_by_test_id("clerkstate_variables_and_methods")).to_be_visible() 132 | expect(page.get_by_test_id("clerk_loaded_and_signed_in_out_areas")).to_be_visible() 133 | expect(page.get_by_test_id("better_on_load_handling")).to_be_visible() 134 | expect(page.get_by_test_id("on_auth_change_callbacks")).to_be_visible() 135 | expect(page.get_by_test_id("clerkuser_info")).to_be_visible() 136 | expect(page.get_by_test_id("sign-in_and_sign-up_pages")).to_be_visible() 137 | expect(page.get_by_test_id("user_profile_management")).to_be_visible() 138 | 139 | 140 | def test_sign_up(page: Page): 141 | """Check sign-up button takes you to a sign-up form. 142 | 143 | Note: Can't actually test signing up in headless mode because of bot detection. 144 | """ 145 | page.pause() 146 | page.get_by_role("button", name="Sign up").click() 147 | expect(page.get_by_role("heading")).to_contain_text("Create your account") 148 | 149 | 150 | class SetupError(Exception): 151 | """Error raised when the test setup is incorrect.""" 152 | 153 | 154 | @pytest.mark.usefixtures("create_test_user") 155 | def test_sign_in(page: Page): 156 | """Check sign-in button takes you to a sign-in form. 157 | 158 | Note: Can't actually test signing in in headless mode because of bot detection. 159 | """ 160 | 161 | page.get_by_role("button", name="Sign in").click() 162 | expect(page.get_by_role("heading")).to_contain_text("Sign in to") 163 | page.get_by_role("textbox", name="Email address").click() 164 | page.get_by_role("textbox", name="Email address").fill(TEST_EMAIL) 165 | page.pause() 166 | page.get_by_role("button", name="Continue", exact=True).click() 167 | page.get_by_role("textbox", name="Password").click() 168 | page.get_by_role("textbox", name="Password").fill(TEST_PASSWORD) 169 | page.get_by_role("button", name="Continue", exact=True).click() 170 | expect(page.get_by_test_id("sign_out")).not_to_be_visible() 171 | 172 | page.pause() 173 | 174 | 175 | @pytest.fixture 176 | def sign_in(page: Page, create_test_user: User) -> User: 177 | """Sign in the test user.""" 178 | assert create_test_user.email_addresses is not None 179 | assert create_test_user.email_addresses[0].email_address == TEST_EMAIL 180 | page.get_by_role("button", name="Sign in").click() 181 | page.get_by_role("textbox", name="Email address").click() 182 | page.get_by_role("textbox", name="Email address").fill(TEST_EMAIL) 183 | page.get_by_role("button", name="Continue", exact=True).click() 184 | page.get_by_role("textbox", name="Password").click() 185 | page.get_by_role("textbox", name="Password").fill(TEST_PASSWORD) 186 | page.get_by_role("button", name="Continue", exact=True).click() 187 | # Wait until we are back at the demo page signed in 188 | expect(page.get_by_test_id("sign_out")).not_to_be_visible() 189 | 190 | return create_test_user 191 | 192 | 193 | @pytest.mark.usefixtures("sign_in") 194 | def test_sign_out(page: Page): 195 | """Check sign-out button signs out the user.""" 196 | page.get_by_role("button", name="Sign out").click() 197 | expect(page.get_by_test_id("sign_in")).to_be_visible() 198 | expect(page.get_by_test_id("sign_up")).to_be_visible() 199 | expect(page.get_by_test_id("sign_out")).not_to_be_visible() 200 | 201 | 202 | def test_signed_in_state(page: Page, sign_in: User): 203 | """Check a signed-in user sees expected state of app.""" 204 | assert sign_in.id is not None 205 | page.get_by_test_id("clerkstate_variables_and_methods").hover() 206 | page.pause() 207 | expect(page.get_by_test_id("is_hydrated")).to_contain_text("true") 208 | expect(page.get_by_test_id("auth_checked")).to_contain_text("true") 209 | expect(page.get_by_test_id("is_signed_in")).to_contain_text("true") 210 | expect(page.get_by_test_id("user_id")).to_contain_text(sign_in.id) 211 | 212 | page.get_by_test_id("clerk_loaded_and_signed_in_out_areas").hover() 213 | page.pause() 214 | expect(page.get_by_test_id("you_are_signed_in")).to_contain_text( 215 | "You are signed in." 216 | ) 217 | expect(page.get_by_test_id("you_are_signed_in")).to_be_visible() 218 | expect(page.get_by_test_id("you_are_signed_out")).not_to_be_visible() 219 | 220 | page.get_by_test_id("better_on_load_handling").hover() 221 | page.pause() 222 | expect(page.get_by_test_id("info_from_load")).to_contain_text( 223 | "clerkstate.auth_checked: True" 224 | ) 225 | -------------------------------------------------------------------------------- /custom_components/reflex_clerk_api/clerk_provider.py: -------------------------------------------------------------------------------- 1 | import asyncio 2 | import logging 3 | import os 4 | import time 5 | import uuid 6 | from typing import Any, Callable, ClassVar, TypeVar 7 | 8 | import authlib.jose.errors as jose_errors 9 | import clerk_backend_api 10 | import reflex as rx 11 | from authlib.jose import JWTClaims, jwt 12 | from reflex.event import EventCallback, EventType, IndividualEventType 13 | from reflex.utils.exceptions import ImmutableStateError 14 | 15 | from reflex_clerk_api.base import ClerkBase 16 | 17 | from .models import Appearance 18 | 19 | 20 | class ReflexClerkApiError(Exception): 21 | pass 22 | 23 | 24 | class MissingSecretKeyError(ReflexClerkApiError): 25 | pass 26 | 27 | 28 | class MissingUserError(ReflexClerkApiError): 29 | pass 30 | 31 | 32 | class ClerkState(rx.State): 33 | is_signed_in: bool = False 34 | """Whether the user is logged in.""" 35 | 36 | auth_checked: bool = False 37 | """Whether the auth state of the user has been checked yet. 38 | I.e., has Clerk sent a response to the frontend yet.""" 39 | 40 | claims: JWTClaims | None = None 41 | """The JWT claims of the user, if they are logged in.""" 42 | user_id: str | None = None 43 | """The clerk user ID of the user, if they are logged in.""" 44 | 45 | # NOTE: ClassVar tells reflex it doesn't need to include these in the persisted state per instance. 46 | _auth_wait_timeout_seconds: ClassVar[float] = 1.0 47 | _secret_key: ClassVar[str | None] = None 48 | """The Clerk secret_key set during clerk_provider creation.""" 49 | _on_load_events: ClassVar[dict[uuid.UUID, EventType[()]]] = {} 50 | _dependent_handlers: ClassVar[dict[int, EventCallback]] = {} 51 | _client: ClassVar[clerk_backend_api.Clerk | None] = None 52 | _jwk_keys: ClassVar[dict[str, Any] | None] = None 53 | "JWK keys from Clerk for decoding any users JWT tokens (only required once per instance)." 54 | _last_jwk_reset: ClassVar[float] = 0.0 55 | _claims_options: ClassVar[dict[str, Any]] = { 56 | # "iss": {"value": "https://.clerk.accounts.dev"}, 57 | "exp": {"essential": True}, 58 | "nbf": {"essential": True}, 59 | # "azp": {"essential": False, "values": ["http://localhost:3000", "https://example.com"]}, 60 | } 61 | 62 | @classmethod 63 | def register_dependent_handler(cls, handler: EventCallback) -> None: 64 | """Register a handler to be called any time this state updates. 65 | 66 | I.e. Any events that should be triggered on login/logout. 67 | """ 68 | assert isinstance(handler, rx.EventHandler) 69 | hash_id = hash((handler.state_full_name, handler.fn)) 70 | logging.debug(f"Dependent hash_id: {hash_id}") 71 | cls._dependent_handlers[hash_id] = handler 72 | 73 | @classmethod 74 | def set_auth_wait_timeout_seconds(cls, seconds: float) -> None: 75 | """Sets the max time to wait for initial auth check before running other on_load events. 76 | 77 | Note: on_load events will still be run after a timed out auth check. 78 | Check ClerkState.auth_checked to see if auth check is complete. 79 | """ 80 | cls._auth_wait_timeout_seconds = seconds 81 | 82 | @classmethod 83 | def set_claims_options(cls, claims_options: dict[str, Any]) -> None: 84 | """Set the claims options for the JWT claims validation.""" 85 | cls._claims_options = claims_options 86 | 87 | @property 88 | def client(self) -> clerk_backend_api.Clerk: 89 | if self._client is None: 90 | self._set_client() 91 | assert self._client is not None 92 | return self._client 93 | 94 | @rx.event(background=True) 95 | async def set_clerk_session(self, token: str) -> EventType: 96 | """Manually obtain user session information via the Clerk JWT. 97 | 98 | This event is triggered by the frontend via the ClerkSessionSynchronizer/ClerkProvider component. 99 | 100 | Note: Only the parts that modify the per-instance state need to be in an `async with self` block. 101 | """ 102 | logging.debug("Setting Clerk session") 103 | jwks = await self._get_jwk_keys() 104 | try: 105 | decoded: JWTClaims = jwt.decode( 106 | token, {"keys": jwks}, claims_options=self._claims_options 107 | ) 108 | except jose_errors.DecodeError as e: 109 | # E.g. DecodeError -- Something went wrong just getting the JWT 110 | # On next attempt, new JWKs will be fetched 111 | async with self: 112 | self.auth_error = e 113 | self._request_jwk_reset() 114 | logging.warning(f"JWT decode error: {e}") 115 | return ClerkState.clear_clerk_session 116 | try: 117 | # Validate the token according to the claim options (e.g. iss, exp, nbf, azp.) 118 | decoded.validate() 119 | except (jose_errors.InvalidClaimError, jose_errors.MissingClaimError) as e: 120 | logging.warning(f"JWT token is invalid: {e}") 121 | return ClerkState.clear_clerk_session 122 | 123 | async with self: 124 | self.is_signed_in = True 125 | self.claims = decoded 126 | self.user_id = str(decoded.get("sub")) 127 | self.auth_checked = True 128 | return list(self._dependent_handlers.values()) 129 | 130 | @rx.event 131 | def clear_clerk_session(self) -> EventType: 132 | """Clear the Clerk session information. 133 | 134 | This event is triggered by the frontend via the ClerkSessionSynchronizer/ClerkProvider component. 135 | """ 136 | logging.debug("Clearing Clerk session") 137 | self.reset() 138 | self.auth_checked = True 139 | return list(self._dependent_handlers.values()) 140 | 141 | @rx.event(background=True) 142 | async def wait_for_auth_check(self, uid: uuid.UUID | str) -> EventType: 143 | """Wait for the Clerk authentication to complete (event sent from frontend). 144 | 145 | Can't just use a blocking wait_for_auth_check because we are really waiting for the frontend event trigger to run, so we need to not block that while we wait. 146 | 147 | This can then return on_load events once auth_checked is True. 148 | """ 149 | uid = uuid.UUID(uid) if isinstance(uid, str) else uid 150 | logging.debug(f"Waiting for auth check: {uid} ({type(uid)})") 151 | 152 | on_loads = self._on_load_events.get(uid, None) 153 | if on_loads is None: 154 | logging.warning("Waited for auth, but no on_load events registered.") 155 | on_loads = [] 156 | 157 | start_time = time.time() 158 | while time.time() - start_time < self._auth_wait_timeout_seconds: 159 | if self.auth_checked: 160 | logging.debug("Auth check complete") 161 | return on_loads 162 | logging.debug("...waiting for auth...") 163 | # TODO: Ideally, wait on some event instead of sleeping 164 | await asyncio.sleep(0.05) 165 | logging.warning("Auth check timed out") 166 | return on_loads 167 | 168 | @classmethod 169 | def _set_secret_key(cls, secret_key: str) -> None: 170 | if not secret_key: 171 | raise MissingSecretKeyError("secret_key must be set (and not empty)") 172 | cls._secret_key = secret_key 173 | 174 | @classmethod 175 | def _set_on_load_events(cls, uid: uuid.UUID, on_load_events: EventType[()]) -> None: 176 | logging.debug(f"Registing on_load events: {uid}") 177 | cls._on_load_events[uid] = on_load_events 178 | 179 | @classmethod 180 | def _set_client(cls) -> None: 181 | if cls._secret_key: 182 | secret_key = cls._secret_key 183 | else: 184 | if "CLERK_SECRET_KEY" not in os.environ: 185 | raise MissingSecretKeyError( 186 | "CLERK_SECRET_KEY either needs to be passed into clerk_provider(...) or set as an environment variable." 187 | ) 188 | secret_key = os.environ["CLERK_SECRET_KEY"] 189 | client = clerk_backend_api.Clerk(bearer_auth=secret_key) 190 | cls._client = client 191 | 192 | @classmethod 193 | def _set_jwk_keys(cls, keys: dict[str, Any] | None) -> None: 194 | cls._jwk_keys = keys 195 | 196 | @classmethod 197 | def _request_jwk_reset(cls) -> None: 198 | """Reset the JWK keys so they will be re-fetched on next attempt. 199 | 200 | Only do so if it has been a while since last reset (to prevent malicious tokens from forcing 201 | constant re-fetching). 202 | """ 203 | now = time.time() 204 | if now - cls._last_jwk_reset < 10: 205 | logging.warning("JWK reset requested too soon") 206 | return 207 | cls._last_jwk_reset = time.time() 208 | cls._jwk_keys = None 209 | 210 | async def _get_jwk_keys(self) -> dict[str, Any]: 211 | """Get the JWK keys from the Clerk API. 212 | 213 | Note: Cannot be a property because it requires async call to populate. 214 | Only needs to be done once (will be refreshed on errors). 215 | """ 216 | if self._jwk_keys: 217 | return self._jwk_keys 218 | jwks = await self.client.jwks.get_jwks_async() 219 | assert jwks is not None 220 | assert jwks.keys is not None 221 | keys = jwks.model_dump()["keys"] 222 | self._set_jwk_keys(keys) 223 | return keys 224 | 225 | # @rx.event 226 | # def force_reset(self) -> None: 227 | # """Force a reset of the Clerk state. 228 | # 229 | # E.g. During development testing. 230 | # """ 231 | # self.reset() 232 | # self._set_jwk_keys(None) 233 | # return rx.toast.success("Forced reset complete.") 234 | 235 | 236 | class NotRegisteredError(ReflexClerkApiError): 237 | pass 238 | 239 | 240 | class ClerkUser(rx.State): 241 | """Convenience class for using Clerk User information. 242 | 243 | This only contains a subset of the information available. Create your own state if you need more. 244 | 245 | Note: For this to be updated on login/logout events, it must be registered on the ClerkState. 246 | """ 247 | 248 | first_name: str = "" 249 | last_name: str = "" 250 | username: str = "" 251 | email_address: str = "" 252 | has_image: bool = False 253 | image_url: str = "" 254 | 255 | # Set to True when the state is registered on the ClerkState to avoid registering it multiple times. 256 | _is_registered: ClassVar[bool] = False 257 | 258 | @rx.event 259 | async def load_user(self) -> None: 260 | try: 261 | user: clerk_backend_api.models.User = await get_user(self) 262 | except MissingUserError: 263 | logging.debug("Clearing user state") 264 | self.reset() 265 | return 266 | 267 | logging.debug("Updating user state") 268 | self.first_name = ( 269 | user.first_name 270 | if user.first_name and user.first_name != clerk_backend_api.UNSET 271 | else "" 272 | ) 273 | self.last_name = ( 274 | user.last_name 275 | if user.last_name and user.last_name != clerk_backend_api.UNSET 276 | else "" 277 | ) 278 | self.username = ( 279 | user.username 280 | if user.username and user.username != clerk_backend_api.UNSET 281 | else "" 282 | ) 283 | self.email_address = ( 284 | user.email_addresses[0].email_address if user.email_addresses else "" 285 | ) 286 | self.has_image = True if user.has_image is True else False 287 | self.image_url = user.image_url or "" 288 | 289 | 290 | class ClerkSessionSynchronizer(rx.Component): 291 | """ClerkSessionSynchronizer component. 292 | 293 | This is slightly adapted from Elliot Kroo's reflex-clerk. 294 | """ 295 | 296 | tag = "ClerkSessionSynchronizer" 297 | 298 | def add_imports( 299 | self, 300 | ) -> rx.ImportDict: 301 | addl_imports: rx.ImportDict = { 302 | "@clerk/clerk-react": ["useAuth"], 303 | "react": ["useContext", "useEffect"], 304 | "$/utils/context": ["EventLoopContext"], 305 | "$/utils/state": ["ReflexEvent"], 306 | } 307 | return addl_imports 308 | 309 | def add_custom_code(self) -> list[str]: 310 | clerk_state_name = ClerkState.get_full_name() 311 | 312 | return [ 313 | """ 314 | function ClerkSessionSynchronizer({ children }) { 315 | const { getToken, isLoaded, isSignedIn } = useAuth() 316 | const [ addEvents, connectErrors ] = useContext(EventLoopContext) 317 | 318 | useEffect(() => { 319 | if (isLoaded && !!addEvents) { 320 | if (isSignedIn) { 321 | getToken().then(token => { 322 | addEvents([ReflexEvent("%s.set_clerk_session", {token})]) 323 | }) 324 | } else { 325 | addEvents([ReflexEvent("%s.clear_clerk_session")]) 326 | } 327 | } 328 | }, [isSignedIn]) 329 | 330 | return ( 331 | <>{children} 332 | ) 333 | } 334 | """ 335 | % (clerk_state_name, clerk_state_name) 336 | ] 337 | 338 | 339 | InitialState = dict[str, Any] 340 | # Should this be EventSpec instead? 341 | JSCallable = Callable 342 | 343 | 344 | class ClerkProvider(ClerkBase): 345 | """ClerkProvider component.""" 346 | 347 | # The React component tag. 348 | tag = "ClerkProvider" 349 | 350 | # NOTE: This might be relevant to getting apperance.base_theme to work. 351 | # lib_dependencies: list[str] = ["@clerk/themes"] 352 | # def add_imports(self) -> rx.ImportDict: 353 | # return { 354 | # "@clerk/themes": ["dark", "neobrutalism", "shadesOfPurple"], 355 | # } 356 | 357 | # The props of the React component. 358 | # Note: when Reflex compiles the component to Javascript, 359 | # `snake_case` property names are automatically formatted as `camelCase`. 360 | # The prop names may be defined in `camelCase` as well. 361 | # some_prop: rx.Var[str] = "some default value" 362 | # some_other_prop: rx.Var[int] = 1 363 | 364 | # Event triggers declaration if any. 365 | # Below is equivalent to merging `{ "on_change": lambda e: [e] }` 366 | # onto the default event triggers of parent/base Component. 367 | # The function defined for the `on_change` trigger maps event for the javascript 368 | # trigger to what will be passed to the backend event handler function. 369 | # on_change: rx.EventHandler[lambda e: [e]] 370 | 371 | after_multi_session_single_sign_out_url: str = "" 372 | """The URL to navigate to after a successful sign-out from multiple sessions.""" 373 | 374 | after_sign_out_url: str = "" 375 | """The full URL or path to navigate to after a successful sign-out.""" 376 | 377 | allowed_redirect_origins: list[str | str] = [] 378 | """An optional list of domains to validate user-provided redirect URLs against.""" 379 | 380 | allowed_redirect_protocols: list[str] = [] 381 | """An optional list of protocols to validate user-provided redirect URLs against.""" 382 | 383 | # NOTE: `apperance.base_theme` does not work yet. 384 | appearance: Appearance | None = None 385 | """Optional object to style your components. Will only affect Clerk components.""" 386 | 387 | clerk_js_url: str = "" 388 | """Define the URL that @clerk/clerk-js should be hot-loaded from.""" 389 | 390 | clerk_js_variant: str | None = None 391 | """If your web application only uses control components, set this to 'headless'.""" 392 | 393 | clerk_js_version: str = "" 394 | """Define the npm version for @clerk/clerk-js.""" 395 | 396 | # domain: str | JSCallable[[str], bool] = "" 397 | domain: str = "" 398 | """Required if your application is a satellite application. Sets the domain.""" 399 | 400 | dynamic: bool = False 401 | """(For Next.js only) Indicates whether Clerk should make dynamic auth data available.""" 402 | 403 | # initial_state: InitialState | None = None 404 | # """Provide an initial state of the Clerk client during server-side rendering.""" 405 | 406 | # is_satellite: bool | JSCallable[[str], bool] = False 407 | is_satellite: bool = False 408 | """Whether the application is a satellite application.""" 409 | 410 | # Not implemented 411 | # localization: Localization | None = None 412 | # See https://clerk.com/docs/customization/localization#clerk-localizations for more info. 413 | # """Optional object to localize your components. Will only affect Clerk components.""" 414 | 415 | nonce: str = "" 416 | """Nonce value passed to the @clerk/clerk-js script tag for CSP implementation.""" 417 | 418 | publishable_key: str = "" 419 | """The Clerk Publishable Key for your instance, found on the API keys page in the Clerk Dashboard.""" 420 | 421 | # proxy_url: str | JSCallable[[str], str] = "" 422 | proxy_url: str = "" 423 | """The URL of the proxy server to use for Clerk API requests.""" 424 | 425 | router_push: JSCallable[[str], None | Any] | None = None 426 | """A function to push a new route into the history stack for navigation.""" 427 | 428 | router_replace: JSCallable[[str], None | Any] | None = None 429 | """A function to replace the current route in the history stack for navigation.""" 430 | 431 | # sdk_metadata: dict[str, str] = {"name": "", "version": "", "environment": ""} 432 | # """Contains information about the SDK that the host application is using.""" 433 | 434 | # select_initial_session: Callable[[Any], None | Any] | None = None 435 | # """Function to override the default behavior of using the last active session during client initialization.""" 436 | 437 | sign_in_fallback_redirect_url: str = "/" 438 | """The fallback URL to redirect to after the user signs in if there's no redirect_url in the path.""" 439 | 440 | sign_up_fallback_redirect_url: str = "/" 441 | """The fallback URL to redirect to after the user signs up if there's no redirect_url in the path.""" 442 | 443 | sign_in_force_redirect_url: str = "" 444 | """URL to always redirect to after the user signs in.""" 445 | 446 | sign_up_force_redirect_url: str = "" 447 | """URL to always redirect to after the user signs up.""" 448 | 449 | sign_in_url: str = "" 450 | """URL used for any redirects that might happen, pointing to your primary application on the client-side.""" 451 | 452 | sign_up_url: str = "" 453 | """URL used for any redirects that might happen, pointing to your primary application on the client-side.""" 454 | 455 | standard_browser: bool = True 456 | """Indicates whether ClerkJS assumes cookies can be set (browser setup).""" 457 | 458 | support_email: str = "" 459 | """Optional support email for display in authentication screens.""" 460 | 461 | sync_host: str = "" 462 | """URL of the web application that the Chrome Extension will sync the authentication state from.""" 463 | 464 | telemetry: bool | dict[str, bool] | None = None 465 | """Controls whether Clerk will collect telemetry data.""" 466 | 467 | touch_session: bool = True 468 | """Indicates whether the Clerk Frontend API touch endpoint is called during page focus to keep the last active session alive.""" 469 | 470 | waitlist_url: str = "" 471 | """The full URL or path to the waitlist page.""" 472 | 473 | @classmethod 474 | def create(cls, *children, **props) -> "ClerkProvider": 475 | return super().create(*children, **props) 476 | 477 | def add_custom_code(self) -> list[str]: 478 | return [] 479 | 480 | 481 | def on_load(on_load_events: EventType[()] | None) -> list[IndividualEventType[()]]: 482 | """Use this to wrap any on_load events that should happen after Clerk has checked authentication. 483 | 484 | Args: 485 | on_load_events: The events to run after authentication is checked. 486 | 487 | Examples: 488 | app.add_page(..., on_load=clerk.on_load()) 489 | """ 490 | if on_load_events is None: 491 | return [] 492 | on_load_list = ( 493 | on_load_events if isinstance(on_load_events, list) else [on_load_events] 494 | ) 495 | 496 | # Add the on_load events to a registry in the ClerkState instead of actually passing them to on_load. 497 | # Then, the wait_for_auth_check event will return the on_load events once auth_checked is True. 498 | # Can't just use a blocking wait_for_auth_check because we are really waiting for the frontend event trigger to run, 499 | # so we need to not block that while we wait. 500 | uid = uuid.uuid4() 501 | ClerkState._set_on_load_events(uid, on_load_list) 502 | return [ClerkState.wait_for_auth_check(uid)] 503 | 504 | 505 | T = TypeVar("T", bound=rx.State) 506 | 507 | 508 | async def _get_state_within_handler( 509 | current_state: rx.State, desired_state: type[T] 510 | ) -> T: 511 | """Get the desired state from within an event handler. 512 | 513 | Note: Need to be in `async with self` block if called from background event. 514 | """ 515 | try: 516 | state = await current_state.get_state(desired_state) 517 | except ImmutableStateError: 518 | async with current_state: 519 | state = await current_state.get_state(desired_state) 520 | return state 521 | 522 | 523 | async def get_user(current_state: rx.State) -> clerk_backend_api.models.User: 524 | """Get the User object from Clerk given the currently logged in user. 525 | 526 | Note: Must be used within an event handler in order to get the appropriate clerk_state. 527 | 528 | Args: 529 | current_state: The `self` state from the current event handler. 530 | 531 | Examples: 532 | 533 | ```python 534 | class State(rx.State): 535 | @rx.event 536 | async def handle_getting_user_email(self) -> EventType: 537 | user = await clerk.get_user(self) 538 | return rx.toast.info(f"User: {user.email}") 539 | ``` 540 | """ 541 | clerk_state = await _get_state_within_handler(current_state, ClerkState) 542 | user_id = clerk_state.user_id 543 | if user_id is None: 544 | raise MissingUserError("No user_id to get user for") 545 | user = await clerk_state.client.users.get_async(user_id=user_id) 546 | if user is None: 547 | raise MissingUserError("No user found") 548 | return user 549 | 550 | 551 | def register_on_auth_change_handler(handler: EventCallback) -> None: 552 | """Register a handler to be called any time the user logs in or out. 553 | 554 | Args: 555 | handler: The event handler function to be called. 556 | """ 557 | ClerkState.register_dependent_handler(handler) 558 | 559 | 560 | def clerk_provider( 561 | *children, 562 | publishable_key: str, 563 | secret_key: str | None = None, 564 | register_user_state: bool = False, 565 | appearance: Appearance | None = None, 566 | **props, 567 | ) -> rx.Component: 568 | """ 569 | Create a ClerkProvider component to wrap your app/page that uses clerk authentication. 570 | 571 | Note: can also use `wrap_app` to wrap the entire app. 572 | 573 | Args: 574 | children: The children components to wrap. 575 | publishable_key: The Clerk Publishable Key for your instance. 576 | secret_key: Your Clerk app's Secret Key, which you can find in the Clerk Dashboard. It will be prefixed with sk_test_ in development instances and sk_live_ in production instances. Do not expose this on the frontend with a public environment variable. 577 | register_user_state: Whether to register the ClerkUser state to automatically load user information on login. 578 | appearance: Optional object to style your components. Will only affect Clerk components. 579 | """ 580 | if secret_key: 581 | ClerkState._set_secret_key(secret_key) 582 | 583 | if register_user_state: 584 | register_on_auth_change_handler(ClerkUser.load_user) 585 | 586 | return ClerkProvider.create( 587 | ClerkSessionSynchronizer.create(*children), 588 | publishable_key=publishable_key, 589 | appearance=appearance, 590 | **props, 591 | ) 592 | 593 | 594 | def wrap_app( 595 | app: rx.App, 596 | publishable_key: str, 597 | secret_key: str | None = None, 598 | register_user_state: bool = False, 599 | appearance: Appearance | None = None, 600 | **props, 601 | ) -> rx.App: 602 | """Wraps the entire app with the ClerkProvider. 603 | 604 | For multi-page apps where all pages require Clerk authentication components (including knowing if the user 605 | is **not** signed in). 606 | 607 | Args: 608 | app: The Reflex app to wrap. 609 | publishable_key: The Clerk Publishable Key for your instance. 610 | secret_key: Your Clerk app's Secret Key. (not needed for frontend only) 611 | register_user_state: Whether to register the ClerkUser state to automatically load user information on login. 612 | """ 613 | # 1 makes this the first wrapper around the content 614 | # (0 would place it after, 100 would also wrap default reflex wrappers) 615 | app.app_wraps[(1, "ClerkProvider")] = lambda _: clerk_provider( 616 | publishable_key=publishable_key, 617 | secret_key=secret_key, 618 | register_user_state=register_user_state, 619 | appearance=appearance, 620 | **props, 621 | ) 622 | return app 623 | -------------------------------------------------------------------------------- /clerk_api_demo/clerk_api_demo/clerk_api_demo.py: -------------------------------------------------------------------------------- 1 | """Welcome to Reflex! This file showcases the custom component in a basic app.""" 2 | 3 | import logging 4 | import os 5 | from textwrap import dedent 6 | 7 | import reflex as rx 8 | import reflex_clerk_api as clerk 9 | from dotenv import load_dotenv 10 | from reflex.event import EventType 11 | from rxconfig import config 12 | 13 | # Set up debug logging with a console handler 14 | logging.basicConfig(level=logging.INFO, handlers=[logging.StreamHandler()]) 15 | # Just a very quick check to see which loggers are actually active in the console 16 | logging.debug("Logging DEBUG") 17 | logging.info("Logging INFO") 18 | logging.warning("Logging WARNING") 19 | 20 | 21 | load_dotenv() 22 | 23 | filename = f"{config.app_name}/{config.app_name}.py" 24 | 25 | 26 | class State(rx.State): 27 | """The app state.""" 28 | 29 | # Store information that is populated during an on_load event within clerk.on_load wrapper. 30 | info_from_load: str = "Not loaded yet." 31 | # Same as above but without the clerk.on_load wrapper. 32 | info_from_load_without_wrapper: str = "Not loaded yet." 33 | # Store information whenever the user logs in or out. 34 | last_auth_change: str = "No changes yet." 35 | 36 | @rx.event 37 | async def do_something_on_load(self) -> EventType: 38 | """Example of a handler that should run on_load, but *after* the ClerkState is updated. 39 | 40 | E.g., The handler needs to know whether the user is logged in or not. 41 | """ 42 | clerk_state = await self.get_state(clerk.ClerkState) 43 | self.info_from_load = f"""\ 44 | State.is_hydrated: {self.is_hydrated} 45 | clerkstate.auth_checked: {clerk_state.auth_checked} 46 | ClerkState.is_signed_in: {clerk_state.is_signed_in} 47 | """ 48 | return rx.toast.info("On load event has finished") 49 | 50 | @rx.event 51 | async def do_something_on_load_without_wrapper(self) -> None: 52 | clerk_state = await self.get_state(clerk.ClerkState) 53 | self.info_from_load_without_wrapper = f"""\ 54 | State.is_hydrated: {self.is_hydrated} 55 | clerkstate.auth_checked: {clerk_state.auth_checked} 56 | ClerkState.is_signed_in: {clerk_state.is_signed_in} 57 | """ 58 | 59 | @rx.event 60 | async def do_something_on_log_in_or_out(self) -> EventType | None: 61 | """Demo handler that should run on user login or logout. 62 | 63 | To make this run it is registered via 64 | `clerk.register_on_auth_change_handler(State.do_something_on_log_in_or_out)` 65 | """ 66 | clerk_state = await self.get_state(clerk.ClerkState) 67 | old_val = self.last_auth_change 68 | first_load = old_val == "No changes yet." 69 | new_val = "User signed in" if clerk_state.is_signed_in else "User signed out" 70 | self.last_auth_change = new_val 71 | if not first_load and not old_val == new_val: 72 | return rx.toast.info(new_val, position="top-center") 73 | return None 74 | 75 | 76 | def demo_page_header_and_description() -> rx.Component: 77 | return rx.vstack( 78 | rx.hstack( 79 | rx.heading("reflex-clerk-api demo", size="9"), 80 | rx.text(clerk.__version__), 81 | align="baseline", 82 | ), 83 | rx.heading( 84 | "Custom", 85 | rx.link(rx.code("reflex"), href="https://reflex.dev"), 86 | "components that wrap Clerk react components (", 87 | rx.link( 88 | rx.code("@clerk/clerk-react"), 89 | href="https://www.npmjs.com/package/@clerk/clerk-react", 90 | ), 91 | ") and interact with the Clerk backend API.", 92 | size="4", 93 | ), 94 | rx.heading( 95 | "See the ", 96 | rx.link( 97 | "overview of Clerk components", 98 | href="https://clerk.com/docs/components/overview", 99 | ), 100 | " for more info on the wrapped components.", 101 | size="5", 102 | ), 103 | rx.divider(), 104 | rx.text( 105 | "Note: This is intended to be roughly a drop-in replacement of the ", 106 | rx.code("kroo/reflex-clerk"), 107 | " package that is no longer maintained.", 108 | ), 109 | rx.heading( 110 | "In addition to wrapping the basic components (and in comparison to Kroo's implementation), this additionally:", 111 | size="5", 112 | ), 113 | rx.unordered_list( 114 | rx.list_item( 115 | "uses Clerk's maintained python backend api (", 116 | rx.link( 117 | "clerk-backend-api", 118 | href="https://pypi.org/project/clerk-backend-api/", 119 | ), 120 | ")", 121 | ), 122 | rx.list_item( 123 | "is fully asynchronous, using ", 124 | rx.code("async/await"), 125 | " for all requests to the Clerk backend", 126 | ), 127 | rx.list_item("supports reflex 0.8.x"), 128 | rx.list_item( 129 | "adds a helper for handling ", 130 | rx.code("on_load"), 131 | " events that require knowledge of user authentication status. (i.e. ensuring the ClerkState is updated first)", 132 | ), 133 | rx.list_item( 134 | "adds a way to register event handlers to be called on authentication changes (login/logout)" 135 | ), 136 | rx.list_item("Checks JWT tokens are actually valid (not expired etc.)"), 137 | ), 138 | rx.accordion.root( 139 | rx.accordion.item( 140 | header="Migration notes", 141 | content=migration_notes(), 142 | ), 143 | variant="soft", 144 | collapsible=True, 145 | ), 146 | ) 147 | 148 | 149 | copy_button = rx.button( 150 | rx.icon("copy"), 151 | variant="soft", 152 | position="absolute", 153 | top="8px", 154 | right="0", 155 | ) 156 | 157 | 158 | def getting_started() -> rx.Component: 159 | return rx.vstack( 160 | rx.heading("Getting Started", size="6"), 161 | rx.text("Install with pip: "), 162 | rx.code_block( 163 | "pip install reflex-clerk-api", 164 | language="bash", 165 | can_copy=True, 166 | copy_button=copy_button, 167 | ), 168 | rx.text("Or with a package manager (uv/poetry):"), 169 | rx.code_block( 170 | "uv add reflex-clerk-api", 171 | language="bash", 172 | can_copy=True, 173 | copy_button=copy_button, 174 | ), 175 | rx.heading( 176 | "Import the package", 177 | size="5", 178 | ), 179 | rx.code_block( 180 | "import reflex_clerk_api as clerk", 181 | language="python", 182 | can_copy=True, 183 | copy_button=copy_button, 184 | ), 185 | rx.accordion.root( 186 | rx.accordion.item( 187 | header="Minimal example", 188 | content=rx.code_block( 189 | dedent("""\ 190 | import reflex_clerk_api as clerk 191 | 192 | def index() -> rx.Component: 193 | return clerk.clerk_provider( 194 | clerk.clerk_loaded( 195 | clerk.signed_in( 196 | clerk.sign_on( 197 | rx.button("Sign out"), 198 | ), 199 | ), 200 | clerk.signed_out( 201 | rx.button("Sign in"), 202 | ), 203 | ), 204 | publishable_key=os.environ["CLERK_PUBLISHABLE_KEY"], 205 | secret_key=os.environ["CLERK_SECRET_KEY"], 206 | register_user_state=True, 207 | ) 208 | """), 209 | language="python", 210 | ), 211 | ), 212 | collapsible=True, 213 | variant="soft", 214 | ), 215 | ) 216 | 217 | 218 | def migration_notes() -> rx.Component: 219 | return rx.vstack( 220 | rx.unordered_list( 221 | rx.list_item( 222 | "update your import to be from `reflex_clerk_api` instead of `reflex_clerk`" 223 | ), 224 | rx.list_item( 225 | rx.markdown( 226 | "use `clerk.add_sign_in_page(...)` and `clerk.add_sign_up_page(...)` instead of `clerk.install_pages(...)`" 227 | ) 228 | ), 229 | rx.list_item( 230 | rx.markdown( 231 | "wrap `on_load` events with `clerk.on_load()` to ensure the ClerkState is updated before other on_load events (i.e. is_signed_in will be accurate)" 232 | ) 233 | ), 234 | rx.list_item( 235 | rx.markdown( 236 | "use `await clerk.get_user(self)` inside event handlers instead of `clerk_state.user` to explicitly retrieve user information when desired" 237 | ) 238 | ), 239 | rx.list_item( 240 | "Note that you can use the `clerk_backend_api` directly if desired (it is a dependency of this plugin anyway)" 241 | ), 242 | ), 243 | ) 244 | 245 | 246 | def data_list_item(label: str, value: rx.Component | str) -> rx.Component: 247 | return rx.data_list.item( 248 | rx.data_list.label(label), 249 | rx.data_list.value(value), 250 | ) 251 | 252 | 253 | close_icon = rx.icon( 254 | "x", 255 | position="absolute", 256 | top="8px", 257 | right="8px", 258 | background_color=rx.color("tomato", 6), 259 | _hover=dict(background_color=rx.color("tomato", 7)), 260 | padding="2px", 261 | border_radius="5px", 262 | z_index="5", 263 | ) 264 | 265 | 266 | def demo_card( 267 | heading: str, description: str | rx.Component, demo: rx.Component 268 | ) -> rx.Component: 269 | card = rx.card( 270 | rx.vstack( 271 | rx.heading(heading, size="5"), 272 | rx.text(description) if isinstance(description, str) else description, 273 | ), 274 | max_width="30em", 275 | _hover=dict(background=rx.color("gray", 4)), 276 | height="100%", 277 | ) 278 | 279 | content_popover_desktop = rx.hover_card.root( 280 | rx.hover_card.trigger( 281 | card, 282 | data_testid=heading.lower().replace(" ", "_").replace("/", "_"), 283 | ), 284 | rx.hover_card.content( 285 | demo, 286 | avoid_collisions=True, 287 | ), 288 | ) 289 | content_popover_mobile = rx.popover.root( 290 | rx.popover.trigger( 291 | card, 292 | ), 293 | rx.popover.content( 294 | rx.popover.close( 295 | close_icon, 296 | ), 297 | demo, 298 | ), 299 | ) 300 | return rx.fragment( 301 | rx.desktop_only(content_popover_desktop), 302 | rx.mobile_and_tablet(content_popover_mobile), 303 | ) 304 | 305 | 306 | def current_clerk_state_values() -> rx.Component: 307 | demo = rx.vstack( 308 | rx.text("State variables:"), 309 | rx.data_list.root( 310 | data_list_item( 311 | "State.is_hydrated", 312 | rx.text(State.is_hydrated, data_testid="is_hydrated"), 313 | ), 314 | data_list_item( 315 | "ClerkState.auth_checked", 316 | rx.text(clerk.ClerkState.auth_checked, data_testid="auth_checked"), 317 | ), 318 | data_list_item( 319 | "ClerkState.is_logged_in", 320 | rx.text(clerk.ClerkState.is_signed_in, data_testid="is_signed_in"), 321 | ), 322 | data_list_item( 323 | "ClerkState.user_id", 324 | rx.text(clerk.ClerkState.user_id, data_testid="user_id"), 325 | ), 326 | ), 327 | rx.divider(), 328 | rx.text("State methods:"), 329 | rx.unordered_list( 330 | rx.list_item( 331 | rx.code("ClerkState.register_dependent_handler(handler)"), 332 | " -- Classmethod to register a handler to be called after the ClerkState is updated", 333 | ), 334 | rx.list_item( 335 | rx.code("ClerkState.set_auth_wait_timeout_seconds(seconds)"), 336 | " -- Set the timeout for waiting for the auth check to complete", 337 | ), 338 | rx.list_item( 339 | rx.code("ClerkState.set_claims_options(claims_options)"), 340 | " -- Set JWT claims options", 341 | ), 342 | rx.list_item( 343 | rx.code("clerk_state.client"), 344 | " -- Property to access the clerk_backend_api client", 345 | ), 346 | ), 347 | ) 348 | return demo_card( 349 | "ClerkState variables and methods", 350 | "State variables and methods available on the `ClerkState` object.", 351 | demo, 352 | ) 353 | 354 | 355 | def on_load_demo() -> rx.Component: 356 | state_using_on_load_wrapper = rx.card( 357 | rx.vstack( 358 | rx.text( 359 | "Info from `on_load` event ", 360 | rx.text.strong("inside"), 361 | " of `clerk.on_load` wrapper:", 362 | ), 363 | rx.divider(), 364 | rx.text( 365 | State.info_from_load, 366 | data_testid="info_from_load", 367 | ), 368 | ) 369 | ) 370 | state_not_using_on_load_wrapper = rx.card( 371 | rx.vstack( 372 | rx.text( 373 | "Info from `on_load` event ", 374 | rx.text.strong("outside"), 375 | " of `clerk.on_load` wrapper:", 376 | ), 377 | rx.divider(), 378 | rx.text( 379 | State.info_from_load_without_wrapper, 380 | data_testid="info_from_load_without_wrapper", 381 | ), 382 | ) 383 | ) 384 | demo = rx.vstack( 385 | rx.markdown( 386 | dedent("""\ 387 | Wrapping the `on_load` events is necessary because the ClerkState authentication is triggered from a frontend event that can't be guaranteed to run before the other `on_load` events. 388 | 389 | Sometimes the event outside the `clerk.on_load` wrapper will run before the ClerkState is updated (sometimes not). 390 | """) 391 | ), 392 | rx.grid( 393 | state_using_on_load_wrapper, 394 | state_not_using_on_load_wrapper, 395 | spacing="3", 396 | columns="2", 397 | width="100%", 398 | ), 399 | ) 400 | return demo_card( 401 | "Better on_load handling", 402 | rx.text( 403 | "Wrap ", 404 | rx.code("on_load"), 405 | " events with ", 406 | rx.code( 407 | "clerk.on_load(...)", 408 | " to ensure the ClerkState is updated before events run.", 409 | ), 410 | ), 411 | demo, 412 | ) 413 | 414 | 415 | def on_auth_change_demo() -> rx.Component: 416 | demo = rx.vstack( 417 | rx.text("By registering an event handler method like this:"), 418 | rx.code_block( 419 | "clerk.register_on_auth_change_handler(State.do_something_on_log_in_or_out)", 420 | language="python", 421 | wrap_long_lines=True, 422 | width="100%", 423 | ), 424 | rx.text( 425 | "The event handler will be called any time the authentication state of the user changes. In this demo, you'll see a toast top-center when you log in or out as well as the state variable change below." 426 | ), 427 | rx.text(f"State.last_auth_change={State.last_auth_change}"), 428 | width="100%", 429 | ) 430 | return demo_card( 431 | "On auth change callbacks", 432 | "You can register a method to be called when the user logs in or out.", 433 | demo, 434 | ) 435 | 436 | 437 | def clerk_loaded_demo() -> rx.Component: 438 | signed_in_area = rx.card( 439 | rx.vstack( 440 | rx.text("You'll only see content below if you are signed in"), 441 | rx.divider(), 442 | clerk.signed_in( 443 | rx.text("You are signed in.", data_testid="you_are_signed_in"), 444 | clerk.sign_out_button(rx.button("Sign out", width="100%")), 445 | ), 446 | ) 447 | ) 448 | signed_out_area = rx.card( 449 | rx.vstack( 450 | rx.text("You'll only see content below if you are signed out"), 451 | rx.divider(), 452 | clerk.signed_out( 453 | rx.text("You are signed out.", data_testid="you_are_signed_out"), 454 | clerk.sign_in_button(rx.button("Sign in", width="100%")), 455 | ), 456 | ) 457 | ) 458 | 459 | demo = rx.fragment( 460 | clerk.clerk_loading( 461 | rx.text("Clerk is loading..."), 462 | rx.spinner(size="3"), 463 | ), 464 | clerk.clerk_loaded( 465 | rx.vstack( 466 | rx.text("Clerk is loaded!"), 467 | rx.grid( 468 | signed_in_area, 469 | signed_out_area, 470 | columns="2", 471 | spacing="3", 472 | ), 473 | align="center", 474 | ), 475 | ), 476 | ) 477 | return demo_card( 478 | "Clerk loaded and signed in/out areas", 479 | rx.markdown( 480 | "Demo of `clerk_loaded`, `clerk_loading`, and `signed_in`, `signed_out` components." 481 | ), 482 | demo, 483 | ) 484 | 485 | 486 | def links_to_demo_pages() -> rx.Component: 487 | demo = rx.vstack( 488 | rx.markdown( 489 | dedent("""\ 490 | To use the built-in pages, just do: 491 | 492 | ```python 493 | clerk.add_sign_in_page(app) 494 | clerk.add_sign_up_page(app) 495 | ``` 496 | 497 | But, you can also create your own with more customization.""") 498 | ), 499 | clerk.signed_out( 500 | rx.grid( 501 | rx.link(rx.button("Go to sign up page", width="100%"), href="/sign-up"), 502 | rx.link(rx.button("Go to sign in page", width="100%"), href="/sign-in"), 503 | width="100%", 504 | columns="2", 505 | spacing="3", 506 | ) 507 | ), 508 | clerk.signed_in( 509 | rx.text("Sign out to see links to default sign-in and sign-up pages."), 510 | clerk.sign_out_button(rx.button("Sign out", width="100%")), 511 | ), 512 | ) 513 | return demo_card( 514 | "Sign-in and sign-up pages", 515 | "Some basic sign-in and sign-up pages are implemented for easy use. You can also create your own.", 516 | demo, 517 | ) 518 | 519 | 520 | def user_info_demo() -> rx.Component: 521 | demo = rx.vstack( 522 | rx.markdown( 523 | dedent("""\ 524 | To enable this behaviour, when creating the `clerk_provider`, set `register_user_state=True`. 525 | ```clerk.clerk_provider(..., register_user_state=True)``` 526 | 527 | This is not enabled by default to avoid unnecessary api calls to the Clerk backend. Also note that only a subset of user information is retrieved by the ClerkUser state. 528 | 529 | Full user information can be retrieved easily within event handler methods via `await clerk.get_user(self)` that will return a full `clerk_backend_api.models.User` model. 530 | 531 | Test credentials will not have a name or image by default. 532 | """) 533 | ), 534 | clerk.signed_in( 535 | rx.hstack( 536 | rx.card( 537 | rx.data_list.root( 538 | data_list_item("first name", clerk.ClerkUser.first_name), 539 | data_list_item("last name", clerk.ClerkUser.last_name), 540 | data_list_item("username", clerk.ClerkUser.username), 541 | data_list_item("email", clerk.ClerkUser.email_address), 542 | data_list_item("has image", rx.text(clerk.ClerkUser.has_image)), 543 | ), 544 | # border=f"1px solid {rx.color('gray', 6)}", 545 | # padding="2em", 546 | ), 547 | rx.avatar(src=clerk.ClerkUser.image_url, fallback="No image", size="9"), 548 | width="100%", 549 | justify="center", 550 | spacing="5", 551 | ) 552 | ), 553 | clerk.signed_out(rx.text("Sign in to see user information.")), 554 | ) 555 | 556 | return demo_card( 557 | "ClerkUser info", 558 | "To conveniently use basic information within the frontend, you can use the `clerk.ClerkUser` state.", 559 | demo, 560 | ) 561 | 562 | 563 | def user_profile_demo() -> rx.Component: 564 | demo = rx.vstack( 565 | rx.text( 566 | "Either include the ", 567 | rx.code("clerk.user_profile"), 568 | " component that renders a UI within your page.", 569 | ), 570 | rx.popover.root( 571 | rx.popover.trigger( 572 | rx.button("Show in popover"), 573 | ), 574 | rx.popover.content( 575 | rx.popover.close( 576 | close_icon, 577 | ), 578 | clerk.user_profile(), 579 | max_width="1000px", 580 | avoid_collisions=True, 581 | ), 582 | ), 583 | rx.text( 584 | "Or you can redirect the user by rendering ", 585 | rx.code("clerk.redirect_to_user_profile()"), 586 | ". However, this will redirect as soon as it is rendered, so it's a bit tricky to use.", 587 | ), 588 | width="100%", 589 | ) 590 | 591 | return demo_card( 592 | "User profile management", 593 | "Users can manage their profile via the Clerk interface.", 594 | demo, 595 | ) 596 | 597 | 598 | def demo_header() -> rx.Component: 599 | demo_intro = rx.vstack( 600 | rx.text( 601 | "The demos below are using a development Clerk API key, so you can try out everything with fake credentials." 602 | ), 603 | rx.text("To simply log in, you can use the email/password combination."), 604 | ) 605 | clerk_user_info = rx.box( 606 | clerk.clerk_loaded( 607 | rx.cond( 608 | clerk.ClerkState.is_signed_in, 609 | rx.card( 610 | rx.hstack( 611 | rx.data_list.root( 612 | data_list_item( 613 | "Clerk user_id", rx.text(clerk.ClerkState.user_id) 614 | ), 615 | data_list_item("User button", clerk.user_button()), 616 | ), 617 | ), 618 | ), 619 | rx.text("Sign in to see user info."), 620 | ) 621 | ), 622 | clerk.clerk_loading(rx.spinner(size="3")), 623 | ) 624 | 625 | test_user_and_pass = rx.card( 626 | rx.vstack( 627 | rx.data_list.root( 628 | data_list_item("username", rx.code("test+clerk_test@gmail.com")), 629 | data_list_item("password", rx.code("test-clerk-password")), 630 | ), 631 | rx.hstack( 632 | clerk.signed_in( 633 | clerk.sign_out_button(rx.button("Sign out", data_testid="sign_out")) 634 | ), 635 | clerk.signed_out( 636 | rx.hstack( 637 | clerk.sign_in_button( 638 | rx.button("Sign in", data_testid="sign_in") 639 | ), 640 | clerk.sign_up_button( 641 | rx.button("Sign up", data_testid="sign_up") 642 | ), 643 | ), 644 | ), 645 | ), 646 | ), 647 | ) 648 | using_demo_instructions = rx.box( 649 | rx.text( 650 | "Or if you want test signing up, you can use any email with ", 651 | rx.code("+clerk_test"), 652 | " appended to it. E.g., ", 653 | rx.code("any_email+clerk_test@anydomain.com"), 654 | ".", 655 | ), 656 | rx.text( 657 | "Use any password you like, and the verification code will be ", 658 | rx.code("424242"), 659 | ".", 660 | ), 661 | rx.text( 662 | "More info on test credentials can be found ", 663 | rx.link( 664 | "in the Clerk documentation.", 665 | href="https://clerk.com/docs/testing/test-emails-and-phones", 666 | ), 667 | ), 668 | ) 669 | 670 | return rx.vstack( 671 | rx.heading("Demos", size="6"), 672 | rx.grid( 673 | demo_intro, 674 | clerk_user_info, 675 | test_user_and_pass, 676 | using_demo_instructions, 677 | columns=rx.breakpoints(initial="1", sm="2"), 678 | spacing="4", 679 | ), 680 | ) 681 | 682 | 683 | def index() -> rx.Component: 684 | clerk.register_on_auth_change_handler(State.do_something_on_log_in_or_out) 685 | 686 | # Note: Using `clerk.wrap_app(...)` instead of `clerk.clerk_provider(...)` here. 687 | return rx.box( 688 | rx.vstack( 689 | rx.flex( 690 | demo_page_header_and_description(), 691 | getting_started(), 692 | spacing="7", 693 | direction=rx.breakpoints(initial="column", sm="row"), 694 | ), 695 | # rx.button("Dev reset", on_click=clerk.ClerkState.force_reset), 696 | rx.divider(), 697 | demo_header(), 698 | rx.grid( 699 | current_clerk_state_values(), 700 | clerk_loaded_demo(), 701 | on_load_demo(), 702 | on_auth_change_demo(), 703 | user_info_demo(), 704 | links_to_demo_pages(), 705 | user_profile_demo(), 706 | columns=rx.breakpoints(initial="1", sm="2", md="3", xl="4"), 707 | spacing="4", 708 | align="stretch", 709 | ), 710 | align="center", 711 | spacing="7", 712 | ), 713 | height="100vh", 714 | max_width="100%", 715 | overflow_y="auto", 716 | padding="2em", 717 | ) 718 | 719 | 720 | # Add state and page to the app. 721 | app = rx.App() 722 | 723 | # This wraps the entire app (all pages) with the ClerkProvider. 724 | clerk.wrap_app( 725 | app, 726 | publishable_key=os.environ["CLERK_PUBLISHABLE_KEY"], 727 | secret_key=os.environ.get("CLERK_SECRET_KEY"), 728 | register_user_state=True, 729 | # NOTE: Colors customizable via the `Appearance` object. (baseTheme is not yet implemented) 730 | # appearance=Appearance( 731 | # variables=Variables( 732 | # color_primary="#111A27", 733 | # color_background="#C2F3FF", 734 | # ), 735 | # ), 736 | ) 737 | 738 | # NOTE: Use the `clerk.on_load` to ensure that the ClerkState is updated *before* any other on_load events are run. 739 | # The `ClerkState` is updated by an event sent from the frontend that is not guaranteed to run before the reflex on_load events. 740 | app.add_page( 741 | index, 742 | on_load=[ 743 | *clerk.on_load([State.do_something_on_load]), 744 | State.do_something_on_load_without_wrapper, 745 | ], 746 | ) 747 | clerk.add_sign_in_page(app) 748 | clerk.add_sign_up_page(app) 749 | --------------------------------------------------------------------------------