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

# iOS Guide

> Documentation about the Spam Blocking SDK for iOS

The Spam Control SDK blocks and identifies spam calls on iOS using Apple's Call Directory Extensions. It manages the spam database, user-blocked numbers, and caller identification through the `SpamControl` singleton.

Requires **iOS 14+**.

The integration uses up to **three Call Directory Extensions**, each backed by its own App Group. Configure only the ones you need, since each feature requires its extension:

| Extension      | Purpose                                        | Extension type    |
| -------------- | ---------------------------------------------- | ----------------- |
| Dataset        | Blocks numbers from the spam database          | `.blockSpams`     |
| User           | Blocks numbers manually added by the user      | `.blockManual`    |
| Identification | Adds caller ID / spam labels to incoming calls | `.callIdentifier` |

## Integration

### Step 1: Add the SDK to your project

The SDK is distributed as a precompiled `FeatureSpamControl.xcframework`, provided by the Bemobi team. Drag it into your Xcode project, then under each target's **General > Frameworks, Libraries, and Embedded Content**:

* **Main app**: add the framework as **Embed & Sign**.
* **Each extension** (next steps): add it as **Do Not Embed**, so it is linked but not duplicated in the extension bundle.

Import it with `import FeatureSpamControl`.

### Step 2: Create the Call Directory Extensions

Add one target per extension you need. For each, go to **File > New > Target**, select **Call Directory Extension** under iOS, and name it (e.g. `SpamControlDatasetDirectory`, `SpamControlUserDirectory`, `SpamControlIdentificationDirectory`).

### Step 3: Configure App Groups

Create one App Group per extension and share it between the main app and the matching extension. In each target's **Signing & Capabilities** tab, add the **App Groups** capability:

* Main app: all three groups
* Dataset extension: `group.your.app.dataset`
* User extension: `group.your.app.user`
* Identification extension: `group.your.app.identification`

Each extension must only have access to its own group.

The same groups must be declared in each target's `.entitlements` file. Example for the main app:

```xml theme={null}
<key>com.apple.security.application-groups</key>
<array>
    <string>group.your.app.dataset</string>
    <string>group.your.app.user</string>
    <string>group.your.app.identification</string>
</array>
```

Each extension's `.entitlements` lists only its own group.

> The App Group identifiers are yours to choose, but they must be **identical** everywhere they appear: the capability, the `.entitlements` file, the handler's `appGroup:` argument (Step 5), and the `ExtensionBlockModel` (Step 6).

### Step 4: Link `FeatureSpamControl` to each extension

For every extension target, add `FeatureSpamControl` under **General > Frameworks, Libraries, and Embedded Content** and set it to **Do Not Embed** (see Step 1).

### Step 5: Implement the extension handlers

In each extension's `CallDirectoryHandler.swift`, delegate to the matching Wave handler.

The User and Identification handlers take a `hintIdentifier`: the label shown on the incoming call screen (e.g. `"Blocked"`, `"Spam"`).

**Dataset extension** (`WaveCallDatasetDirectoryHandler`):

```swift theme={null}
import CallDirectory
import FeatureSpamControl

class CallDirectoryHandler: CXCallDirectoryProvider {

    private let directory = WaveCallDatasetDirectoryHandler(appGroup: "group.your.app.dataset")

    override func beginRequest(with context: CXCallDirectoryExtensionContext) {
        context.delegate = self
        directory.beginRequest(with: context) { result in
            switch result {
            case .success: context.completeRequest()
            case .failure(let error): context.cancelRequest(withError: error)
            }
        }
    }
}

extension CallDirectoryHandler: CXCallDirectoryExtensionContextDelegate {
    func requestFailed(for extensionContext: CXCallDirectoryExtensionContext, withError error: Error) {
        directory.requestFailed(for: extensionContext, withError: error)
    }
}
```

**User extension** (`WaveCallUserDirectoryHandler`):

```swift theme={null}
import Foundation
import CallKit
import FeatureSpamControl

class CallDirectoryHandler: CXCallDirectoryProvider {

    private let directory = WaveCallUserDirectoryHandler(
        appGroup: "group.your.app.user",
        hintIdentifier: "Blocked"
    )

    override func beginRequest(with context: CXCallDirectoryExtensionContext) {
        context.delegate = self
        do {
            try directory.beginRequest(with: context)
        } catch {
            context.cancelRequest(withError: NSError(domain: "CallDirectoryHandler", code: 1))
            return
        }
        context.completeRequest()
    }
}

extension CallDirectoryHandler: CXCallDirectoryExtensionContextDelegate {
    func requestFailed(for extensionContext: CXCallDirectoryExtensionContext, withError error: Error) {
        directory.requestFailed(for: extensionContext, withError: error)
    }
}
```

