Getting Started

Using AI to Code? Copy This Prompt

Paste this prompt into ChatGPT, Claude, Cursor, or any AI coding assistant to automatically integrate the Sequence SDK into your app.

Overview

Sequence lets you build beautiful onboarding screens in our visual editor, then show them natively in your iOS app. No design skills needed.

🎨Design onboarding visually
πŸ“±Renders natively in your app
⚑Update without app releases
πŸ“ŠBuilt-in analytics & A/B tests

Installation

1

Open Xcode Package Manager

File β†’ Add Packages

Paste this URL:

https://github.com/Musgrav/sequence-swift

Important: Use Branch, Not Version

For Dependency Rule, select Branch and type main. Do not use a version number.

Quick Start

How It Works

Splash Screen
β†’
Sequence Onboarding
β†’
Paywall
β†’
Main App
2

Get Your API Credentials

From your Sequence dashboard

Go to Settings in your dashboard to find:

appIdapiKey
3

Initialize the SDK

Add to your App's init()

MyApp.swift
import Sequence

@main
struct MyApp: App {
    init() {
        Sequence.shared.configure(
            appId: "your-app-id",
            apiKey: "your-api-key",
            baseURL: "https://screensequence.com"
        )
    }

    var body: some Scene {
        WindowGroup {
            ContentView()
        }
    }
}
4

Show the Onboarding

Add WebViewOnboardingView to your app

ContentView.swift
import SwiftUI
import Sequence

struct ContentView: View {
    @StateObject private var sequence = Sequence.shared

    var body: some View {
        Group {
            if sequence.isOnboardingCompleted {
                // Show your paywall or main app
                MainAppView()
            } else {
                // Sequence handles everything with pixel-perfect rendering
                WebViewOnboardingView {
                    print("Onboarding finished!")
                }
            }
        }
    }
}

Use WebViewOnboardingView

Always use WebViewOnboardingView (not OnboardingView) for pixel-perfect WYSIWYG rendering that matches the editor exactly.
5

Create Your Flow in the Dashboard

Design your screens visually

Go to Flows in your dashboard, add screens, drag in blocks, and hit Publish. Your app will automatically show the new onboarding.

You're all set!

Your app will now fetch and display your onboarding flow. Changes you make in the dashboard appear instantlyβ€”no app update needed.

Core Concepts

Architecture

Sequence follows a decoupled architecture where your onboarding configuration lives on our servers and is fetched by the SDK at runtime:

How It Works
β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”     β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”     β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚  Your Mobile    β”‚     β”‚    Sequence     β”‚     β”‚    Sequence     β”‚
β”‚      App        │────▢│      API        │◀────│   Dashboard     β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜     β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜     β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
        β”‚                       β”‚
        β”‚  1. Fetch config      β”‚
        │◀──────────────────────│
        β”‚                       β”‚
        β”‚  2. Render screens    β”‚
        β”‚  3. Collect data      β”‚
        β”‚  4. Track events      β”‚
        │──────────────────────▢│
        β”‚                       β”‚

Key benefits of this architecture:

  • Instant updates: Change your onboarding without releasing a new app version
  • A/B testing: The API serves different variants to different users
  • Analytics: All events flow back to the dashboard for analysis
  • Offline support: Configurations are cached locally for reliability

Screens & Flows

A Flow is a collection of Screens that users navigate through. Each screen has a type that determines its purpose:

Screen TypePurposeCommon Use Cases
welcomeFirst impression screenApp intro, value proposition
featureHighlight a featureFeature tours, benefits
permissionRequest permissionsNotifications, location, camera
carouselMulti-slide contentFeature showcase, testimonials
celebrationSuccess/completionSignup complete, achievement
nativeCustom native screenLogin, complex forms
fillerTransitional contentLoading states, progress

Screen Transitions

Configure how screens animate when navigating:

// Available transitions
type ScreenTransition =
  | 'none'           // No animation
  | 'fade'           // Opacity fade
  | 'slide-left'     // Slide from right to left
  | 'slide-right'    // Slide from left to right
  | 'slide-up'       // Slide from bottom
  | 'slide-down'     // Slide from top
  | 'fade-slide-left'  // Fade + slide left
  | 'fade-slide-right' // Fade + slide right
  | 'scale';         // Scale up from center

Content Blocks

Screens are composed of Content Blocks β€” reusable UI components that you arrange to build your screens. Sequence provides 11 block types:

BlockDescriptionData Collection
textHeadings, body text, captionsNo
imageImages with various fit modesNo
buttonInteractive buttons with presetsNo
inputText fields (email, phone, password, etc.)Yes
checklistSingle/multi-select optionsYes
sliderNumeric range selectionYes
progressProgress indicators (bar, dots, ring)No
spacerVertical spacingNo
dividerHorizontal line separatorNo
iconIcons and emojisNo
feature-cardCard with headline and bodyNo

Block Styling

Every block supports universal styling options including padding, margin, colors, shadows, border radius, and animations. Configure these in the visual editor.

Data Collection

Input blocks (input, checklist, slider) collect user data during onboarding. Each block has a unique id that becomes the key in the collected data object:

