> ## 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.

# iOS

> Add the Plugchoice SDK to an iOS 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 iOS SDK is the Swift package `PlugchoiceSDK`. It shows Link, the hosted flow at `connect.plugchoice.com`, in a sheet, and does what a web page can't: join a charger's Wi-Fi, find and reach chargers on the local network, talk Bluetooth to them and scan a QR code. It works with UIKit and SwiftUI.

## Install

**Requirements**: iOS 16 or later, and Xcode 16 or later. On iOS 18 and later the SDK joins a charger's Wi-Fi through AccessorySetupKit; on iOS 16 and 17 it uses the classic join prompt.

Add the package with Swift Package Manager. In Xcode, choose **File › Add Package Dependencies…**, enter `https://github.com/plugchoice/mobile-sdk`, pick **Up to Next Minor Version** from `0.4.0`, and add the `PlugchoiceSDK` library to your app target.

In a `Package.swift`:

```swift Package.swift theme={null}
dependencies: [
    .package(url: "https://github.com/plugchoice/mobile-sdk", .upToNextMinor(from: "0.4.0")),
],
targets: [
    .target(name: "YourApp", dependencies: [
        .product(name: "PlugchoiceSDK", package: "mobile-sdk"),
    ]),
]
```

Then import it where you use it:

```swift theme={null}
import PlugchoiceSDK
```

## Configure your app

### Capabilities

In Xcode, open your app target's **Signing & Capabilities** and add:

| Capability | Entitlement | Why |
| - | - | - |
| **Hotspot Configuration** | `com.apple.developer.networking.HotspotConfiguration` | Joining a charger's own Wi-Fi |
| **Access Wi-Fi Information** | `com.apple.developer.networking.wifi-info` | Reading the current network name, to confirm the join |

Your App ID needs the same capabilities in the Apple Developer portal.

### Info.plist

| Key | Why | Required |
| - | - | - |
| `NSLocalNetworkUsageDescription` | Talking to chargers on the local network: a charger's own Wi-Fi, or the home Wi-Fi | Yes |
| `NSAppTransportSecurity` › `NSAllowsLocalNetworking` (`true`) | Plain HTTP to chargers on the local network | Yes |
| `NSBluetoothAlwaysUsageDescription` | Setting chargers up over Bluetooth. App Store Connect asks for it anyway, because the SDK links CoreBluetooth. Without it the SDK doesn't use Bluetooth. | Yes |
| `NSBonjourServices` | Finding chargers on the home Wi-Fi. iOS only looks for the service types an app declares. Declare at least `_alfen._tcp`, `_http._tcp` and `_https._tcp`. | Recommended |
| `NSLocationWhenInUseUsageDescription` | Reading the network name after joining a charger's Wi-Fi | Recommended |
| `NSCameraUsageDescription` | Scanning the QR code on a charger. Without it, the user types the code. | Recommended |
| `NSAccessorySetupKitSupports` (`[WiFi]`) | On iOS 18 and later, one prompt per charger instead of a "Join network?" prompt on every join | Recommended |

```xml Info.plist theme={null}
<key>NSLocalNetworkUsageDescription</key>
<string>Connect to your charger on your local network.</string>
<key>NSBluetoothAlwaysUsageDescription</key>
<string>Use Bluetooth to set up your charger.</string>
<key>NSLocationWhenInUseUsageDescription</key>
<string>Confirm that your phone joined your charger's Wi-Fi.</string>
<key>NSCameraUsageDescription</key>
<string>Scan the QR code on your charger.</string>
<key>NSAppTransportSecurity</key>
<dict>
    <key>NSAllowsLocalNetworking</key>
    <true/>
</dict>
<key>NSBonjourServices</key>
<array>
    <string>_alfen._tcp</string>
    <string>_http._tcp</string>
    <string>_https._tcp</string>
</array>
<key>NSAccessorySetupKitSupports</key>
<array>
    <string>WiFi</string>
</array>
```

Write the usage strings for your users: iOS shows them in its permission prompts. The SDK asks for each permission only when the flow needs it.

<Warning>
  Don't add `Bluetooth` to `NSAccessorySetupKitSupports`. It would limit CoreBluetooth in your whole app to accessories set up through AccessorySetupKit, and the SDK's Bluetooth would stop working.
</Warning>

## Create the SDK

Create one `Plugchoice` instance for your app's lifetime. Give it a callback that fetches a client secret from your server:

```swift theme={null}
import PlugchoiceSDK

let plugchoice = Plugchoice(fetchClientSecret: { action in
    // action.action ("add", "network", …), action.chargerId, action.siteId
    try await backend.plugchoiceClientSecret(for: action)
})
```

The callback calls your own endpoint, which creates a client session scoped to the action and returns its secret. [Get started](/sdk/get-started) shows that endpoint. In the app, it can look like this:

```swift theme={null}
struct Backend {
    let baseURL: URL

    func plugchoiceClientSecret(for action: LinkAction) async throws -> String {
        var request = URLRequest(url: baseURL.appending(path: "plugchoice/client-secret"))
        request.httpMethod = "POST"
        request.setValue("application/json", forHTTPHeaderField: "Content-Type")
        // Add your own authentication: your server needs to know who is signed in.
        request.httpBody = try JSONEncoder().encode([
            "action": action.action,
            "chargerId": action.chargerId,
            "siteId": action.siteId,
        ])
        let (data, response) = try await URLSession.shared.data(for: request)
        guard (response as? HTTPURLResponse)?.statusCode == 200 else {
            throw URLError(.badServerResponse)
        }
        struct Answer: Decodable { let clientSecret: String }
        return try JSONDecoder().decode(Answer.self, from: data).clientSecret
    }
}
```