**Identification extension** (`WaveCallIdentificationHandler`):

```swift theme={null}
import Foundation
import CallKit
import FeatureSpamControl

class CallDirectoryHandler: CXCallDirectoryProvider {

    private let directory = WaveCallIdentificationHandler(
        appGroup: "group.your.app.identification",
        hintIdentifier: "Spam"
    )

    override func beginRequest(with context: CXCallDirectoryExtensionContext) {
        context.delegate = self
        directory.beginRequest(with: context) { result in
            switch result {
            case .success: context.completeRequest()
            case .failure(let error): context.cancelRequest(withError: error)
            }
        }
    }
}

extension CallDirectoryHandler: CXCallDirectoryExtensionContextDelegate {
    func requestFailed(for extensionContext: CXCallDirectoryExtensionContext, withError error: Error) {
        directory.requestFailed(for: extensionContext, withError: error)
    }
}
```

### Step 6: Add the contacts permission

Add `NSContactsUsageDescription` to your main app's `Info.plist`. This is required only for **manual blocking** (User extension); the Dataset and Identification extensions work without contacts access.

```xml theme={null}
<key>NSContactsUsageDescription</key>
<string>We need access to your contacts to let you block or unblock numbers and tell them apart from your known contacts.</string>
```

### Step 7: Configure the SDK

Configuration has two parts: declaring the extensions (once) and setting the API credentials (whenever they become available).

**Configure extensions.** Call this once at startup (`AppDelegate`, `@main`, or an init service), passing one `ExtensionBlockModel` per extension. The `appGroup` and `bundleIdentifier` must match the App Groups from Step 3 and the extension targets' bundle identifiers:

```swift theme={null}
import FeatureSpamControl

SpamControlConfiguration.shared.configureExtensions(with: [
    ExtensionBlockModel(
        appGroup: "group.your.app.dataset",
        bundleIdentifier: "com.yourcompany.app.DatasetDirectory",
        extensionType: .blockSpams
    ),
    ExtensionBlockModel(
        appGroup: "group.your.app.identification",
        bundleIdentifier: "com.yourcompany.app.CallIdentification",
        extensionType: .callIdentifier
    ),
    ExtensionBlockModel(
        appGroup: "group.your.app.user",
        bundleIdentifier: "com.yourcompany.app.UserDirectory",
        extensionType: .blockManual
    )
])
```

**Set credentials and start protection.** Call `setup` once you have the API key (provided by the Bemobi team) and the user's phone number. It can be called after login; the phone number is not needed at launch. It **must** be called before any database operation, otherwise downloads fail with authentication errors.

```swift theme={null}
SpamControlConfiguration.shared.setup(
    apiKey: "YOUR_API_KEY",            // provided by the Bemobi team
    phoneNumber: "5511982619489",      // 55 + area code + number
    environment: .production           // or .stage for testing
)

Task {
    try await SpamControl.shared.downloadFullDatabase()                  // download the spam database
    try await SpamControl.shared.applySpamPhoneNumberBlocking()          // enable database blocking
    try await SpamControl.shared.applyIdentificationSpamPhoneNumberBlocking() // enable caller ID labels
}
```

`setup` can be called again if credentials change (e.g. a different user logs in); previous data is cleared automatically. Changing the environment between calls also clears all stored data.

> Database operations require a **physical device**. `downloadFullDatabase()` throws on the iOS Simulator.

#### Example: enabling protection after login

Extensions are configured once at startup. Credentials and the database, however, are best set up right after the user logs in, when the phone number is available. A typical login handler looks like this:

```swift theme={null}
import FeatureSpamControl

final class AuthenticationService {

    func handleSuccessfulLogin(apiKey: String, phoneNumber: String) {
        // 1. Store credentials (extensions were already configured in AppDelegate).
        SpamControlConfiguration.shared.setup(
            apiKey: apiKey,
            phoneNumber: phoneNumber,
            environment: .production
        )

        // 2. Download the database and turn on blocking and identification.
        Task {
            do {
                try await SpamControl.shared.downloadFullDatabase()
                try await SpamControl.shared.applySpamPhoneNumberBlocking()
                try await SpamControl.shared.applyIdentificationSpamPhoneNumberBlocking()
            } catch {
                print("Failed to start spam protection: \(error.localizedDescription)")
            }
        }
    }
}
```

The call order matters: `setup` first, then `downloadFullDatabase`, then the two `apply...` calls that load the data into the extensions.

### Step 8: Enable Call Blocking & Identification

The user must enable the extensions in iOS Settings. Build and run on a **physical device**, then go to **Settings > Phone > Call Blocking & Identification** and toggle on each of your extensions. You can open this screen directly with `SpamControl.shared.openSettings()`.

