Skip to content

Login Button Widget

The @flowsta/login-button package provides beautiful, pre-styled "Login with Flowsta" buttons for all major frameworks.

Installation

bash
npm install @flowsta/login-button

Supported Frameworks

FrameworkImport PathStatus
React@flowsta/login-button/react✅ React 16.8+
Vue 3@flowsta/login-button/vue✅ Composition API
Qwik@flowsta/login-button/qwik✅ Resumable
Vanilla JS@flowsta/login-button/vanilla✅ Framework-agnostic

Button Variants

The widget comes with 6 pre-designed variants:

VariantDescriptionBest For
dark_pillDark background, pill shapeLight backgrounds
dark_rectangleDark background, rectangleLight backgrounds, formal UI
light_pillWhite background, pill shapeDark backgrounds
light_rectangleWhite background, rectangleDark backgrounds, formal UI
neutral_pillGray background, pill shapeAny background
neutral_rectangleGray background, rectangleAny background

Visual Preview

Button Variants

React

Basic Usage

tsx
import { FlowstaLoginButton } from '@flowsta/login-button/react';

function App() {
  return (
    <FlowstaLoginButton
      clientId="your_client_id"
      redirectUri="https://yourapp.com/auth/callback"
      scope="profile email"
      variant="dark_pill"
      onSuccess={(data) => console.log('Code:', data.code)}
      onError={(error) => console.error('Error:', error)}
    />
  );
}

Props

PropTypeRequiredDescription
clientIdstringYour Flowsta app's client ID
redirectUristringWhere to redirect after login
scopestringSpace-separated scopes ("profile email")
variantButtonVariantButton style (default: "dark_pill")
statestringCSRF protection state parameter
onSuccess(data) => voidCallback on successful authorization
onError(error) => voidCallback on error

Full Example

tsx
import { useState } from 'react';
import { FlowstaLoginButton } from '@flowsta/login-button/react';

function LoginPage() {
  const [loading, setLoading] = useState(false);
  const [error, setError] = useState<string | null>(null);

  const handleSuccess = (data: { code: string; state: string }) => {
    setLoading(true);
    
    // Send to your backend
    fetch('/api/auth/callback', {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({ code: data.code, state: data.state }),
    })
      .then(res => res.json())
      .then(user => {
        // Redirect to dashboard
        window.location.href = '/dashboard';
      })
      .catch(err => {
        setError(err.message);
        setLoading(false);
      });
  };

  const handleError = (error: Error) => {
    setError(error.message);
  };

  return (
    <div className="login-page">
      <h1>Welcome to My App</h1>
      
      {error && (
        <div className="error-message">
          {error}
        </div>
      )}

      {loading ? (
        <div>Loading...</div>
      ) : (
        <FlowstaLoginButton
          clientId={import.meta.env.VITE_FLOWSTA_CLIENT_ID}
          redirectUri={`${window.location.origin}/auth/callback`}
          scope="profile email"
          variant="dark_pill"
          onSuccess={handleSuccess}
          onError={handleError}
        />
      )}
    </div>
  );
}

Vue 3

Basic Usage

vue
<script setup lang="ts">
import { FlowstaLoginButtonVue } from '@flowsta/login-button/vue';

const handleSuccess = (data: { code: string; state: string }) => {
  console.log('Authorization code:', data.code);
};

const handleError = (error: Error) => {
  console.error('Login error:', error);
};
</script>

<template>
  <FlowstaLoginButtonVue
    client-id="your_client_id"
    redirect-uri="https://yourapp.com/auth/callback"
    scope="profile email"
    variant="dark_pill"
    @success="handleSuccess"
    @error="handleError"
  />
</template>

Props

PropTypeRequiredDescription
client-idstringYour Flowsta app's client ID
redirect-uristringWhere to redirect after login
scopestringSpace-separated scopes
variantstringButton style (default: "dark_pill")
statestringCSRF protection state

Events

EventPayloadDescription
@success{ code: string, state: string }Emitted on successful auth
@errorErrorEmitted on error

Full Example

vue
<script setup lang="ts">
import { ref } from 'vue';
import { FlowstaLoginButtonVue } from '@flowsta/login-button/vue';

const loading = ref(false);
const error = ref<string | null>(null);

const handleSuccess = async (data: { code: string; state: string }) => {
  loading.value = true;
  error.value = null;

  try {
    const response = await fetch('/api/auth/callback', {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({ code: data.code, state: data.state }),
    });

    if (!response.ok) throw new Error('Authentication failed');

    const user = await response.json();
    window.location.href = '/dashboard';
  } catch (err) {
    error.value = err instanceof Error ? err.message : 'Unknown error';
    loading.value = false;
  }
};

