# Referral code input components

> The ready-made referral code fields for React Native, SwiftUI, Jetpack Compose, Flutter and React — props, defaults, theming, privacy and building your own.

Every InfluTo SDK ships a ready-made field where users type a creator's referral code or a free-access code. It prefills the code when a referral link brought the user, checks it with InfluTo, and applies it. Use it as is, or build your own field on [`validateCode`](https://docs.influ.to/reference/sdk.md#validatecode) and [`applyCode`](https://docs.influ.to/reference/sdk.md#applycode) — see [the last section](#build-your-own-field) and the [referral code guide](https://docs.influ.to/guides/referral-codes.md).

## Which component

| Platform | Component | Import | Where it ships |
|---|---|---|---|
| React Native / Expo | `ReferralCodeInput` | `import { ReferralCodeInput } from '@influto/react-native-sdk/ui'` | In `@influto/react-native-sdk` |
| iOS (SwiftUI) | `InfluToReferralCodeInput` | `import InfluTo` | In the Swift package (iOS 16+) |
| Android (Jetpack Compose) | `InfluToReferralCodeInput` | `import to.influ.sdk.ui.InfluToReferralCodeInput` | Separate artifact `to.influ:android-sdk-ui` (Material 3) |
| Flutter | `ReferralCodeInput` | `import 'package:influto/influto.dart'` | In `influto` |
| Web (React) | `ReferralCodeInput`, `useInfluToAttribution` | `import { ReferralCodeInput } from '@influto/web/react'` | In `@influto/web` (needs React 17+) |

