> ## Documentation Index
> Fetch the complete documentation index at: https://rain-sandbox-trial.mintlify.site/llms.txt
> Use this file to discover all available pages before exploring further.

# iOS Quickstart

> Authenticate a user, create an embedded wallet, and send your first transaction using the Rain iOS SDK.

This guide walks you through a wallet integration in Swift, from initializing the SDK and authenticating a user to retrieving their wallet address and sending tokens on Base Sepolia.

### Before you begin

[Install the Rain SDK](/sdks/embedded-wallets/installation#ios) and configure a Base Sepolia RPC endpoint.

All wallet operations in this quickstart run directly in your iOS application. You do not need a Rain API key.

<Steps>
  <Step title="Create the provider">
    `RainProvider` is the Rain wallet: it owns login, wallet creation, and sessions. The wallet backend's identity is embedded in the SDK, so the default configuration is the product. Create one provider per app and keep it for the app's lifetime.

    ```swift theme={null}
    import RainWallet

    let wallet = RainProvider()
    ```

    To react to a session that dies and can't be refreshed, or to enable passkeys, pass a `RainWalletConfig`:

    ```swift theme={null}
    let wallet = RainProvider(RainWalletConfig(
        passkeyDomain: "example.com",              // nil (default) leaves passkeys off
        onSessionExpired: {                        // fires once per session death; not on logout
            Task { @MainActor in showLoginScreen() }
        }
    ))
    ```
  </Step>

  <Step title="Log in: restore a session or start a login">
    On launch, wait for the SDK to restore a persisted session. A returning user skips the code entirely.

    ```swift theme={null}
    await wallet.awaitSessionRestore()

    if wallet.hasActiveSession() {
        // Returning user: go straight to the wallet.
    } else {
        // New device or logged out: run the one-time-code flow below.
    }
    ```

    `wallet.authState` publishes `.loading`, `.authenticated`, or `.unauthenticated` on every change, and `wallet.currentAuthState()` is its snapshot, so a login screen can bind to it instead of polling.
  </Step>

  <Step title="Send a one-time code">
    Send a login code to the user's email address or phone number. The contact is the account's identity: a first login with it signs the user up, and later logins with the same contact find the same account.

    ```swift theme={null}
    try await wallet.sendLoginCode(to: .email("user@example.com"))
    // or by SMS:
    // try await wallet.sendLoginCode(to: .phone("+15551234567"))
    ```

    Calling `sendLoginCode` again for the same contact issues a new code and replaces the pending one, which is how you implement **Resend code**. A blank email or a phone number that isn't a valid international number throws `RainError.invalidConfig` before anything leaves the device.
  </Step>

  <Step title="Confirm the code and create the wallet">
    Pass the code the user typed. On a first login the SDK signs the user up and creates **one wallet holding an Ethereum account and a Solana account**, from a single seed, in the same request. On a returning login it finds the existing account.

    ```swift theme={null}
    do {
        try await wallet.confirmLoginCode(code)
    } catch let error as RainError where error == .invalidLoginCode {
        // RAIN_203: wrong, expired, or already-used code. Keep the screen up and let the user retype it.
    }
    ```

    A wrong code keeps the pending challenge, so the user can retype it without requesting a new one. Codes expire after 5 minutes and lock after 3 wrong attempts.

    <Check>
      The user is logged in and their wallet exists. A successful login also ends the user's sessions on other devices; the signed-out device's `onSessionExpired` fires on its next call.
    </Check>
  </Step>

  <Step title="Build the SDK and resolve the wallet">
    Register the provider with the chains you want to use and build the SDK. Then resolve the wallet-bound `RainClient`. Resolution suspends once, while the SDK materializes the wallet, and is cached afterwards.

    ```swift theme={null}
    let rain = try RainSdk.builder()
        .rpcEndpoints([
            RainChain.baseSepolia: "https://sepolia.base.org",
            RainChain.solanaDevnet: "https://api.devnet.solana.com",
        ])
        .register(wallet)
        .build()

    let client = try await rain.provider(.rain)

    let evmAddress = try await client.getWalletAddress()                              // "0x…"
    let solanaAddress = try await client.getWalletAddress(chainId: RainChain.solanaDevnet) // base58
    ```

    Resolving before a session is live throws `RainError.tokenExpired` (`RAIN_201`). Send the EVM address (and the Solana address, if you use Solana) to your backend: creating the user's Rain application requires it. See [Wallet creation](/docs/embedded-wallets/wallet-creation).
  </Step>

  <Step title="Send a transaction">
    Fund the wallet with testnet USDC first (the [Circle faucet](https://faucet.circle.com/) supports Base Sepolia), then send some of it to another address you control. Amounts are `Decimal` in human units; the SDK resolves the token's decimals and converts exactly.

    ```swift theme={null}
    let usdc = "0x036CbD53842c5426634e7929541eC2318f3dCF7e"   // Base Sepolia USDC
    let recipient = "0x..."                                     // an address you control

    let balance = try await client.getBalance(
        chainId: RainChain.baseSepolia,
        token: .contract(address: usdc)
    )
    print(balance.formatted, balance.symbol ?? "")   // "10" "USDC"

    let result = try await client.sendToken(
        chainId: RainChain.baseSepolia,
        contractAddress: usdc,
        to: recipient,
        amount: 1.5
    )
    print(result.transactionHash)
    ```

    Gas is sponsored by default, so the wallet doesn't need ETH to send. Native sends use `client.sendNative(chainId:to:amount:)`.

    <Check>
      You've logged in, created a wallet, and sent a token. Everything else on `RainClient` follows the same shape: [balances and history](/docs/embedded-wallets/balances-and-history), [receiving funds](/docs/embedded-wallets/receiving-funds), and [card funding](/docs/embedded-wallets/card-funding-and-settlement).
    </Check>
  </Step>
</Steps>

## Log in with a passkey instead

Users can log in with an existing passkey or add a passkey to their account for future authentication.

Configure `passkeyDomain` during SDK setup. The `anchor` parameter specifies the window used to display the system passkey prompt.

```swift theme={null}
// Returning user with a passkey on this account:
try await wallet.loginWithPasskey(anchor: window)

// Logged in with a code? Register a passkey for next time:
try await wallet.addPasskey(anchor: window)
```

Use `signUpWithPasskey(anchor:)` only when creating a new account. Returning users should use `loginWithPasskey(anchor:)` to access their existing wallet.

See [**Authentication options**](/docs/embedded-wallets/authentication) for additional configuration and recovery guidance.

## Log out and clean up

Use `logout()` to end the user's session and `close()` to release SDK resources.

```swift theme={null}
try await wallet.logout()   // clears the stored session; safe when none exists
rain.close()                // tears down resolved clients and stops the provider's session watcher
```

Calling `rain.close()` permanently closes the SDK instance. Create a new `RainSdk` instance before the next login. Use `rain.reset()` instead if you only need to clear resolved clients while keeping the provider active.

## Using an existing wallet provider

If your application already uses Portal, Privy, or your own Turnkey organization, register the corresponding provider adapter instead of `RainProvider`. Authentication and session management remain with your existing provider. Subsequent wallet operations use the same `RainClient` interface.

```swift theme={null}
import RainPortal   // re-exports RainCore, like RainWallet does

let rain = try RainSdk.builder()
    .rpcEndpoints([RainChain.baseSepolia: "https://sepolia.base.org"])
    .register(PortalProvider(PortalConfig(sessionToken: portalSessionToken)))
    .build()

let client = try await rain.provider(.portal)
```

For setup instructions and supported capabilities, see [Third party wallet providers](/sdks/embedded-wallets/third-party-providers).

## What's next

<Columns cols={3}>
  <Card title="Features" icon="table" href="/sdks/embedded-wallets/features">
    What each platform and chain supports.
  </Card>

  <Card title="Testing" icon="vial" href="/sdks/embedded-wallets/testing">
    Testnets, test tokens, and the pre-go-live checklist.
  </Card>

  <Card title="Error reference" icon="circle-exclamation" href="/sdks/embedded-wallets/error-reference">
    Every `RAIN_*` code and how to handle it.
  </Card>
</Columns>
