Documentation

Prepare Journeys on iOS

Preload a Journey for instant SwiftUI or UIKit presentation.

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.

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

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.

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:

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:

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:

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:

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:

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.

AI agent or LLM? Read this page as markdown

On this page