Example: Collected Data
// When onboarding completes, you receive:
{
  "user_name": "John Doe",           // From input block with id="user_name"
  "interests": ["fitness", "music"], // From checklist block with id="interests"
  "experience_level": 3,             // From slider block with id="experience_level"
}

// Handle in your onComplete callback:
<OnboardingModal
  onComplete={(collectedData) => {
    // Save to your backend
    saveUserPreferences(collectedData);

    // Or use locally
    if (collectedData.interests?.includes('fitness')) {
      showFitnessContent();
    }
  }}
/>

Validation

Configure validation rules to ensure users provide required information:

  • Input blocks: Mark as required, set input types for format validation
  • Checklist blocks: Set minimum/maximum selections
  • Navigation: Users cannot proceed until validation passes

Variable System

The variable system lets you create personalized onboarding experiences by collecting user data and displaying it dynamically throughout your flow. Ask for a user's name on screen 1, then greet them by name on screen 2.

✏️Collect data via input blocks
πŸ”—Reference with {fieldName} syntax
πŸ”’Math expressions: {hours * 365}
πŸ‘οΈLive preview in editor canvas

Overview

How Variables Work

Input Block
β†’
fieldName: "name"
β†’
Text: "Hi {name}!"
β†’
Shows: "Hi Alex!"

Variables are created when users interact with data-collecting blocks (input, checklist, slider, etc.). Each block has a fieldName property that defines the variable name. You can then reference this variable anywhere using {fieldName} syntax.

Assigning Variables

Variables are created using these data-collecting blocks. Each block stores its value under the specified fieldName.

Block TypeCreates VariableValue TypeExample
inputYesString"Alex"
checklistYesString[]["fitness", "nutrition"]
sliderYesNumber5
quiz-questionYesString"option_a"
goal-selectorYesString[]["goal_1", "goal_2"]

Example: Creating an Input Variable

In the editor, drag an Input block onto your screen. In the properties panel, set the Field Name to a snake_case identifier:

Input block configuration
{
  "type": "input",
  "content": {
    "fieldName": "user_name",     // This creates the variable
    "label": "What's your name?",
    "placeholder": "Enter your name",
    "inputType": "text"
  }
}

Variable Naming Rules

Use snake_case for all field names (e.g., user_name, daily_hours). Names must be unique across your entire flow.

Referencing Variables

Once a variable is created, reference it in any text content using curly braces:

Using variables in text
// In any text block, button, or feature card:
"Welcome back, {user_name}!"

// The variable gets replaced with the actual value:
"Welcome back, Alex!"

Where Variables Work

You can use {fieldName} syntax in:

  • Text blocks: Headlines, body text, captions
  • Button text: Dynamic button labels
  • Feature cards: Headlines and body content
  • Any text content in the editor

Math Expressions & Formatting

The variable system supports math expressions and number formatting for creating compelling, calculated content.

Math Expressions

Use standard math operators inside curly braces:

Math expression examples
// Basic multiplication
"That's {hours_per_day * 365} hours per year!"
// With hours_per_day = 2 β†’ "That's 730 hours per year!"

// Division
"You'll save {minutes_saved * 365 / 60} hours annually"
// With minutes_saved = 30 β†’ "You'll save 182.5 hours annually"

// Complex expressions
"{daily_calories * 7} weekly calories"
"{monthly_savings * 12} saved per year"

Number Formatting

Use the | number filter to format large numbers with commas:

Number formatting
// Without formatting
"You've taken {total_steps} steps"
// Shows: "You've taken 2458 steps"

// With | number formatting
"You've taken {total_steps | number} steps"
// Shows: "You've taken 2,458 steps"
ExpressionWith value = 1234567Result
{value}12345671234567
{value | number}12345671,234,567
{value * 2}12345672469134
{value * 2 | number}12345672,469,134

Live Preview in Editor

The editor canvas shows live previews of your variables with mock data. Instead of seeing raw {name} text, you'll see interpolated values like "Alex".

✨

What You See in the Editor

Mock data is automatically generated

Template:
Hi {name}, welcome!
Canvas shows:
Hi Alex, welcome!

The properties panel still shows the raw template text so you can edit it. The canvas preview helps you visualize the final user experience.

Mock Data Generation

The editor generates realistic mock data based on your field names. A field namedemail shows an email address, name shows a name, hours_per_dayshows a number, etc.

Data Storage (API)

User-submitted data is stored incrementally as users progress through your flow. This allows you to capture partial data even if users drop off before completing.

How It Works

  • Incremental saves: Data is saved after each input (debounced 500ms)
  • Session tracking: Each onboarding session has a unique ID
  • Completion tracking: completed_at is set when flow finishes
  • Partial data capture: Even incomplete flows save their progress

Submissions API

Access collected data via the API:

GET /api/v1/submissions/{appId}
// Response
{
  "submissions": [
    {
      "id": "sub_123",
      "flow_id": "flow_abc",
      "user_id": "user_456",
      "session_id": "session_789",
      "data": {
        "name": "Alex",
        "email": "alex@example.com",
        "goals": ["fitness", "nutrition"]
      },
      "last_screen_id": "screen_3",
      "last_screen_index": 3,
      "completed_at": "2024-01-15T10:30:00Z",
      "created_at": "2024-01-15T10:25:00Z"
    }
  ],
  "total": 1
}