The SDK calls `fetchClientSecret` when Link opens, while the hosted flow loads. It calls it again, with the same action, whenever the secret expires (after 60 minutes), so a user can take as long as they need. The secret never goes into a URL, and the SDK never logs it.

## Open Link

Open Link with an action, from a view controller:

```swift theme={null}
plugchoice.link.present(.addCharger(), from: viewController) { result in
    // Called once, on the main thread, after the sheet is dismissed.
}
```

In SwiftUI, use the `plugchoiceLink` modifier. Setting `isPresented` to `false` closes Link with `cancelled`.

```swift theme={null}
struct ChargerView: View {
    let plugchoice: Plugchoice
    let chargerId: String
    @State private var showLink = false

    var body: some View {
        Button("Network settings") { showLink = true }
            .plugchoiceLink(isPresented: $showLink, plugchoice: plugchoice, action: .network(chargerId: chargerId)) { result in
                // Handle the result.
            }
    }
}
```

The actions:

| Action | Helper |
| - | - |
| Add a charger, optionally at one of your user's sites | `.addCharger()`, `.addCharger(siteId:)` |
| Change a charger's network | `.network(chargerId:)` |
| Set a charger up | `.setup(chargerId:)` |
| Reconnect a charger to Plugchoice | `.reconnect(chargerId:)` |
| An action this SDK version has no helper for yet | `.custom(_:chargerId:)` |

The SDK passes the action to the hosted flow without reading it, so a new action works through `.custom` before it gets a helper. Your client session's scope must cover the action's charger or site. [Actions](/sdk/concepts/actions) explains each one.

Link is a sheet the user can't swipe away. It keeps the screen awake while it's open, because a charger's Wi-Fi connection drops when the phone locks.

## Handle the result

The completion gets a `LinkResult`:

<ResponseField name="status" type="LinkResult.Status">
  `.success`, `.cancelled` or `.error`.
</ResponseField>

<ResponseField name="action" type="String">
  The action the link session did. The user may have picked another one from the list of what a charger can do; otherwise it's the action you opened.
</ResponseField>

<ResponseField name="sessionId" type="String?">
  The link session, when the hosted flow started one. `nil` when Link closed before that.
</ResponseField>

<ResponseField name="devices" type="[Device]">
  The devices the link session reported, each `{ type, id }`. On `.success`, the devices it finished. `type` is `"charger"` today; ignore types you don't know.
</ResponseField>

<ResponseField name="error" type="LinkError?">
  On `.error`: a `code`, and a `message` for your logs.
</ResponseField>

```swift theme={null}
plugchoice.link.present(.addCharger(), from: viewController) { result in
    switch result.status {
    case .success:
        let chargerIds = result.devices.filter { $0.type == "charger" }.map(\.id)
        // Tell your server about result.sessionId; it confirms the outcome.
    case .cancelled:
        break // The user left. Nothing changed.
    case .error:
        log(result.error?.description ?? "unknown")
    }
}
```

The result is for your app's UI. Before you rely on it, your server confirms it with [Get a link session](/sdk/api/get-link-session) (`GET /sdk/v1/link-sessions/{id}`). [Link sessions](/sdk/concepts/link-sessions) explains why.

## Errors

On `.error`, `result.error?.code` says why. The SDK sets two codes itself:

| Code | When |
| - | - |
| `pageLoadFailed` | The hosted flow didn't load (no internet, server unreachable). The SDK showed a "The page did not load" screen with **Try again**, and the user closed it. |
| `internal` | The SDK couldn't run the hosted flow. |

Every other code comes from the hosted flow. The one to know is `clientSecretUnavailable`: your `fetchClientSecret` threw, returned an empty string or took longer than 30 seconds. The user then sees this screen, with **Try again**, and the result reports `clientSecretUnavailable` if they close it.

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

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

## Show actions where they work

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

* The charger's [capability](/sdk/concepts/capabilities) for it is `capable`. Your server reads it with [Get a charger](/sdk/api/get-charger).
* Every transport in the capability's `needs` is in `Plugchoice.transports()`.

```swift theme={null}
// A capability as your server passed it on from the SDK API:
// { "capable": true, "needs": ["ble"] }
struct Capability: Decodable {
    let capable: Bool
    let needs: [String]
}

func canOffer(_ capability: Capability) -> Bool {
    capability.capable && Set(capability.needs).isSubset(of: Plugchoice.transports())
}
```

`Plugchoice.transports()` asks for no permission. It returns:

* `wifi`, `http` and `socket`: always.
* `lan`: when your Info.plist declares at least one type in `NSBonjourServices`.
* `ble`: on a device with Bluetooth LE (not the simulator), when your Info.plist has `NSBluetoothAlwaysUsageDescription`.

## Test on a real device

The simulator can open Link and run the flow up to the charger, but it can't reach one. Use an iPhone for:

* joining a charger's Wi-Fi, and the AccessorySetupKit prompt on iOS 18 and later
* the Local Network prompt, and finding chargers on the home Wi-Fi
* everything Bluetooth (the simulator has no Bluetooth)
* scanning a QR code with the camera

The [example app](https://github.com/plugchoice/mobile-sdk/tree/main/ios/Example) opens Link with a client secret you paste, for any action.


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