# AppDoor Expo SDK

Expo SDK for [AppDoor](https://github.com/woosu-xyz/appdoor) — the open-source alternative to Branch.io, AppsFlyer OneLink, and Firebase Dynamic Links. Add deferred deep linking, mobile attribution, and smart link routing to your Expo app. Self-hosted, privacy-first, no per-click pricing. Pure Expo modules — no native linking required.

[![npm version](https://img.shields.io/npm/v/@appdoor/expo-sdk.svg)](https://www.npmjs.com/package/@appdoor/expo-sdk)
[![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)

## Features

- Deferred deep linking (install attribution) — deterministic on Android (Play Install Referrer) and iOS (clipboard hand-off token, no fingerprinting), with a probabilistic device-fingerprint fallback on Android/web
- Direct deep linking with server-side URL resolution
- Event tracking with offline queue (persists across app restarts)
- Revenue tracking
- Programmatic link creation
- Pure Expo modules - no native linking required (`expo-device`, `expo-application`, `expo-localization`, `expo-linking`; `expo-clipboard` optional, enables iOS deferred attribution)

## Installation

```bash
npx expo install @appdoor/expo-sdk expo-device expo-application expo-localization expo-linking @react-native-async-storage/async-storage
```

`expo-clipboard` is an **optional** peer dependency that enables iOS deferred
attribution (the clipboard hand-off token). Add it if you ship on iOS:

```bash
npx expo install expo-clipboard
```

Without it, iOS deferred deep links fall back to organic (a one-time warning is
logged); Android is unaffected (it uses the Play Install Referrer). The SDK
imports `expo-clipboard` lazily, so its absence never breaks the build.

## Quick Start

```typescript
import AppDoor from '@appdoor/expo-sdk';

// Initialize (call once at app startup)
const response = await AppDoor.initialize({
  // baseUrl defaults to https://appdoor.app — pass it only to point at a local server
  debug: true,                   // Optional, enables verbose logging
  attributionWindowHours: 168,   // Optional, default 7 days
});

// Check attribution
if (response.attributed) {
  console.log('Install attributed!', response.deepLinkData);
}

// Listen for deferred deep links (new installs)
AppDoor.onDeferredDeepLink((data) => {
  if (data) {
    console.log('Deferred deep link:', data.shortCode);
    // Navigate to content
  }
});

// Listen for direct deep links (existing users)
AppDoor.onDeepLink((url, data) => {
  console.log('Deep link received:', url);
  if (data?.customParameters?.route) {
    // Navigate to route
  }
});
```

## Expo Router: add `app/+native-intent.ts` (required)

AppDoor links open your app with a URL whose path is `/<app-slug>/<code>` (Expo Router hands `redirectSystemPath` the **full** URL, scheme and host included — e.g. `https://<domain>/myapp/<code>` or `myapp:///myapp/<code>`), which is **not** a screen in your app — so Expo Router renders its built-in "Unmatched Route" screen on every link open unless you intercept it. The SDK already resolves the URL and navigates via your `onDeepLink` / `onDeferredDeepLink` callback; `+native-intent.ts` just drops the raw short-link path so Expo Router doesn't claim it first. **Don't** resolve the short code with your own `fetch` — the SDK owns resolution.

```typescript
// app/+native-intent.ts
const APP_SLUG = 'myapp'; // must equal your AppDoor registry slug

export function redirectSystemPath({ path }: { path: string; initial: boolean }) {
  try {
    // Expo Router passes the FULL incoming URL here (scheme + host), e.g.
    //   https://<domain>/myapp/<code>   (universal / app link)
    //   myapp:///myapp/<code>           (custom-scheme fallback)
    // Strip scheme + host first — otherwise firstSegment is "https:"/"myapp:"
    // and the slug check never matches, so the short link is never dropped.
    let pathname = path;
    const scheme = pathname.match(/^[a-zA-Z][\w+.-]*:\/\//);
    if (scheme) {
      const afterScheme = pathname.slice(scheme[0].length);
      const slash = afterScheme.indexOf('/');
      pathname = slash === -1 ? '' : afterScheme.slice(slash); // drop host, keep /path
    }
    const firstSegment = pathname.replace(/^\/+/, '').split(/[/?#]/)[0];
    if (firstSegment === APP_SLUG) return null; // AppDoor short link → SDK routes it
  } catch {}
  return path;
}
```

Rebuild the app after adding this file — it's build-time native config, not a hot-reloadable JS change.

## API Reference

### Initialization

```typescript
await AppDoor.initialize(config);    // Returns InstallAttributionResponse
AppDoor.isInitialized;               // boolean getter
```

### Deep Linking

```typescript
AppDoor.onDeferredDeepLink(callback); // Deferred (install attribution)
AppDoor.onDeepLink(callback);         // Direct (multiple callbacks supported)
AppDoor.handleDeepLink(url);          // Manual URL pass-through
```

### Event Tracking

```typescript
await AppDoor.trackEvent(name, properties?);
await AppDoor.trackRevenue(amount, currency, properties?);
await AppDoor.flushEvents();          // Flush offline queue
await AppDoor.clearEventQueue();      // Clear without sending
AppDoor.queuedEventCount;            // Queue size getter
```

Every tracked event (including auto screen views) is automatically attributed to the deep link that drove the visit using a last-click model — so re-engagement campaigns are credited to the link the user tapped, not their original install link.

### Automatic Screen Tracking

Auto-emit a `screen_view` event on each navigation — no manual per-screen calls — so your dashboard can show an **attributed screen-flow funnel** per campaign. Opt-in, via [React Navigation](https://reactnavigation.org/) (an **optional** peer dependency):

```typescript
import { createNavigationContainerRef } from '@react-navigation/native';

export const navigationRef = createNavigationContainerRef();

await AppDoor.initialize({
  autoTrackNavigation: true, // screen names only (privacy-safe default)
  navigationRef,             // also pass this to <NavigationContainer ref={navigationRef}>
});
```

**Route params are OFF by default** (they can contain PII). To capture specific non-PII params, opt in with an explicit allow-list:

```typescript
autoTrackNavigation: { captureParams: ['productId', 'category'] }
```

Apps that don't use React Navigation simply omit `navigationRef` — nothing changes and there's no required dependency. Rapid transitions are debounced and same-screen re-renders deduped.

### Link Creation

The mobile SDK does not create links — the control plane is behind Cloudflare Access, so links are created server-side with `@appdoor/server-sdk` (or the AppDoor MCP / CLI) and the app only receives them.

### Attribution Data

```typescript
await AppDoor.getInstallId();    // Cached install UUID
await AppDoor.getInstallData();  // Cached DeepLinkData
await AppDoor.isFirstLaunch();   // First launch status
```

### Data Management

```typescript
await AppDoor.clearData();  // Wipe all stored SDK data
AppDoor.reset();             // Return to uninitialized state
```

### Named Exports

```typescript
import { AppDoorSDK, AppDoorError, AppDoorErrorCode } from '@appdoor/expo-sdk';
import type {
  AppDoorConfig,
  DeepLinkData,
  InstallAttributionResponse,
  UTMParameters,
  DeviceFingerprint,
  CreateLinkOptions,
  CreateLinkResult,
  EventRequest,
  AutoTrackNavigationOptions,
  DeferredDeepLinkCallback,
  DeepLinkCallback,
} from '@appdoor/expo-sdk';
```

## Error Handling

All SDK errors are instances of `AppDoorError` with a `.code` property:

| Code                    | When                                                           |
|-------------------------|----------------------------------------------------------------|
| `NOT_INITIALIZED`       | Method called before `initialize()`                            |
| `ALREADY_INITIALIZED`   | `initialize()` called twice                                    |
| `INVALID_CONFIGURATION` | Bad config (HTTP on non-localhost, invalid attribution window) |
| `NETWORK_ERROR`         | Network request failed after retries                           |
| `INVALID_RESPONSE`      | Server returned non-2xx response                               |
| `DECODING_ERROR`        | Failed to parse server response                                |
| `INVALID_EVENT_DATA`    | Empty event name or negative revenue                           |
| `MISSING_API_KEY`       | `createLink()` called without API key                          |

## Offline Resilience

Events that fail to send are automatically queued in AsyncStorage (max 100 events, persists across app restarts). Successfully tracked events trigger a queue flush attempt. You can also manually flush or clear the queue.

## Configuration

| Field                    | Type      | Required   | Default   | Description                       |
|--------------------------|-----------|------------|-----------|-----------------------------------|
| `baseUrl`                | `string`  | No         | `https://appdoor.app` | AppDoor server URL (origin, no `/api`) |
| `debug`                  | `boolean` | No         | `false`   | Enable verbose logging            |
| `attributionWindowHours` | `number`  | No         | `168`     | Attribution window (1–2160 hours) |
| `autoTrackNavigation`    | `boolean \| { captureParams?: string[]; debounceMs?: number }` | No | `false` | Auto-emit `screen_view` from React Navigation (screen names only unless params are allow-listed) |
| `navigationRef`          | `NavigationContainerRefLike` | No | - | React Navigation ref; required when `autoTrackNavigation` is enabled |

## Other SDKs

| Platform | Package |
|----------|---------|
| React Native | [`@appdoor/expo-sdk`](https://github.com/woosu-xyz/appdoor) |
| iOS (Swift) | [AppDoorSDK](https://github.com/woosu-xyz/appdoor) |
| Android (Kotlin) | [AppDoorSDK](https://github.com/woosu-xyz/appdoor) |

## Contributing

Contributions are welcome! Please open an issue or pull request.

## License

MIT License - see [LICENSE](LICENSE) for details.

## Support

- **Documentation:** [github.com/woosu-xyz/appdoor](https://github.com/woosu-xyz/appdoor)
- **Issues:** [GitHub Issues](https://github.com/woosu-xyz/appdoor/issues)
- **Core Engine:** [AppDoor Core](https://github.com/woosu-xyz/appdoor) — self-hosted open-source backend
- **Cloud Platform:** [AppDoor Cloud](https://appdoor.app) — hosted SaaS with dashboard, teams, and billing