Query Parameters

ParameterTypeDescription
flow_idstringFilter by specific flow
user_idstringFilter by user ID
completedbooleanOnly completed submissions
incompletebooleanOnly incomplete submissions
limitnumberMax results (default: 100)
offsetnumberPagination offset

Database Migration

Run this migration to create the submissions table:

migration-onboarding-submissions.sql
CREATE TABLE IF NOT EXISTS public.onboarding_submissions (
  id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
  app_id UUID NOT NULL REFERENCES public.apps(id) ON DELETE CASCADE,
  flow_id UUID NOT NULL REFERENCES public.flows(id) ON DELETE CASCADE,
  user_id TEXT NOT NULL,
  device_id TEXT,
  session_id TEXT NOT NULL,
  data JSONB NOT NULL DEFAULT '{}',
  last_screen_id TEXT,
  last_screen_index INT,
  completed_at TIMESTAMPTZ,
  created_at TIMESTAMPTZ DEFAULT NOW(),
  updated_at TIMESTAMPTZ DEFAULT NOW()
);

-- Indexes
CREATE INDEX idx_onboarding_submissions_app_user
  ON public.onboarding_submissions(app_id, user_id);
CREATE INDEX idx_onboarding_submissions_flow
  ON public.onboarding_submissions(flow_id);
CREATE INDEX idx_onboarding_submissions_session
  ON public.onboarding_submissions(session_id);

Complete Example Flow

Here's a complete example of a personalized onboarding flow:

1

Screen 1: Collect Name

Input with fieldName: "name"

{
  "type": "input",
  "content": {
    "fieldName": "name",
    "label": "What should we call you?",
    "placeholder": "Your name"
  }
}
2

Screen 2: Use Name + Collect Hours

Personalized greeting with slider

// Text block using the name variable
{
  "type": "text",
  "content": {
    "text": "Great to meet you, {name}!",
    "variant": "h1"
  }
}

// Slider to collect hours
{
  "type": "slider",
  "content": {
    "fieldName": "hours_daily",
    "label": "Hours per day",
    "min": 1,
    "max": 8,
    "defaultValue": 2
  }
}
3

Screen 3: Calculated Impact

Math expression with both variables

{
  "type": "text",
  "content": {
    "text": "{name}, you'll reclaim {hours_daily * 365 | number} hours this year!",
    "variant": "h2"
  }
}

// With name="Alex" and hours_daily=2:
// "Alex, you'll reclaim 730 hours this year!"

Pro Tips

  • β€’ Collect important data (like name, email) early in the flow
  • β€’ Use math expressions to show impact and create urgency
  • β€’ Format large numbers with | number for readability
  • β€’ Test your flow with the live preview to see personalization in action

Auth & Permissions

Sequence supports native Sign In with Apple, permission requests, and app review prompts directly from your onboarding flow. This guide walks you through setting up each feature.

Overview

The SDK uses namespaced action identifiers to trigger native functionality. When a user taps a button, the WebView sends the action to the native SDK, which handles the authentication or permission request, then sends the result back to advance the flow.

How It Works

User taps button
β†’
WebView sends action
β†’
Native SDK handles
β†’
Result sent back
β†’
Flow advances

Available Action Identifiers

CategoryIdentifierDescription
Authauth.appleSign In with Apple (built-in)
Authauth.googleSign In with Google (delegate)
Authauth.emailEmail authentication (delegate)
Permissionpermission.notificationsPush notification permission
Permissionpermission.locationLocation when in use
Permissionpermission.locationAlwaysBackground location
Permissionpermission.cameraCamera access
Permissionpermission.photosPhoto library access
Permissionpermission.trackingApp Tracking Transparency
Systemsystem.reviewRequest App Store review
Systemsystem.openSettingsOpen app settings

Sign In with Apple

Sign In with Apple is built into the SDK. The SDK handles the native Apple Sign In UIβ€”you receive the credential in your delegate and send the identityToken to your backend for verification.

How Returning Users Work

Apple provides a stable userIdentifier that's the same every time a user signs in with your app. Your backend should store this ID to recognize returning users. On first sign-in, Apple also provides email and name (if user consents). On subsequent sign-ins, only the userIdentifier and identityToken are providedβ€”your backend uses the token to verify the user and look them up in your database.
1

Add Sign In with Apple Capability

In Xcode project settings

  • Open your project in Xcode
  • Select your target β†’ Signing & Capabilities tab
  • Click + Capability β†’ Add "Sign in with Apple"

Apple Developer Account Required

You need an Apple Developer account and your app must be registered in App Store Connect for Sign In with Apple to work.
2

Set Up the Delegate

To receive authentication credentials

The SDK handles the Apple Sign In UI automatically. You just need to set up a delegate to receive the credentials and send them to your backend:

AppDelegate.swift
import UIKit
import Sequence

@main
class AppDelegate: UIResponder, UIApplicationDelegate, SequenceDelegate {

