# Vortex React Native SDK

Configuration-driven invitation forms for React Native apps.

## Overview

The Vortex React Native SDK provides a React Native component for rendering customizable invitation forms in your React Native applications. The SDK fetches configuration from the [Vortex](https://vortexsoftware.com) platform and dynamically renders the appropriate UI.

## Requirements

- React 18+
- React Native 0.75.0+

## Installation

```bash
# Using pnpm (recommended)
pnpm add @teamvortexsoftware/vortex-react-native

# Using npm
npm install @teamvortexsoftware/vortex-react-native

# Using yarn
yarn add @teamvortexsoftware/vortex-react-native
```

## Quick Start

### Basic Usage

```tsx
import { VortexInvite } from '@teamvortexsoftware/vortex-react-native';
import { useState, useEffect } from 'react';

function MyInviteScreen() {
  const [jwt, setJwt] = useState<string | null>(null);

  useEffect(() => {
    // Generate JWT for authenticated user
    const generateJwt = async () => {
      try {
        const response = await fetch('https://your-api.com/api/vortex/jwt', {
          method: 'POST',
          headers: { 'Content-Type': 'application/json' },
        });
        const data = await response.json();
        setJwt(data.jwt);
      } catch (error) {
        console.error('Failed to generate JWT:', error);
      }
    };

    generateJwt();
  }, []);

  if (!jwt) {
    return <Text>Loading...</Text>;
  }

  return (
    <VortexInvite
      componentId="my-component-id"
      jwt={jwt}
      scope="team-123"
      scopeType="team"
      onInvite={(data) => {
        console.log('Invitation sent:', data);
      }}
      onError={(error) => {
        console.error('Invitation error:', error);
      }}
    />
  );
}
```

### With Group Context

```tsx
import { VortexInvite } from '@teamvortexsoftware/vortex-react-native';

function TeamInviteScreen({ teamId, teamName }: { teamId: string; teamName: string }) {
  const { jwt, error } = useVortexAuth();

  if (error) {
    return <Text>Error: {error.message}</Text>;
  }

  return (
    <VortexInvite
      componentId={process.env.EXPO_PUBLIC_COMPONENT_ID}
      jwt={jwt || ''}
      scope={teamId}
      scopeType="team"
      onInvite={(result) => {
        console.log('Invitations sent:', result);
      }}
    />
  );
}
```

### Complete Example with Modal

```tsx
import { useState, useEffect } from 'react';
import { Modal, View, Button, StyleSheet } from 'react-native';
import { VortexInvite } from '@teamvortexsoftware/vortex-react-native';

function InviteModal({
  visible,
  onClose,
  workspaceId,
}: {
  visible: boolean;
  onClose: () => void;
  workspaceId: string;
}) {
  const [jwt, setJwt] = useState<string>('');

  useEffect(() => {
    if (visible) {
      // Generate JWT when modal opens
      fetchJwt().then(setJwt);
    }
  }, [visible]);

  return (
    <Modal visible={visible} animationType="slide">
      <View style={styles.container}>
        <View style={styles.header}>
          <Button title="Close" onPress={onClose} />
        </View>

        <VortexInvite
          componentId={process.env.EXPO_PUBLIC_COMPONENT_ID}
          jwt={jwt}
          scope={workspaceId}
          scopeType="workspace"
          onInvite={(result) => {
            console.log('Invitations sent successfully');
            onClose();
          }}
          onError={(error) => {
            console.error('Failed to send invitations:', error);
          }}
        />
      </View>
    </Modal>
  );
}

const styles = StyleSheet.create({
  container: {
    flex: 1,
    backgroundColor: '#fff',
  },
  header: {
    padding: 16,
    borderBottomWidth: 1,
    borderBottomColor: '#e0e0e0',
  },
});
```

### Invite Contacts

The Invite Contacts component displays a list of contacts that can be invited via SMS. Unlike the Contact Import feature (which fetches contacts from the device), Invite Contacts receives a pre-populated list of contacts from your app.

**Basic Usage:**

```tsx
import { VortexInvite, InviteContactsConfig, InviteContactsContact } from '@teamvortexsoftware/vortex-react-native';

const contacts: InviteContactsContact[] = [
  { name: "Alice Johnson", phoneNumber: "+1 (555) 123-4567" },
  { name: "Bob Smith", phoneNumber: "+1 (555) 234-5678" },
  { name: "Carol Davis", phoneNumber: "+1 (555) 345-6789" }
];

<VortexInvite
  componentId="your-component-id"
  jwt={jwt}
  inviteContactsConfig={{ contacts }}
  onInvite={(result) => {
    console.log('Invitations sent:', result);
  }}
/>
```

**How It Works:**

1. The component shows an "Invite your contacts" entry with a contact count
2. Tapping it navigates to a searchable list of contacts
3. Each contact has an "Invite" button that:
   - Creates an SMS invitation via the Vortex API
   - Opens the device's SMS app (on supported devices)
   - Pre-fills the message with the invitation link

**InviteContactsContact Properties:**

```typescript
// Simple usage - just name and phone number (ID is auto-generated)
{
  name: string;            // Display name
  phoneNumber: string;     // Phone number for SMS
  avatarUrl?: string;      // Optional avatar image URL
  metadata?: Record<string, any>; // Optional custom metadata
}

// Full usage - with custom ID
{
  id: string;              // Custom unique identifier
  name: string;            // Display name
  phoneNumber: string;     // Phone number for SMS
  avatarUrl?: string;      // Optional avatar image URL
  metadata?: Record<string, any>; // Optional custom metadata
}
```

**InviteContactsConfig Properties:**

```typescript
{
  contacts: InviteContactsContact[];  // List of contacts to display
}
```

### Find Friends

The Find Friends component displays a list of contacts provided by your app. Each contact has a "Connect" button that creates an invitation with `targetType: internalId`.

**Basic Usage:**

```tsx
import { VortexInvite, FindFriendsConfig, FindFriendsContact } from '@teamvortexsoftware/vortex-react-native';

<VortexInvite
  componentId="your-component-id"
  jwt={jwt}
  findFriendsConfig={{
    contacts: [
      { internalId: "user-123", name: "Alice Johnson", subtitle: "@alice" },
      { internalId: "user-456", name: "Bob Smith", subtitle: "@bob" }
    ],
    onInvitationCreated: (contact) => {
      // Called after an invitation is successfully created
      console.log('Invitation created for', contact.name);
    }
  }}
/>
```

**How It Works:**

1. Your app provides a list of contacts with internal IDs (users already in your platform)
2. The component displays them with a "Connect" button (text configurable via widget config)
3. When the user taps "Connect", the SDK creates an invitation via the Vortex API with `targetType: internalId`
4. The `onInvitationCreated` callback is called after a successful invitation
5. The section is hidden when there are no contacts to display

**FindFriendsContact Properties:**

```typescript
{
  internalId: string;          // Required: ID in your platform
  name: string;                // Required: Display name
  subtitle?: string;           // Optional: Secondary text (e.g., username)
  avatarUrl?: string;          // Optional: Avatar image URL
  metadata?: Record<string, any>; // Optional: Custom metadata
}
```

**FindFriendsConfig Properties:**

```typescript
{
  contacts: FindFriendsContact[];                        // Required: List of contacts to display
  onInvitationCreated?: (contact: FindFriendsContact) => void; // Optional: Called after successful invitation
}
```

### Invitation Suggestions

The Invitation Suggestions component displays a list of suggested contacts provided by your app. Each contact has an "Invite" button and a dismiss (X) button. When the user taps "Invite", an invitation with `targetType: internalId` is created. The dismiss button removes the suggestion from the list.

**Basic Usage:**

```tsx
import { VortexInvite, InvitationSuggestionsConfig, InvitationSuggestionContact } from '@teamvortexsoftware/vortex-react-native';

<VortexInvite
  componentId="your-component-id"
  jwt={jwt}
  invitationSuggestionsConfig={{
    contacts: [
      {
        internalId: "user-123",
        name: "Alice Johnson",
        subtitle: "@alice",
        avatarUrl: "https://example.com/alice-avatar.jpg"
      },
      {
        internalId: "user-456",
        name: "Bob Smith",
        subtitle: "@bob"
      }
    ],
    onDismiss: (contact) => {
      // Called when user taps the X button
      console.log('Dismissed suggestion for', contact.name);
    },
    onInvitationCreated: (contact) => {
      console.log('Invitation created for', contact.name);
    },
    onInvitationFailed: (contact, error) => {
      console.error('Failed to invite', contact.name, error);
    }
  }}
/>
```

**How It Works:**

1. Your app provides a list of suggested contacts with internal IDs
2. The component displays them with an "Invite" button and a dismiss (X) button
3. When the user taps "Invite", the SDK creates an invitation via the Vortex API with `targetType: internalId`
4. The `onInvitationCreated` or `onInvitationFailed` callback is called based on the result
5. When the user taps the X button, the `onDismiss` callback is called and the contact is removed from the list

**InvitationSuggestionContact Properties:**

```typescript
{
  internalId: string;          // Required: ID in your platform
  name: string;                // Required: Display name
  subtitle?: string;           // Optional: Secondary text (e.g., username)
  avatarUrl?: string;          // Optional: Avatar image URL (rendered instead of initials if provided)
  metadata?: Record<string, any>; // Optional: Custom metadata
}
```

**InvitationSuggestionsConfig Properties:**

```typescript
{
  contacts: InvitationSuggestionContact[];                      // Required: List of contacts to display
  onDismiss: (contact: InvitationSuggestionContact) => void;    // Required: Called when user dismisses a suggestion
  onInvitationCreated?: (contact: InvitationSuggestionContact) => void;  // Called after successful invitation
  onInvitationFailed?: (contact: InvitationSuggestionContact, error: Error) => void;  // Called on failure
}
```

### Search Box

The Search Box component displays a search input with a search button. When the user taps the search button, your app's `onSearch` callback is invoked to return matching contacts. The results are rendered below the search box with "Connect" buttons, identical to the Find Friends component.

**Basic Usage:**

```tsx
import { VortexInvite, SearchBoxConfig, SearchBoxContact } from '@teamvortexsoftware/vortex-react-native';

<VortexInvite
  componentId="your-component-id"
  jwt={jwt}
  searchBoxConfig={{
    onSearch: async (query) => {
      // Return matching contacts from your backend or local data
      const results = await myAPI.searchUsers(query);
      return results.map(user => ({
        internalId: user.id,
        name: user.displayName,
        subtitle: `@${user.username}`,
        avatarUrl: user.avatarUrl
      }));
    },
    onInvitationCreated: (contact) => {
      console.log('Invitation created for', contact.name);
    }
  }}
/>
```

**How It Works:**

1. The component renders a search text field with a configurable placeholder and a search button
2. An optional title can be displayed above the search box (configured in the widget editor)
3. When the user taps the search button, your `onSearch` callback is called with the query string
4. Your callback returns a list of `SearchBoxContact` objects
5. The matching contacts are rendered below the search box with a "Connect" button next to each
6. If the search returns no results, a configurable "no results" message is displayed
7. When the user taps "Connect", the SDK creates an invitation via the Vortex API with `targetType: internalId` and the contact is removed from the list (identical to Find Friends behavior)

**SearchBoxConfig Properties:**

```typescript
{
  onSearch: (query: string) => Promise<SearchBoxContact[]>;      // Required: Search callback
  onInvitationCreated?: (contact: SearchBoxContact) => void;     // Optional: Called after successful invitation
}
```

> **Note:** The placeholder text, connect button text, no-results message, and title are all configurable from the widget editor.

### Incoming Invitations

The Incoming Invitations component displays invitations the user has received, with Accept and Delete actions.

**Basic Usage:**

```tsx
import { VortexInvite, IncomingInvitationsConfig } from '@teamvortexsoftware/vortex-react-native';

<VortexInvite
  componentId="your-component-id"
  jwt={jwt}
  incomingInvitationsConfig={{
    onAccept: async (invitation) => {
      // Handle acceptance (return true to proceed with API call)
      await myAPI.acceptInvitation(invitation.id);
      return true;
    },
    onDelete: async (invitation) => {
      // Handle deletion (return true to proceed with API call)
      return true;
    }
  }}
/>
```

**With Internal Invitations:**

You can merge your app's invitations with Vortex API invitations:

```tsx
<VortexInvite
  componentId="your-component-id"
  jwt={jwt}
  incomingInvitationsConfig={{
    internalInvitations: [
      {
        id: "internal-1",
        name: "Alice Johnson",
        subtitle: "alice@example.com",
        avatarUrl: "https://example.com/avatar.jpg"
      }
    ],
    onAccept: async (invitation) => {
      if (invitation.isVortexInvitation) {
        // Vortex invitation: return true to let SDK call the Vortex API
        return true;
      } else {
        // Internal/app invitation: handle it yourself
        await myAPI.acceptInvitation(invitation.id);
        return true;  // Return true to remove from list
      }
    },
    onDelete: async (invitation) => {
      if (invitation.isVortexInvitation) {
        return true;  // Let SDK handle the API call
      } else {
        await myAPI.deleteInvitation(invitation.id);
        return true;  // Return true to remove from list
      }
    }
  }}
/>
```

**Identifying Invitation Source:**

Use the `isVortexInvitation` property to determine where an invitation came from:
- `true`: Fetched from the Vortex API — the SDK will handle accept/delete API calls
- `false`: Provided by your app via `internalInvitations` — your app must handle the action

**IncomingInvitationsConfig Properties:**

```typescript
{
  internalInvitations?: IncomingInvitationItem[];  // App-provided invitations (isVortexInvitation = false)
  onAccept?: (invitation: IncomingInvitationItem) => Promise<boolean>;  // Called when user accepts
  onDelete?: (invitation: IncomingInvitationItem) => Promise<boolean>;  // Called when user deletes
}
```

**Callback Return Values:**

| Invitation Source | Return `true` | Return `false` |
|-------------------|---------------|----------------|
| Vortex (`isVortexInvitation === true`) | SDK calls Vortex API, removes from list | Cancels the action |
| Internal (`isVortexInvitation === false`) | Removes from list (no API call) | Keeps in list |

### Outgoing Invitations

The Outgoing Invitations component displays invitations the user has sent, with a Cancel action.

**Basic Usage:**

```tsx
import { VortexInvite, OutgoingInvitationsConfig } from '@teamvortexsoftware/vortex-react-native';

<VortexInvite
  componentId="your-component-id"
  jwt={jwt}
  outgoingInvitationsConfig={{
    onCancel: async (invitation) => {
      // Handle cancellation (return true to proceed with API call)
      return true;
    }
  }}
/>
```

**With Internal Invitations:**

You can merge your app's outgoing invitations with Vortex API invitations:

```tsx
<VortexInvite
  componentId="your-component-id"
  jwt={jwt}
  outgoingInvitationsConfig={{
    internalInvitations: [
      {
        id: "internal-1",
        name: "Bob Smith",
        subtitle: "user-12345",  // e.g., user ID or other identifier
        avatarUrl: "https://example.com/avatar.jpg"
      }
    ],
    onCancel: async (invitation) => {
      if (invitation.isVortexInvitation) {
        // Vortex invitation: return true to let SDK call the Vortex API
        return true;
      } else {
        // Internal/app invitation: handle it yourself
        await myAPI.cancelInvitation(invitation.id);
        return true;  // Return true to remove from list
      }
    }
  }}
/>
```

**Identifying Invitation Source:**

Use the `isVortexInvitation` property to determine where an invitation came from:
- `true`: Fetched from the Vortex API — the SDK will handle cancel API calls
- `false`: Provided by your app via `internalInvitations` — your app must handle the action

**OutgoingInvitationsConfig Properties:**

```typescript
{
  internalInvitations?: OutgoingInvitationItem[];  // App-provided invitations (isVortexInvitation = false)
  onCancel?: (invitation: OutgoingInvitationItem) => Promise<boolean>;  // Called when user cancels
}
```

**Callback Return Values:**

| Invitation Source | Return `true` | Return `false` |
|-------------------|---------------|----------------|
| Vortex (`isVortexInvitation === true`) | SDK calls Vortex API, removes from list | Cancels the action |
| Internal (`isVortexInvitation === false`) | Removes from list (no API call) | Keeps in list |

## Prefetch for Instant Rendering

The SDK supports prefetching widget configurations to eliminate loading delays when opening the invite form. This uses a **stale-while-revalidate** pattern: cached configurations are shown immediately while fresh data is fetched in the background.

### Basic Prefetch Example

```tsx
import { VortexInvite, usePrefetchWidgetConfiguration } from '@teamvortexsoftware/vortex-react-native';
import { useState, useEffect } from 'react';

function InviteModal({ onClose, groupId }) {
  const [jwt, setJwt] = useState<string | undefined>();
  
  // Fetch JWT on mount
  useEffect(() => {
    fetchJwt().then(setJwt);
  }, []);

  // Prefetch widget configuration as soon as JWT is available
  const { widgetConfiguration } = usePrefetchWidgetConfiguration({
    componentId: process.env.EXPO_PUBLIC_COMPONENT_ID,
    vortexApiUrl: 'https://client-api.vortexsoftware.com',
    jwt,
    enabled: !!jwt, // Only prefetch when JWT is ready
  });

  return (
    <VortexInvite
      componentId={process.env.EXPO_PUBLIC_COMPONENT_ID}
      jwt={jwt}
      widgetConfiguration={widgetConfiguration} // Pass prefetched config
      scope={groupId}
      scopeType="team"
      onClose={onClose}
    />
  );
}
```

### How It Works

1. **Without Prefetching** (default behavior):
   - Component mounts → Shows loading spinner
   - Fetches configuration → Renders form

2. **With Prefetching** (optimized):
   - Parent prefetches configuration early (when JWT becomes available)
   - Component mounts → **Renders form immediately** (no loading spinner!)
   - Fetches latest configuration in background
   - Updates form if configuration changed

### Benefits

✅ **Instant rendering** - No loading spinner when modal/screen is shown  
✅ **Always fresh** - Component still fetches latest configuration in background  
✅ **Automatic updates** - Form updates if configuration changes on server  
✅ **Backward compatible** - Works perfectly without prefetching (shows loading spinner as before)

## Optional Features

The SDK supports optional native features that enhance the user experience. These features require additional libraries that you can install based on your needs.

### Why Optional?

These features use native code that must be explicitly installed in your app. The SDK gracefully degrades when these libraries aren't installed:

- **Gradients** → Falls back to solid colors
- **Haptics** → Silently skipped
- **QR Codes** → Shows placeholder message
- **Clipboard** → Falls back to web clipboard API, or shows error with install instructions
- **Contacts** → Shows error with install instructions when contacts import is triggered
- **Google Sign-In** → Shows warning when Google contacts import is triggered

### Configuration

Specify which libraries you've installed via the unified `modules` prop on `VortexInvite`:

```tsx
<VortexInvite
  componentId="your-component-id"
  jwt={jwt}
  // Optional feature modules (unified configuration)
  modules={{
    gradient: 'expo-linear-gradient',
    haptics: 'expo-haptics',
    qrCode: 'react-native-qrcode-svg',
    clipboard: 'expo-clipboard',
    contacts: 'expo-contacts',
    googleSignin: '@react-native-google-signin/google-signin',
  }}
/>
```

### Available Features

| Feature | Property | Options | Description |
|---------|----------|---------|-------------|
| Gradients | `modules.gradient` | `'expo-linear-gradient'` or `'react-native-linear-gradient'` | Gradient backgrounds on buttons |
| Haptics | `modules.haptics` | `'expo-haptics'` | Haptic feedback on button presses |
| QR Codes | `modules.qrCode` | `'react-native-qrcode-svg'` or `'react-qr-code'` | QR code display for shareable links |
| Clipboard | `modules.clipboard` | `'expo-clipboard'` or `'@react-native-clipboard/clipboard'` | Clipboard operations (copy link) |
| Contacts | `modules.contacts` | `'expo-contacts'` | Device contacts import |
| Google Sign-In | `modules.googleSignin` | `'@react-native-google-signin/google-signin'` | Google Contacts import |
| Sharing | `modules.sharing` | `'expo-sharing'` | Native share sheet |

### Installation by Project Type

#### Expo Projects (Recommended)

```bash
# Gradients
npx expo install expo-linear-gradient

# Haptics
npx expo install expo-haptics

# QR Codes (requires both packages)
npx expo install react-native-qrcode-svg react-native-svg

# Clipboard
npx expo install expo-clipboard

# Contacts (device contacts import)
npx expo install expo-contacts

# Google Sign-In (Google contacts import)
npm install @react-native-google-signin/google-signin

# Sharing
npx expo install expo-sharing
```

Then configure:

```tsx
<VortexInvite
  componentId="your-component-id"
  jwt={jwt}
  modules={{
    gradient: 'expo-linear-gradient',
    haptics: 'expo-haptics',
    qrCode: 'react-native-qrcode-svg',
    clipboard: 'expo-clipboard',
    contacts: 'expo-contacts',
    googleSignin: '@react-native-google-signin/google-signin',
    sharing: 'expo-sharing',
  }}
/>
```

#### Bare React Native Projects

```bash
# Gradients
npm install react-native-linear-gradient
cd ios && pod install

# QR Codes (requires both packages)
npm install react-native-qrcode-svg react-native-svg
cd ios && pod install

# Clipboard
npm install @react-native-clipboard/clipboard
cd ios && pod install

# Contacts (device contacts import)
npx expo install expo-contacts
# or for bare RN, follow expo-contacts installation docs

# Google Sign-In (Google contacts import)
npm install @react-native-google-signin/google-signin
cd ios && pod install
```

Then configure:

```tsx
<VortexInvite
  componentId="your-component-id"
  jwt={jwt}
  modules={{
    gradient: 'react-native-linear-gradient',
    qrCode: 'react-native-qrcode-svg',
    clipboard: '@react-native-clipboard/clipboard',
    contacts: 'expo-contacts',
    googleSignin: '@react-native-google-signin/google-signin',
  }}
/>
```

### Helpful Warnings

In development mode, the SDK will print helpful warnings when:
- A feature is detected in your widget configuration but no module is specified
- A module is specified but not installed

Example warning:
```
[Vortex] ⚠️ Gradient backgrounds detected in widget configuration, but no gradient module specified.

To enable gradients on native platforms, add the modules prop to <VortexInvite>:

  For Expo projects:
    <VortexInvite modules={{ gradient: 'expo-linear-gradient' }} ... />
    Then run: npx expo install expo-linear-gradient
```

---

## Deferred Deep Linking

Deferred deep linking allows your app to retrieve invitation context even when a user installs the app after clicking an invitation link. When a user clicks an invitation link but doesn't have the app installed, they're redirected to the App Store or Play Store. After installation, the SDK can match the device fingerprint to retrieve the original invitation context.

### Basic Usage

Call `VortexDeferredLinks.retrieveDeferredDeepLink` when the user signs in or when the app session is restored:

```tsx
import { VortexDeferredLinks } from '@teamvortexsoftware/vortex-react-native';

// In your authentication manager or app initialization
async function onUserSignedIn(vortexJwt: string) {
  try {
    const result = await VortexDeferredLinks.retrieveDeferredDeepLink({ jwt: vortexJwt });

    if (result.matched && result.deepLink) {
      console.log('Found pending invitation!');
      console.log('Invitation ID:', result.invitationId);
      console.log('Deep Link:', result.deepLink);
      console.log('Metadata:', result.metadata);
      // Handle the invitation (e.g., show UI, auto-join group, navigate to specific screen)
    }
  } catch (error) {
    console.error('Deferred link check failed:', error);
  }
}
```

### Response Types

**MatchFingerprintResponse:**
```typescript
interface MatchFingerprintResponse {
  matched: boolean;           // Whether a matching invitation was found
  invitationId?: string;      // The original invitation ID
  deepLink?: string;          // Deep link URL to handle
  metadata?: Record<string, any>;  // Additional metadata
}
```

### Best Practices

1. **Call on authentication**: Check for deferred deep links immediately after user sign-in or session restore
2. **Use Vortex JWT**: The endpoint requires a Vortex JWT token (not your app's auth token)
3. **Handle once**: Clear or mark the invitation as handled after processing to avoid showing it repeatedly
4. **Graceful degradation**: The check may fail (network issues, no match found) - handle errors gracefully

### Example Integration with Navigation

```tsx
import { useEffect } from 'react';
import { VortexDeferredLinks } from '@teamvortexsoftware/vortex-react-native';

function App() {
  const [jwt, setJwt] = useState<string | null>(null);
  const navigation = useNavigation();

  useEffect(() => {
    if (jwt) {
      checkForPendingInvitations();
    }
  }, [jwt]);

  async function checkForPendingInvitations() {
    try {
      const result = await VortexDeferredLinks.retrieveDeferredDeepLink({ jwt });

      if (result.matched && result.deepLink) {
        // Parse deep link and navigate
        const params = parseDeepLink(result.deepLink);
        navigation.navigate('TeamScreen', { teamId: params.teamId });
      }
    } catch (error) {
      // Log error but don't block the user
      console.error('Deferred deep link check failed:', error);
    }
  }

  return <YourApp />;
}
```

## Unfurl Configuration

When invitation links are shared on social platforms (iMessage, WhatsApp, Facebook, Twitter, etc.), the `UnfurlConfig` allows you to customize the Open Graph metadata that appears in the link preview card.

### Usage

```tsx
<VortexInvite
  componentId="your-component-id"
  jwt={jwt}
  scope={teamId}
  scopeType="team"
  unfurlConfig={{
    title: "Join our team!",
    description: "You've been invited to collaborate on an exciting project",
    image: "https://example.com/preview-image.png",
    siteName: "My App",
    type: "website"
  }}
  onInvite={(result) => {
    console.log('Invitations sent:', result);
  }}
/>
```

### UnfurlConfig Properties

| Property | Type | Description |
|----------|------|-------------|
| `title` | `string?` | The title shown in the link preview (`og:title`) |
| `description` | `string?` | The description shown in the link preview (`og:description`) |
| `image` | `string?` | URL to the image shown in the link preview (`og:image`). Must be a valid URL. |
| `siteName` | `string?` | The site name shown in the link preview (`og:site_name`) |
| `type` | `string?` | The Open Graph type (`og:type`). Defaults to "website" if not provided. |

### Priority Chain

The backend uses this priority for unfurl values:
1. **Invitation metadata** (values from `UnfurlConfig`) - highest priority
2. **Domain profile unfurlData** - fallback from account settings in Vortex dashboard
3. **Hardcoded defaults** - last resort

## Features

**Invitation Methods:**
- Email invitations with validation
- Copy shareable link to clipboard
- Native share sheet integration
- SMS sharing
- QR code generation
- WhatsApp, LINE, and other social messaging integrations

**Contact Import:**
- Device Contacts integration (iOS/Android)
- Google Contacts integration (via Google Sign-In)

**Advanced Components:**
- **Invite Contacts** - Display a list of contacts for SMS invitation
- **Find Friends** - Display contacts with "Connect" buttons for internal invitations
- **Invitation Suggestions** - Show suggested contacts with invite/dismiss actions
- **Search Box** - Search and invite users from your platform
- **Incoming Invitations** - Display and manage received invitations
- **Outgoing Invitations** - Display and manage sent invitations

**Core Capabilities:**
- Dynamic form rendering from server configuration
- JWT authentication
- Group/team/workspace context support
- Real-time loading states and error handling
- Customizable UI based on widget configuration
- Deferred deep linking via fingerprint matching
- Prefetching for instant rendering
- Open Graph unfurl metadata customization

**Optional Native Features:**
- Linear gradients (Expo or React Native)
- Haptic feedback (Expo)
- QR code generation (react-native-qrcode-svg or react-qr-code)
- Enhanced clipboard (Expo or React Native Community)
- Device contacts import (Expo)
- Google Contacts import (via Google Sign-In)
- Native share sheet (Expo)

---

## API Reference

### VortexInvite

The main React Native component for rendering invitation forms.

**Props:**

| Prop | Type | Required | Description |
|------|------|----------|-------------|
| `componentId` | `string` | ✅ | Unique identifier for the Vortex component |
| `jwt` | `string` | ✅ | JWT token for authenticated user |
| `scope` | `string` | ✅ | Scope identifier (e.g., team ID, workspace ID) |
| `scopeType` | `string` | ✅ | Type of scope (e.g., "team", "workspace", "project") |
| `widgetConfiguration` | `WidgetConfiguration` | ❌ | Pre-fetched widget configuration |
| `onInvite` | `(result: any) => void` | ❌ | Callback when invitations are successfully sent |
| `onError` | `(error: Error) => void` | ❌ | Callback when an error occurs |
| `onClose` | `() => void` | ❌ | Callback when the component is closed |
| `templateVariables` | `Record<string, string>` | ❌ | Variables for invitation templates |
| `analyticsSegmentation` | `Record<string, any>` | ❌ | Analytics tracking data |
| `modules` | `VortexModules` | ❌ | Unified configuration for optional native libraries (see below) |
| `inviteContactsConfig` | `InviteContactsConfig` | ❌ | Configuration for Invite Contacts feature |
| `findFriendsConfig` | `FindFriendsConfig` | ❌ | Configuration for Find Friends feature |
| `invitationSuggestionsConfig` | `InvitationSuggestionsConfig` | ❌ | Configuration for Invitation Suggestions feature |
| `searchBoxConfig` | `SearchBoxConfig` | ❌ | Configuration for Search Box feature |
| `incomingInvitationsConfig` | `IncomingInvitationsConfig` | ❌ | Configuration for Incoming Invitations feature |
| `outgoingInvitationsConfig` | `OutgoingInvitationsConfig` | ❌ | Configuration for Outgoing Invitations feature |
| `unfurlConfig` | `UnfurlConfig` | ❌ | Configuration for Open Graph unfurl metadata |

**`modules` Object Properties:**

| Property | Type | Description |
|----------|------|-------------|
| `gradient` | `'expo-linear-gradient'` \| `'react-native-linear-gradient'` | Gradient library for button backgrounds |
| `haptics` | `'expo-haptics'` | Haptics library for touch feedback |
| `qrCode` | `'react-native-qrcode-svg'` \| `'react-qr-code'` | QR code library |
| `clipboard` | `'expo-clipboard'` \| `'@react-native-clipboard/clipboard'` | Clipboard library |
| `contacts` | `'expo-contacts'` | Device contacts import |
| `googleSignin` | `'@react-native-google-signin/google-signin'` | Google Contacts import |
| `sharing` | `'expo-sharing'` | Sharing library |

### usePrefetchWidgetConfiguration

Hook for prefetching widget configuration.

**Options:**

| Option | Type | Required | Description |
|--------|------|----------|-------------|
| `componentId` | `string` | ✅ | Your widget component ID |
| `vortexApiUrl` | `string` | ✅ | Vortex API URL |
| `jwt` | `string` | ❌ | JWT token (or use `user`) |
| `user` | `string \| object` | ❌ | User data for unsigned JWT |
| `enabled` | `boolean` | ❌ | Conditionally enable/disable prefetching (default: true) |

**Return Value:**

| Property | Type | Description |
|----------|------|-------------|
| `widgetConfiguration` | `WidgetConfiguration \| undefined` | The prefetched configuration |
| `isLoading` | `boolean` | True while fetching |
| `error` | `Error \| null` | Error if fetch failed |

### VortexDeferredLinks

Class for handling deferred deep linking - track invitation attribution even when the app wasn't installed at the time the invitation link was clicked.

**Methods:**

| Method | Parameters | Returns | Description |
|--------|------------|---------|-------------|
| `collectDeviceFingerprint()` | none | `DeviceFingerprint` | Collects device fingerprint data for matching |
| `retrieveDeferredDeepLink(options)` | `RetrieveDeferredDeepLinkOptions` | `Promise<MatchFingerprintResponse>` | Retrieves deferred deep link by matching fingerprint |

**Example:**

```tsx
import { VortexDeferredLinks } from '@teamvortexsoftware/vortex-react-native';

// On app first launch after install
async function checkDeferredDeepLink(jwt: string) {
  const result = await VortexDeferredLinks.retrieveDeferredDeepLink({ jwt });
  if (result.matched && result.deepLink) {
    console.log('User came from invitation:', result.invitationId);
    // Navigate to appropriate screen based on deepLink
  }
}
```

**Types:**

```typescript
interface DeviceFingerprint {
  platform: 'ios' | 'android';
  osVersion: string;
  deviceModel: string;
  deviceBrand: string;
  timezone: string;
  language: string;
  screenWidth: number;
  screenHeight: number;
  carrierName?: string;
  totalMemory?: number;
}

interface MatchFingerprintResponse {
  matched: boolean;
  invitationId?: string;
  deepLink?: string;
  metadata?: Record<string, any>;
}

interface RetrieveDeferredDeepLinkOptions {
  jwt: string;
  baseURL?: string; // Defaults to production
}
```

### Type Definitions

**InviteContactsConfig:**
```typescript
interface InviteContactsConfig {
  contacts: InviteContactsContact[];
}

interface InviteContactsContact {
  id?: string;
  name: string;
  phoneNumber: string;
  avatarUrl?: string;
  metadata?: Record<string, any>;
}
```

**FindFriendsConfig:**
```typescript
interface FindFriendsConfig {
  contacts: FindFriendsContact[];
  onInvitationCreated?: (contact: FindFriendsContact) => void;
}

interface FindFriendsContact {
  internalId: string;
  name: string;
  subtitle?: string;
  avatarUrl?: string;
  metadata?: Record<string, any>;
}
```

**InvitationSuggestionsConfig:**
```typescript
interface InvitationSuggestionsConfig {
  contacts: InvitationSuggestionContact[];
  onDismiss: (contact: InvitationSuggestionContact) => void;
  onInvitationCreated?: (contact: InvitationSuggestionContact) => void;
  onInvitationFailed?: (contact: InvitationSuggestionContact, error: Error) => void;
}

interface InvitationSuggestionContact {
  internalId: string;
  name: string;
  subtitle?: string;
  avatarUrl?: string;
  metadata?: Record<string, any>;
}
```

**SearchBoxConfig:**
```typescript
interface SearchBoxConfig {
  onSearch: (query: string) => Promise<SearchBoxContact[]>;
  onInvitationCreated?: (contact: SearchBoxContact) => void;
}

interface SearchBoxContact {
  internalId: string;
  name: string;
  subtitle?: string;
  avatarUrl?: string;
  metadata?: Record<string, any>;
}
```

**IncomingInvitationsConfig:**
```typescript
interface IncomingInvitationsConfig {
  internalInvitations?: IncomingInvitationItem[];
  onAccept?: (invitation: IncomingInvitationItem) => Promise<boolean>;
  onDelete?: (invitation: IncomingInvitationItem) => Promise<boolean>;
}

interface IncomingInvitationItem {
  id: string;
  name: string;
  subtitle?: string;
  avatarUrl?: string;
  isVortexInvitation: boolean;
  metadata?: Record<string, any>;
}
```

**OutgoingInvitationsConfig:**
```typescript
interface OutgoingInvitationsConfig {
  internalInvitations?: OutgoingInvitationItem[];
  onCancel?: (invitation: OutgoingInvitationItem) => Promise<boolean>;
}

interface OutgoingInvitationItem {
  id: string;
  name: string;
  subtitle?: string;
  avatarUrl?: string;
  isVortexInvitation: boolean;
  metadata?: Record<string, any>;
}
```

**UnfurlConfig:**
```typescript
interface UnfurlConfig {
  title?: string;
  description?: string;
  image?: string;
  siteName?: string;
  type?: string;
}
```

## Authentication

Your backend should generate JWTs for authenticated users:

### Node.js Example

```typescript
import { generateJwt } from '@teamvortexsoftware/vortex-node-22-sdk';

const jwt = generateJwt({
  user: {
    id: user.id,
    email: user.email,
    adminScopes: user.isAdmin ? ['autojoin'] : [],
  },
  apiKey: process.env.VORTEX_API_KEY,
});
```

### Express/Fastify Example

```typescript
app.post('/api/vortex/jwt', async (req, res) => {
  const user = await getCurrentUser(req);

  const jwt = generateJwt({
    user: {
      id: user.id,
      email: user.email,
      adminScopes: user.isAutojoinAdmin ? ['autojoin'] : [],
    },
    apiKey: process.env.VORTEX_API_KEY,
  });

  res.json({ jwt });
});
```

## Configuration

The SDK automatically fetches and caches widget configuration from your Vortex backend. The configuration determines:

- Available invitation methods
- Form fields and validation
- UI theme and styling
- Enabled features

### Environment Variables

Set up your environment variables in `.env`:

```bash
EXPO_PUBLIC_COMPONENT_ID=your-component-id-here
```

## Error Handling

The SDK provides error callbacks for handling failures:

```tsx
<VortexInvite
  componentId="my-component-id"
  jwt={jwt}
  scope={teamId}
  scopeType="team"
  onError={(error) => {
    // Handle specific error types
    if (error.message.includes('network')) {
      Alert.alert('Network Error', 'Please check your connection and try again.');
    } else if (error.message.includes('unauthorized')) {
      Alert.alert('Authentication Error', 'Please log in again.');
    } else {
      Alert.alert('Error', error.message);
    }
  }}
/>
```

Common error scenarios:
- **Network errors** - Connection issues or API unavailable
- **Authentication errors** - Invalid or expired JWT
- **Configuration errors** - Invalid component ID or missing configuration
- **Validation errors** - Invalid form input (e.g., malformed email)

## Development

```bash
# Install dependencies
pnpm install

# Build the package
pnpm run build

# Run type checking
pnpm run type-check

# Run linting
pnpm run lint

# Watch mode for development
pnpm run dev
```

## License

Apache License 2.0 - see [LICENSE](LICENSE) file for details.

## Support

For questions or issues:

- 📚 Documentation: https://docs.vortexsoftware.com
- 📧 Email: support@vortexsoftware.com
- 🐛 GitHub Issues: https://github.com/teamvortexsoftware/vortex/issues
