# Prepare Journeys on iOS (https://www.getfounderhq.com/docs/journeys/mobile-preparation-ios)

`FounderHQJourneys` can prepare a published Journey before the user opens it.
Preparation loads the Journey configuration and its renderer in parallel, then
keeps one hidden `WKWebView` ready for presentation.

<Callout type="info" title="Before you start">
Install `FounderHQJourneys` 0.8.0 or later. You need a published Journey, its
Journey ID, and a publishable key (`fhq_pk_XXXX`).
</Callout>

## Prepare from SwiftUI

Create one `JourneyHost` for the screen or flow that presents the Journey. Keep
that host alive while the user can open, close, and reopen the Journey.

```swift
import FounderHQJourneys
import SwiftUI

struct OnboardingEntryView: View {
    @State private var showJourney = false
    @StateObject private var host = JourneyHost(configuration: .init(
        apiKey: "fhq_pk_XXXX",
        journeyID: "journey_123",
        identity: JourneyIdentity(externalID: "usr_1042")
    ))

    var body: some View {
        Button("Start onboarding") {
            showJourney = true
        }
        .task {
            try? await host.prepare()
        }
        .fullScreenCover(isPresented: $showJourney) {
            JourneyView(host: host)
        }
    }
}
```

`prepare()` is safe to call more than once. Concurrent calls share the same
work, and a fresh preparation returns immediately. `JourneyView(host:)` calls
`present()` when it appears and `dismiss()` when it leaves.

For UIKit, keep the same host in the owning view controller or coordinator:

```swift
let host = JourneyHost(configuration: configuration)

Task { try? await host.prepare() }

let journey = JourneyViewController(host: host)
present(journey, animated: true)
```

You can also call `try await host.present()` yourself when managing the host's
view directly. Observe `host.readiness` and `host.isPresented` when native UI
needs to reflect its state.

## Prepared and direct presentation

A current renderer advertises preparation and visibility support. The SDK
initializes it while hidden and waits until the first screen's essential layout
and assets have rendered. Hidden preparation does not start presentation
analytics. Showing the Journey reuses that same `WKWebView`.

An older renderer that does not advertise those capabilities uses direct
presentation. The SDK still fetches its configuration ahead of time, but waits
to initialize the renderer until the Journey is visible. This avoids recording
a hidden visit and keeps older apps working during a renderer rollout.

Each new presentation receives a new client session. After dismissal, a capable
renderer resets the flow while hidden so reopening starts at the first screen
without allocating another web view.

## Loading, errors, and retries

The built-in loading state is a text-free progress indicator with an accessible
label. It avoids animation when Reduce Motion is enabled. Presentation has a
15-second deadline; the built-in error state gives the user a generic message
and a **Try again** action. A retry attempts preparation again.

Pass custom SwiftUI views when your app needs its own treatment:

```swift
JourneyView(
    host: host,
    loadingView: AnyView(MyJourneyLoader()),
    errorView: { error, retry in
        AnyView(MyJourneyError(onRetry: retry))
    }
)
```

Avoid showing `error.localizedDescription` directly in customer-facing UI. It
may contain network or renderer details. Send the error to your logging system
and keep the visible message focused on retrying or leaving the flow.

## Freshness and offline starts

FounderHQ returns a refresh interval, which the SDK clamps to 10–30 minutes.
While foregrounded, the host schedules one refresh when due, whether prepared
or active. A successful refresh during an active Journey stages the next
configuration without changing the displayed content or answers.

For the first 10 minutes, a ready preparation opens immediately. Between its
refresh time and 30 minutes, it still opens immediately and refreshes
asynchronously. Content older than 30 minutes requires a successful fetch
before a new presentation. An active Journey is not automatically ended when
its original preparation passes that age.

Only successful configuration responses, or authenticated unchanged responses
matching cached JSON, renew freshness. Failures do not renew it or schedule an
automatic retry loop. Presenting again, resuming, or explicitly retrying can
make another attempt after backoff. Requests coalesce and honor `Retry-After`.

A definitive 401, 403, or confirmed Journey 404 discards prepared content and
stops active interaction. **Try again**, `prepare()`, or `present()` performs
fresh authorization with the same configuration; it never reuses denied
content. Downloaded content can remain visible offline until the device
receives a denial. Restarting the app requires fresh authorization for new
presentations, independently of pending answer delivery.

## Identity, authorization, and capture

The publishable key authorizes displaying the Journey. `JourneyIdentity`
controls who the response is attributed to; it does not grant access. When the
signed-in person, publishable key, Journey ID, base URL, or other configuration
changes, call:

```swift
host.updateConfiguration(updatedConfiguration)
```

The host invalidates the prepared content and prepares the new scope. This
prevents a Journey prepared for one person from being shown to another.

Capture is enabled by default and is separate from display authorization. Set
`capture: nil` to disable it. Capture request bodies pass through the native SDK
unchanged. Every queued event keeps the visitor, presentation session, Journey
revision, and context recorded when the event was created. If an answer is sent
later, including after an app restart or identity change, it remains attached
to that original immutable revision and session.

Publishing while a Journey is open does not replace its active content. The
next successful preparation can use the new revision; the open presentation
and its delayed answers stay tied to the revision that the user saw.

## Dispose of a host

Call `dispose()` when the owning flow is permanently finished:

```swift
host.dispose()
```

Disposal cancels preparation and refresh work, removes lifecycle and bridge
handlers, stops the renderer, and makes the host unavailable for future
presentation. Use `dismiss()` when the Journey may be opened again. Memory
warnings unload hidden renderer content while preserving an active Journey.
An unloaded preparation can rebuild its renderer within the same freshness
window.

## Upgrade from an earlier version

Update the Swift Package Manager dependency to 0.8.0 or the CocoaPods entry to:

```ruby
pod 'FounderHQJourneys', '~> 0.8.0'
```

Existing `JourneyView(configuration:...)` and `JourneyViewController`
initializers remain supported. They create and manage a host for that view, so
an existing integration can upgrade without changing its presentation code.

To gain preparation and repeated presentation, move the configuration and
callbacks into a long-lived `JourneyHost`, call `prepare()` before the launch
action, and pass the host to `JourneyView(host:)` or
`JourneyViewController(host:)`. Keep one host per presentation surface and call
`updateConfiguration(_:)` whenever its identity or Journey scope changes.