    func application(_ application: UIApplication,
                     didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {

        // Configure Sequence
        Sequence.shared.configure(
            appId: "your-app-id",
            apiKey: "your-api-key",
            baseURL: "https://screensequence.com"
        )

        // Set the delegate to receive auth callbacks
        Sequence.shared.delegate = self

        return true
    }

    // MARK: - SequenceDelegate

    func sequence(_ sequence: Sequence, didCompleteAppleSignIn credential: AppleAuthCredential) {
        // credential contains:
        // - userIdentifier: Unique user ID (stable across app reinstalls)
        // - identityToken: JWT to verify with Apple's servers
        // - authorizationCode: One-time code for your backend
        // - email: User's email (only on first sign in)
        // - fullName: User's name (only on first sign in)

        guard let token = credential.identityTokenString else {
            Sequence.shared.sendFailureResult(error: "No identity token")
            return
        }

        // Send token to your backend for verification
        // Your backend verifies with Apple, then creates/retrieves the user
        Task {
            do {
                try await YourAuthService.signInWithApple(token: token)

                // Success! Tell Sequence to advance the flow
                Sequence.shared.sendSuccessResult()
            } catch {
                // Auth failed - tell Sequence so it can handle the error
                Sequence.shared.sendFailureResult(error: error.localizedDescription)
            }
        }
    }

    func sequence(_ sequence: Sequence, didFailAppleSignIn error: Error) {
        print("Apple Sign In failed: \(error.localizedDescription)")
        // SDK already sends failure result, flow won't advance
    }
}
3

Add the Button in the Editor

Drag from the component palette

  • In the Sequence editor, open the Components panel
  • Drag the Sign in with Apple button onto your screen
  • The button is pre-configured with the correct action identifier
  • Publish your flow and test on a real device

Test on Real Device

Sign In with Apple does not work in the iOS Simulator. Always test on a real device.

That's it!

When users tap the button, the native Apple Sign In sheet appears. On success, your delegate receives the credentials and the flow automatically advances to the next screen.

Sign In with Google

Google Sign In requires the Google Sign-In SDK, which you must integrate separately. The Sequence SDK notifies your delegate when Google Sign In is requested.

1

Install Google Sign-In SDK

Add the GoogleSignIn package

Package URL
https://github.com/google/GoogleSignIn-iOS

Follow Google's setup instructions for configuring your OAuth client ID and adding the URL scheme to your Info.plist.

2

Handle in Your Delegate

Implement the custom action handler

AppDelegate.swift
import GoogleSignIn

extension AppDelegate: SequenceDelegate {