const handleError = (err: Error) => {
  error.value = err.message;
};
</script>

<template>
  <div class="login-page">
    <h1>Welcome to My App</h1>

    <div v-if="error" class="error-message">
      {{ error }}
    </div>

    <div v-if="loading">
      <p>Logging you in...</p>
    </div>
    <FlowstaLoginButtonVue
      v-else
      client-id="your_client_id"
      redirect-uri="https://yourapp.com/auth/callback"
      scope="profile email"
      variant="dark_pill"
      @success="handleSuccess"
      @error="handleError"
    />
  </div>
</template>

Qwik

Basic Usage

tsx
import { component$, $ } from '@builder.io/qwik';
import { FlowstaLoginButtonQwik } from '@flowsta/login-button/qwik';

export default component$(() => {
  const handleSuccess = $((data: { code: string; state: string }) => {
    console.log('Authorization code:', data.code);
  });

  const handleError = $((error: Error) => {
    console.error('Login error:', error);
  });

  return (
    <FlowstaLoginButtonQwik
      clientId="your_client_id"
      redirectUri="https://yourapp.com/auth/callback"
      scope="profile email"
      variant="dark_pill"
      onSuccess$={handleSuccess}
      onError$={handleError}
    />
  );
});

Props

PropTypeRequiredDescription
clientIdstringYour Flowsta app's client ID
redirectUristringWhere to redirect after login
scopestringSpace-separated scopes
variantButtonVariantButton style (default: "dark_pill")
statestringCSRF protection state
onSuccess$QRL<(data) => void>Success callback (QRL wrapped)
onError$QRL<(error) => void>Error callback (QRL wrapped)

Qwik QRLs

Qwik requires event handlers to be wrapped with $() for lazy loading. Use onSuccess$ and onError$ (with dollar sign).

Full Example

tsx
import { component$, $, useSignal } from '@builder.io/qwik';
import { FlowstaLoginButtonQwik } from '@flowsta/login-button/qwik';

export default component$(() => {
  const loading = useSignal(false);
  const error = useSignal<string | null>(null);

  const handleSuccess = $(async (data: { code: string; state: string }) => {
    loading.value = true;
    error.value = null;

    try {
      const response = await fetch('/api/auth/callback', {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify({ code: data.code, state: data.state }),
      });

      if (!response.ok) throw new Error('Authentication failed');

      window.location.href = '/dashboard';
    } catch (err) {
      error.value = err instanceof Error ? err.message : 'Unknown error';
      loading.value = false;
    }
  });

  const handleError = $((err: Error) => {
    error.value = err.message;
  });

  return (
    <div class="login-page">
      <h1>Welcome to My App</h1>

      {error.value && (
        <div class="error-message">
          {error.value}
        </div>
      )}

      {loading.value ? (
        <div>Logging you in...</div>
      ) : (
        <FlowstaLoginButtonQwik
          clientId={import.meta.env.VITE_FLOWSTA_CLIENT_ID}
          redirectUri={`${window.location.origin}/auth/callback`}
          scope="profile email"
          variant="dark_pill"
          onSuccess$={handleSuccess}
          onError$={handleError}
        />
      )}
    </div>
  );
});

Vanilla JavaScript

Basic Usage

html
<!DOCTYPE html>
<html>
<head>
  <title>Login with Flowsta</title>
</head>
<body>
  <div id="login-button"></div>

  <script type="module">
    import { FlowstaLoginButton } from '@flowsta/login-button/vanilla';

    const button = new FlowstaLoginButton({
      clientId: 'your_client_id',
      redirectUri: 'https://yourapp.com/auth/callback',
      scope: 'profile email',
      variant: 'dark_pill',
      
      onSuccess: (data) => {
        console.log('Authorization code:', data.code);
      },
      
      onError: (error) => {
        console.error('Login error:', error);
      },
    });

    button.mount('#login-button');
  </script>
</body>
</html>

Options

OptionTypeRequiredDescription
clientIdstringYour Flowsta app's client ID
redirectUristringWhere to redirect after login
scopestringSpace-separated scopes
variantstringButton style (default: "dark_pill")
statestringCSRF protection state
onSuccessfunctionSuccess callback
onErrorfunctionError callback

Methods

javascript
const button = new FlowstaLoginButton(options);

// Mount to DOM element
button.mount('#login-button');
button.mount(document.getElementById('login-button'));

// Unmount (cleanup)
button.unmount();

// Trigger login programmatically
button.login();

Full Example

