<!-- AI agents: this is the Markdown version of an AppNorth documentation page. Index: https://www.appnorth.app/llms.txt -->

# Swift API

Use this reference to look up a signature, then follow the linked task guide for integration. It covers the public SDK declarations, protocol requirements, enum cases, and public decoding/raw-value initializers. Each entry has an explicit anchor. Swift-generated protocol witnesses such as equality and hashing use their standard Swift behavior.

## Choose an API {#choose-api}

- [Configure a client](#appnorthclient) and [load your own UI](/docs/custom-ui).
- [Present community](#appnorthview) or [support chat](#appnorthchatview).
- [Install identity](#appnorthidentitycredential) and [submit private feedback](#appnorthcreatefeedback).
- [Style views](#appnorththeme), [override labels](#appnorthcopy), or [configure paywalls](#appnorthpaywalls).
- [Queue writes](#appnorthofflinequeue), [cache reads](#appnorthreadcache), or [parse links](#appnorthdeeplinks).
- [Configure push](#appnorth) and [route notification taps](/docs/deep-links#push-chat).

Response structs without an explicit public value initializer are read from the client or a Decoder; do not rely on an internal memberwise initializer. Observable models and the paywall controller run on the main actor. Published properties documented with `{ get }` are externally read-only; their `$property` projections expose observation, not permission to mutate.

```swift title="ReadCommunity.swift"
import AppNorth

func firstCommunityPage(client: AppNorthClient) async throws -> AppNorthPage<AppNorthIdea> {
    let boards = try await client.boards()
    return try await client.ideas(board: boards.first?.slug, sort: .top)
}
```

## AppNorthTokenProviding {#appnorthtokenproviding}

Supplies a stable anonymous token. A custom provider must be safe for concurrent access and return a token accepted by the server.

`protocol AppNorthTokenProviding : Sendable`

### `token()` {#appnorthtokenproviding-token}

`func token() -> String`

## AppNorthAnonymousToken {#appnorthanonymoustoken}

Thread-safe anonymous voter token backed by UserDefaults. The default key is `appnorth.voter`; a valid legacy token is migrated. Removing stored data or using another installation can create a different token. This is not verified identity.

`final class AppNorthAnonymousToken`

### `init(defaults:key:)` {#appnorthanonymoustoken-init-defaults-key}

`init(defaults: UserDefaults = .standard, key: String = "appnorth.voter")`

### `token()` {#appnorthanonymoustoken-token}

`func token() -> String`

## AppNorthViewModel {#appnorthviewmodel}

Main-actor observable community state. Optional read caching applies to initial load; recreate the model after account changes to discard previously loaded arrays.

`@MainActor final class AppNorthViewModel`

### `boards` {#appnorthviewmodel-boards}

`@Published @MainActor var boards: [AppNorthBoard] { get }`

### `ideas` {#appnorthviewmodel-ideas}

`@Published @MainActor var ideas: [AppNorthIdea] { get }`

### `isLoading` {#appnorthviewmodel-isloading}

`@Published @MainActor var isLoading: Bool { get }`

### `error` {#appnorthviewmodel-error}

`@Published @MainActor var error: AppNorthError? { get }`

### `showWatermark` {#appnorthviewmodel-showwatermark}

`@Published @MainActor var showWatermark: Bool { get }`

### `selectedBoard` {#appnorthviewmodel-selectedboard}

`@Published @MainActor var selectedBoard: String? { get set }`

### `sort` {#appnorthviewmodel-sort}

`@Published @MainActor var sort: AppNorthIdeaSort { get set }`

### `client` {#appnorthviewmodel-client}

`@MainActor let client: AppNorthClient`

### `init(client:cache:)` {#appnorthviewmodel-init-client-cache}

`@MainActor init(client: AppNorthClient, cache: AppNorthReadCache? = nil)`

### `load()` {#appnorthviewmodel-load}

`@MainActor func load() async`

Loads fresh cached values if available, then boards and the first idea page; loads branding independently.

### `reloadIdeas()` {#appnorthviewmodel-reloadideas}

`@MainActor func reloadIdeas() async`

Reloads the first idea page for the selected board and sort and stores successful results in the optional cache.

## AppNorthView {#appnorthview}

Complete native community screen with its own navigation stack, board/sort controls, idea details, creation, and optional floating chat entry. Loads the first idea page.

`@MainActor struct AppNorthView`

### `init(client:copy:theme:uiConfiguration:cache:paywalls:)` {#appnorthview-init-client-copy-theme-uiconfiguration-cache-paywalls}

`@MainActor init(client: AppNorthClient, copy: AppNorthCopy = .init(), theme: AppNorthTheme = .init(), uiConfiguration: AppNorthUIConfiguration = .init(), cache: AppNorthReadCache? = nil, paywalls: AppNorthPaywalls? = nil)`

### `body` {#appnorthview-body}

`@MainActor var body: some View { get }`

The content and behavior of the view. When you implement a custom view, you must implement a computed `body` property to provide the content for your view. Return a view that's composed of built-in views that SwiftUI provides, plus other composite views that you've already defined: struct MyView: View { var body: some View { Text("Hello, World!") } } For more information about composing views and a view hierarchy, see <doc:Declaring-a-Custom-View>.

## AppNorthScreenshotCapturing {#appnorthscreenshotcapturing}

Host-supplied capture adapter. Return a supported base64 image data URL; the SDK provides no automatic screen capture.

`protocol AppNorthScreenshotCapturing : Sendable`

### `capture()` {#appnorthscreenshotcapturing-capture}

`func capture() async throws -> String`

## AppNorthScreenshotRedacting {#appnorthscreenshotredacting}

Host-supplied redaction adapter. Return a changed, valid image data URL; unchanged output is rejected by the capture helper.

`protocol AppNorthScreenshotRedacting : Sendable`

### `redact(_:)` {#appnorthscreenshotredacting-redact}

`func redact(_ screenshot: String) async throws -> String`

## AppNorthChatEntry {#appnorthchatentry}

A displayed chat row, including local delivery state. Obtain entries from `AppNorthChatModel`; there is no public memberwise initializer.

`struct AppNorthChatEntry`

### `id` {#appnorthchatentry-id}

`var id: String`

The stable identity of the entity associated with this instance.

### `sender` {#appnorthchatentry-sender}

`let sender: AppNorthChatMessage.Sender`

### `body` {#appnorthchatentry-body}

`let body: String`

SwiftUI view content. Present the view itself rather than invoking this property directly.

### `actionCard` {#appnorthchatentry-actioncard}

`var actionCard: AppNorthActionCard?`

### `openedAt` {#appnorthchatentry-openedat}

`var openedAt: String?`

### `tappedAt` {#appnorthchatentry-tappedat}

`var tappedAt: String?`

First review-card tap recorded by the server; this does not confirm a submitted review.

### `state` {#appnorthchatentry-state}

`var state: AppNorthChatEntry.State`

### `images` {#appnorthchatentry-images}

`var images: [AppNorthChatImage]`

Photos shown with the optimistic or delivered bubble. Failed sends retain their
photos for retry.

## AppNorthChatEntry.State {#appnorthchatentry-state-type}

Local delivery state: `sending`, `sent`, or `failed`. Failed entries can be retried with their original key.

`enum State`

### `sent` {#appnorthchatentry-state-sent}

`case sent`

### `sending` {#appnorthchatentry-state-sending}

`case sending`

### `failed` {#appnorthchatentry-state-failed}

`case failed`

## AppNorthChatModel {#appnorthchatmodel}

Main-actor conversation model with optimistic entries and retry handling. A custom owner must stop polling while hidden or inactive and discard the model on account changes. See [support chat](/docs/support-chat).

`@MainActor final class AppNorthChatModel`

### `maximumLength` {#appnorthchatmodel-maximumlength}

`@MainActor static let maximumLength: Int`

### `entries` {#appnorthchatmodel-entries}

`@Published @MainActor var entries: [AppNorthChatEntry] { get }`

### `isAvailable` {#appnorthchatmodel-isavailable}

`@Published @MainActor var isAvailable: Bool { get }`

### `hasLoaded` {#appnorthchatmodel-hasloaded}

`@Published @MainActor var hasLoaded: Bool { get }`

### `lastLoadFailed` {#appnorthchatmodel-lastloadfailed}

`@Published @MainActor var lastLoadFailed: Bool { get }`

### `init(client:paywalls:)` {#appnorthchatmodel-init-client-paywalls}

`@MainActor init(client: AppNorthClient, paywalls: AppNorthPaywalls? = nil)`

### `startPolling()` {#appnorthchatmodel-startpolling}

`@MainActor func startPolling()`

Starts a refresh loop at five-second intervals; cancels any earlier polling task.

### `stopPolling()` {#appnorthchatmodel-stoppolling}

`@MainActor func stopPolling()`

Cancels polling. Call when your custom chat screen hides or becomes inactive.

### `refresh()` {#appnorthchatmodel-refresh}

`@MainActor func refresh() async`

Fetches and merges server messages, updating availability and failure state. Discards results for a changed identity scope.

### `send(_:images:)` {#appnorthchatmodel-send}

`@discardableResult @MainActor func send(_ text: String, images: [AppNorthChatImageUpload] = []) -> Bool`

Trims text and caps it at 2,000 Swift characters. Returns true when a local entry was queued, not when delivery succeeds.

### `retry(_:)` {#appnorthchatmodel-retry}

`@MainActor func retry(_ entry: AppNorthChatEntry)`

Retries a failed entry with its original idempotency key.

## AppNorthChatView {#appnorthchatview}

Native conversation content. Owns a model and visible/foreground polling; your app supplies presentation and dismissal. Accepts copy, theme, and an optional paywall controller.

`@MainActor struct AppNorthChatView`

### `init(client:copy:theme:paywalls:)` {#appnorthchatview-init-client-copy-theme-paywalls}

`@MainActor init(client: AppNorthClient, copy: AppNorthCopy = .init(), theme: AppNorthTheme = .init(), paywalls: AppNorthPaywalls? = nil)`

### `body` {#appnorthchatview-body}

`@MainActor var body: some View { get }`

The content and behavior of the view. When you implement a custom view, you must implement a computed `body` property to provide the content for your view. Return a view that's composed of built-in views that SwiftUI provides, plus other composite views that you've already defined: struct MyView: View { var body: some View { Text("Hello, World!") } } For more information about composing views and a view hierarchy, see <doc:Declaring-a-Custom-View>.

## AppNorthHTTPTransport {#appnorthhttptransport}

Implement this protocol for a custom transport or deterministic tests. `URLSession` already conforms. Return both response bytes and the HTTP response; transport errors become `AppNorthError.network`.

`protocol AppNorthHTTPTransport : Sendable`

### `data(for:)` {#appnorthhttptransport-data-for}

`func data(for request: URLRequest) async throws -> (Data, URLResponse)`

## AppNorthClient {#appnorthclient}

A Sendable HTTP client. Copies share the same in-memory identity store. Both initializers support an anonymous token provider, an optional credential, and an injectable clock. See [custom UI](/docs/custom-ui).

`struct AppNorthClient`

### `configuration` {#appnorthclient-configuration}

`let configuration: AppNorthConfiguration`

### `init(configuration:session:tokenProvider:identityCredential:clock:)` {#appnorthclient-init-configuration-session-tokenprovider-identitycredential-clock}

`init(configuration: AppNorthConfiguration, session: URLSession = .shared, tokenProvider: any AppNorthTokenProviding = AppNorthAnonymousToken(), identityCredential: AppNorthIdentityCredential? = nil, clock: @escaping @Sendable () -> Date = { Date() })`

### `init(configuration:transport:tokenProvider:identityCredential:clock:)` {#appnorthclient-init-configuration-transport-tokenprovider-identitycredential-clock}

`init(configuration: AppNorthConfiguration, transport: any AppNorthHTTPTransport, tokenProvider: any AppNorthTokenProviding = AppNorthAnonymousToken(), identityCredential: AppNorthIdentityCredential? = nil, clock: @escaping @Sendable () -> Date = { Date() })`

### `setIdentityCredential(_:)` {#appnorthclient-setidentitycredential}

`func setIdentityCredential(_ credential: AppNorthIdentityCredential)`

Replaces the in-memory credential for subsequent requests. Does not recreate screens or clear host storage.

### `clearIdentityCredential()` {#appnorthclient-clearidentitycredential}

`func clearIdentityCredential()`

Removes the credential only. Dismiss and discard account-specific views and storage separately.

### `hasValidIdentity(at:)` {#appnorthclient-hasvalididentity-at}

`func hasValidIdentity(at date: Date = Date()) -> Bool`

Checks that a credential exists and has not expired at the supplied date; does not verify its signature.

### `canParticipate(_:at:)` {#appnorthclient-canparticipate-at}

`func canParticipate(_ policy: AppNorthParticipation, at date: Date = Date()) -> Bool`

Evaluates the policy against local credential expiry. The server makes the authorization decision.

### `boards()` {#appnorthclient-boards}

`func boards() async throws -> [AppNorthBoard]`

Returns the project’s boards, statuses, and policies.

### `widgetConfig()` {#appnorthclient-widgetconfig}

`func widgetConfig() async throws -> AppNorthWidgetConfig`

Loads public presentation configuration independently of community data.

### `ideas(board:status:query:sort:cursor:limit:)` {#appnorthclient-ideas-board-status-query-sort-cursor-limit}

`func ideas(board: String? = nil, status: String? = nil, query: String? = nil, sort: AppNorthIdeaSort = .top, cursor: String? = nil, limit: Int = 20) async throws -> AppNorthPage<AppNorthIdea>`

Returns one page. Limit defaults to 20 and is clamped to 1–50. Preserve filters when reusing the opaque cursor.

### `idea(_:)` {#appnorthclient-idea}

`func idea(_ handle: String) async throws -> AppNorthIdea`

Loads one idea by ID or slug.

### `comments(for:cursor:limit:)` {#appnorthclient-comments-for-cursor-limit}

`func comments(for handle: String, cursor: String? = nil, limit: Int = 20) async throws -> AppNorthPage<AppNorthComment>`

Returns a paginated comment list for an idea ID or slug; limit is clamped to 1–50.

### `createIdea(board:title:description:authorName:idempotencyKey:)` {#appnorthclient-createidea-board-title-description-authorname-idempotencykey}

`func createIdea(board: String, title: String, description: String = "", authorName: String? = nil, idempotencyKey: String = UUID().uuidString) async throws -> AppNorthCreation<AppNorthIdea>`

Creates an idea. Retain the same idempotency key and payload on retries; inspect the moderation flag.

### `setVote(on:voted:)` {#appnorthclient-setvote-on-voted}

`func setVote(on handle: String, voted: Bool) async throws -> AppNorthVote`

Sets the desired vote state, returning the resulting state, count, and whether it changed.

### `createComment(on:body:authorName:idempotencyKey:)` {#appnorthclient-createcomment-on-body-authorname-idempotencykey}

`func createComment(on handle: String, body: String, authorName: String? = nil, idempotencyKey: String = UUID().uuidString) async throws -> AppNorthCreation<AppNorthComment>`

Creates a comment. Preserve key and payload on retries; inspect the moderation flag.

### `createFeedback(_:idempotencyKey:)` {#appnorthclient-createfeedback-idempotencykey}

`func createFeedback(_ input: AppNorthCreateFeedback, idempotencyKey: String = UUID().uuidString) async throws -> AppNorthFeedbackReceipt`

Submits private feedback and returns its receipt. Reuse the same key only for the same user intention.

### `feedbackThread(_:)` {#appnorthclient-feedbackthread}

`func feedbackThread(_ id: String) async throws -> AppNorthFeedbackThread`

Requires a signed credential and server-verified ownership. Returns only the report and public staff replies.

### `chatStatus()` {#appnorthclient-chatstatus}

`func chatStatus() async throws -> AppNorthChatStatus`

Reads availability and unread state without marking messages read.

### `chatMessages(after:)` {#appnorthclient-chatmessages-after}

`func chatMessages(after: String? = nil) async throws -> AppNorthChatThread`

Reads messages and marks team replies read. Pass a server message ID as `after` for newer messages.

### `sendChatMessage(_:imageTokens:idempotencyKey:)` {#appnorthclient-sendchatmessage-idempotencykey}

`func sendChatMessage(_ body: String, imageTokens: [String] = [], idempotencyKey: String = UUID().uuidString) async throws -> AppNorthChatMessage`

Sends chat text and up to three uploaded image tokens. Text may be empty when images are attached. Reuse the key and tokens when retrying the same message.

### `registerPushToken(_:environment:)` {#appnorthclient-registerpushtoken-environment}

`func registerPushToken(_ token: Data, environment: AppNorthPushEnvironment) async throws`

Registers the raw APNs device token for the current chat identity. Call after APNs supplies a token and again after an identity change. Pass the APNs environment matching your app's signing configuration. Empty tokens or tokens larger than 256 bytes throw `AppNorthError.invalidConfiguration` before a request is made.

### `unregisterPushToken(_:environment:)` {#appnorthclient-unregisterpushtoken-environment}

`func unregisterPushToken(_ token: Data, environment: AppNorthPushEnvironment) async throws`

Removes the token registration using the current identity and the same environment used to register it. Await this before clearing the signed identity on logout. Token validation matches registration; request failures propagate so your app can retry.

### `reportPaywalls(_:idempotencyKey:)` {#appnorthclient-reportpaywalls-idempotencykey}

`func reportPaywalls(_ snapshot: AppNorthPaywallSnapshot, idempotencyKey: String = UUID().uuidString) async throws`

Replaces the reporter’s device snapshot. Reuse the same key for a retry.

### `markActionCardOpened(_:)` {#appnorthclient-markactioncardopened}

`func markActionCardOpened(_ messageId: String) async throws`

Reports first display for review cards and first tap for offer cards with a deterministic `card-open-<messageId>` key. Does not report a purchase or a submitted review.

### `markActionCardTapped(_:)` {#appnorthclient-markactioncardtapped}

`func markActionCardTapped(_ messageId: String) async throws`

Reports a review-card tap using the deterministic `card-tap-<messageId>` key. It does not invoke StoreKit or confirm that its prompt appeared.

### `uploadChatImage(_:contentType:)` {#appnorthclient-uploadchatimage-contenttype}

`func uploadChatImage(_ data: Data, contentType: String = "image/jpeg") async throws -> AppNorthChatImageUpload`

Uploads up to 5 MB of JPEG, PNG or WebP through a signed storage PUT and completes
server-side pixel validation and metadata removal. Custom UIs prepare their own
resized pixels; the built-in iOS picker handles this automatically. Pass the
returned `imageToken` in `sendChatMessage(_:imageTokens:idempotencyKey:)`.
At most three image tokens may accompany a message; its text may be empty.
Keep the same tokens and idempotency key on retry. See [Photos in chat](/docs/support-chat#photos-in-chat).

## AppNorthPushEnvironment {#appnorthpushenvironment}

`enum AppNorthPushEnvironment: String, Codable, Sendable`

Choose the APNs environment matching your app's signing configuration; the SDK does not infer it.

### `sandbox` {#appnorthpushenvironment-sandbox}

`case sandbox`

### `production` {#appnorthpushenvironment-production}

`case production`

## AppNorth {#appnorth}

`@MainActor enum AppNorth`

Optional push facade for one configured chat client at a time. Your app owns notification permission prompts, APNs registration, its notification-center delegate, and navigation. The SDK does not swizzle callbacks. See the [push chat example](/docs/deep-links#push-chat).

### `configurePush(client:environment:badgeHandler:routeHandler:)` {#appnorth-configurepush-client-environment-badgehandler-routehandler}

```swift
@MainActor static func configurePush(
    client: AppNorthClient,
    environment: AppNorthPushEnvironment,
    badgeHandler: @escaping @MainActor (Int) -> Void,
    routeHandler: @escaping @MainActor (AppNorthLink) -> Void
)
```

Stores the same client used by chat, the APNs environment, and your callbacks. `badgeHandler` receives 0 or 1 to indicate unread chat replies, not a message count; combine it with any other badge sources in your app. `routeHandler` receives accepted chat destinations and must present your chat UI.

### `registerPushToken(_:)` {#appnorth-registerpushtoken}

`@MainActor static func registerPushToken(_ token: Data) async throws`

Forwards the raw token to the configured client's `registerPushToken(_:environment:)`. Throws `AppNorthError.invalidConfiguration` if `configurePush` has not been called. Call again after changing the chat identity.

### `unregisterPushToken(_:)` {#appnorth-unregisterpushtoken}

`@MainActor static func unregisterPushToken(_ token: Data) async throws`

Forwards removal to the configured client and environment. Await before logout or disabling push; failures propagate for retry. Throws `AppNorthError.invalidConfiguration` without a configured client.

### `handleNotificationTap(_:)` {#appnorth-handlenotificationtap}

`@discardableResult @MainActor static func handleNotificationTap(_ userInfo: [AnyHashable: Any]) -> Bool`

Pass the notification response's `userInfo`. Returns true and invokes `routeHandler` only when the `appnorth` payload has `type: "chat"`, an `appId` equal to the configured client's `configuration.appId`, and a parseable chat URL using the `appnorth` scheme. Unconfigured, foreign-app, and malformed payloads return false without routing. It does not open a screen or authenticate the user.

### `syncUnreadBadge()` {#appnorth-syncunreadbadge}

`@discardableResult @MainActor static func syncUnreadBadge() async throws -> Int`

Fetches `chatStatus()` and returns 1 only when chat is available and has unread replies; otherwise returns 0. Calls `badgeHandler` only if the identity scope and push configuration are still current. Request failures preserve the existing badge and propagate. Throws `AppNorthError.invalidConfiguration` without a configured client. Call on foreground and after reading with a custom UI; built-in chat also calls it after a successful read.

### `resetPush()` {#appnorth-resetpush}

`@MainActor static func resetPush()`

Calls the current badge handler with 0 and releases the client and callbacks. This only resets local state: await `unregisterPushToken(_:)` before resetting and clearing identity to remove the server registration.

## AppNorthColorPair {#appnorthcolorpair}

A light/dark SwiftUI color pair, or one color shared by both appearances.

`struct AppNorthColorPair`

### `light` {#appnorthcolorpair-light}

`var light: Color`

### `dark` {#appnorthcolorpair-dark}

`var dark: Color`

### `init(light:dark:)` {#appnorthcolorpair-init-light-dark}

`init(light: Color, dark: Color)`

### `init(_:)` {#appnorthcolorpair-init}

`init(_ color: Color)`

Uses one color in both appearances.

### `color(in:)` {#appnorthcolorpair-color-in}

`func color(in appearance: ColorScheme) -> Color`

Selects the dark color for dark appearance and the light color otherwise.

## AppNorthConfiguration {#appnorthconfiguration}

Validated server URL, public App ID, and allowed write origin. Use HTTPS in production and pass the server base URL without `/api/v1`. See [installation](/docs/install#configure).

`struct AppNorthConfiguration`

### `apiURL` {#appnorthconfiguration-apiurl}

`let apiURL: URL`

### `appId` {#appnorthconfiguration-appid}

`let appId: String`

### `origin` {#appnorthconfiguration-origin}

`let origin: URL`

### `init(apiURL:appId:origin:)` {#appnorthconfiguration-init-apiurl-appid-origin}

`init(apiURL: URL, appId: String, origin: URL) throws`

## AppNorthError {#appnortherror}

Typed client failures. Server errors preserve the HTTP status, machine code, and optional field errors; see [error handling](/docs/custom-ui#errors).

`enum AppNorthError`

### `invalidConfiguration(_:)` {#appnortherror-invalidconfiguration}

`case invalidConfiguration(String)`

### `invalidIdentityCredential(_:)` {#appnortherror-invalididentitycredential}

`case invalidIdentityCredential(String)`

### `invalidResponse` {#appnortherror-invalidresponse}

`case invalidResponse`

### `network(_:)` {#appnortherror-network}

`case network(String)`

### `server(status:code:fields:)` {#appnortherror-server-status-code-fields}

`case server(status: Int, code: String, fields: [String : [String]]?)`

### `decoding(_:)` {#appnortherror-decoding}

`case decoding(String)`

### `errorDescription` {#appnortherror-errordescription}

`var errorDescription: String? { get }`

A localized message describing what error occurred.

## AppNorthCopy {#appnorthcopy}

Mutable SDK labels. The default follows the host app language; unsupported languages fall back to English. Each string property below overrides the corresponding label. See [language](/docs/language).

`struct AppNorthCopy`

### `feedback` {#appnorthcopy-feedback}

`var feedback: String`

Override this label in the copy value passed to your screen.

### `board` {#appnorthcopy-board}

`var board: String`

Override this label in the copy value passed to your screen.

### `allBoards` {#appnorthcopy-allboards}

`var allBoards: String`

Override this label in the copy value passed to your screen.

### `noDescription` {#appnorthcopy-nodescription}

`var noDescription: String`

Override this label in the copy value passed to your screen.

### `comments` {#appnorthcopy-comments}

`var comments: String`

Override this label in the copy value passed to your screen.

### `writeComment` {#appnorthcopy-writecomment}

`var writeComment: String`

Override this label in the copy value passed to your screen.

### `send` {#appnorthcopy-send}

`var send: String`

Override this label in the copy value passed to your screen.

### `signInToComment` {#appnorthcopy-signintocomment}

`var signInToComment: String`

Override this label in the copy value passed to your screen.

### `commentsDisabled` {#appnorthcopy-commentsdisabled}

`var commentsDisabled: String`

Override this label in the copy value passed to your screen.

### `newIdea` {#appnorthcopy-newidea}

`var newIdea: String`

Override this label in the copy value passed to your screen.

### `title` {#appnorthcopy-title}

`var title: String`

Override this label in the copy value passed to your screen.

### `description` {#appnorthcopy-description}

`var description: String`

Override this label in the copy value passed to your screen.

### `yourName` {#appnorthcopy-yourname}

`var yourName: String`

Override this label in the copy value passed to your screen.

### `cancel` {#appnorthcopy-cancel}

`var cancel: String`

Override this label in the copy value passed to your screen.

### `signInToSubmit` {#appnorthcopy-signintosubmit}

`var signInToSubmit: String`

Override this label in the copy value passed to your screen.

### `submissionDisabled` {#appnorthcopy-submissiondisabled}

`var submissionDisabled: String`

Override this label in the copy value passed to your screen.

### `anonymous` {#appnorthcopy-anonymous}

`var anonymous: String`

Override this label in the copy value passed to your screen.

### `team` {#appnorthcopy-team}

`var team: String`

Override this label in the copy value passed to your screen.

### `vote` {#appnorthcopy-vote}

`var vote: String`

Override this label in the copy value passed to your screen.

### `removeVote` {#appnorthcopy-removevote}

`var removeVote: String`

Override this label in the copy value passed to your screen.

### `error` {#appnorthcopy-error}

`var error: String`

Override this label in the copy value passed to your screen.

### `open` {#appnorthcopy-open}

`var open: String`

Override this label in the copy value passed to your screen.

### `closed` {#appnorthcopy-closed}

`var closed: String`

Override this label in the copy value passed to your screen.

### `pending` {#appnorthcopy-pending}

`var pending: String`

Override this label in the copy value passed to your screen.

### `poweredBy` {#appnorthcopy-poweredby}

`var poweredBy: String`

Override this label in the copy value passed to your screen.

### `somethingWentWrong` {#appnorthcopy-somethingwentwrong}

`var somethingWentWrong: String`

Override this label in the copy value passed to your screen.

### `seeTranslation` {#appnorthcopy-seetranslation}

`var seeTranslation: String`

Override this label in the copy value passed to your screen.

### `seeOriginal` {#appnorthcopy-seeoriginal}

`var seeOriginal: String`

Override this label in the copy value passed to your screen.

### `noIdeas` {#appnorthcopy-noideas}

`var noIdeas: String`

Override this label in the copy value passed to your screen.

### `filter` {#appnorthcopy-filter}

`var filter: String`

Override this label in the copy value passed to your screen.

### `activateToSwitchFilter` {#appnorthcopy-activatetoswitchfilter}

`var activateToSwitchFilter: String`

Override this label in the copy value passed to your screen.

### `charactersRemaining` {#appnorthcopy-charactersremaining}

`var charactersRemaining: String`

Override this label in the copy value passed to your screen.

### `chat` {#appnorthcopy-chat}

`var chat: String`

Override this label in the copy value passed to your screen.

### `writeMessage` {#appnorthcopy-writemessage}

`var writeMessage: String`

Override this label in the copy value passed to your screen.

### `chatEmpty` {#appnorthcopy-chatempty}

`var chatEmpty: String`

Override this label in the copy value passed to your screen.

### `sendFailed` {#appnorthcopy-sendfailed}

`var sendFailed: String`

Override this label in the copy value passed to your screen.

### `done` {#appnorthcopy-done}

`var done: String`

Override this label in the copy value passed to your screen.

### `init()` {#appnorthcopy-init}

`init()`

Texts in the language the host app is currently displayed in.

### `init(language:)` {#appnorthcopy-init-language}

`init(language: String)`

Texts in a specific language, e.g. `"de"` or `"pt-BR"`. Unsupported languages fall back to English.

### `spanish` {#appnorthcopy-spanish}

`static var spanish: AppNorthCopy { get }`

### Photo control copy

`var attachPhotos: String` — photo picker button.

`var removePhoto: String` — remove a selected photo.

`var attachedPhoto: String` — accessible image description.

`var photoLimits: String` — count and size guidance.

All four properties have bundled translations and can be overridden.

## AppNorthCreateIdeaView {#appnorthcreateideaview}

Native creation form with board selection and policy-aware submission. The completion callback lets your host reload after creation.

`@MainActor struct AppNorthCreateIdeaView`

### `init(client:boards:copy:theme:onCreated:)` {#appnorthcreateideaview-init-client-boards-copy-theme-oncreated}

`@MainActor init(client: AppNorthClient, boards: [AppNorthBoard], copy: AppNorthCopy = .init(), theme: AppNorthTheme = .init(), onCreated: @escaping () -> Void = {})`

### `body` {#appnorthcreateideaview-body}

`@MainActor var body: some View { get }`

The content and behavior of the view. When you implement a custom view, you must implement a computed `body` property to provide the content for your view. Return a view that's composed of built-in views that SwiftUI provides, plus other composite views that you've already defined: struct MyView: View { var body: some View { Text("Hello, World!") } } For more information about composing views and a view hierarchy, see <doc:Declaring-a-Custom-View>.

## AppNorthLinkKind {#appnorthlinkkind}

Supported custom-link destinations: idea, private feedback, or the current chat.

`enum AppNorthLinkKind`

### `idea` {#appnorthlinkkind-idea}

`case idea`

### `feedback` {#appnorthlinkkind-feedback}

`case feedback`

### `chat` {#appnorthlinkkind-chat}

`case chat`

The current identity's chat; the parsed link's `value` is `"current"`.

### `init(rawValue:)` {#appnorthlinkkind-init-rawvalue}

`init?(rawValue: String)`

Creates a new instance with the specified raw value. If there is no value of the type that corresponds with the specified raw value, this initializer returns `nil`. For example: enum PaperSize: String { case A4, A5, Letter, Legal } print(PaperSize(rawValue: "Legal")) // Prints "Optional(PaperSize.Legal)" print(PaperSize(rawValue: "Tabloid")) // Prints "nil" - Parameter rawValue: The raw value to use for the new instance.

## AppNorthLink {#appnorthlink}

Parsed destination kind and decoded identifier. The link carries no authorization.

`struct AppNorthLink`

### `kind` {#appnorthlink-kind}

`let kind: AppNorthLinkKind`

### `value` {#appnorthlink-value}

`let value: String`

## AppNorthDeepLinks {#appnorthdeeplinks}

Custom-scheme URL builders and parser. Parsing does not validate the application scheme; the host must check it. See [deep links](/docs/deep-links).

`enum AppNorthDeepLinks`

### `idea(scheme:handle:)` {#appnorthdeeplinks-idea-scheme-handle}

`static func idea(scheme: String, handle: String) throws -> URL`

Creates a custom-scheme idea URL and percent-encodes its handle as a single path component.

### `feedback(scheme:id:)` {#appnorthdeeplinks-feedback-scheme-id}

`static func feedback(scheme: String, id: String) throws -> URL`

Creates a custom-scheme private-report URL; it conveys no identity or access rights.

### `chat(scheme:)` {#appnorthdeeplinks-chat-scheme}

`static func chat(scheme: String) throws -> URL`

Creates `<scheme>://appnorth/chat/current`. Invalid schemes throw `AppNorthError.invalidConfiguration`. The destination contains no user or conversation ID; your app presents chat with the intended client's current identity.

### `parse(_:)` {#appnorthdeeplinks-parse}

`static func parse(_ url: URL) -> AppNorthLink?`

Parses the host/path only. Reject unrecognized schemes in your app before calling this method.

## AppNorthIdeaDetailView {#appnorthideadetailview}

Native idea detail and comments. Pass the actual board comment policy; the initializer defaults to `anyone`, while the server still enforces its policy.

`@MainActor struct AppNorthIdeaDetailView`

### `init(client:idea:copy:theme:uiConfiguration:commentPolicy:)` {#appnorthideadetailview-init-client-idea-copy-theme-uiconfiguration-commentpolicy}

`@MainActor init(client: AppNorthClient, idea: AppNorthIdea, copy: AppNorthCopy = .init(), theme: AppNorthTheme = .init(), uiConfiguration: AppNorthUIConfiguration = .init(), commentPolicy: AppNorthParticipation = .anyone)`

### `body` {#appnorthideadetailview-body}

`@MainActor var body: some View { get }`

The content and behavior of the view. When you implement a custom view, you must implement a computed `body` property to provide the content for your view. Return a view that's composed of built-in views that SwiftUI provides, plus other composite views that you've already defined: struct MyView: View { var body: some View { Text("Hello, World!") } } For more information about composing views and a view hierarchy, see <doc:Declaring-a-Custom-View>.

## AppNorthInputLimits {#appnorthinputlimits}

Client-side limits for idea titles (100), descriptions (5,000), and comments (2,000). Server validation remains authoritative.

`enum AppNorthInputLimits`

### `ideaTitle` {#appnorthinputlimits-ideatitle}

`static let ideaTitle: Int`

### `ideaDescription` {#appnorthinputlimits-ideadescription}

`static let ideaDescription: Int`

### `comment` {#appnorthinputlimits-comment}

`static let comment: Int`

## AppNorthParticipation {#appnorthparticipation}

Per-action board policy. Local permission checks guide UI; server enforcement remains authoritative.

`enum AppNorthParticipation`

### `anyone` {#appnorthparticipation-anyone}

`case anyone`

### `identified` {#appnorthparticipation-identified}

`case identified`

### `disabled` {#appnorthparticipation-disabled}

`case disabled`

### `allowsParticipation(hasValidIdentity:)` {#appnorthparticipation-allowsparticipation-hasvalididentity}

`func allowsParticipation(hasValidIdentity: Bool) -> Bool`

Anyone returns true; identified returns the supplied identity flag; disabled returns false.

### `init(rawValue:)` {#appnorthparticipation-init-rawvalue}

`init?(rawValue: String)`

Creates a new instance with the specified raw value. If there is no value of the type that corresponds with the specified raw value, this initializer returns `nil`. For example: enum PaperSize: String { case A4, A5, Letter, Legal } print(PaperSize(rawValue: "Legal")) // Prints "Optional(PaperSize.Legal)" print(PaperSize(rawValue: "Tabloid")) // Prints "nil" - Parameter rawValue: The raw value to use for the new instance.

## AppNorthBoardStatus {#appnorthboardstatus}

A board status definition. `isFinal` drives open/closed filtering; `color` is a server-provided hexadecimal color.

`struct AppNorthBoardStatus`

### `id` {#appnorthboardstatus-id}

`let id: String`

The stable identity of the entity associated with this instance.

### `slug` {#appnorthboardstatus-slug}

`let slug: String`

### `label` {#appnorthboardstatus-label}

`let label: String`

### `color` {#appnorthboardstatus-color}

`let color: String`

### `position` {#appnorthboardstatus-position}

`let position: Int`

### `isDefault` {#appnorthboardstatus-isdefault}

`let isDefault: Bool`

### `isFinal` {#appnorthboardstatus-isfinal}

`let isFinal: Bool`

### `init(from:)` {#appnorthboardstatus-init-from}

`init(from decoder: any Decoder) throws`

Creates a new instance by decoding from the given decoder. This initializer throws an error if reading from the decoder fails, or if the data read is corrupted or otherwise invalid. - Parameter decoder: The decoder to read data from.

## AppNorthBoardPolicies {#appnorthboardpolicies}

Independent policies for creating ideas, voting, and commenting.

`struct AppNorthBoardPolicies`

### `submission` {#appnorthboardpolicies-submission}

`let submission: AppNorthParticipation`

### `vote` {#appnorthboardpolicies-vote}

`let vote: AppNorthParticipation`

### `comment` {#appnorthboardpolicies-comment}

`let comment: AppNorthParticipation`

### `init(from:)` {#appnorthboardpolicies-init-from}

`init(from decoder: any Decoder) throws`

Creates a new instance by decoding from the given decoder. This initializer throws an error if reading from the decoder fails, or if the data read is corrupted or otherwise invalid. - Parameter decoder: The decoder to read data from.

## AppNorthBoard {#appnorthboard}

A server-provided community board, including independent submission/vote/comment policies and status definitions.

`struct AppNorthBoard`

### `id` {#appnorthboard-id}

`let id: String`

The stable identity of the entity associated with this instance.

### `slug` {#appnorthboard-slug}

`let slug: String`

### `name` {#appnorthboard-name}

`let name: String`

### `description` {#appnorthboard-description}

`let description: String?`

### `position` {#appnorthboard-position}

`let position: Int`

### `moderation` {#appnorthboard-moderation}

`let moderation: String`

### `policies` {#appnorthboard-policies}

`let policies: AppNorthBoardPolicies`

### `statuses` {#appnorthboard-statuses}

`let statuses: [AppNorthBoardStatus]`

### `init(from:)` {#appnorthboard-init-from}

`init(from decoder: any Decoder) throws`

Creates a new instance by decoding from the given decoder. This initializer throws an error if reading from the decoder fails, or if the data read is corrupted or otherwise invalid. - Parameter decoder: The decoder to read data from.

## AppNorthIdeaStatus {#appnorthideastatus}

The status attached to an idea: slug, display label, and hexadecimal color.

`struct AppNorthIdeaStatus`

### `slug` {#appnorthideastatus-slug}

`let slug: String`

### `label` {#appnorthideastatus-label}

`let label: String`

### `color` {#appnorthideastatus-color}

`let color: String`

### `init(from:)` {#appnorthideastatus-init-from}

`init(from decoder: any Decoder) throws`

Creates a new instance by decoding from the given decoder. This initializer throws an error if reading from the decoder fails, or if the data read is corrupted or otherwise invalid. - Parameter decoder: The decoder to read data from.

## AppNorthIdea {#appnorthidea}

A community idea returned by the server. Use its ID or slug for detail and write operations. Counts and vote state are server-derived.

`struct AppNorthIdea`

### `id` {#appnorthidea-id}

`let id: String`

The stable identity of the entity associated with this instance.

### `slug` {#appnorthidea-slug}

`let slug: String`

### `boardSlug` {#appnorthidea-boardslug}

`let boardSlug: String`

### `title` {#appnorthidea-title}

`let title: String`

### `description` {#appnorthidea-description}

`let description: String`

### `authorName` {#appnorthidea-authorname}

`let authorName: String?`

### `status` {#appnorthidea-status}

`let status: AppNorthIdeaStatus?`

### `voteCount` {#appnorthidea-votecount}

`let voteCount: Int`

### `commentCount` {#appnorthidea-commentcount}

`let commentCount: Int`

### `hasVoted` {#appnorthidea-hasvoted}

`let hasVoted: Bool`

### `pinned` {#appnorthidea-pinned}

`let pinned: Bool`

### `moderation` {#appnorthidea-moderation}

`let moderation: String?`

### `createdAt` {#appnorthidea-createdat}

`let createdAt: String`

### `lastActivityAt` {#appnorthidea-lastactivityat}

`let lastActivityAt: String`

### `init(from:)` {#appnorthidea-init-from}

`init(from decoder: any Decoder) throws`

Creates a new instance by decoding from the given decoder. This initializer throws an error if reading from the decoder fails, or if the data read is corrupted or otherwise invalid. - Parameter decoder: The decoder to read data from.

## AppNorthWidgetConfig {#appnorthwidgetconfig}

Public presentation configuration. Failure to load this resource need not block community reads.

`struct AppNorthWidgetConfig`

### `branding` {#appnorthwidgetconfig-branding}

`let branding: AppNorthWidgetConfig.Branding`

### `features` {#appnorthwidgetconfig-features}

`let features: AppNorthWidgetConfig.Features?`

### `languages` {#appnorthwidgetconfig-languages}

`let languages: [String]?`

### `init(from:)` {#appnorthwidgetconfig-init-from}

`init(from decoder: any Decoder) throws`

Creates a new instance by decoding from the given decoder. This initializer throws an error if reading from the decoder fails, or if the data read is corrupted or otherwise invalid. - Parameter decoder: The decoder to read data from.

## AppNorthWidgetConfig.Branding {#appnorthwidgetconfig-branding-type}

Server-controlled branding flags. `watermark` controls the built-in attribution.

`struct Branding`

### `watermark` {#appnorthwidgetconfig-branding-watermark}

`let watermark: Bool`

### `init(from:)` {#appnorthwidgetconfig-branding-init-from}

`init(from decoder: any Decoder) throws`

Creates a new instance by decoding from the given decoder. This initializer throws an error if reading from the decoder fails, or if the data read is corrupted or otherwise invalid. - Parameter decoder: The decoder to read data from.

## AppNorthWidgetConfig.Features {#appnorthwidgetconfig-features-type}

Optional feature configuration. Chat availability also depends on the current identity.

`struct Features`

### `chat` {#appnorthwidgetconfig-features-chat}

`let chat: AppNorthChatMode?`

### `init(from:)` {#appnorthwidgetconfig-features-init-from}

`init(from decoder: any Decoder) throws`

Creates a new instance by decoding from the given decoder. This initializer throws an error if reading from the decoder fails, or if the data read is corrupted or otherwise invalid. - Parameter decoder: The decoder to read data from.

## AppNorthChatMode {#appnorthchatmode}

Chat participation mode. Unknown decoded values fall back to `disabled`.

`enum AppNorthChatMode`

### `disabled` {#appnorthchatmode-disabled}

`case disabled`

### `identified` {#appnorthchatmode-identified}

`case identified`

### `anyone` {#appnorthchatmode-anyone}

`case anyone`

### `init(from:)` {#appnorthchatmode-init-from}

`init(from decoder: any Decoder) throws`

Creates a new instance by decoding from the given decoder. This initializer throws an error if reading from the decoder fails, or if the data read is corrupted or otherwise invalid. - Parameter decoder: The decoder to read data from.

### `init(rawValue:)` {#appnorthchatmode-init-rawvalue}

`init?(rawValue: String)`

Creates a new instance with the specified raw value. If there is no value of the type that corresponds with the specified raw value, this initializer returns `nil`. For example: enum PaperSize: String { case A4, A5, Letter, Legal } print(PaperSize(rawValue: "Legal")) // Prints "Optional(PaperSize.Legal)" print(PaperSize(rawValue: "Tabloid")) // Prints "nil" - Parameter rawValue: The raw value to use for the new instance.

## AppNorthChatStatus {#appnorthchatstatus}

Chat availability and unread state without reading the conversation.

`struct AppNorthChatStatus`

### `available` {#appnorthchatstatus-available}

`let available: Bool`

### `hasUnread` {#appnorthchatstatus-hasunread}

`let hasUnread: Bool`

### `init(from:)` {#appnorthchatstatus-init-from}

`init(from decoder: any Decoder) throws`

Creates a new instance by decoding from the given decoder. This initializer throws an error if reading from the decoder fails, or if the data read is corrupted or otherwise invalid. - Parameter decoder: The decoder to read data from.

## AppNorthChatMessage {#appnorthchatmessage}

A server message, optionally carrying an action card and its first-open timestamp.

`struct AppNorthChatMessage`

### `kind` {#appnorthchatmessage-kind}

`var kind: String?`

### `actionCard` {#appnorthchatmessage-actioncard}

`var actionCard: AppNorthActionCard?`

### `openedAt` {#appnorthchatmessage-openedat}

`var openedAt: String?`

### `tappedAt` {#appnorthchatmessage-tappedat}

`var tappedAt: String?`

First review-card tap recorded by the server; this does not confirm a submitted review.

### `id` {#appnorthchatmessage-id}

`let id: String`

The stable identity of the entity associated with this instance.

### `sender` {#appnorthchatmessage-sender}

`let sender: AppNorthChatMessage.Sender`

### `body` {#appnorthchatmessage-body}

`let body: String`

SwiftUI view content. Present the view itself rather than invoking this property directly.

### `createdAt` {#appnorthchatmessage-createdat}

`let createdAt: String`

### `init(from:)` {#appnorthchatmessage-init-from}

`init(from decoder: any Decoder) throws`

Creates a new instance by decoding from the given decoder. This initializer throws an error if reading from the decoder fails, or if the data read is corrupted or otherwise invalid. - Parameter decoder: The decoder to read data from.

### `images` {#appnorthchatmessage-images}

`var images: [AppNorthChatImage]?`

Optional for backward compatibility with messages from older servers. URLs are
private and expire; reload the thread to refresh them.

## AppNorthChatMessage.Sender {#appnorthchatmessage-sender-type}

Conversation side. Unknown sender values decode as `team` for forward compatibility.

`enum Sender`

### `user` {#appnorthchatmessage-sender-user}

`case user`

### `team` {#appnorthchatmessage-sender-team}

`case team`

### `init(from:)` {#appnorthchatmessage-sender-init-from}

`init(from decoder: any Decoder) throws`

Future sender kinds render as the team side instead of failing.

### `init(rawValue:)` {#appnorthchatmessage-sender-init-rawvalue}

`init?(rawValue: String)`

Creates a new instance with the specified raw value. If there is no value of the type that corresponds with the specified raw value, this initializer returns `nil`. For example: enum PaperSize: String { case A4, A5, Letter, Legal } print(PaperSize(rawValue: "Legal")) // Prints "Optional(PaperSize.Legal)" print(PaperSize(rawValue: "Tabloid")) // Prints "nil" - Parameter rawValue: The raw value to use for the new instance.

## AppNorthChatThread {#appnorthchatthread}

Conversation availability and the returned messages. A request with an `after` cursor can contain only newer messages.

`struct AppNorthChatThread`

### `available` {#appnorthchatthread-available}

`let available: Bool`

### `messages` {#appnorthchatthread-messages}

`let messages: [AppNorthChatMessage]`

### `init(from:)` {#appnorthchatthread-init-from}

`init(from decoder: any Decoder) throws`

Creates a new instance by decoding from the given decoder. This initializer throws an error if reading from the decoder fails, or if the data read is corrupted or otherwise invalid. - Parameter decoder: The decoder to read data from.

## AppNorthComment {#appnorthcomment}

A community comment. `official` indicates an official reply when provided by the server.

`struct AppNorthComment`

### `id` {#appnorthcomment-id}

`let id: String`

The stable identity of the entity associated with this instance.

### `authorName` {#appnorthcomment-authorname}

`let authorName: String?`

### `body` {#appnorthcomment-body}

`let body: String`

SwiftUI view content. Present the view itself rather than invoking this property directly.

### `createdAt` {#appnorthcomment-createdat}

`let createdAt: String`

### `official` {#appnorthcomment-official}

`let official: Bool?`

### `init(from:)` {#appnorthcomment-init-from}

`init(from decoder: any Decoder) throws`

Creates a new instance by decoding from the given decoder. This initializer throws an error if reading from the decoder fails, or if the data read is corrupted or otherwise invalid. - Parameter decoder: The decoder to read data from.

## AppNorthIdeaSort {#appnorthideasort}

Server sorting choices: most voted (`top`), newest (`recent`), or most discussed (`discussed`).

`enum AppNorthIdeaSort`

### `top` {#appnorthideasort-top}

`case top`

### `recent` {#appnorthideasort-recent}

`case recent`

### `discussed` {#appnorthideasort-discussed}

`case discussed`

### `init(rawValue:)` {#appnorthideasort-init-rawvalue}

`init?(rawValue: String)`

Creates a new instance with the specified raw value. If there is no value of the type that corresponds with the specified raw value, this initializer returns `nil`. For example: enum PaperSize: String { case A4, A5, Letter, Legal } print(PaperSize(rawValue: "Legal")) // Prints "Optional(PaperSize.Legal)" print(PaperSize(rawValue: "Tabloid")) // Prints "nil" - Parameter rawValue: The raw value to use for the new instance.

## AppNorthPage {#appnorthpage}

One page of typed items and an opaque optional next cursor. A nil cursor ends pagination.

`struct AppNorthPage<Element> where Element : Sendable`

### `items` {#appnorthpage-items}

`let items: [Element]`

### `nextCursor` {#appnorthpage-nextcursor}

`let nextCursor: String?`

## AppNorthVote {#appnorthvote}

The resulting vote state and count. `applied` distinguishes a state change from a repeated request.

`struct AppNorthVote`

### `voted` {#appnorthvote-voted}

`let voted: Bool`

### `voteCount` {#appnorthvote-votecount}

`let voteCount: Int`

### `applied` {#appnorthvote-applied}

`let applied: Bool`

### `init(from:)` {#appnorthvote-init-from}

`init(from decoder: any Decoder) throws`

Creates a new instance by decoding from the given decoder. This initializer throws an error if reading from the decoder fails, or if the data read is corrupted or otherwise invalid. - Parameter decoder: The decoder to read data from.

## AppNorthCreation {#appnorthcreation}

A created value plus `moderated`, which indicates that publication may await approval.

`struct AppNorthCreation<Element> where Element : Sendable`

### `value` {#appnorthcreation-value}

`let value: Element`

### `moderated` {#appnorthcreation-moderated}

`let moderated: Bool`

## AppNorthFeedbackType {#appnorthfeedbacktype}

Private report category: problem, suggestion, or comment.

`enum AppNorthFeedbackType`

### `problem` {#appnorthfeedbacktype-problem}

`case problem`

### `suggestion` {#appnorthfeedbacktype-suggestion}

`case suggestion`

### `comment` {#appnorthfeedbacktype-comment}

`case comment`

### `init(rawValue:)` {#appnorthfeedbacktype-init-rawvalue}

`init?(rawValue: String)`

Creates a new instance with the specified raw value. If there is no value of the type that corresponds with the specified raw value, this initializer returns `nil`. For example: enum PaperSize: String { case A4, A5, Letter, Legal } print(PaperSize(rawValue: "Legal")) // Prints "Optional(PaperSize.Legal)" print(PaperSize(rawValue: "Tabloid")) // Prints "nil" - Parameter rawValue: The raw value to use for the new instance.

## AppNorthViewport {#appnorthviewport}

Positive width and height for the reported screen. The server accepts dimensions up to 20,000.

`struct AppNorthViewport`

### `width` {#appnorthviewport-width}

`let width: Int`

### `height` {#appnorthviewport-height}

`let height: Int`

### `init(width:height:)` {#appnorthviewport-init-width-height}

`init(width: Int, height: Int)`

### `init(from:)` {#appnorthviewport-init-from}

`init(from decoder: any Decoder) throws`

Creates a new instance by decoding from the given decoder. This initializer throws an error if reading from the decoder fails, or if the data read is corrupted or otherwise invalid. - Parameter decoder: The decoder to read data from.

## AppNorthJSONValue {#appnorthjsonvalue}

Codable JSON values for diagnostic metadata. Sanitize content before sending it.

`indirect enum AppNorthJSONValue`

### `string(_:)` {#appnorthjsonvalue-string}

`case string(String)`

### `number(_:)` {#appnorthjsonvalue-number}

`case number(Double)`

### `bool(_:)` {#appnorthjsonvalue-bool}

`case bool(Bool)`

### `object(_:)` {#appnorthjsonvalue-object}

`case object([String : AppNorthJSONValue])`

### `array(_:)` {#appnorthjsonvalue-array}

`case array([AppNorthJSONValue])`

### `null` {#appnorthjsonvalue-null}

`case null`

### `init(from:)` {#appnorthjsonvalue-init-from}

`init(from decoder: any Decoder) throws`

Creates a new instance by decoding from the given decoder. This initializer throws an error if reading from the decoder fails, or if the data read is corrupted or otherwise invalid. - Parameter decoder: The decoder to read data from.

### `encode(to:)` {#appnorthjsonvalue-encode-to}

`func encode(to encoder: any Encoder) throws`

Encodes the value using its wire representation.

## AppNorthDiagnosticCategory {#appnorthdiagnosticcategory}

Diagnostic kind: console, error, network, or breadcrumb.

`enum AppNorthDiagnosticCategory`

### `console` {#appnorthdiagnosticcategory-console}

`case console`

### `error` {#appnorthdiagnosticcategory-error}

`case error`

### `network` {#appnorthdiagnosticcategory-network}

`case network`

### `breadcrumb` {#appnorthdiagnosticcategory-breadcrumb}

`case breadcrumb`

### `init(rawValue:)` {#appnorthdiagnosticcategory-init-rawvalue}

`init?(rawValue: String)`

Creates a new instance with the specified raw value. If there is no value of the type that corresponds with the specified raw value, this initializer returns `nil`. For example: enum PaperSize: String { case A4, A5, Letter, Legal } print(PaperSize(rawValue: "Legal")) // Prints "Optional(PaperSize.Legal)" print(PaperSize(rawValue: "Tabloid")) // Prints "nil" - Parameter rawValue: The raw value to use for the new instance.

## AppNorthDiagnosticLevel {#appnorthdiagnosticlevel}

Diagnostic severity: debug, info, warn, or error.

`enum AppNorthDiagnosticLevel`

### `debug` {#appnorthdiagnosticlevel-debug}

`case debug`

### `info` {#appnorthdiagnosticlevel-info}

`case info`

### `warn` {#appnorthdiagnosticlevel-warn}

`case warn`

### `error` {#appnorthdiagnosticlevel-error}

`case error`

### `init(rawValue:)` {#appnorthdiagnosticlevel-init-rawvalue}

`init?(rawValue: String)`

Creates a new instance with the specified raw value. If there is no value of the type that corresponds with the specified raw value, this initializer returns `nil`. For example: enum PaperSize: String { case A4, A5, Letter, Legal } print(PaperSize(rawValue: "Legal")) // Prints "Optional(PaperSize.Legal)" print(PaperSize(rawValue: "Tabloid")) // Prints "nil" - Parameter rawValue: The raw value to use for the new instance.

## AppNorthFeedbackDiagnostic {#appnorthfeedbackdiagnostic}

One host-sanitized diagnostic with optional severity, timestamp, and JSON metadata. Requires the host’s collection consent.

`struct AppNorthFeedbackDiagnostic`

### `category` {#appnorthfeedbackdiagnostic-category}

`let category: AppNorthDiagnosticCategory`

### `level` {#appnorthfeedbackdiagnostic-level}

`let level: AppNorthDiagnosticLevel?`

### `message` {#appnorthfeedbackdiagnostic-message}

`let message: String`

### `timestamp` {#appnorthfeedbackdiagnostic-timestamp}

`let timestamp: String?`

### `metadata` {#appnorthfeedbackdiagnostic-metadata}

`let metadata: [String : AppNorthJSONValue]?`

### `init(category:level:message:timestamp:metadata:)` {#appnorthfeedbackdiagnostic-init-category-level-message-timestamp-metadata}

`init(category: AppNorthDiagnosticCategory, level: AppNorthDiagnosticLevel? = nil, message: String, timestamp: String? = nil, metadata: [String : AppNorthJSONValue]? = nil)`

### `init(from:)` {#appnorthfeedbackdiagnostic-init-from}

`init(from decoder: any Decoder) throws`

Creates a new instance by decoding from the given decoder. This initializer throws an error if reading from the decoder fails, or if the data read is corrupted or otherwise invalid. - Parameter decoder: The decoder to read data from.

## AppNorthDeviceType {#appnorthdevicetype}

Device context values accepted by the shared wire contract. Their presence is not a promise of SDK support for other platforms; this guide covers iOS.

`enum AppNorthDeviceType`

### `phone` {#appnorthdevicetype-phone}

`case phone`

### `tablet` {#appnorthdevicetype-tablet}

`case tablet`

### `desktop` {#appnorthdevicetype-desktop}

`case desktop`

### `tv` {#appnorthdevicetype-tv}

`case tv`

### `unknown` {#appnorthdevicetype-unknown}

`case unknown`

### `init(rawValue:)` {#appnorthdevicetype-init-rawvalue}

`init?(rawValue: String)`

Creates a new instance with the specified raw value. If there is no value of the type that corresponds with the specified raw value, this initializer returns `nil`. For example: enum PaperSize: String { case A4, A5, Letter, Legal } print(PaperSize(rawValue: "Legal")) // Prints "Optional(PaperSize.Legal)" print(PaperSize(rawValue: "Tabloid")) // Prints "nil" - Parameter rawValue: The raw value to use for the new instance.

## AppNorthDeviceContext {#appnorthdevicecontext}

Optional bounded device/app context. Strings are flattened and truncated to 120 Swift characters. No automatic device collection or secret redaction is performed.

`struct AppNorthDeviceContext`

### `appName` {#appnorthdevicecontext-appname}

`let appName: String?`

### `appVersion` {#appnorthdevicecontext-appversion}

`let appVersion: String?`

### `buildVersion` {#appnorthdevicecontext-buildversion}

`let buildVersion: String?`

### `deviceModel` {#appnorthdevicecontext-devicemodel}

`let deviceModel: String?`

### `deviceType` {#appnorthdevicecontext-devicetype}

`let deviceType: AppNorthDeviceType?`

### `operatingSystem` {#appnorthdevicecontext-operatingsystem}

`let operatingSystem: String?`

### `systemVersion` {#appnorthdevicecontext-systemversion}

`let systemVersion: String?`

### `init(appName:appVersion:buildVersion:deviceModel:deviceType:operatingSystem:systemVersion:)` {#appnorthdevicecontext-init-appname-appversion-buildversion-devicemodel-devicetype-operatingsystem-systemversion}

`init(appName: String? = nil, appVersion: String? = nil, buildVersion: String? = nil, deviceModel: String? = nil, deviceType: AppNorthDeviceType? = nil, operatingSystem: String? = nil, systemVersion: String? = nil)`

### `init(from:)` {#appnorthdevicecontext-init-from}

`init(from decoder: any Decoder) throws`

Creates a new instance by decoding from the given decoder. This initializer throws an error if reading from the decoder fails, or if the data read is corrupted or otherwise invalid. - Parameter decoder: The decoder to read data from.

## AppNorthCreateFeedback {#appnorthcreatefeedback}

Private report input. Required fields are type, message, HTTP(S) page URL, and viewport; optional diagnostics and screenshot must already be consented and sanitized. See [private feedback](/docs/private-feedback).

`struct AppNorthCreateFeedback`

### `type` {#appnorthcreatefeedback-type}

`let type: AppNorthFeedbackType`

### `message` {#appnorthcreatefeedback-message}

`let message: String`

### `email` {#appnorthcreatefeedback-email}

`let email: String?`

### `pageURL` {#appnorthcreatefeedback-pageurl}

`let pageURL: URL`

### `pageTitle` {#appnorthcreatefeedback-pagetitle}

`let pageTitle: String?`

### `locale` {#appnorthcreatefeedback-locale}

`let locale: String?`

### `viewport` {#appnorthcreatefeedback-viewport}

`let viewport: AppNorthViewport`

### `userAgent` {#appnorthcreatefeedback-useragent}

`let userAgent: String?`

### `deviceContext` {#appnorthcreatefeedback-devicecontext}

`let deviceContext: AppNorthDeviceContext?`

### `diagnostics` {#appnorthcreatefeedback-diagnostics}

`let diagnostics: [AppNorthFeedbackDiagnostic]`

### `screenshot` {#appnorthcreatefeedback-screenshot}

`let screenshot: String?`

### `init(type:message:email:pageURL:pageTitle:locale:viewport:userAgent:deviceContext:diagnostics:screenshot:)` {#appnorthcreatefeedback-init-type-message-email-pageurl-pagetitle-locale-viewport-useragent-devicecontext-diagnostics-screenshot}

`init(type: AppNorthFeedbackType, message: String, email: String? = nil, pageURL: URL, pageTitle: String? = nil, locale: String? = nil, viewport: AppNorthViewport, userAgent: String? = nil, deviceContext: AppNorthDeviceContext? = nil, diagnostics: [AppNorthFeedbackDiagnostic] = [], screenshot: String? = nil)`

### `init(from:)` {#appnorthcreatefeedback-init-from}

`init(from decoder: any Decoder) throws`

Creates a new instance by decoding from the given decoder. This initializer throws an error if reading from the decoder fails, or if the data read is corrupted or otherwise invalid. - Parameter decoder: The decoder to read data from.

## AppNorthFeedbackReceipt {#appnorthfeedbackreceipt}

Submission receipt. `screenshotStored` reports actual attachment retention and `deduplicated` identifies a replayed creation.

`struct AppNorthFeedbackReceipt`

### `id` {#appnorthfeedbackreceipt-id}

`let id: String`

### `correlationId` {#appnorthfeedbackreceipt-correlationid}

`let correlationId: String`

### `screenshotStored` {#appnorthfeedbackreceipt-screenshotstored}

`let screenshotStored: Bool`

### `deduplicated` {#appnorthfeedbackreceipt-deduplicated}

`let deduplicated: Bool`

### `init(from:)` {#appnorthfeedbackreceipt-init-from}

`init(from decoder: any Decoder) throws`

Creates a new instance by decoding from the given decoder. This initializer throws an error if reading from the decoder fails, or if the data read is corrupted or otherwise invalid. - Parameter decoder: The decoder to read data from.

## AppNorthFeedbackReply {#appnorthfeedbackreply}

A public staff reply within the author’s private report thread.

`struct AppNorthFeedbackReply`

### `id` {#appnorthfeedbackreply-id}

`let id: String`

The stable identity of the entity associated with this instance.

### `message` {#appnorthfeedbackreply-message}

`let message: String`

### `createdAt` {#appnorthfeedbackreply-createdat}

`let createdAt: String`

### `init(from:)` {#appnorthfeedbackreply-init-from}

`init(from decoder: any Decoder) throws`

Creates a new instance by decoding from the given decoder. This initializer throws an error if reading from the decoder fails, or if the data read is corrupted or otherwise invalid. - Parameter decoder: The decoder to read data from.

## AppNorthFeedbackThread {#appnorthfeedbackthread}

The verified author’s report and public staff replies. Excludes contact data, screenshots, diagnostics, and internal notes.

`struct AppNorthFeedbackThread`

### `id` {#appnorthfeedbackthread-id}

`let id: String`

The stable identity of the entity associated with this instance.

### `type` {#appnorthfeedbackthread-type}

`let type: AppNorthFeedbackType`

### `status` {#appnorthfeedbackthread-status}

`let status: String`

### `message` {#appnorthfeedbackthread-message}

`let message: String`

### `createdAt` {#appnorthfeedbackthread-createdat}

`let createdAt: String`

### `updatedAt` {#appnorthfeedbackthread-updatedat}

`let updatedAt: String`

### `replies` {#appnorthfeedbackthread-replies}

`let replies: [AppNorthFeedbackReply]`

### `init(from:)` {#appnorthfeedbackthread-init-from}

`init(from decoder: any Decoder) throws`

Creates a new instance by decoding from the given decoder. This initializer throws an error if reading from the decoder fails, or if the data read is corrupted or otherwise invalid. - Parameter decoder: The decoder to read data from.

## AppNorthOfflineQueueStorage {#appnorthofflinequeuestorage}

Host-provided queue persistence. Keep it separate from cache storage and scope retention to your account lifecycle.

`protocol AppNorthOfflineQueueStorage : Sendable`

### `load()` {#appnorthofflinequeuestorage-load}

`func load() async throws -> Data?`

Returns stored bytes, or nil when no snapshot exists. Storage failures may throw.

### `save(_:)` {#appnorthofflinequeuestorage-save}

`func save(_ data: Data?) async throws`

Replaces stored bytes; nil means remove the persisted snapshot.

## AppNorthOfflineQueueError {#appnorthofflinequeueerror}

Queue errors. `full` means another write could not be queued without exceeding capacity.

`enum AppNorthOfflineQueueError`

### `full` {#appnorthofflinequeueerror-full}

`case full`

## AppNorthOfflineQueue {#appnorthofflinequeue}

Opt-in actor for network-failed idea, comment, and feedback writes. Default capacity is 50. Storage and flush scheduling belong to the host; votes and chat are not queue operations.

`actor AppNorthOfflineQueue`

### `init(storage:limit:)` {#appnorthofflinequeue-init-storage-limit}

`init(storage: any AppNorthOfflineQueueStorage, limit: Int = 50)`

### `submitIdea(using:board:title:description:authorName:idempotencyKey:)` {#appnorthofflinequeue-submitidea-using-board-title-description-authorname-idempotencykey}

`@discardableResult func submitIdea(using client: AppNorthClient, board: String, title: String, description: String = "", authorName: String? = nil, idempotencyKey: String = UUID().uuidString) async throws -> Bool`

Attempts the write immediately; returns true on success, false when a network failure is queued. Other errors propagate. Preserve the idempotency key on replay.

### `submitComment(using:handle:body:authorName:idempotencyKey:)` {#appnorthofflinequeue-submitcomment-using-handle-body-authorname-idempotencykey}

`@discardableResult func submitComment(using client: AppNorthClient, handle: String, body: String, authorName: String? = nil, idempotencyKey: String = UUID().uuidString) async throws -> Bool`

Attempts the write immediately; returns true on success, false when a network failure is queued. Other errors propagate. Preserve the idempotency key on replay.

### `submitFeedback(using:input:idempotencyKey:)` {#appnorthofflinequeue-submitfeedback-using-input-idempotencykey}

`@discardableResult func submitFeedback(using client: AppNorthClient, input: AppNorthCreateFeedback, idempotencyKey: String = UUID().uuidString) async throws -> Bool`

Attempts the write immediately; returns true on success, false when a network failure is queued. Other errors propagate. Preserve the idempotency key on replay.

### `pendingCount()` {#appnorthofflinequeue-pendingcount}

`func pendingCount() async throws -> Int`

Loads storage if needed and returns the total number of queued entries across scopes.

### `flush(using:)` {#appnorthofflinequeue-flush-using}

`func flush(using client: AppNorthClient) async throws`

Replays only matching identity scopes. Stops on network failure; other errors throw and leave the entry pending.

## AppNorthPaywallProduct {#appnorthpaywallproduct}

Product metadata: localized display price, ISO-8601 billing period, and optional trial duration. Use `P0D` for one-time products.

`struct AppNorthPaywallProduct`

### `id` {#appnorthpaywallproduct-id}

`let id: String`

### `price` {#appnorthpaywallproduct-price}

`let price: String`

Localized display price, including currency.

### `period` {#appnorthpaywallproduct-period}

`let period: String`

ISO-8601 duration. Use P0D for a one-time product.

### `trial` {#appnorthpaywallproduct-trial}

`let trial: String?`

### `init(id:price:period:trial:)` {#appnorthpaywallproduct-init-id-price-period-trial}

`init(id: String, price: String, period: String, trial: String? = nil)`

### `init(from:)` {#appnorthpaywallproduct-init-from}

`init(from decoder: any Decoder) throws`

Creates a new instance by decoding from the given decoder. This initializer throws an error if reading from the decoder fails, or if the data read is corrupted or otherwise invalid. - Parameter decoder: The decoder to read data from.

## AppNorthPaywall {#appnorthpaywall}

One offering in the device catalog. Title is team-only; the initializer sets source to `revenuecat`. `presentable` and `available` both gate opening.

`struct AppNorthPaywall`

### `id` {#appnorthpaywall-id}

`let id: String`

The stable identity of the entity associated with this instance.

### `title` {#appnorthpaywall-title}

`let title: String`

Team-only label; never rendered on the customer's card.

### `source` {#appnorthpaywall-source}

`let source: String`

### `presentable` {#appnorthpaywall-presentable}

`let presentable: Bool`

### `available` {#appnorthpaywall-available}

`var available: Bool`

### `products` {#appnorthpaywall-products}

`let products: [AppNorthPaywallProduct]`

### `init(id:title:presentable:available:products:)` {#appnorthpaywall-init-id-title-presentable-available-products}

`init(id: String, title: String, presentable: Bool, available: Bool = true, products: [AppNorthPaywallProduct] = [])`

### `init(from:)` {#appnorthpaywall-init-from}

`init(from decoder: any Decoder) throws`

Creates a new instance by decoding from the given decoder. This initializer throws an error if reading from the decoder fails, or if the data read is corrupted or otherwise invalid. - Parameter decoder: The decoder to read data from.

## AppNorthSubscriptionMetadata {#appnorthsubscriptionmetadata}

Device-reported subscription state, separate from signed identity. Dates use YYYY-MM-DD; renews and expires dates are mutually exclusive. Sandbox sets environment to `sandbox`.

`struct AppNorthSubscriptionMetadata`

### `subscription_status` {#appnorthsubscriptionmetadata-subscription-status}

`let subscription_status: AppNorthSubscriptionMetadata.Status`

### `plan` {#appnorthsubscriptionmetadata-plan}

`let plan: String?`

### `renews_on` {#appnorthsubscriptionmetadata-renews-on}

`let renews_on: String?`

### `expires_on` {#appnorthsubscriptionmetadata-expires-on}

`let expires_on: String?`

### `environment` {#appnorthsubscriptionmetadata-environment}

`let environment: String?`

### `init(status:plan:renewsOn:expiresOn:sandbox:)` {#appnorthsubscriptionmetadata-init-status-plan-renewson-expireson-sandbox}

`init(status: AppNorthSubscriptionMetadata.Status, plan: String? = nil, renewsOn: String? = nil, expiresOn: String? = nil, sandbox: Bool = false)`

### `init(from:)` {#appnorthsubscriptionmetadata-init-from}

`init(from decoder: any Decoder) throws`

Creates a new instance by decoding from the given decoder. This initializer throws an error if reading from the decoder fails, or if the data read is corrupted or otherwise invalid. - Parameter decoder: The decoder to read data from.

## AppNorthSubscriptionMetadata.Status {#appnorthsubscriptionmetadata-status-type}

Whether the reported subscription is active or absent. This device metadata is not a trusted entitlement check.

`enum Status`

### `active` {#appnorthsubscriptionmetadata-status-active}

`case active`

### `none` {#appnorthsubscriptionmetadata-status-none}

`case none`

### `init(rawValue:)` {#appnorthsubscriptionmetadata-status-init-rawvalue}

`init?(rawValue: String)`

Creates a new instance with the specified raw value. If there is no value of the type that corresponds with the specified raw value, this initializer returns `nil`. For example: enum PaperSize: String { case A4, A5, Letter, Legal } print(PaperSize(rawValue: "Legal")) // Prints "Optional(PaperSize.Legal)" print(PaperSize(rawValue: "Tabloid")) // Prints "nil" - Parameter rawValue: The raw value to use for the new instance.

## AppNorthPaywallSnapshot {#appnorthpaywallsnapshot}

Validated catalog and optional plan metadata. Limits: 20 paywalls, 10 products per paywall, 40 products total. Validation also runs during encoding.

`struct AppNorthPaywallSnapshot`

### `paywalls` {#appnorthpaywallsnapshot-paywalls}

`let paywalls: [AppNorthPaywall]`

### `plan` {#appnorthpaywallsnapshot-plan}

`let plan: AppNorthSubscriptionMetadata?`

### `init(paywalls:plan:)` {#appnorthpaywallsnapshot-init-paywalls-plan}

`init(paywalls: [AppNorthPaywall], plan: AppNorthSubscriptionMetadata? = nil) throws`

### `validate()` {#appnorthpaywallsnapshot-validate}

`func validate() throws`

Throws invalidConfiguration for invalid catalog or metadata fields; never silently trims the catalog.

### `encode(to:)` {#appnorthpaywallsnapshot-encode-to}

`func encode(to encoder: any Encoder) throws`

Encodes the value using its wire representation.

### `init(from:)` {#appnorthpaywallsnapshot-init-from}

`init(from decoder: any Decoder) throws`

Creates a new instance by decoding from the given decoder. This initializer throws an error if reading from the decoder fails, or if the data read is corrupted or otherwise invalid. - Parameter decoder: The decoder to read data from.

## AppNorthActionCard {#appnorthactioncard}

Server card envelope. Only offer cards are supported by the paywall controller; unknown kinds remain disabled. `titleAtSend` is a team label, not customer-facing copy.

`struct AppNorthActionCard`

### `kind` {#appnorthactioncard-kind}

`let kind: String`

### `paywallId` {#appnorthactioncard-paywallid}

`let paywallId: String?`

### `titleAtSend` {#appnorthactioncard-titleatsend}

`let titleAtSend: String`

### `buttonLabel` {#appnorthactioncard-buttonlabel}

`let buttonLabel: String`

### `body` {#appnorthactioncard-body}

`let body: String?`

SwiftUI view content. Present the view itself rather than invoking this property directly.

### `available` {#appnorthactioncard-available}

`var available: Bool`

### `init(from:)` {#appnorthactioncard-init-from}

`init(from decoder: any Decoder) throws`

Creates a new instance by decoding from the given decoder. This initializer throws an error if reading from the decoder fails, or if the data read is corrupted or otherwise invalid. - Parameter decoder: The decoder to read data from.

## AppNorthPaywalls {#appnorthpaywalls}

Main-actor paywall controller; retain one per chat identity. Configure catalog, availability, and presentation before passing it to chat. See [paywall cards](/docs/paywall-cards).

`@MainActor final class AppNorthPaywalls`

### `snapshot` {#appnorthpaywalls-snapshot}

`@Published @MainActor var snapshot: AppNorthPaywallSnapshot? { get }`

### `availabilityProvider` {#appnorthpaywalls-availabilityprovider}

`@MainActor var availabilityProvider: ((String) async -> Bool)?`

### `onPaywall` {#appnorthpaywalls-onpaywall}

`@MainActor var onPaywall: ((String) -> Void)?`

### `onOfferOpened` {#appnorthpaywalls-onofferopened}

`@MainActor var onOfferOpened: ((String, String) -> Void)?`

Called once per card in this controller. Server receipts also deduplicate across app launches. This signals a tap, not a completed purchase.

### `init(client:)` {#appnorthpaywalls-init-client}

`@MainActor init(client: AppNorthClient)`

### `setPaywalls(_:plan:)` {#appnorthpaywalls-setpaywalls-plan}

`@MainActor func setPaywalls(_ values: [AppNorthPaywall], plan: AppNorthSubscriptionMetadata? = nil) throws`

Validates and replaces the complete catalog and optional metadata. Does not send until refresh.

### `refreshPaywalls(invalidate:)` {#appnorthpaywalls-refreshpaywalls-invalidate}

`@MainActor func refreshPaywalls(invalidate: Bool = false) async throws`

Re-evaluates availability, reports the snapshot, and retries pending receipts. Concurrent refreshes coalesce; invalidate after state changes.

### `isAvailable(_:)` {#appnorthpaywalls-isavailable}

`@MainActor func isAvailable(_ card: AppNorthActionCard) -> Bool`

Requires an offer card, server availability, a matching identity, a presentation callback, and a presentable available snapshot entry.

### `open(_:messageId:alreadyOpened:)` {#appnorthpaywalls-open-messageid-alreadyopened}

`@MainActor func open(_ card: AppNorthActionCard, messageId: String, alreadyOpened: Bool = false) async`

Invokes presentation for an available card and reports its first tap. Already-opened cards may present again without another first-open event.

## AppNorthReadCacheStorage {#appnorthreadcachestorage}

Host-provided cache persistence. Use a different storage instance or file from the offline queue.

`protocol AppNorthReadCacheStorage : Sendable`

### `load()` {#appnorthreadcachestorage-load}

`func load() async throws -> Data?`

Returns stored bytes, or nil when no snapshot exists. Storage failures may throw.

### `save(_:)` {#appnorthreadcachestorage-save}

`func save(_ data: Data?) async throws`

Replaces stored bytes; nil means remove the persisted snapshot.

## AppNorthReadCache {#appnorthreadcache}

Opt-in actor storing boards and idea arrays. Default TTL is 300 seconds; stale or missing entries return nil. See [offline and cache](/docs/offline-and-cache).

`actor AppNorthReadCache`

### `init(storage:ttl:clock:)` {#appnorthreadcache-init-storage-ttl-clock}

`init(storage: any AppNorthReadCacheStorage, ttl: TimeInterval = 300, clock: @escaping @Sendable () -> Date = { Date() })`

### `boards(scope:)` {#appnorthreadcache-boards-scope}

`func boards(scope: String) async throws -> [AppNorthBoard]?`

Returns cached values only while fresh under the configured TTL; otherwise nil.

### `ideas(scope:query:)` {#appnorthreadcache-ideas-scope-query}

`func ideas(scope: String, query: String) async throws -> [AppNorthIdea]?`

Returns cached values only while fresh under the configured TTL; otherwise nil.

### `storeBoards(_:scope:)` {#appnorthreadcache-storeboards-scope}

`func storeBoards(_ boards: [AppNorthBoard], scope: String) async throws`

Stores the supplied values with the current timestamp under the specified scope and query, then persists them.

### `storeIdeas(_:scope:query:)` {#appnorthreadcache-storeideas-scope-query}

`func storeIdeas(_ ideas: [AppNorthIdea], scope: String, query: String) async throws`

Stores the supplied values with the current timestamp under the specified scope and query, then persists them.

### `clear(scope:)` {#appnorthreadcache-clear-scope}

`func clear(scope: String) async throws`

Removes the scope’s boards and all idea queries in that scope, then persists the new snapshot.

## AppNorthTheme {#appnorththeme}

Native view colors. Primary defaults to the host accent; optional secondary, tertiary, and text pairs follow appearance. See [appearance](/docs/appearance).

`struct AppNorthTheme`

### `primary` {#appnorththeme-primary}

`var primary: Color`

### `secondary` {#appnorththeme-secondary}

`var secondary: AppNorthColorPair?`

### `tertiary` {#appnorththeme-tertiary}

`var tertiary: AppNorthColorPair?`

### `text` {#appnorththeme-text}

`var text: AppNorthColorPair?`

### `init(primary:secondary:tertiary:text:)` {#appnorththeme-init-primary-secondary-tertiary-text}

`init(primary: Color = Self.defaultPrimary, secondary: AppNorthColorPair? = nil, tertiary: AppNorthColorPair? = nil, text: AppNorthColorPair? = nil)`

### `defaultPrimary` {#appnorththeme-defaultprimary}

`static var defaultPrimary: Color { get }`

## AppNorthTranslateButton {#appnorthtranslatebutton}

Idea translation control policy. Translation UI requires iOS 18 or later even when set to `always`.

`enum AppNorthTranslateButton`

### `automatic` {#appnorthtranslatebutton-automatic}

`case automatic`

### `always` {#appnorthtranslatebutton-always}

`case always`

### `hide` {#appnorthtranslatebutton-hide}

`case hide`

## AppNorthUpvoteIcon {#appnorthupvoteicon}

Built-in vote icon options or a host-provided SF Symbol name.

`enum AppNorthUpvoteIcon`

### `systemName(_:)` {#appnorthupvoteicon-systemname}

`case systemName(String)`

### `thumbsUp` {#appnorthupvoteicon-thumbsup}

`case thumbsUp`

### `arrowUp` {#appnorthupvoteicon-arrowup}

`case arrowUp`

### `chevronUp` {#appnorthupvoteicon-chevronup}

`case chevronUp`

## AppNorthUIConfiguration {#appnorthuiconfiguration}

Native view behavior. Defaults: open/closed filter on, arrow-up vote icon, automatic translation, chat button on.

`struct AppNorthUIConfiguration`

### `showsOpenClosedFilter` {#appnorthuiconfiguration-showsopenclosedfilter}

`var showsOpenClosedFilter: Bool`

### `upvoteIcon` {#appnorthuiconfiguration-upvoteicon}

`var upvoteIcon: AppNorthUpvoteIcon`

### `translateButton` {#appnorthuiconfiguration-translatebutton}

`var translateButton: AppNorthTranslateButton`

### `showsChatButton` {#appnorthuiconfiguration-showschatbutton}

`var showsChatButton: Bool`

Shows the floating chat button when chat is enabled for the project.

### `init(showsOpenClosedFilter:upvoteIcon:translateButton:showsChatButton:)` {#appnorthuiconfiguration-init-showsopenclosedfilter-upvoteicon-translatebutton-showschatbutton}

`init(showsOpenClosedFilter: Bool = true, upvoteIcon: AppNorthUpvoteIcon = .arrowUp, translateButton: AppNorthTranslateButton = .automatic, showsChatButton: Bool = true)`

## AppNorthIdentityProperty {#appnorthidentityproperty}

A scalar property included in a v2 identity signature. Preserve the exact backend-issued values.

`enum AppNorthIdentityProperty`

### `string(_:)` {#appnorthidentityproperty-string}

`case string(String)`

### `number(_:)` {#appnorthidentityproperty-number}

`case number(Double)`

### `boolean(_:)` {#appnorthidentityproperty-boolean}

`case boolean(Bool)`

### `encode(to:)` {#appnorthidentityproperty-encode-to}

`func encode(to encoder: any Encoder) throws`

Encodes the value using its wire representation.

## AppNorthIdentityCredential {#appnorthidentitycredential}

An in-memory credential issued by your authenticated backend. Construction validates shape; only the server verifies the signature. See [verified identity](/docs/verified-identity).

`struct AppNorthIdentityCredential`

### `email` {#appnorthidentitycredential-email}

`let email: String`

### `expiresAt` {#appnorthidentitycredential-expiresat}

`let expiresAt: Int64`

### `signature` {#appnorthidentitycredential-signature}

`let signature: String`

### `properties` {#appnorthidentitycredential-properties}

`let properties: [String : AppNorthIdentityProperty]?`

### `init(email:expiresAt:signature:properties:)` {#appnorthidentitycredential-init-email-expiresat-signature-properties}

`init(email: String, expiresAt: Int64, signature: String, properties: [String : AppNorthIdentityProperty]? = nil) throws`

### `isValid(at:)` {#appnorthidentitycredential-isvalid-at}

`func isValid(at date: Date = Date()) -> Bool`

Checks expiry only. A valid return value does not authenticate the signature.

## `captureAppNorthScreenshot(consent:adapter:redactor:)` {#captureappnorthscreenshot-consent-adapter-redactor}

`func captureAppNorthScreenshot(consent: Bool, adapter: (any AppNorthScreenshotCapturing)?, redactor: (any AppNorthScreenshotRedacting)?) async -> String?`

Captures only with consent and both adapters. Returns nil on any failure, unsupported data URL, oversize data, or unchanged redactor output. See [consented capture](/docs/private-feedback#capture).

## RevenueCat helper {#appnorthrevenuecatpaywalls}

`@MainActor public final class AppNorthRevenueCatPaywalls`

This optional source helper lives outside the SDK binary. Copy `Helpers/AppNorthRevenueCatPaywalls.swift` into your app target and add RevenueCat dependencies as described in [paywall cards](/docs/paywall-cards#helper).

### `init(client:syncsSubscriptionMetadata:presenter:)` {#appnorthrevenuecatpaywalls-init-client-syncssubscriptionmetadata-presenter}

`init(client: AppNorthClient, syncsSubscriptionMetadata: Bool = true, presenter: (() -> UIViewController?)? = nil)`

Create after configuring Purchases and the matching AppNorth identity. Starts observation and the first reload; retain for the session.

### `paywalls` {#appnorthrevenuecatpaywalls-paywalls}

`let paywalls: AppNorthPaywalls`

Pass this controller to AppNorthChatView or AppNorthView.

### `onError` {#appnorthrevenuecatpaywalls-onerror}

`var onError: ((Error) -> Void)?`

Receives offering-load or synchronization failures. Avoid logging identity or customer details.

### `reload()` {#appnorthrevenuecatpaywalls-reload}

`func reload()`

Loads all offerings and customer information, then refreshes availability and snapshot metadata. Concurrent reloads coalesce.

### `stop()` {#appnorthrevenuecatpaywalls-stop}

`func stop()`

Stops observation/reload tasks, removes the foreground observer, disables presentation, dismisses the paywall, and clears the catalog. Call before changing accounts; create a new helper for the next session.

## Verify {#verify}

1. Import `AppNorth` in your app target and compile the example above with a configured client.
2. Use public initializers exactly as listed; keep main-actor objects on the main actor.
3. Run the verification steps in the relevant task guide against your own project. A successful compile does not verify server policy, account ownership, or purchase configuration.

## Review request card activity {#review-request-activity}

`AppNorthClient.markActionCardTapped(_:) async throws` reports the first tap on a `review_request` card. `markActionCardOpened(_:)` reports its first display. Both use stable, separate idempotency keys and the current chat identity; neither reports a submitted review or guarantees that StoreKit displayed a prompt.

`AppNorthChatMessage.tappedAt` and `AppNorthChatEntry.tappedAt` are optional server timestamp strings, alongside `openedAt`. `AppNorthChatModel` updates the existing entry when polling returns new card activity. `AppNorthChatView` presents review cards without a paywall controller and uses `AppStore.requestReview(in:)` on iOS 16+. See [review requests in support chat](/docs/support-chat#review-requests) for sending, automatic resolution settings, and Apple's presentation limits.

## AppNorthChatImage {#appnorthchatimage}

`struct AppNorthChatImage: Codable, Hashable, Identifiable, Sendable`

| Property | Type     | Meaning                                                 |
| -------- | -------- | ------------------------------------------------------- |
| `id`     | `String` | Image identifier.                                       |
| `url`    | `URL?`   | Private, expiring URL; nil when storage is unavailable. |
| `width`  | `Int`    | Sanitized image width in pixels.                        |
| `height` | `Int`    | Sanitized image height in pixels.                       |

## AppNorthChatImageUpload {#appnorthchatimageupload}

`struct AppNorthChatImageUpload: Codable, Hashable, Sendable`

| Property     | Type                | Meaning                                           |
| ------------ | ------------------- | ------------------------------------------------- |
| `imageToken` | `String`            | App- and sender-bound token, valid for 24 hours.  |
| `image`      | `AppNorthChatImage` | Sanitized preview; its URL expires in 10 minutes. |

Use `AppNorthChatModel.send(_:images:)` to queue completed uploads with optimistic
bubbles, or pass their tokens directly to `AppNorthClient.sendChatMessage`.