    func sequence(_ sequence: Sequence,
                  didReceiveCustomAction identifier: String,
                  awaitingResult: Bool) -> Bool {

        if identifier == "auth.google" {
            // Present Google Sign In
            guard let windowScene = UIApplication.shared.connectedScenes.first as? UIWindowScene,
                  let rootVC = windowScene.windows.first?.rootViewController else {
                Sequence.shared.sendFailureResult(error: "No root view controller")
                return true
            }

            GIDSignIn.sharedInstance.signIn(withPresenting: rootVC) { result, error in
                if let error = error {
                    Sequence.shared.sendFailureResult(error: error.localizedDescription)
                    return
                }

                guard let user = result?.user,
                      let idToken = user.idToken?.tokenString else {
                    Sequence.shared.sendFailureResult(error: "No ID token")
                    return
                }

                // Send credentials to your backend
                self.handleGoogleCredentials(idToken: idToken, user: user)

                // Tell Sequence to advance the flow
                Sequence.shared.sendSuccessResult(data: [
                    "email": user.profile?.email ?? "",
                    "name": user.profile?.name ?? ""
                ])
            }

            return true // We handled this action
        }

        return false // Not handled
    }
}

Continue with Email

Email authentication gives users a password-based sign-in option. Unlike Apple Sign In, the SDK delegates this to your app since you need to handle password collection and storage with your own backend (e.g., Supabase Auth, Firebase Auth).

!

Important: New vs Returning Users

You cannot reliably check if an email exists before authentication (most auth providers don't expose this for security reasons). Instead, ask the user if they have an existing account, then handle errors gracefully if they choose wrong.

1

Handle the Delegate Callback

Ask users if they have an account

AppDelegate.swift
extension AppDelegate: SequenceDelegate {

    func sequenceDidRequestEmailSignIn(_ sequence: Sequence) {
        guard let windowScene = UIApplication.shared.connectedScenes.first as? UIWindowScene,
              let rootVC = windowScene.windows.first?.rootViewController else {
            Sequence.shared.sendFailureResult(error: "No root view controller")
            return
        }

        // Ask user if they have an account (don't try to auto-detect!)
        let alert = UIAlertController(
            title: "Continue with Email",
            message: "Do you have an existing account?",
            preferredStyle: .alert
        )

        alert.addAction(UIAlertAction(title: "Sign In", style: .default) { _ in
            self.showSignInFlow(on: rootVC)
        })

        alert.addAction(UIAlertAction(title: "Create Account", style: .default) { _ in
            self.showSignUpFlow(on: rootVC)
        })

        alert.addAction(UIAlertAction(title: "Cancel", style: .cancel) { _ in
            Sequence.shared.sendCancelledResult()
        })

        rootVC.present(alert, animated: true)
    }
}
2

Handle Sign In (Returning Users)

Skip onboarding for existing users

AppDelegate.swift
private func showSignInFlow(on viewController: UIViewController) {
    // Collect email and password from user...

    Task {
        do {
            // 1. Authenticate with your backend
            try await AuthService.shared.signIn(email: email, password: password)

            // 2. Verify with Sequence backend
            let result = try await Sequence.shared.verifyEmailAuth(
                email: email,
                givenName: nil,
                familyName: nil
            )

            // 3. For returning users, exit onboarding entirely
            //    RootView should then check subscription status and navigate appropriately
            if !result.isNewUser {
                Sequence.shared.markOnboardingCompleted()
            }

            // 4. Send success to advance the flow
            Sequence.shared.sendSuccessResult(data: [
                "email": email,
                "isNewUser": result.isNewUser
            ])
        } catch {
            // Show error, let user retry
            Sequence.shared.sendFailureResult(error: error.localizedDescription)
        }
    }
}

Calling markOnboardingCompleted() for returning users ensures they go directly to your main app (or paywall) instead of continuing through onboarding screens meant for new users.

3

Handle Sign Up (New Users)

Gracefully handle "already exists" errors

AppDelegate.swift
private func showSignUpFlow(on viewController: UIViewController) {
    // Collect email and password from user...

    Task {
        do {
            try await AuthService.shared.signUp(email: email, password: password)

            // Verify with Sequence
            let result = try await Sequence.shared.verifyEmailAuth(
                email: email,
                givenName: username,
                familyName: nil
            )

            Sequence.shared.sendSuccessResult(data: [
                "email": email,
                "isNewUser": true
            ])
        } catch {
            let errorMsg = error.localizedDescription.lowercased()

            // If account already exists, redirect to sign in
            if errorMsg.contains("already") ||
               errorMsg.contains("registered") ||
               errorMsg.contains("exists") {
                showSignInFlow(on: viewController)  // Redirect to sign in
            } else {
                // Other error - let user retry sign up
                Sequence.shared.sendFailureResult(error: error.localizedDescription)
            }
        }
    }
}

When a user selects "Create Account" but their email already exists, redirect them to sign in instead of showing an error. This creates a smoother experience when users forget they already have an account.

Backend Auth API

Sequence provides backend API endpoints to verify authentication tokens and track new vs returning users. These endpoints verify tokens with Apple/Google, manage user records, and return whether a user is new or returning.

When to Use These Endpoints

Use these endpoints after receiving credentials from native sign-in (Apple, Google, or Email). The SDK's verifyAppleAuth, verifyGoogleAuth, and verifyEmailAuth methods call these endpoints automatically.

POST /api/v1/auth/apple

Verify Apple Sign In credentials and track user authentication.

Request
POST /api/v1/auth/apple
Content-Type: application/json
x-api-key: your-api-key

{
  "identity_token": "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9...",
  "user_identifier": "001234.abcd1234efgh5678.1234",
  "email": "user@example.com",        // Optional (only on first sign-in)
  "given_name": "John",               // Optional (only on first sign-in)
  "family_name": "Doe"                // Optional (only on first sign-in)
}
Response
{
  "is_new_user": true,   // false for returning users
  "user_id": "uuid-of-user-record"
}

POST /api/v1/auth/google

Verify Google Sign In credentials and track user authentication.

Request
POST /api/v1/auth/google
Content-Type: application/json
x-api-key: your-api-key

{
  "id_token": "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9...",
  "email": "user@gmail.com",          // Optional
  "given_name": "John",               // Optional
  "family_name": "Doe"                // Optional
}
Response
{
  "is_new_user": true,   // false for returning users
  "user_id": "uuid-of-user-record"
}

POST /api/v1/auth/email

Track email-based authentication. Unlike Apple/Google, this endpoint does not verify tokensβ€”your app handles the actual authentication with your auth provider.

Request
POST /api/v1/auth/email
Content-Type: application/json
x-api-key: your-api-key

{
  "email": "user@example.com",
  "given_name": "John",               // Optional
  "family_name": "Doe"                // Optional
}
Response
{
  "is_new_user": true,   // false for returning users
  "user_id": "uuid-of-user-record"
}

Using the Response

The is_new_user field determines how to handle the user in your app:

Example Usage
// After receiving auth credentials...
let result = try await Sequence.shared.verifyAppleAuth(
    identityToken: token,
    userIdentifier: userId,
    email: email,
    givenName: givenName,
    familyName: familyName
)

if result.isNewUser {
    // Continue onboarding flow for new users
    Sequence.shared.sendSuccessResult()
} else {
    // Skip to main app for returning users
    Sequence.shared.markOnboardingCompleted()
    Sequence.shared.sendSuccessResult()
}

Permissions

The SDK handles all iOS permission requests automatically. Just add the required Info.plist entries and use the appropriate button in your flow.

Info.plist Requirements

Add the usage description keys for each permission you request. iOS requires these strings to explain why your app needs access.

Info.plist entries
<!-- Notifications -->
<key>NSUserNotificationsUsageDescription</key>
<string>We'll send you reminders and updates about your progress.</string>

<!-- Location -->
<key>NSLocationWhenInUseUsageDescription</key>
<string>We use your location to show nearby content.</string>

<key>NSLocationAlwaysAndWhenInUseUsageDescription</key>
<string>We use your location to send you relevant notifications.</string>

<!-- Camera -->
<key>NSCameraUsageDescription</key>
<string>We need camera access to let you take photos.</string>

<!-- Photos -->
<key>NSPhotoLibraryUsageDescription</key>
<string>We need access to your photos to let you choose a profile picture.</string>

<!-- Tracking (iOS 14+) -->
<key>NSUserTrackingUsageDescription</key>
<string>We use this to provide personalized ads and improve your experience.</string>

Permission Buttons

Each permission has a pre-configured button in the editor's Components panel:

Notifications

permission.notifications

Location

permission.location

Camera

permission.camera

πŸ–ΌοΈ
Photos

permission.photos

Result Handling

The SDK sends back the permission result (granted or denied) via the callback. By default, the flow advances regardless of the result. Configure custom "On Success" and "On Failure" actions in the button properties if you need different behavior.

App Review

Request an App Store review from within your onboarding flow. This uses Apple's StoreKit API, which may or may not show the review prompt depending on Apple's guidelines.

Add in Editor
// The button uses this action identifier:
system.review

// Or create manually:
{
  "type": "custom",
  "identifier": "system.review",
  "awaitResult": true,
  "onSuccess": { "type": "next" }
}

Apple Guidelines

Apple limits how often the review prompt is shown. The API may not display the prompt every time it's called. The flow will still advance regardless.

Custom Handlers

For any action identifier not built into the SDK, implement the delegate method to handle it yourself:

Handling Custom Actions
extension AppDelegate: SequenceDelegate {

    func sequence(_ sequence: Sequence,
                  didReceiveCustomAction identifier: String,
                  awaitingResult: Bool) -> Bool {

        switch identifier {
        case "myapp.connectFitness":
            // Present your fitness app connection UI
            connectToFitnessApp { success in
                if success {
                    Sequence.shared.sendSuccessResult()
                } else {
                    Sequence.shared.sendFailureResult(error: "Connection failed")
                }
            }
            return true

        case "myapp.skipForNow":
            // Just advance without doing anything
            Sequence.shared.sendSuccessResult()
            return true

        default:
            return false // Not handled, let SDK handle or show error
        }
    }
}

Result Methods

When handling custom actions that await a result, use these methods to send the outcome:

MethodWhen to UseEffect
sendSuccessResult(data:)Action completed successfullyExecutes onSuccess action (default: next screen)
sendFailureResult(error:)Action failedExecutes onFailure action (if configured)
sendCancelledResult()User cancelledExecutes onCancel action (if configured)
Result Methods
// Success with optional data
Sequence.shared.sendSuccessResult(data: [
    "userId": "12345",
    "planType": "premium"
])

// Success without data
Sequence.shared.sendSuccessResult()

// Failure with error message
Sequence.shared.sendFailureResult(error: "Network connection failed")

// User cancelled
Sequence.shared.sendCancelledResult()

Migration Guide

This guide walks you through replacing your existing onboarding implementation with Sequence. Whether you have a simple tutorial or a complex multi-screen flow, we'll help you migrate smoothly.

Before You Start

Audit Your Current Onboarding

Document your existing onboarding to plan the migration:

  • How many screens do you have?
  • What data do you collect from users?
  • What actions happen on each screen (permissions, API calls)?
  • How do you track completion status?
  • Do you have any A/B tests running?

Prepare Your Sequence Dashboard

  • Create your app in the Sequence dashboard
  • Copy your appId and apiKey from Settings
  • Create a new Flow that mirrors your existing screens

Feature Parity

Ensure your Sequence flow replicates all critical functionality before removing your old onboarding. Use the preview feature to test thoroughly.

Step-by-Step Migration

Step 1: Install the SDK

Add Sequence alongside your existing onboarding (don't remove anything yet):

In Xcode, go to File β†’ Add Packages and enter:

Package URL
https://github.com/Musgrav/sequence-swift

Step 2: Initialize the SDK

Configure Sequence without changing existing functionality:

MyApp.swift
import Sequence

@main
struct MyApp: App {
    init() {
        Sequence.shared.configure(
            appId: "your-app-id",
            apiKey: "your-api-key"
        )
    }

    var body: some Scene {
        WindowGroup {
            // Your existing app code - unchanged
            ExistingAppWithOldOnboarding()
        }
    }
}

Step 3: Create a Feature Flag

Use a feature flag to gradually roll out the new onboarding:

OnboardingType.swift
import Foundation

enum OnboardingType: String {
    case legacy
    case sequence
}

class OnboardingTypeManager: ObservableObject {
    @Published var type: OnboardingType = .legacy

    init() {
        checkOnboardingType()
    }

    private func checkOnboardingType() {
        // Check if user should see new onboarding
        // Options: remote config, random assignment, user cohort, etc.
        if let assigned = UserDefaults.standard.string(forKey: "onboarding_type"),
           let onboardingType = OnboardingType(rawValue: assigned) {
            self.type = onboardingType
            return
        }

        // Random 50/50 assignment for new users
        let newType: OnboardingType = Bool.random() ? .sequence : .legacy
        UserDefaults.standard.set(newType.rawValue, forKey: "onboarding_type")
        self.type = newType
    }
}

Step 4: Implement Side-by-Side

Show either the old or new onboarding based on the flag:

OnboardingGate.swift
import SwiftUI
import Sequence

struct OnboardingGate<Content: View>: View {
    @StateObject private var typeManager = OnboardingTypeManager()
    @StateObject private var sequence = Sequence.shared
    @State private var showLegacy = true
    let content: () -> Content

    var body: some View {
        Group {
            if typeManager.type == .legacy && showLegacy {
                LegacyOnboarding { data in
                    handleOnboardingData(data)
                    showLegacy = false
                }
            } else if typeManager.type == .sequence && !sequence.isOnboardingCompleted {
                WebViewOnboardingView {
                    // Onboarding completed
                }
            } else {
                content()
            }
        }
    }

    private func handleOnboardingData(_ data: [String: Any]) {
        // Your existing data handling logic
        // Works the same for both onboarding types
        saveUserPreferences(data)
        trackOnboardingComplete(data)
    }
}

Step 5: Handle Custom Actions

If your onboarding has custom screens (login, permissions), use the delegate pattern:

// Set up the delegate to handle custom actions
Sequence.shared.delegate = self

// Implement the delegate methods
extension YourViewController: SequenceDelegate {
    func sequence(_ sequence: Sequence, didRequestNativeScreen screenId: String, data: [String: Any]) -> Bool {
        if screenId == "login-screen" {
            // Show your existing login UI
            presentLoginScreen {
                // Continue onboarding after login
                Sequence.shared.track(.screenCompleted, screenId: screenId)
            }
            return true // Indicates you're handling this screen
        }
        return false
    }

    func sequence(_ sequence: Sequence, didTriggerCustomAction action: String, screenId: String) {
        switch action {
        case "request-notifications":
            requestNotificationPermission()
        case "connect-social":
            showSocialConnect()
        default:
            break
        }
    }
}

Step 6: Monitor and Iterate

Compare metrics between old and new onboarding:

  • Completion rates (visible in Sequence Analytics)
  • Time to complete
  • Drop-off points
  • User feedback

Step 7: Remove Legacy Code

Once you're confident in the new onboarding, remove the feature flag and legacy implementation:

// Final clean implementation
@main
struct MyApp: App {
    init() {
        Sequence.shared.configure(
            appId: "your-app-id",
            apiKey: "your-api-key"
        )
    }

    var body: some Scene {
        WindowGroup {
            ContentView()
        }
    }
}

struct ContentView: View {
    @StateObject private var sequence = Sequence.shared

    var body: some View {
        Group {
            if sequence.isOnboardingCompleted {
                MainAppView()
            } else {
                WebViewOnboardingView {
                    // Onboarding completed
                }
            }
        }
    }
}

Data Mapping

Map your existing data collection to Sequence block IDs:

Your Current FieldSequence Block TypeBlock ID
Name inputinputuser_name
Email input[object Object] (email type)user_email
Interest selectionchecklistinterests
Experience slidersliderexperience_level

Keep Block IDs Consistent

Use the same block IDs as your existing field names. This way, your data handling code works without modification.

Testing Your Migration

Pre-Launch Checklist

  • All screens render correctly on iOS and Android
  • Navigation between screens works as expected
  • All data is collected and passed to onComplete
  • Custom actions (permissions, login) function properly
  • Analytics events are appearing in the dashboard
  • Offline behavior works (cached config loads)
  • Completion state persists across app restarts

Testing Commands

Reset onboarding state for testing
import Sequence

// Clear completion status to re-show onboarding
Sequence.shared.reset()

// Or reset just the onboarding state
Sequence.shared.resetOnboarding()

SDK Reference

Complete API reference for the Sequence Swift SDK. All classes and methods are available from the Sequence module.

import Sequence

// Core singleton
Sequence.shared

// Configuration
Sequence.shared.configure(appId: String, apiKey: String, baseURL: String?)

// User identification
Sequence.shared.identify(userId: String)
Sequence.shared.reset()

// Event tracking
Sequence.shared.track(_ eventType: EventType, screenId: String?, properties: [String: Any]?)

// Onboarding state
Sequence.shared.isOnboardingCompleted
Sequence.shared.resetOnboarding()

Sequence Client

The Sequence.shared singleton manages SDK state, API communication, and event tracking. Initialize it in your app's entry point.

Methods

MethodParametersReturnsDescription
configure()appId: String, apiKey: String, baseURL: String?VoidInitialize the SDK with your credentials
fetchConfig()noneasync throws -> OnboardingConfigFetch onboarding configuration from API
identify()userId: StringVoidAssociate events with a user ID
reset()noneVoidClear user data and completion status
track()_ eventType: EventType, screenId: String?, properties: [String: Any]?VoidTrack a custom event

Usage Examples

Using the Sequence singleton
import Sequence

class AuthManager {
    // Identify user after login
    func handleLogin(userId: String) {
        Sequence.shared.identify(userId: userId)
    }

    // Track custom event
    func handleButtonPress() {
        Sequence.shared.track(.buttonTapped, screenId: "welcome-screen", properties: [
            "button_label": "Get Started"
        ])
    }

    // Reset on logout
    func handleLogout() {
        Sequence.shared.reset()
    }
}

Configuration

Configure the Sequence SDK in your app's entry point. The configuration must be called before using any other SDK methods.

Parameters

ParameterTypeRequiredDescription
appIdStringYesYour app ID from the dashboard
apiKeyStringYesYour API key (keep secret)
baseURLString?NoCustom API URL (optional)

Configuration Example

import Sequence

// Configure in your App init
Sequence.shared.configure(
    appId: "your-app-id",
    apiKey: "your-api-key",
    baseURL: "https://screensequence.com"
)

Properties

Observable Properties

Sequence.shared is an ObservableObject, so you can use @StateObject or @ObservedObject to observe changes.

import SwiftUI
import Sequence

struct MyView: View {
    @StateObject private var sequence = Sequence.shared

    var body: some View {
        VStack {
            // Observe configuration state
            if sequence.isConfigured {
                Text("SDK is configured")
            }

            // Observe loading state
            if sequence.isLoading {
                ProgressView()
            }

            // Observe onboarding completion
            if sequence.isOnboardingCompleted {
                MainAppView()
            } else {
                OnboardingView { }
            }

            // Access screens
            Text("\(sequence.screens.count) screens in this flow")

            // Access experiment info
            if let experiment = sequence.experimentInfo {
                Text("Variant: \(experiment.variantName)")
            }
        }
    }
}

Available Properties

PropertyTypeDescription
isConfiguredBoolSDK has been initialized
isLoadingBoolCurrently fetching config
errorError?Last error encountered
configOnboardingConfig?Current onboarding configuration
screens[Screen]Array of onboarding screens
isOnboardingCompletedBoolWhether onboarding is complete
experimentInfoExperimentInfo?A/B test variant information

Views

WebViewOnboardingView (Recommended)

Full-screen SwiftUI view that renders your onboarding flow using a WebView for pixel-perfect WYSIWYG rendering. This is the recommended view to use for production apps.

ParameterTypeRequiredDescription
onComplete(() -&gt; Void)?NoCalled when onboarding completes
onDataCollected(([String: Any]) -&gt; Void)?NoCalled with collected user data

Why WebViewOnboardingView?

WebViewOnboardingView renders your flows exactly as designed in the editor with proper scaling on all device sizes. Always use this instead of OnboardingView for production apps.
WebViewOnboardingView example
import SwiftUI
import Sequence

struct ContentView: View {
    @StateObject private var sequence = Sequence.shared

    var body: some View {
        Group {
            if sequence.isOnboardingCompleted {
                MainAppView()
            } else {
                WebViewOnboardingView(
                    onComplete: {
                        print("Onboarding completed!")
                    },
                    onDataCollected: { data in
                        print("User data:", data)
                        saveToBackend(data)
                    }
                )
            }
        }
    }
}

OnboardingView (Legacy)

Native SwiftUI view that renders an approximation of your onboarding flow. Not recommended for production use as it may not match the editor exactly.

Avoid OnboardingView

OnboardingView uses native SwiftUI components which may not render identically to what you see in the editor. Always use WebViewOnboardingView for production apps.

Delegate Protocol

Implement SequenceDelegate to handle custom screens and actions.

import Sequence

class MyAppDelegate: SequenceDelegate {
    // Handle custom native screens
    func sequence(_ sequence: Sequence, didRequestNativeScreen screenId: String, data: [String: Any]) -> Bool {
        if screenId == "auth-screen" {
            showAuthFlow()
            return true // We're handling this
        }
        return false // Let Sequence handle it
    }

    // Handle custom button actions
    func sequence(_ sequence: Sequence, didTriggerCustomAction action: String, screenId: String) {
        if action == "open-settings" {
            if let url = URL(string: UIApplication.openSettingsURLString) {
                UIApplication.shared.open(url)
            }
        }
    }
}

// Set the delegate
Sequence.shared.delegate = MyAppDelegate()

Swift Types

Key types used in the Swift SDK:

Core Types
// Onboarding configuration from API
struct OnboardingConfig: Codable {
    let version: Int
    let screens: [Screen]
    let progressIndicator: FlowProgressIndicator?
    let transitions: [ScreenTransitionConfig]?
    let experiment: ExperimentInfo?
}

// Individual screen
struct Screen: Codable, Identifiable {
    let id: String
    let appId: String
    let flowId: String?
    let name: String
    let type: ScreenType
    let order: Int
    let content: ScreenContent
    let transition: ScreenTransition?
    let createdAt: String
    let updatedAt: String
}

enum ScreenType: String, Codable {
    case welcome
    case feature
    case carousel
    case permission
    case celebration
    case native
    case filler
}

// Event types for tracking
enum EventType {
    case screenViewed
    case screenFirstViewed
    case screenCompleted
    case screenSkipped
    case screenDroppedOff
    case buttonTapped
    case onboardingStarted
    case onboardingCompleted
    case experimentVariantAssigned
}
Block Types
enum BlockType: String, Codable {
    case text
    case image
    case button
    case input
    case checklist
    case slider
    case progress
    case spacer
    case divider
    case icon
    case featureCard = "feature-card"
}

// Experiment info
struct ExperimentInfo: Codable {
    let id: String
    let variantId: String
    let variantName: String
}

Full Type Definitions

For complete type definitions, check the Swift SDK source code or use Xcode's code completion.
Sequence Documentation
Found an issue? Let us know