npm package: @followgate/js
Description: FollowGate SDK - Grow your audience with every download. Require social actions (follow, repost) before users can access your app.
Author: FollowGate
Latest version: 0.15.6

# @followgate/js

**Grow your audience with every download.** Require social actions (follow, repost) before users can access your app.

FollowGate is inspired by [Hypeddit](https://hypeddit.com) (for musicians), but built for software developers and app creators.

## Installation

```bash
npm install @followgate/js
```

## Quick Start (Built-in Modal)

Just a few lines of code - no custom UI needed:

```typescript
import { FollowGate } from '@followgate/js';

// Initialize once (e.g., in your app's entry point)
FollowGate.init({
  appId: 'your-app-id',
  apiKey: 'fg_live_xxx',
  userId: user.id, // Recommended for server-side verification
  // Target handle is configured in the Dashboard - no code needed!
  onComplete: () => {
    // User completed all actions - redirect to your app
    router.push('/dashboard');
  },
});

// Show the modal (that's it!)
FollowGate.show();
```

The SDK renders a beautiful, fully-functional modal with:

- Username input step
- Follow action with intent URL
- Repost action (optional, configured in Dashboard)
- Confirmation step with custom text support
- X button to close/cancel (redirects to `cancelUrl` if configured)
- Dark/light themes
- All styling included (no CSS needed)

## When to Show the Modal

```typescript
// After sign-up (recommended)
const handleSignUpSuccess = () => {
  FollowGate.show();
};

// Or check if already unlocked
if (!FollowGate.isUnlocked()) {
  FollowGate.show();
}
```

## Handling Success (Important!)

**The `onComplete` callback is called ONLY after the user successfully completes all required actions.** This is the right place to:

- Create the user account in your database
- Grant access to premium features
- Redirect to your app

```typescript
FollowGate.init({
  appId: 'your-app-id',
  apiKey: 'fg_live_xxx',
  // Handle configured in Dashboard!

  onComplete: async () => {
    // User completed all steps - NOW create the account!
    const user = FollowGate.getUser();

    await fetch('/api/create-account', {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({
        xUsername: user?.username, // Their X/Twitter username
        platform: user?.platform, // 'twitter'
      }),
    });

    // Then redirect to your app
    router.push('/dashboard');
  },
});
```

**Why this matters:** Don't create accounts before `onComplete` is called. Users might close the modal without completing the actions. Only when `onComplete` fires, you know they actually followed/reposted.

### Event Listeners for Fine-Grained Control

```typescript
// Fired when a SINGLE action is completed (follow OR repost)
FollowGate.on('complete', (data) => {
  console.log('Action completed:', data);
  // { platform: 'twitter', action: 'follow', target: 'your_handle', username: 'their_username' }
});

// Fired when ALL actions are completed (same timing as onComplete)
FollowGate.on('unlocked', (data) => {
  console.log('Gate unlocked:', data);
  // { username: 'their_username', actions: [{ platform, action, target }, ...] }
});
```

## Username Input (2 Methods)

The SDK needs the user's social media username to work. There are exactly 2 ways to provide it:

**Method 1: User enters username manually (default)**
If no username is passed, the modal shows a Welcome step where the user types their username.

```typescript
FollowGate.init({
  appId: 'your-app-id',
  apiKey: 'fg_live_xxx',
  // Handle configured in Dashboard!
});

FollowGate.show(); // Modal starts with username input step
```

**Method 2: App passes username via code (skips Welcome step)**
If your app already knows the user's username (e.g. from your own auth system), pass it directly. The Welcome step is skipped entirely.

```typescript
FollowGate.init({
  appId: 'your-app-id',
  apiKey: 'fg_live_xxx',
  twitter: {
    username: 'their_x_username', // skips Welcome step
  },
});

FollowGate.show(); // Modal starts directly with Follow step
```

> **Note:** No Twitter API calls are made in either case. The SDK uses intent URLs only.

## Configuration Options

```typescript
FollowGate.init({
  // Required
  appId: 'your-app-id', // Your App ID from the dashboard
  apiKey: 'fg_live_xxx', // Your API key (starts with fg_live_ or fg_test_)

  // Twitter/X Configuration (all optional - configure in Dashboard!)
  twitter: {
    overrideHandle: 'other_handle', // Override Dashboard handle (highest priority)
    overridePostUrl: 'https://x.com/user/status/123', // Override Dashboard post URL for repost
    username: 'their_username', // Pre-fill user's X username (skips Welcome step)
  },

  // Callback when user completes all actions
  onComplete: () => {
    router.push('/dashboard');
  },

  // Optional
  userId: 'clerk_user_123', // User ID for per-user unlock status (recommended with auth)
  debug: false, // Enable console logging for debugging
  accentColor: '#6366f1', // Customize primary button color (hex)
  theme: 'dark', // 'dark' | 'light' - modal appearance
  apiUrl: 'https://...', // Custom API URL (for self-hosted)
  forceShow: false, // Always show modal, even if user already unlocked
});
```

**Dashboard Configuration:** Target handle, required actions, custom texts, redirect URLs, and skip options are all configured in the [Dashboard](https://followgate.app) and fetched automatically by the SDK.

**Handle Priority:**

1. `twitter.overrideHandle` (explicit code override - highest priority)
2. Dashboard `targetHandle` (default)

### Handle & Target Formats

The SDK automatically normalizes usernames - you can use `@username` or `username`, both work:

| Platform     | Action | Target Format                       | Example                              |
| ------------ | ------ | ----------------------------------- | ------------------------------------ |
| **Twitter**  | follow | Username (without @)                | `lukasvanuden`                       |
| **Twitter**  | repost | Post URL or Tweet ID                | `https://x.com/user/status/123`      |
| **Twitter**  | like   | Post URL or Tweet ID                | `https://x.com/user/status/123`      |
| **Bluesky**  | follow | Handle (with or without @)          | `user.bsky.social`                   |
| **Bluesky**  | repost | AT URI or post path                 | `user.bsky.social/post/xyz`          |
| **LinkedIn** | follow | Company name or prefixed identifier | `company:anthropic` or `in:username` |

## API Reference

### Modal Methods

```typescript
// Show the FollowGate modal (async - fetches config first)
// If user is already unlocked, calls onComplete immediately
await FollowGate.show();

// Hide the modal programmatically
// Pass true to redirect to cancelUrl (if configured)
FollowGate.hide();
FollowGate.hide(true); // redirect to cancelUrl

// Check if user has completed all actions
FollowGate.isUnlocked(); // true | false

// Reset user's session (for testing)
FollowGate.reset();

// Clear only unlock status, keep user data (v0.13.0+)
FollowGate.clearUnlockStatus();
```

### Advanced Usage

For custom UI implementations (when you don't want to use the built-in modal):

```typescript
// Set username manually (@ is automatically removed)
FollowGate.setUsername('your_username'); // or '@your_username' - both work!

// Check if username is set
FollowGate.hasUsername(); // true | false

// Open intent URLs directly (opens in new window)
// For follow: target = username (without @)
await FollowGate.openIntent({
  platform: 'twitter',
  action: 'follow',
  target: 'lukasvanuden', // NOT @lukasvanuden
});

// For repost: target = tweet ID
await FollowGate.openIntent({
  platform: 'twitter',
  action: 'repost',
  target: '1234567890123456789', // Tweet ID from URL
});

// Mark action as completed (tracks locally + sends to API)
await FollowGate.complete({
  platform: 'twitter',
  action: 'follow',
  target: 'lukasvanuden',
});

// Mark gate as unlocked (user has completed all required actions)
await FollowGate.unlock();

// Get current user info
FollowGate.getUser();
// Returns: { username: 'their_username', platform: 'twitter' } | null

// Get completed actions
FollowGate.getCompletedActions();
// Returns: [{ platform: 'twitter', action: 'follow', target: 'lukasvanuden' }, ...]

// Get full unlock status
FollowGate.getUnlockStatus();
// Returns: {
//   unlocked: boolean,
//   username?: string,
//   completedActions?: CompleteOptions[]
// }
```

### Server-Side Verification

When `userId` is set, completions are automatically saved to the server. You can verify users from your backend:

```typescript
// Check if user is verified (server-side)
const isVerified = await FollowGate.isVerified();
// Returns: true | false

// Get full verification status
const status = await FollowGate.getVerificationStatus();
// Returns: {
//   verified: boolean,
//   userId: string,
//   completedAt?: string,  // ISO date string
//   actions?: [{ action: 'follow', target: 'lukasvanuden' }, ...]
// }
```

**Use Case: Conditional Features**

```typescript
// In your app (e.g., Chrome extension, Electron app)
FollowGate.init({
  appId: 'your-app-id',
  apiKey: 'fg_live_xxx',
  userId: clerkUserId,
  // Handle configured in Dashboard!
});

const isVerified = await FollowGate.isVerified();

if (isVerified) {
  // User completed FollowGate - Full access
  enableAllFeatures();
} else {
  // User hasn't completed - Limited access
  enableTrialMode();
}
```

### Event Listeners

```typescript
FollowGate.on('complete', (data) => {
  console.log('Action completed:', data);
});

FollowGate.on('unlocked', (data) => {
  console.log('Gate unlocked:', data);
});

FollowGate.on('error', (error) => {
  console.error('Error:', error);
});

// Remove listener
FollowGate.off('complete', handler);
```

## Supported Platforms

| Platform  | Actions                    |
| --------- | -------------------------- |
| Twitter/X | `follow`, `repost`, `like` |
| Bluesky   | `follow`, `repost`, `like` |
| LinkedIn  | `follow`                   |

## Framework Examples

### Next.js (App Router)

```typescript
// app/welcome/page.tsx
'use client';

import { FollowGate } from '@followgate/js';
import { useRouter } from 'next/navigation';
import { useEffect } from 'react';

export default function WelcomePage() {
  const router = useRouter();

  useEffect(() => {
    FollowGate.init({
      appId: process.env.NEXT_PUBLIC_FOLLOWGATE_APP_ID!,
      apiKey: process.env.NEXT_PUBLIC_FOLLOWGATE_API_KEY!,
      // Handle configured in Dashboard!
      onComplete: () => router.push('/dashboard'),
    });

    // Show modal if not already unlocked
    if (!FollowGate.isUnlocked()) {
      FollowGate.show();
    } else {
      router.push('/dashboard');
    }
  }, [router]);

  return <div>Loading...</div>;
}
```

### With Clerk Auth

```typescript
// app/welcome/page.tsx
'use client';

import { FollowGate } from '@followgate/js';
import { useRouter } from 'next/navigation';
import { useUser } from '@clerk/nextjs';
import { useEffect } from 'react';

export default function WelcomePage() {
  const router = useRouter();
  const { user } = useUser();

  useEffect(() => {
    if (!user) return;

    FollowGate.init({
      appId: process.env.NEXT_PUBLIC_FOLLOWGATE_APP_ID!,
      apiKey: process.env.NEXT_PUBLIC_FOLLOWGATE_API_KEY!,
      userId: user.id, // Per-user unlock status (recommended!)
      // Handle configured in Dashboard!
      onComplete: () => router.push('/dashboard'),
    });

    if (!FollowGate.isUnlocked()) {
      FollowGate.show();
    } else {
      router.push('/dashboard');
    }
  }, [user, router]);

  return <div>Loading...</div>;
}
```

**Why use `userId`?** Without it, unlock status is stored per-browser. If User A completes the gate on a shared computer, User B would also be "unlocked". With `userId`, each user has their own unlock status.

## TypeScript

Full TypeScript support included:

```typescript
import type {
  Platform,
  SocialAction,
  FollowGateConfig,
  TwitterConfig,
  CompleteOptions,
  UserInfo,
  UnlockStatus,
  VerificationStatus,
} from '@followgate/js';
```

## Pricing

**FollowGate is 100% free.** No tiers, no limits, no billing. Use it for as many apps and users as you want.

## Links

- [Dashboard](https://followgate.app)
- [Documentation](https://followgate.app/docs)
- [GitHub](https://github.com/JustFF5/FollowGate)
- [Live Demo](https://follow-gate-web-demo.vercel.app)

## License

MIT