Changes may take a few moments to apply. If an extension does not appear or does not work, verify that the App Groups, entitlements, and bundle identifiers match across the main app and the extension.

## API reference

All methods are accessed through `SpamControl.shared` and most are `async`/`throws`. Configuration helpers live on `SpamControlConfiguration.shared`. Only Brazilian numbers (`55` + area code + number, e.g. `5511987654321`) are processed.

### Extension status

Report whether the user has enabled each extension under Settings > Phone > Call Blocking & Identification.

* `isCallDatasetDirectoryEnabled() -> Bool`: whether the Dataset (spam database) extension is enabled.
* `isCallUserDirectoryEnabled() -> Bool`: whether the User (manual blocking) extension is enabled.
* `isCallIdentificationDirectoryEnabled() -> Bool`: whether the Identification (caller ID) extension is enabled.
* `areAllExtensionsEnabled() -> Bool`: whether all three extensions are enabled.
* `openSettings()`: opens the system Call Blocking & Identification screen (iOS 13.4+).

```swift theme={null}
let dataset = await SpamControl.shared.isCallDatasetDirectoryEnabled()
let all = try await SpamControl.shared.areAllExtensionsEnabled()
try await SpamControl.shared.openSettings()
```

### Spam database

* `downloadFullDatabase()`: downloads the latest spam database. Throttled to once per 24h (a no-op if called sooner) and throws on the Simulator.
* `applySpamPhoneNumberBlocking()`: loads the database into the Dataset extension so matching calls are blocked.
* `applyIdentificationSpamPhoneNumberBlocking()`: loads the database into the Identification extension so matching calls are labeled.
* `totalPhoneNumbersAtDatabase() -> Int?`: number of phone entries stored locally.

Call `downloadFullDatabase()` on each app launch to keep the database fresh; the 24h throttle prevents redundant downloads.

```swift theme={null}
try await SpamControl.shared.downloadFullDatabase()
try await SpamControl.shared.applySpamPhoneNumberBlocking()
try await SpamControl.shared.applyIdentificationSpamPhoneNumberBlocking()
let total = try await SpamControl.shared.totalPhoneNumbersAtDatabase()
```

### Categories

Categories let the user block whole groups of spam numbers at once.

* `downloadSpamCategories(cacheValidityDuration:) -> [SpamCategory]`: lists available categories. Uses the cache while valid (default 3600s / 1h).
* `toggleCategoryBlockState(categoryId:) -> [SpamCategory]`: flips one category's block state and returns the updated list.
* `blockSpamPhoneNumberByCategories(for: [NSNumber])`: persists the selected category IDs and reloads the extension.

```swift theme={null}
public struct SpamCategory {
    public let id: Int
    public let category: String     // display name
    public let description: String
    public var isBlocked: Bool
}
```

```swift theme={null}
let categories = try await SpamControl.shared.downloadSpamCategories()
let updated = try await SpamControl.shared.toggleCategoryBlockState(categoryId: 1)
try await SpamControl.shared.blockSpamPhoneNumberByCategories(for: [1, 2, 3])
```

### Manual blocking

