# Add InfluTo to a Capacitor, Ionic, Cordova or web app

> Use @influto/web in Capacitor, Ionic, Cordova, PWAs and browsers — capture referral links, accept typed codes, and connect RevenueCat or store purchases.

`@influto/web` adds creator attribution to any app whose code runs in a WebView or a browser: Capacitor (including Ionic React, Angular and Vue), Apache Cordova, installed PWAs, Trusted Web Activities and plain web apps. It follows the same contract as the native SDKs, and adds web-only ways to pick up a referral exactly: the launch URL, deep links, and the Google Play Install Referrer. Plan for an afternoon; most of it goes into the purchase side.

| | |
|---|---|
| Package | `@influto/web`, with subpaths `/capacitor`, `/cordova`, `/pwa`, `/revenuecat` and `/react` |
| Latest version | 1.0.1 |
| Registry | [npm](https://www.npmjs.com/package/@influto/web) |
| Runtimes | Capacitor / Ionic, Cordova, PWAs and TWAs, browsers — any ES2019 browser or WebView |
| Builds | ESM, CommonJS, and a script-tag build |
| Dependencies | None. React 17+ only if you use `@influto/web/react` |

## Install

```bash
npm install @influto/web
# Capacitor / Ionic apps (device info, storage, deep links):
npm install @capacitor/device @capacitor/preferences @capacitor/app
```

The Capacitor plugins are optional but recommended: `@capacitor/device` for device details, `@capacitor/preferences` for storage that survives WebView resets, `@capacitor/app` for deep links. Run `npx cap sync` after adding them. Missing plugins fall back to browser defaults.

No bundler? Load `https://cdn.jsdelivr.net/npm/@influto/web@1/dist/index.global.js` with a `<script>` tag and use `window.InfluTo`.

## Initialize

```ts
import InfluTo from '@influto/web';
import { capacitorAdapters } from '@influto/web/capacitor'; // Capacitor / Ionic only

// Call once at startup (Capacitor: after platform ready; PWA: on page load)
await InfluTo.initialize({
  apiKey: 'YOUR_API_KEY',
  appVersion: '1.2.3',
  ...(await capacitorAdapters()),   // omit in a plain PWA / browser
  // RevenueCat apps: revenueCat: revenueCatCapacitor(Purchases)  (from '@influto/web/revenuecat')
});
```

Call `initialize` once, at the app root, and wait for it before calling anything else:

| Runtime | When |
|---|---|
| Capacitor / Ionic | At app start, after the platform is ready — not inside an Ionic page, which can be cached and re-entered |
| Cordova | After `deviceready`: `await whenDeviceReady()` from `@influto/web/cordova` first |
| PWA / browser | On page load |

`initialize` throws when it fails — a wrong key, no network — with an `InfluToApiError` (`status`, `body`) for API errors. Catch it, log it, and carry on: the next launch tries again. If it hasn't succeeded, `checkAttribution` and `reportPurchase` throw "not initialized". It resolves to the app's init result (also available later from `getInitResult()`). A typical Capacitor startup, in order:

```ts file="influto.ts"
import InfluTo from '@influto/web';
import { capacitorAdapters, listenForCapacitorLaunchUrls } from '@influto/web/capacitor';
import { revenueCatCapacitor } from '@influto/web/revenuecat';
import { Purchases } from '@revenuecat/purchases-capacitor';

export async function startInfluTo() {
  await Purchases.configure({ apiKey: 'appl_…' }); // RevenueCat apps: configure it first
  try {
    await InfluTo.initialize({
      apiKey: import.meta.env.VITE_INFLUTO_API_KEY,
      appVersion: '1.4.0',
      ...(await capacitorAdapters()),
      revenueCat: revenueCatCapacitor(Purchases),
    });
  } catch (error) {
    console.warn('InfluTo initialize failed', error); // retried on the next launch
    return;
  }
  await listenForCapacitorLaunchUrls(InfluTo); // deep links → referral claim
  const attribution = await InfluTo.checkAttribution();
  const { appUserID } = await Purchases.getAppUserID();
  await InfluTo.identifyUser(appUserID);
}
```

The API key starts with `it_` and is on **Setup & SDK**; one key works for development and production. Keep it out of committed source — for example in a gitignored `.env.local` read through your bundler.

| Option | Default | What it does |
|---|---|---|
| `apiKey` | required | The app's SDK key (`it_…`) |
| `debug` | `false` | Logs `[InfluTo]` lines to the console |
| `appVersion` | `"unknown"` | Your app version, sent with the startup call |
| `runtime` | detected | `capacitor`, `cordova`, `twa`, `pwa` or `browser` |
| `storage`, `device`, `http` | `localStorage`, browser detection, `fetch` | Adapters; `capacitorAdapters()` and `cordovaAdapters()` fill `storage` / `device` / `runtime` for you |
| `revenueCat` | none | RevenueCat binding — see [Purchases](#purchases) |
| `captureLaunchCode` | `true` | Read `influto_code` / `influto_click` from the page URL at startup |
| `cleanUrl` | `true` | Remove the `influto_*` parameters from the address bar afterwards |
| `purchaseSource` | none | Store-direct apps: where purchases come from — see [Purchases](#purchases) |
| `autoCapture` | `true` | Report purchases from `purchaseSource` automatically |
| `oneTimeProductIds` | none | Your Android one-time and consumable product IDs |

## Detect referred users

```ts
const attribution = await InfluTo.checkAttribution();
if (attribution.attributed) {
  // User came from a referral — show a special offer / extended trial.
  // With a RevenueCat binding the SDK also sets influto_code + influto_referral.
  console.log('Referred by', attribution.referralCode);
}

// Web deep links: referral links send web visitors to your app URL with
// ?influto_code=…&influto_click=… — the SDK claims them automatically.
```

| Field | Meaning |
|---|---|
| `attributed` | `true` when the install came from a creator's link or code |
| `referralCode` | The creator's code, uppercase |
| `attributionMethod` | `link_param` (the link carried the code into the app), `ip_device_match` (link-click matching) or `manual_entry` (a code the user applied) |
| `clickedAt`, `clickId` | When the link was clicked, and which click was claimed |
| `message` | A short explanation, for logs |

Attribution is decided once per install and stored; later calls return the stored answer. Only a failed request is retried. `checkAttribution` is fail-soft: on a network or server error it returns `attributed: false` and stores nothing. In React, `useInfluToAttribution(enabled)` from `@influto/web/react` returns `{ loading, attribution, error, refresh }`; pass `enabled = false` until `initialize` has resolved.

The web SDK can claim a referral exactly, from the code the link carries, in three ways. A code picked up later re-opens an earlier "organic" answer on the next `checkAttribution` call; an attributed install keeps its creator. Without a carried code, InfluTo falls back to link-click matching within 72 hours — see [attribution](https://docs.influ.to/advanced/attribution.md).

### The launch URL (PWAs and web apps)

Set **Settings → General → Web app URL**. Referral links send desktop visitors — and phone visitors when your app has no store listing for their platform — to that URL with `?influto_code=…&influto_click=…` appended. `initialize` reads both from the query or a hash-router fragment, stores them, and cleans the address bar.

### Deep links (Capacitor and Cordova)

`listenForCapacitorLaunchUrls(InfluTo)` passes the cold-start URL and every `appUrlOpen` event to the SDK; it returns a function that removes the listener. In Cordova, or with your own handler, call `InfluTo.handleLaunchUrl(url)`. Call either after `initialize` and before `checkAttribution`.

### The Play Install Referrer (Android)

Google Play links from InfluTo carry the code and click in Play's `referrer` parameter. On first launch, read the referrer string with an Install Referrer plugin (`@capgo/capacitor-install-referrer` or `cordova-play-install-referrer`) and hand it over after `initialize`, before `checkAttribution`:

```ts
await InfluTo.captureInstallReferrer(referrer); // the raw referrer string from the plugin
const attribution = await InfluTo.checkAttribution();
```

## Identify the user

Call `identifyUser(appUserId, properties?)` as soon as you know who the user is, with the same ID your purchases are keyed on — for RevenueCat, the app user ID (see the startup example). InfluTo uses it to credit a RevenueCat purchase when the `influto_code` attribute didn't make it; `checkAccess`, `setReferralCode` and `reportPurchase` use it by default. Call it once per sign-in, ideally in the user's first session — not on every render.

## Track events

```ts
await InfluTo.trackEvent({ eventType: 'paywall_viewed', appUserId: userId, properties: { placement: 'onboarding' } });
```

Events show on **Real-Time Events** and never create conversions or commission. Every call gets an event ID, so an accidental double call counts once. For `trial_started`, `subscription_purchased` and `subscription_renewed` the ID is derived from the event, user and properties, so the same event counts once even across launches; include something that changes per occurrence in `properties` when the event can repeat, or pass `eventId` yourself.

## Let users type a code

```ts
// Validate, then apply a code the user typed
const result = await InfluTo.applyCode('FITGURU30');
if (result.valid && result.applied) {
  // Unlock the offer
}

// React / Ionic React: drop in the ready-made field
import { ReferralCodeInput } from '@influto/web/react';
<ReferralCodeInput onCodeApplied={(code) => {/* ... */}} />

// Free-access codes: (await InfluTo.setReferralCode(code)).grantsAccess → unlock
// premium, and gate premium on (await InfluTo.checkAccess()).hasAccess.
```

`applyCode(code, appUserId?)` validates the code and applies it if it's valid. Codes are trimmed and uppercased for you. An invalid code comes back with `valid: false` and an `error_code`: `INVALID_FORMAT`, `CODE_NOT_FOUND`, `CODE_EXPIRED` or `NETWORK_ERROR`. If you build your own field, write your own copy: `message` and `influencer` can contain the creator's personal name.

`ReferralCodeInput` from `@influto/web/react` is a plain, unstyled form: an input and a button. It pre-fills a link-attributed code and applies the code on submit.

| Prop | Default | Notes |
|---|---|---|
| `onCodeApplied(code, result)` | — | The code was valid and applied |
| `onInvalid(result)` | — | The code was rejected |
| `appUserId` | identified user | The user to bind the code to |
| `prefill` | `true` | Pre-fill from a link-attributed install |
| `showCampaignName` / `showReferrerName` | `false` / `false` | Show a creator's name only with their consent |
| `placeholder`, `buttonLabel` | `'Referral or access code'`, `'Apply'` | Texts, for translation |
| `className`, `style`, `inputClassName`, `buttonClassName` | — | Styling |

Angular, Vue or no framework: build the field yourself on `applyCode`. More on the UX: [Let users enter a referral code](https://docs.influ.to/guides/referral-codes.md). All props: [UI components](https://docs.influ.to/reference/ui.md).

## Purchases

InfluTo only pays commission on purchases it has verified with the store. Choose exactly one path per platform — see [Choose a validation path](https://docs.influ.to/integrations.md).

### RevenueCat

Pass a binding as `revenueCat`. When `checkAttribution` finds a referral, or a code is applied, the SDK sets `influto_code` (the code) and `influto_referral` (the string `"true"`) through it. Then connect the webhook — see [RevenueCat](https://docs.influ.to/integrations/revenuecat.md).

| Your RevenueCat SDK | Binding (from `@influto/web/revenuecat`) |
|---|---|
| `@revenuecat/purchases-capacitor` 7.5.10+ | `revenueCatCapacitor(Purchases)` |
| `@revenuecat/purchases-js` 1.4.0+ (Web Billing) | `revenueCatJs(() => Purchases.getSharedInstance())` |
| `cordova-plugin-purchases` | `revenueCatCordova(window.Purchases)` |
| Anything else | A function `(attrs) => …`, or any object with `setAttributes(map)` — wrap it in `revenueCatBinding(obj)` to get retries |

The bindings retry a failed write up to 3 times (options `retries`, `backoffMs`, `onError`). To attach one after `initialize`, call `InfluTo.attachRevenueCat(binding)` — before `checkAttribution`.

> [!IMPORTANT]
> The attribute is written when the referral is found, not later. Configure RevenueCat and attach the binding before `checkAttribution`, and call `identifyUser` with the RevenueCat app user ID as the fallback.

### Store-direct

Pass a purchase source as `purchaseSource`. When the app is set to store-direct in **Settings → Validation** and `autoCapture` isn't `false`, `initialize` starts reporting new purchases and back-fills existing ones, each once.

| Runtime | Purchase plugin | Source |
|---|---|---|
| Capacitor | `@capgo/native-purchases` | `capgoNativePurchasesSource(NativePurchases, { oneTimeProductIds })` from `/capacitor` |
| Cordova | `cordova-plugin-purchase` v13 | `cordovaPurchaseSource(CdvPurchase.store, { oneTimeProductIds })` from `/cordova` |
| TWA on Google Play | Digital Goods API (Chrome 101+) | `digitalGoodsSource({ oneTimeProductIds })` from `/pwa` |

The Digital Goods API has no purchase event, so call `InfluTo.syncExistingPurchases()` after each checkout. `requestPlayBillingPurchase(itemId)` from `/pwa` runs a Play Billing checkout and returns `{ purchaseToken }`, or `null` if the user cancelled. You can also drive capture yourself with `enableAutoPurchaseCapture(source, { backSync, oneTimeProductIds })` and `disableAutoPurchaseCapture()`.

- **iOS** needs the StoreKit 2 signed transaction. `capgoNativePurchasesSource` passes it on; with `cordova-plugin-purchase`, check that your setup exposes one, or iOS reports fail.
- **Android one-time products**: the Capacitor and Cordova sources don't carry a price, and a one-time purchase without a price is recorded at $0. Report those yourself with `price` and `currency`. `digitalGoodsSource` looks the price up.

To report purchases yourself instead, set `autoCapture: false` and call `reportPurchase`:

```ts
// Store-direct only (no RevenueCat). Attach a purchase source and purchases are
// captured automatically — TWA/PWA on Play: digitalGoodsSource(); Capacitor:
// capgoNativePurchasesSource(); Cordova: cordovaPurchaseSource(). Or report manually:
await InfluTo.reportPurchase({
  platform: 'android',             // or 'ios'
  purchaseToken: token,            // Android: Play Billing purchaseToken
  // signedTransaction: jws,       // iOS: StoreKit 2 JWS
});
```

- In a browser or PWA, always pass `platform` (`'ios'` or `'android'`); the SDK refuses to guess when it detects `web`.
- It throws an `InfluToApiError`. `503`: the exchange rate was briefly unavailable — retry later. `400`: the platform isn't set to store-direct yet. `409`: an Android one-time purchase isn't in the PURCHASED state yet.
- Android one-time products need `productId`, plus `price` and `currency`.

Set-up, credentials and testing: [Store-direct](https://docs.influ.to/integrations/store-direct.md).

## Free-access codes

Free-access codes give a user premium without a purchase. Redeem one with `setReferralCode(code, appUserId?)`; it uses the identified user when you don't pass one, and a code with no user is refused. The result tells you what happened: `grantsAccess`, `freeAccess`, `entitlement` and `expiresAt`. `applyCode` doesn't return these flags; after it, `result.campaign?.campaign_type === 'FREE_ACCESS'` tells you the code was a free-access code.

Gate premium on your store entitlement or InfluTo's answer: `entitlementActive || (await InfluTo.checkAccess()).hasAccess`. `checkAccess` caches a positive answer for 5 minutes, never caches a negative one, and returns `hasAccess: false` on errors. See [Free access codes](https://docs.influ.to/platform/creator-access-codes.md).

## Verify

After the first successful `initialize`, **Setup & SDK** shows "Integrated — SDK connected" with the platform, `@influto/web` and the SDK version it last saw. With `debug: true` the SDK logs each step, and **Real-Time Events** lists your events and purchases. For the full end-to-end test and the integration status checklist, see [Verify the integration](https://docs.influ.to/integrations/testing.md). To test attribution again, `clearAttribution()` forgets the stored attribution, code and launch code.

## Platform notes

### Capacitor and Ionic

- With `@capacitor/preferences` installed, `capacitorAdapters()` stores the SDK's state there. WebView `localStorage` is wiped when `androidScheme` or `iosScheme` changes; pass `{ usePreferences: false }` to keep `localStorage` anyway.
- The reported platform is the native OS (`ios` or `android`).
- In a web dev build (`ionic serve`) the RevenueCat Capacitor plugin is a stand-in, so attribute writes do nothing there. Test on a device.

### Cordova

- A fresh Cordova template blocks the API twice: add `<access origin="https://influ.to" />` to `config.xml` and `connect-src https://influ.to` to the Content-Security-Policy meta tag.
- Start with `await whenDeviceReady()` and pass `...cordovaAdapters()` to `initialize`; storage stays in `localStorage`.
- RevenueCat has deprecated its Cordova plugin. For a new app, prefer Capacitor.

### PWAs, TWAs and web apps

- Set the **Web app URL** so referral links can hand the code to your app.
- The platform comes from the user agent the same way InfluTo classifies a click: phones are `ios` or `android`, everything else `web`.
- A TWA on Google Play can sell through Play Billing (`digitalGoodsSource()`); web checkout through RevenueCat Web Billing works with `revenueCatJs()`. iOS home-screen web apps can't sell in-app purchases, but attribution still works.
- `isStandalone()` and `hasDigitalGoods()` from `/pwa` tell you whether you're running installed and whether Play Billing is available.

### Script tag

The script-tag build contains the core SDK only; the subpath helpers need a bundler. Pass `revenueCat` as a plain function that calls your RevenueCat SDK's `setAttributes`.
