> ## Documentation Index
> Fetch the complete documentation index at: https://developer.plugchoice.com/llms.txt
> Use this file to discover all available pages before exploring further.

# React Native

> Add the Plugchoice SDK to a React Native or Expo app, open Link and handle the result.

export const Screen = ({name, alt, caption, wide = false}) => {
  const style = wide ? {
    width: '100%'
  } : {
    width: '280px',
    maxWidth: '100%'
  };
  return <figure style={{
    margin: '1.5rem 0',
    textAlign: 'center'
  }}>
      <img className="block dark:hidden" src={`/images/sdk/${name}-light.webp`} alt={alt} style={{
    ...style,
    margin: '0 auto',
    borderRadius: '12px',
    border: '1px solid rgba(128, 128, 128, 0.25)'
  }} />
      <img className="hidden dark:block" src={`/images/sdk/${name}-dark.webp`} alt={alt} style={{
    ...style,
    margin: '0 auto',
    borderRadius: '12px',
    border: '1px solid rgba(128, 128, 128, 0.25)'
  }} />
      {caption ? <figcaption style={{
    marginTop: '0.5rem',
    fontSize: '0.875rem',
    opacity: 0.7
  }}>{caption}</figcaption> : null}
    </figure>;
};

The React Native package brings Link to your Expo or React Native app, on iOS and Android. Its config plugin makes the native changes for you, so you only write TypeScript.

## Install

**Requirements**: Expo SDK 57 or later (React Native 0.86), in a development or release build of your app. Expo Go doesn't include the SDK's native code. iOS 16.4 or later, and Android 10 (API 29) or later.

Install the package:

```bash theme={null}
npx expo install @plugchoice/react-native
```

Add its config plugin to your app config. On every `npx expo prebuild` it adds what the SDK needs on iOS and Android:

```json app.json theme={null}
{
  "expo": {
    "plugins": ["@plugchoice/react-native"]
  }
}
```

Then rebuild the native app: `npx expo prebuild` and `npx expo run:ios` or `npx expo run:android`, or an EAS build.