html
<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>My App - Login</title>
  <style>
    .login-container {
      max-width: 400px;
      margin: 100px auto;
      padding: 40px;
      text-align: center;
      border: 1px solid #e5e7eb;
      border-radius: 8px;
    }
    .error-message {
      color: #dc2626;
      margin-bottom: 20px;
      padding: 12px;
      background: #fee2e2;
      border-radius: 6px;
    }
  </style>
</head>
<body>
  <div class="login-container">
    <h1>Welcome to My App</h1>
    <div id="error-message" class="error-message" style="display: none;"></div>
    <div id="login-button"></div>
  </div>

  <script type="module">
    import { FlowstaLoginButton } from '@flowsta/login-button/vanilla';

    const errorDiv = document.getElementById('error-message');

    const button = new FlowstaLoginButton({
      clientId: 'your_client_id',
      redirectUri: `${window.location.origin}/auth/callback`,
      scope: 'profile email',
      variant: 'dark_pill',
      
      onSuccess: async (data) => {
        try {
          const response = await fetch('/api/auth/callback', {
            method: 'POST',
            headers: { 'Content-Type': 'application/json' },
            body: JSON.stringify({ code: data.code, state: data.state }),
          });

          if (!response.ok) throw new Error('Authentication failed');

          window.location.href = '/dashboard';
        } catch (error) {
          errorDiv.textContent = error.message;
          errorDiv.style.display = 'block';
        }
      },
      
      onError: (error) => {
        errorDiv.textContent = `Login failed: ${error.message}`;
        errorDiv.style.display = 'block';
      },
    });

    button.mount('#login-button');
  </script>
</body>
</html>

Advanced Usage

Custom State Parameter

Generate a random state for CSRF protection:

javascript
import { FlowstaLoginButton } from '@flowsta/login-button/react';

function LoginButton() {
  const [state] = useState(() => 
    Math.random().toString(36).substring(2, 15)
  );

  // Store state in session/localStorage for verification later
  useEffect(() => {
    sessionStorage.setItem('oauth_state', state);
  }, [state]);

  return (
    <FlowstaLoginButton
      clientId="your_client_id"
      redirectUri="https://yourapp.com/auth/callback"
      scope="profile email"
      state={state}
      variant="dark_pill"
      onSuccess={(data) => {
        // Verify state matches
        if (data.state !== sessionStorage.getItem('oauth_state')) {
          console.error('State mismatch - possible CSRF attack');
          return;
        }
        // Continue with authentication...
      }}
    />
  );
}

Dynamic Redirect URI

Use environment variables for different environments:

javascript
const redirectUri = import.meta.env.DEV
  ? 'http://localhost:3000/auth/callback'
  : 'https://yourapp.com/auth/callback';

<FlowstaLoginButton
  clientId="your_client_id"
  redirectUri={redirectUri}
  scope="profile email"
/>

Programmatic Login

Trigger login without button click:

javascript
import { FlowstaLoginButton } from '@flowsta/login-button/vanilla';

const button = new FlowstaLoginButton({
  clientId: 'your_client_id',
  redirectUri: 'https://yourapp.com/auth/callback',
  scope: 'profile',
});

// Don't mount, just use the login method
button.login();

Styling

The button widget uses inline styles and doesn't require external CSS. However, you can wrap it in a container for additional styling:

css
.login-button-container {
  display: flex;
  justify-content: center;
  margin: 40px 0;
}

.login-button-container button {
  cursor: pointer;
  transition: transform 0.2s;
}

.login-button-container button:hover {
  transform: translateY(-2px);
}

TypeScript Support

The package includes full TypeScript definitions:

typescript
import { FlowstaLoginButton, ButtonVariant } from '@flowsta/login-button/react';

interface Props {
  variant?: ButtonVariant; // 'dark_pill' | 'dark_rectangle' | etc.
}

function CustomLogin({ variant = 'dark_pill' }: Props) {
  const handleSuccess = (data: { code: string; state: string }) => {
    // Type-safe data access
  };

  return (
    <FlowstaLoginButton
      clientId="your_client_id"
      redirectUri="https://yourapp.com/auth/callback"
      scope="profile email"
      variant={variant}
      onSuccess={handleSuccess}
    />
  );
}

Browser Support

  • ✅ Chrome/Edge 90+
  • ✅ Firefox 88+
  • ✅ Safari 14+
  • ✅ Opera 76+
  • ❌ IE 11 (not supported)

Bundle Size

PackageMinifiedGzipped
React~8 KB~3 KB
Vue~7 KB~2.5 KB
Qwik~6 KB~2 KB
Vanilla JS~5 KB~2 KB

Next Steps

Need Help?

Documentation licensed under CC BY-SA 4.0.