All of them need [`initialize`](https://docs.influ.to/reference/sdk.md#initialize) to have finished first.

## How they behave

| | React Native | iOS, Android, Flutter | Web (React) |
|---|---|---|---|
| Prefill | `getPrefilledCode()` on mount (`autoPrefill`) | Same (`autoPrefill`) | Same (`prefill`) |
| Checks the code | When the field loses focus, and on the button | While the user types: from 3 characters, 450 ms after the last keystroke | On submit |
| Applies the code | As soon as a check finds it valid (`setReferralCode`) | When the user taps Apply (`applyCode`); the button is enabled only for a valid code | On submit (`applyCode`) |
| Callbacks | `onValidated`, `onApplied`, `onSkip` | `onValidated`, `onApplied` | `onCodeApplied`, `onInvalid` |
| Input | Uppercased, up to 20 characters | Uppercased; letters, digits, `-` and `_` (Flutter: up to 20) | Uppercased |

When a code is rejected, the components show the result's `error` text — the server's English message, such as "Code not found or inactive", or the SDK's "Network error or invalid response" — and fall back to your invalid label only when there's none (Android shows "This code has expired" for an expired code). For fully localized messages, build your own field and map `error_code` yourself.

> [!WARNING]
> The iOS, Android and Flutter components drop `_` as the user types, but creator codes may contain underscores (`A–Z 0–9 _ -`). If any of your creators use one, build your own field.

## Privacy defaults

By default each component shows only the field and whether the code is valid. Two props, both `false` by default, add more:

- `showCampaignName` — shows the campaign's name (React Native also shows its description).
- `showReferrerName` — shows the creator's personal name ("Referred by …"). Turn it on only if your creators agreed to be named.

React Native shows them once the code is valid (which is also when it's applied); iOS, Android and Flutter after the user taps Apply; web in the success message. The raw `validateCode` result always carries the creator's name in `influencer.name` and in `message` — keep those off screen in your own UI too.

## React Native

```tsx
import { ReferralCodeInput } from '@influto/react-native-sdk/ui';

<ReferralCodeInput
  appUserId={userId}
  labels={{ title: 'Got a code?', validateButton: 'Apply' }}
  colors={{ primary: '#111827' }}
  onApplied={(result) => {
    if (result.campaign?.campaign_type === 'FREE_ACCESS') refreshPremium();
  }}
  onSkip={() => navigation.navigate('Paywall')}
/>
```

| Prop | Type | Default | What it does |
|---|---|---|---|
| `appUserId` | `string` | identified user | Passed to `setReferralCode`. Free-access codes need a user. |
| `autoPrefill` | `boolean` | `true` | Fills in the code when the install is attributed. |
| `autoValidate` | `boolean` | `false` | Checks (and so applies) a prefilled code on mount. |
| `validateOnBlur` | `boolean` | `true` | Checks the code when the field loses focus. A valid code is applied right away. |
| `showSkipButton` | `boolean` | `true` | Shows a Skip button — only when `onSkip` is also set. |
| `showCampaignName` | `boolean` | `false` | See [Privacy defaults](#privacy-defaults). |
| `showReferrerName` | `boolean` | `false` | See [Privacy defaults](#privacy-defaults). Also swaps the success text for the server's "Valid code! Referred by …". |
| `onValidated` | `(result: CodeValidationResult) => void` | — | After every check, valid or not (including network errors). |
| `onApplied` | `(result: CodeValidationResult) => void` | — | After a valid code was applied. Receives the validation result. |
| `onSkip` | `() => void` | — | When the user taps Skip. |
| `colors`, `fonts`, `labels`, `style` | objects | see below | Theming. |

Codes shorter than 4 characters are rejected in the component with `INVALID_FORMAT` before any request.

### Theming

| `colors` key | Default |
|---|---|
| `primary` | `#3B82F6` (button, prefilled hint) |
| `success` | `#10B981` |
| `error` | `#EF4444` |
| `text` | `#1F2937` |
| `textSecondary` | `#6B7280` |
| `border` | `#D1D5DB` |
| `inputBackground` | `#F9FAFB` |
| `background` | `#FFFFFF` — accepted but not applied; set `style.container.backgroundColor` instead |

| `fonts` key | Default |
|---|---|
| `family` | `System` on iOS, `Roboto` on Android |
| `sizeTitle` | `18` |
| `sizeInput` | `16` |
| `sizeButton` | `16` |
| `sizeMessage` | `14` |

| `labels` key | Default |
|---|---|
| `title` | `Have a promo code?` |
| `subtitle` | `Add the code you received from a creator or friend` |
| `placeholder` | `Enter code (e.g., FITGURU30)` |
| `validateButton` | `Apply Code` |
| `skipButton` | `Skip` |
| `validatingMessage` | `Validating code...` |
| `validMessage` | `Code applied successfully!` |
| `invalidMessage` | `Invalid code. Please try again.` |
| `errorMessage` | `Unable to validate code. Check your connection.` |
| `prefilledMessage` | `Code detected from your referral link` |

`style` takes React Native styles for `container`, `titleContainer`, `title`, `subtitle`, `inputContainer`, `input`, `buttonContainer`, `validateButton`, `validateButtonText`, `skipButton`, `skipButtonText`, `messageContainer` and `messageText`. They're applied on top of the defaults; the container is a white card with 20 pt padding, rounded corners and a shadow.

## iOS (SwiftUI)

```swift
import SwiftUI
import InfluTo

struct CodeStep: View {
    let userID: String?

    var body: some View {
        InfluToReferralCodeInput(
            appUserId: userID,
            title: "Got a code?",
            onApplied: { result in
                if result.campaign?.campaignType == "FREE_ACCESS" { refreshPremium() }
            }
        )
    }
}
```

| Parameter | Type | Default | What it does |
|---|---|---|---|
| `appUserId` | `String?` | identified user | Passed to `applyCode`. Free-access codes need a user. |
| `autoPrefill` | `Bool` | `true` | Fills in the code when the install is attributed. |
| `autoValidate` | `Bool` | `false` | Checks a prefilled code right away. |
| `showCampaignName` | `Bool` | `false` | See [Privacy defaults](#privacy-defaults). |
| `showReferrerName` | `Bool` | `false` | See [Privacy defaults](#privacy-defaults). |
| `title` | `String?` | `nil` | Optional heading above the field. |
| `labels` | `InfluToReferralCodeInput.Labels` | see below | Texts. |
| `onValidated` | `((CodeValidationResult) -> Void)?` | `nil` | After every check. |
| `onApplied` | `((CodeValidationResult) -> Void)?` | `nil` | After the user taps Apply and the code was recorded (`result.applied` is true). |

`Labels` has four `var`s: `placeholder` (`Referral code`), `apply` (`Apply`), `valid` (`Code applied`) and `invalid` (`This code isn't valid`):

```swift
var labels = InfluToReferralCodeInput.Labels()
labels.placeholder = "Promo code"
labels.apply = "Use code"
```

It's built from a standard `TextField`, `Button` and system text styles, with green and red for the valid and invalid states. Style the surrounding view with ordinary SwiftUI modifiers.

## Android (Jetpack Compose)

```kotlin file="build.gradle.kts"
dependencies {
    implementation("to.influ:android-sdk:1.1.1")
    implementation("to.influ:android-sdk-ui:1.1.1")
}
```

```kotlin
import to.influ.sdk.ui.InfluToReferralCodeInput

InfluToReferralCodeInput(
    appUserId = userId,
    title = "Got a code?",
    onApplied = { result ->
        if (result.campaign?.campaignType == "FREE_ACCESS") refreshPremium()
    },
)
```

| Parameter | Type | Default | What it does |
|---|---|---|---|
| `modifier` | `Modifier` | `Modifier` | Applied to the column that holds the field. |
| `appUserId` | `String?` | identified user | Passed to `applyCode`. Free-access codes need a user. |
| `autoPrefill` | `Boolean` | `true` | Fills in the code when the install is attributed. |
| `autoValidate` | `Boolean` | `false` | Checks a prefilled code right away. |
| `showCampaignName` | `Boolean` | `false` | See [Privacy defaults](#privacy-defaults). |
| `showReferrerName` | `Boolean` | `false` | See [Privacy defaults](#privacy-defaults). |
| `title` | `String?` | `null` | Optional heading. |
| `placeholder` | `String` | `"Referral code"` | The field's label. |
| `applyLabel` | `String` | `"Apply"` | Button text. |
| `validMessage` | `String` | `"Code applied"` | Shown under a valid code. |
| `invalidMessage` | `String` | `"This code isn't valid"` | Shown when the server gives no error text. An expired free-access code shows "This code has expired". |
| `onValidated` | `(CodeValidationResult) -> Unit` | `{}` | After every check. |
| `onApplied` | `(CodeValidationResult) -> Unit` | `{}` | After the user taps Apply and the code was recorded (`result.applied` is true). |

It's built from Material 3 `OutlinedTextField` and `Button`, so it follows your `MaterialTheme`.

## Flutter

```dart
import 'package:influto/influto.dart';

ReferralCodeInput(
  appUserId: userId,
  title: 'Got a code?',
  onApplied: (result) {
    if (result.campaign?.campaignType == 'FREE_ACCESS') refreshPremium();
  },
)
```

| Parameter | Type | Default | What it does |
|---|---|---|---|
| `appUserId` | `String?` | identified user | Passed to `applyCode`. Free-access codes need a user. |
| `autoPrefill` | `bool` | `true` | Fills in the code when the install is attributed. |
| `autoValidate` | `bool` | `false` | Checks a prefilled code right away. |
| `showCampaignName` | `bool` | `false` | See [Privacy defaults](#privacy-defaults). |
| `showReferrerName` | `bool` | `false` | See [Privacy defaults](#privacy-defaults). |
| `title` | `String?` | `null` | Optional heading. |
| `placeholder` | `String` | `'Referral code'` | The field's label. |
| `applyLabel` | `String` | `'Apply'` | Button text. |
| `validMessage` | `String` | `'Code applied'` | Helper text under a valid code. |
| `invalidMessage` | `String` | `"This code isn't valid"` | Shown when the server gives no error text. |
| `onValidated` | `ValueChanged<CodeValidationResult>?` | `null` | After every check. |
| `onApplied` | `ValueChanged<CodeValidationResult>?` | `null` | After the user taps Apply and the code was recorded (`result.applied` is true). |

It uses a Material `TextField` with an outlined border and an `ElevatedButton`, so it follows your `ThemeData`.

## Web (React)

`@influto/web/react` works in React and Ionic React apps.

```tsx
import { ReferralCodeInput, useInfluToAttribution } from '@influto/web/react';

function CodeStep({ ready, userId }: { ready: boolean; userId?: string }) {
  const { loading, attribution } = useInfluToAttribution(ready);
  if (loading) return null;

  return (
    <>
      {attribution?.attributed && <p>Your creator's offer is applied.</p>}
      <ReferralCodeInput
        appUserId={userId}
        className="code-form"
        onCodeApplied={(code, result) => {
          if (result.campaign?.campaign_type === 'FREE_ACCESS') refreshPremium();
        }}
      />
    </>
  );
}
```

### ReferralCodeInput

| Prop | Type | Default | What it does |
|---|---|---|---|
| `appUserId` | `string` | — | Passed to `applyCode`. Without it, free-access codes use the ID from `identifyUser`. |
| `prefill` | `boolean` | `true` | Fills in the code when the install is attributed. |
| `showCampaignName` | `boolean` | `false` | See [Privacy defaults](#privacy-defaults). |
| `showReferrerName` | `boolean` | `false` | See [Privacy defaults](#privacy-defaults). |
| `placeholder` | `string` | `'Referral or access code'` | Input placeholder. |
| `buttonLabel` | `string` | `'Apply'` | Submit button text (shows `…` while checking). |
| `onCodeApplied` | `(code, result) => void` | — | After the code was applied. `result` is the `applyCode` result. |
| `onInvalid` | `(result) => void` | — | When the code was rejected, or valid but couldn't be applied. |
| `className`, `style` | `string`, `CSSProperties` | — | On the `<form>`. |
| `inputClassName`, `buttonClassName` | `string` | — | On the input and the button. |
| `sdk` | `InfluToWebSDK` | default instance | Use an instance from `createInfluTo()`. |

It renders an unstyled `<form>` with an input, a submit button and, after a submit, a `<span role="status">` with `Code applied` or the error (`Invalid code` when the server gives none). Style it with your classes, or target the attributes it sets: `data-influto-status` on the form (`idle`, `checking`, `valid`, `invalid`) and `data-influto-message` on the message.

### useInfluToAttribution

`useInfluToAttribution(enabled = true, sdk = InfluTo)` runs `checkAttribution()` once and returns `loading`, `attribution` (an [AttributionResult](https://docs.influ.to/reference/sdk.md#attributionresult) or `null`), `error` and `refresh()`. Pass `enabled = false` until `initialize()` has resolved — before that, `checkAttribution` throws and the hook reports it as `error`.

## Build your own field

For your own design or fully localized messages, use the SDK calls directly: check the code with `validateCode` while the user types (debounced), and apply it with `applyCode` when they submit.

<!-- tabs -->
```tsx title="React Native / Expo"
// While typing (debounced): show valid / invalid only
const check = await InfluTo.validateCode(text);
setValid(check.valid);

// On submit
const result = await InfluTo.applyCode(text, userId);
if (!result.applied) showError(result.error_code);
else if (result.campaign?.campaign_type === 'FREE_ACCESS' && (await InfluTo.checkAccess(userId)).hasAccess) unlockPremium();
```
```swift title="iOS (Swift)"
// While typing (debounced): show valid / invalid only
let check = await InfluTo.validateCode(text)
isValid = check.valid

// On submit
let result = await InfluTo.applyCode(text, appUserId: userID)
if result.applied != true {
    showError(result.errorCode)
} else if result.campaign?.campaignType == "FREE_ACCESS" {
    let access = await InfluTo.checkAccess(appUserId: userID)
    if access.hasAccess { unlockPremium() }
}
```
```kotlin title="Android (Kotlin)"
// While typing (debounced): show valid / invalid only
val check = InfluTo.validateCode(text)
isValid = check.valid

// On submit
val result = InfluTo.applyCode(text, userId)
if (result.applied != true) showError(result.errorCode)
else if (result.campaign?.campaignType == "FREE_ACCESS" && InfluTo.checkAccess(userId).hasAccess) unlockPremium()
```
```dart title="Flutter (Dart)"
// While typing (debounced): show valid / invalid only
final check = await InfluTo.instance.validateCode(text);
setState(() => isValid = check.valid);

// On submit
final result = await InfluTo.instance.applyCode(text, appUserId: userId);
if (result.applied != true) {
  showError(result.errorCode);
} else if (result.campaign?.campaignType == 'FREE_ACCESS' &&
    (await InfluTo.instance.checkAccess(appUserId: userId)).hasAccess) {
  unlockPremium();
}
```
```ts title="Web / Capacitor / PWA"
// While typing (debounced): show valid / invalid only
const check = await InfluTo.validateCode(text);
setValid(check.valid);

// On submit
const result = await InfluTo.applyCode(text, userId);
if (!result.applied) showError(result.error_code);
else if (result.campaign?.campaign_type === 'FREE_ACCESS' && (await InfluTo.checkAccess(userId)).hasAccess) unlockPremium();
```
<!-- /tabs -->

- Uppercase the input and keep only `A–Z`, `0–9`, `_` and `-`, 4–20 characters ([code rules](https://docs.influ.to/reference/limits-and-errors.md#code-format)). The SDK only trims and uppercases, and the code it stores and writes to RevenueCat is the one you pass — so a stray space or invisible character from a pasted caption must be removed here.
- Map `error_code` / `errorCode` (`INVALID_FORMAT`, `CODE_NOT_FOUND`, `CODE_EXPIRED`, `NETWORK_ERROR`) to your own localized text. `applied` can also be false for a valid code, for example a free-access code without a user ID.
- `applyCode` binds the code to the user from `identifyUser`, or the user ID you pass; free-access codes need one.
- Don't display `influencer` or `message` unless your creators agreed to be named.
- Gate premium on your store entitlement or `checkAccess().hasAccess`, not on the code result alone.