<Note>
  The package is an Expo module. A React Native app without Expo needs Expo modules first (`npx install-expo-modules@latest`), and then makes the native changes by hand, as on the [iOS](/sdk/ios#configure-your-app) and [Android](/sdk/android#configure-your-app) pages.
</Note>

<AccordionGroup>
  <Accordion title="What the config plugin adds">
    On every `npx expo prebuild`, the plugin adds what the SDK needs and keeps what your app already has.

    | Platform | What | Why |
    | - | - | - |
    | iOS | Permission texts for the local network, Bluetooth, location and the camera | iOS shows them when Link first needs each one |
    | iOS | The Hotspot Configuration and Access Wi-Fi Information entitlements | To join a charger's own Wi-Fi, and confirm the phone joined it |
    | iOS | AccessorySetupKit for Wi-Fi | On iOS 18 and later, one prompt per charger instead of one on every join |
    | iOS | Bonjour service types | To find chargers on the home Wi-Fi |
    | iOS | Local networking in App Transport Security | To reach chargers over plain HTTP on the local network |
    | Android | Android 10 (API 29) as the minimum | The SDK needs it |

    The SDK's Android permissions come with the package. Your App ID needs the matching capabilities; EAS Build syncs them from the entitlements. The [iOS](/sdk/ios#configure-your-app) and [Android](/sdk/android#permissions) pages say more about each.

    <Warning>
      Don't add `Bluetooth` to AccessorySetupKit yourself. It would limit Bluetooth in your whole app to accessories set up that way, and Link's Bluetooth would stop working.
    </Warning>
  </Accordion>

  <Accordion title="Use your own permission texts">
    Pass your texts as options to the plugin:

    ```json app.json theme={null}
    {
      "expo": {
        "plugins": [
          ["@plugchoice/react-native", { "cameraPermission": "Scan the QR code on your charger." }]
        ]
      }
    }
    ```

    | Option | What it does |
    | - | - |
    | `cameraPermission` | The camera text. `false` leaves the camera out: your user types the charger's code instead of scanning it. |
    | `locationWhenInUsePermission` | The location text. iOS asks for it to read the Wi-Fi network's name. `false` leaves it out. |
    | `localNetworkPermission` | The local network text. |
    | `bluetoothPermission` | The Bluetooth text. `false` leaves Bluetooth out on iOS. |
    | `accessorySetupKit` | `false` leaves AccessorySetupKit alone. |
    | `bonjourServices` | More service types to look for, such as `"_myvendor._tcp"`. |

    An option wins over a text your app already has, which wins over the plugin's default.
  </Accordion>

  <Accordion title="Peblar chargers on Android">
    Peblar chargers are set up over plain HTTP on their own Wi-Fi, at `http://172.16.0.1`. Android blocks plain HTTP in release builds, and the plugin doesn't change that. Allow it for that one address with a small config plugin of your own:

    ```js plugins/with-charger-cleartext.js theme={null}
    const fs = require('fs');
    const path = require('path');
    const { withAndroidManifest, withDangerousMod } = require('expo/config-plugins');

    const MAIN = `<?xml version="1.0" encoding="utf-8"?>
    <network-security-config>
      <domain-config cleartextTrafficPermitted="true">
        <domain includeSubdomains="false">172.16.0.1</domain>
      </domain-config>
    </network-security-config>
    `;

    // Debug builds load JavaScript from Metro over plain HTTP.
    const DEBUG = `<?xml version="1.0" encoding="utf-8"?>
    <network-security-config>
      <base-config cleartextTrafficPermitted="true" />
    </network-security-config>
    `;

    function write(root, sourceSet, contents) {
      const dir = path.join(root, 'app', 'src', sourceSet, 'res', 'xml');
      fs.mkdirSync(dir, { recursive: true });
      fs.writeFileSync(path.join(dir, 'network_security_config.xml'), contents);
    }

    module.exports = function withChargerCleartext(config) {
      config = withAndroidManifest(config, (config) => {
        const app = config.modResults.manifest.application?.[0];
        if (app) {
          app.$ = app.$ ?? {};
          app.$['android:networkSecurityConfig'] = '@xml/network_security_config';
        }
        return config;
      });
      return withDangerousMod(config, [
        'android',
        (config) => {
          write(config.modRequest.platformProjectRoot, 'main', MAIN);
          write(config.modRequest.platformProjectRoot, 'debug', DEBUG);
          return config;
        },
      ]);
    };
    ```

    Then add it after the SDK's plugin:

    ```json app.json theme={null}
    {
      "expo": {
        "plugins": ["@plugchoice/react-native", "./plugins/with-charger-cleartext"]
      }
    }
    ```
  </Accordion>
</AccordionGroup>

## Configure the SDK

Tell the SDK how to get a client secret from your server. Do this once, when your app starts:

```ts theme={null}
import { configurePlugchoice } from '@plugchoice/react-native';

configurePlugchoice({
  fetchClientSecret: async (action) => {
    const response = await fetch('https://api.example.com/plugchoice/client-secret', {
      method: 'POST',
      // Add your own authentication: your server needs to know who is signed in.
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify(action),
    });
    if (!response.ok) throw new Error(`client secret: HTTP ${response.status}`);
    const { clientSecret } = await response.json();
    return clientSecret;
  },
});
```

Your server creates a client session for the signed-in user and returns its secret. [Get started](/sdk/get-started) shows that endpoint.

<Accordion title="When the SDK calls fetchClientSecret">
  The SDK calls it when Link opens, and again with the same action whenever the secret expires (after an hour), so your user can take as long as they need.

  If it throws, returns an empty string or takes longer than 30 seconds, Link shows an error screen with **Try again**. Log the error in your callback if you need it: only its name reaches the native side. The SDK never logs the secret or puts it in a URL.
</Accordion>

## Open Link

Open Link with what your user wants to do. It shows full screen, and resolves once your user is done:

```ts theme={null}
import { openLink } from '@plugchoice/react-native';

const result = await openLink({ action: 'add' });
```

| To | Call |
| - | - |
| Add a charger | `openLink({ action: 'add' })` |
| Change a charger's Wi-Fi | `openLink({ action: 'network', chargerId })` |
| Set a charger up | `openLink({ action: 'setup', chargerId })` |
| Reconnect a charger | `openLink({ action: 'reconnect', chargerId })` |

To add a charger at one of your user's sites, pass its `siteId` too. [Actions](/sdk/concepts/actions) explains each one.

## Handle the result

When Link closes, you get what happened:

```ts theme={null}
const result = await openLink({ action: 'add' });

if (result.status === 'success') {
  // The chargers your user added. Tell your server, which confirms it with Plugchoice.
  const chargerIds = result.devices.filter((device) => device.type === 'charger').map((device) => device.id);
} else if (result.status === 'error') {
  // Show your own message, and log result.error.
}
// 'cancelled': your user closed Link. Nothing changed.
```

The result is for your app's screen. Before you rely on it, your server confirms it with Plugchoice: see [Link sessions](/sdk/concepts/link-sessions).

<Accordion title="Every field of the result">
  <ResponseField name="status" type="'success' | 'cancelled' | 'error'">
    How Link ended.
  </ResponseField>

  <ResponseField name="action" type="string">
    The action your user did: usually the one you opened, or another they picked from what the charger can do.
  </ResponseField>

  <ResponseField name="sessionId" type="string | null">
    The link session, for your server to confirm. `null` when Link closed before it started one.
  </ResponseField>

  <ResponseField name="devices" type="{ type: string; id: string }[]">
    On `success`, the devices Link finished. `type` is `'charger'` today; ignore types you don't know.
  </ResponseField>

  <ResponseField name="error" type="{ code: string; message?: string }">
    On `error`: a `code`, and a `message` for your logs.
  </ResponseField>
</Accordion>

## Show actions where they work

Not every charger supports every action, and not every phone can do what an action needs. Show a button only when both can:

```ts theme={null}
import { getTransports } from '@plugchoice/react-native';

// A charger's capability, as your server got it from the SDK API:
// { capable: true, needs: ['ble'] }
async function canOffer(capability: { capable: boolean; needs: string[] }) {
  if (!capability.capable) return false;
  const transports = await getTransports();
  return capability.needs.every((need) => transports.includes(need));
}
```

[Capabilities](/sdk/concepts/capabilities) explains both halves.

## Errors

Things can go wrong in two places:

* **Before Link opens**, `openLink` rejects. That only happens when Link can't be shown at all, for example before `configurePlugchoice`, or while another Link screen is open.
* **Inside Link**, your user sees an error screen. When they close it, the result has `status: 'error'`. Show your own message, and log `result.error`.

<AccordionGroup>
  <Accordion title="Why openLink rejects">
    | `error.code` | When |
    | - | - |
    | `ERR_PLUGCHOICE_NOT_CONFIGURED` | `openLink` was called before `configurePlugchoice`. |
    | `ERR_LINK_ALREADY_OPEN` | A Link screen is already open. |
    | `ERR_LINK_CANNOT_PRESENT` | There's no screen to show Link from, for example when your app is in the background. |
    | `ERR_PLUGCHOICE_UNAVAILABLE` | The native code isn't in this build: Expo Go, web, or an app not rebuilt after installing the package. |
    | `ERR_PLUGCHOICE_INTERNAL` | The native code answered with something unexpected. |

    It throws a `TypeError` for an action that isn't `{ action, chargerId?, siteId? }`.
  </Accordion>

  <Accordion title="Error codes in the result">
    | `result.error.code` | When |
    | - | - |
    | `clientSecretUnavailable` | Your `fetchClientSecret` failed, and your user closed the error screen instead of trying again. |
    | `pageLoadFailed` | Link didn't load (no internet), and your user closed the **Try again** screen. |
    | `internal` | The SDK couldn't run Link. On Android: no usable WebView. |

    <Screen name="secret-unavailable" alt="The error screen when the app couldn't get a client secret, with a Try again button" caption="What your user sees when fetchClientSecret fails" />

    Other codes are for your logs; don't branch on them. [Errors](/sdk/errors) lists them all.
  </Accordion>
</AccordionGroup>

## Test your integration

* **On a phone.** A simulator can open Link, but it can't reach a charger: joining a charger's Wi-Fi, finding it on the home Wi-Fi, Bluetooth and scanning a QR code all need a real device.
* **In Jest.** Mock the package with `jest.mock('@plugchoice/react-native')`.


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.