Require the contacts permission and an enabled User extension. Blocking and unblocking return the affected entry IDs (`[Int64]`) and throw a typed `BlockManuallyErrorType` (see [Error handling](#error-handling)).

* `blockUserPhoneNumber(phoneNumber:) -> [Int64]`: blocks a single number.
* `blockUserPhoneNumbers(_: [String]) -> [Int64]`: blocks several numbers at once.
* `retrievedBlockedUserPhoneNumbers() -> [CXCallDirectoryPhoneNumber]`: lists the numbers the user has blocked.
* `unblockPhoneNumber(phoneNumber:) -> [Int64]`: removes a number from the block list.

```swift theme={null}
let ids = try await SpamControl.shared.blockUserPhoneNumber(phoneNumber: "5511987654321")
let blocked = SpamControl.shared.retrievedBlockedUserPhoneNumbers()
try await SpamControl.shared.unblockPhoneNumber(phoneNumber: "5511987654321")
```

### Trusted numbers

Trusted numbers are an allowlist: a trusted number is never blocked or labeled as spam, even if it appears in the spam database. Adding or removing one reloads the affected extensions. The throwing methods throw `TrustedNumbersErrorType`.

* `addTrustedNumber(_ phoneNumber: String)`: marks a number as trusted so it is no longer blocked.
* `removeTrustedNumber(_ phoneNumber: String)`: removes a number from the trusted list, restoring its previous blocking state.
* `getTrustedNumbers() -> [String]`: lists the user's trusted numbers.

```swift theme={null}
try await SpamControl.shared.addTrustedNumber("5511987654321")
let trusted = await SpamControl.shared.getTrustedNumbers()
try await SpamControl.shared.removeTrustedNumber("5511987654321")
```

### Statistics

* `totalBlockedCalls() -> TotalBlockedCalls?`: total number of calls blocked, fetched from the server.

```swift theme={null}
public struct TotalBlockedCalls {
    public let name: String
    public let value: Int   // number of blocked calls
}
```

```swift theme={null}
let totalBlocked = try await SpamControl.shared.totalBlockedCalls()
print(totalBlocked?.value ?? 0)
```

### Network

The SDK monitors connectivity to control downloads. Cellular downloads are disabled by default.

* `isNetworkConnected: Bool`: whether the device has any connection.
* `isOnWiFi: Bool`: whether the device is on Wi-Fi.
* `downloadWithCellularDataEnabled: Bool`: set to `true` to allow downloads over cellular (default `false`).

```swift theme={null}
if SpamControl.shared.isNetworkConnected { /* safe to download */ }
SpamControl.shared.downloadWithCellularDataEnabled = true
```

### Configuration helpers

On `SpamControlConfiguration.shared`:

* `getBundleIdentifierExtension(with:) -> String`: the bundle identifier configured for an extension type.
* `getAppGroupIdentifier(for:) -> String`: the App Group configured for an extension type.
* `getSDKEnviroment() -> SDKEnviroment`: the current environment (`.production` or `.stage`).

```swift theme={null}
let bundleId = try SpamControlConfiguration.shared.getBundleIdentifierExtension(with: .blockSpams)
let appGroup = try SpamControlConfiguration.shared.getAppGroupIdentifier(for: .blockManual)
let environment = try SpamControlConfiguration.shared.getSDKEnviroment()
```

### Disabling the SDK

* `disabledSDK()`: turns the SDK off. It removes the CallKit data from every extension, deletes the local spam database, and clears stored state (categories, last download time, etc.). To turn protection back on, call `setup` and `downloadFullDatabase` again.

```swift theme={null}
try await SpamControl.shared.disabledSDK()
```

## Error handling

The manual blocking methods (`blockUserPhoneNumber`, `blockUserPhoneNumbers`, `unblockPhoneNumber`) throw a typed `BlockManuallyErrorType` you can switch on to give the user a precise message:

```swift theme={null}
do {
    _ = try await SpamControl.shared.blockUserPhoneNumber(phoneNumber: number)
} catch let error as BlockManuallyErrorType {
    switch error {
    case .phoneNumberAlreadyBlocked:
        // The number is already in the block list.
    case .phoneNumberNotBlocked:
        // Tried to unblock a number that wasn't blocked.
    case .phoneNumberInContacts:
        // The number belongs to one of the user's contacts.
    case .invalidPhoneNumberFormat, .emptyPhoneNumber, .phoneNumberInsufficientSignificantDigits:
        // The number is not valid or has too few significant digits.
    case .batchContainsInvalidNumbers:
        // A batch block contained invalid numbers.
    case .callDirectoryIsNotEnabled:
        // The User extension is turned off in Settings.
    case .permissionDenied:
        // Contacts permission is missing.
    case .networkUnavailable:
        // No connection.
    default:
        // Storage, tracking, or unknown failures.
    }
} catch {
    print(error.localizedDescription)
}
```

Other operations (database download, categories, statistics) surface failures as a generic `Error`. Read `error.localizedDescription` for the cause, such as an authentication failure or an unavailable network:

```swift theme={null}
do {
    try await SpamControl.shared.downloadFullDatabase()
} catch {
    print("Spam Control error: \(error.localizedDescription)")
}
```

## SDK size

* **3 to 7 MB**: core functionality (blocking and call management), no UI.
* **10 to 15 MB**: full functionality including a SwiftUI user interface.

## Migration to 2.0

The `Wave` class is deprecated. Use `SpamControlConfiguration` instead.

| Deprecated                                           | Replacement                                                              |
| ---------------------------------------------------- | ------------------------------------------------------------------------ |
| `Wave.shared.setup(apiKey:phoneNumber:)`             | `SpamControlConfiguration.shared.setup(apiKey:phoneNumber:environment:)` |
| `Wave.shared.set*Extension(...)`                     | `SpamControlConfiguration.shared.configureExtensions(with:)`             |
| `Wave.shared.get*Extension()`                        | `SpamControlConfiguration.shared.getBundleIdentifierExtension(with:)`    |
| `SpamControl.shared.forceDownloadFullDatabase(for:)` | `SpamControl.shared.downloadFullDatabase()`                              |
