13 KiB
Onboarding Flow
Related spec: spec/architecture.md | spec/database.md
Overview
First-time setup and migration flows for SimpleX Chat iOS. Covers app initialization, profile creation, server operator selection, notification configuration, and database import/export for device migration. The app uses a Haskell runtime for its core chat engine, with SQLite databases shared between the main app and the Notification Service Extension (NSE).
Prerequisites
- Fresh install of SimpleX Chat from the App Store, or
- Existing install with database archive for import/migration
- iOS 15+ with App Group entitlement configured
Step-by-Step Processes
1. App Initialization Sequence
On every app launch, SimpleXApp.init() executes the following in order:
1. haskell_init() -- Start Haskell runtime system (GHC RTS)
2. UserDefaults.standard.register(defaults:) -- Set default preferences (appDefaults)
3. setGroupDefaults() -- Configure app group shared defaults
4. registerGroupDefaults() -- Register group container defaults
5. setDbContainer() -- Configure database paths in app group container
6. BGManager.shared.register() -- Register background task handlers
7. NtfManager.shared.registerCategories() -- Register notification action categories
Then in ContentView.onAppear:
- If no migration is in progress and authentication is set up,
initChatAndMigrate()is called. - This triggers
chatMigrateInit()to initialize/migrate databases. - Then
startChat()is called to start the chat engine.
2. Fresh Install -- Onboarding Steps
Onboarding is managed by OnboardingStage enum and OnboardingView:
Step 1: SimpleX Info (step1_SimpleXInfo)
SimpleXInfoview is presented.- Explains SimpleX's architecture: no user identifiers, E2E encryption, decentralized servers.
- User taps "Create your profile" to proceed.
Step 2: Create Profile (step2_CreateProfile -- now inline in step 1)
CreateFirstProfileview (embedded in the onboarding flow).- User enters display name (required). Full name is set to empty string.
- Display name is validated via
mkValidName()andcanCreateProfile(). - On "Create":
AppChatState.shared.set(.active) m.currentUser = try apiCreateActiveUser(profile) try startChat() apiCreateActiveUser(Profile(displayName:fullName:shortDescr:))creates the user in the Haskell core.startChat()initializes the chat engine.- Onboarding advances to
step3_ChooseServerOperators.
Step 3: Choose Server Operators (step3_ChooseServerOperators)
OnboardingConditionsViewis presented (simplified conditions acceptance).- User reviews and accepts server operator conditions.
- This configures which SMP/XFTP server operators to use.
- Advances to
step4_SetNotificationsMode.
Step 4: Set Notifications (step4_SetNotificationsMode)
SetNotificationsModeview is presented.- Three options:
- Instant: Requires Apple Push Notification service. Registers device token via
apiRegisterToken(token:notificationMode:). - Periodic: Uses iOS background app refresh. No push token needed.
- Off: No notifications.
- Instant: Requires Apple Push Notification service. Registers device token via
- For instant mode:
apiRegisterTokensendsChatCommand.apiRegisterToken(token:notificationMode:)and receivesChatResponse2.ntfTokenStatus(status). - On completion:
onboardingStageDefault.set(.onboardingComplete).
Onboarding Complete (onboardingComplete)
ChatListViewis shown.- Empty state displays "Add contact" prompt via
ChatHelp. - If delivery receipts haven't been configured:
chatModel.setDeliveryReceipts = truetriggers a prompt.
3. startChat() -- Chat Engine Startup
Called after profile creation or on subsequent app launches:
func startChat(refreshInvitations: Bool = true, onboarding: Bool = false) throws {
1. setNetworkConfig(getNetCfg()) -- Apply network configuration
2. apiCheckChatRunning() -- Check if already running
3. listUsers() -- Load all user profiles
4. getUserChatData() -- Load chats, tags, address, TTL
5. NtfManager.shared.setNtfBadgeCount(...) -- Set badge count
6. refreshCallInvitations() -- Check pending call invitations
7. apiGetNtfToken() -- Get notification token status
8. apiStartChat() -- Start the Haskell chat engine
9. registerToken(token:) -- Register push token if available
10. ChatReceiver.shared.start() -- Start message receive loop
}
4. Database Setup
Location:
- App group container (shared with NSE): determined by
dbContainerGroupDefault - Path prefix:
simplex_v1(DB_FILE_PREFIX) - Chat database:
simplex_v1_chat.db(messages, contacts, groups, settings) - Agent database:
simplex_v1_agent.db(SMP connections, encryption keys, queues)
Initialization:
chatMigrateInit(useKey:confirmMigrations:backgroundMode:)inSimpleXChat/API.swift.- Creates databases if they do not exist.
- Runs pending migrations with confirmation mode.
- Handles database encryption:
- If keychain storage enabled: generates random DB key on first run (
randomDatabasePassword()). - Stores key in keychain via
kcDatabasePassword. initialRandomDBPassphraseGroupDefaulttracks whether using auto-generated key.
- If keychain storage enabled: generates random DB key on first run (
Encryption:
- Optional database encryption passphrase via
DatabaseEncryptionView. apiStorageEncryption(currentKey:newKey:)changes encryption key.testStorageEncryption(key:)validates a key against the database.
5. Database Export (Source Device)
- User navigates to Settings -> Database -> "Export database".
- Chat must be stopped first for data consistency.
- Calls
apiExportArchive(config: ArchiveConfig):func apiExportArchive(config: ArchiveConfig) async throws -> [ArchiveError] - Core creates a ZIP archive containing both databases and file attachments.
- Returns any non-fatal
[ArchiveError](e.g., file access issues). - User transfers the archive to the new device via AirDrop, file share, etc.
6. Database Import (Destination Device)
- On new device: during onboarding or Settings -> Database -> "Import database".
- User selects the archive file.
- Calls
apiImportArchive(config: ArchiveConfig):func apiImportArchive(config: ArchiveConfig) async throws -> [ArchiveError] - Core extracts the archive, replacing local databases.
- Returns any non-fatal
[ArchiveError]. - Chat engine is restarted with the imported data.
- All contacts, groups, messages, and settings are restored.
7. In-App Device Migration
An alternative to manual export/import using direct device-to-device transfer.
Source device (MigrateFromDevice view):
- User navigates to Settings -> Database -> "Migrate to another device".
- App creates a temporary database and uploads archive via XFTP standalone file.
- Generates a migration link containing the file URL and encryption key.
- Displays QR code / share link for the destination device.
Destination device (MigrateToDevice view):
- On new device: onboarding detects migration state or user selects "Migrate".
- Scans/pastes the migration link.
downloadStandaloneFile(user:url:file:ctrl:)downloads the archive from XFTP.standaloneFileInfo(url:ctrl:)validates the file metadata.- Archive is imported, databases are restored.
chatInitTemporaryDatabase(url:key:confirmation:)may be used for temporary DB operations during migration.- Chat engine starts with the migrated data.
If migration is interrupted:
chatModel.migrationStatepreserves state across app restarts.- On next launch,
ContentView.onAppeardetects pending migration and resumes.
8. Additional Profile Creation (Multi-Account)
- From
UserPicker(profile switcher) -> "Add profile". CreateProfileview is presented (distinct fromCreateFirstProfile).- User enters display name and optional bio (max 160 bytes JSON-encoded,
MAX_BIO_LENGTH_BYTES). apiCreateActiveUser(profile)creates additional user.listUsers()andgetUserChatData()refresh the model.- No onboarding steps -- goes directly to chat list.
Data Structures
| Type | Location | Description |
|---|---|---|
OnboardingStage |
Shared/Views/Onboarding/OnboardingView.swift |
Enum: step1_SimpleXInfo, step2_CreateProfile, step3_ChooseServerOperators, step4_SetNotificationsMode, onboardingComplete |
Profile |
SimpleXChat/ChatTypes.swift |
displayName, fullName, image, shortDescr |
User |
SimpleXChat/ChatTypes.swift |
Full user model with profile, userId, and settings |
ArchiveConfig |
SimpleXChat/APITypes.swift |
Configuration for database export/import |
DBMigrationResult |
SimpleXChat/API.swift |
Result of database migration: .ok, .errorNotADatabase, .errorKeychain, etc. |
MigrationConfirmation |
SimpleXChat/API.swift |
Migration confirmation mode: .error, .yesUp, .yesUpDown |
DeviceToken |
SimpleXChat/ChatTypes.swift |
Apple push notification device token |
NtfTknStatus |
SimpleXChat/ChatTypes.swift |
Notification token status: registered, active, expired, etc. |
NotificationsMode |
SimpleXChat/ChatTypes.swift |
.off, .periodic, .instant |
MigrationFileLinkData |
Used in standalone file transfers for device migration | |
AppChatState |
SimpleXChat/ |
Shared state: .active, .stopped, .suspended |
Error Cases
| Error | Cause | Handling |
|---|---|---|
DBMigrationResult.errorNotADatabase |
Wrong encryption key or corrupt DB | Show DatabaseErrorView with options |
DBMigrationResult.errorKeychain |
Keychain access failed | Show error, offer to re-enter passphrase |
DBMigrationResult.errorMigration |
Schema migration failure | Show error with migration details |
duplicateUserError |
Display name already in use | UserProfileAlert.duplicateUserError |
invalidDisplayNameError |
Invalid characters in display name | UserProfileAlert.invalidDisplayNameError |
createUserError |
Core failed to create user | Alert with error details |
invalidNameError(validName) |
Name needs normalization | Alert suggesting the valid name |
| Archive import errors | Missing files, version mismatch | Non-fatal [ArchiveError] displayed |
| Migration interrupted | Network failure, app killed | State preserved in chatModel.migrationState, resumed on next launch |
Key Files
| File | Purpose |
|---|---|
Shared/SimpleXApp.swift |
App entry point: haskell_init, defaults registration, DB container setup, BG tasks |
Shared/AppDelegate.swift |
Push notification registration, URL handling |
Shared/ContentView.swift |
Root view: authentication, onboarding routing, chat initialization |
Shared/Views/Onboarding/OnboardingView.swift |
Onboarding step router, OnboardingStage enum |
Shared/Views/Onboarding/SimpleXInfo.swift |
Step 1: Privacy architecture explanation |
Shared/Views/Onboarding/CreateProfile.swift |
Profile creation: CreateProfile (additional) and CreateFirstProfile (onboarding) |
Shared/Views/Onboarding/ChooseServerOperators.swift |
Step 3: Server operator conditions |
Shared/Views/Onboarding/SetNotificationsMode.swift |
Step 4: Notification mode selection |
Shared/Views/Onboarding/CreateSimpleXAddress.swift |
Optional address creation during onboarding |
Shared/Views/Onboarding/HowItWorks.swift |
Educational content about SimpleX protocol |
Shared/Views/Migration/MigrateFromDevice.swift |
Source device migration UI |
Shared/Views/Migration/MigrateToDevice.swift |
Destination device migration UI |
Shared/Views/Database/DatabaseView.swift |
Database management: export, import, encryption |
Shared/Views/Database/DatabaseEncryptionView.swift |
Database passphrase management |
Shared/Views/Database/DatabaseErrorView.swift |
Database error recovery UI |
Shared/Views/Database/MigrateToAppGroupView.swift |
Legacy migration from Documents to App Group container |
Shared/Model/SimpleXAPI.swift |
startChat, apiCreateActiveUser, apiExportArchive, apiImportArchive, apiRegisterToken |
SimpleXChat/API.swift |
chatMigrateInit, chatInitTemporaryDatabase, low-level DB initialization |
SimpleXChat/FileUtils.swift |
DB file paths, constants (DB_FILE_PREFIX, CHAT_DB, AGENT_DB) |
SimpleXChat/AppGroup.swift |
App group container configuration |
SimpleXChat/KeyChain.swift |
Keychain access for DB passphrase and app passwords |
Shared/Model/BGManager.swift |
Background task registration and scheduling |
Shared/Model/NtfManager.swift |
Notification management and badge counts |
Related Specifications
apps/ios/product/README.md-- Product overview: architecture and capabilitiesapps/ios/product/flows/connection.md-- After onboarding, user establishes first connectionsapps/ios/product/flows/messaging.md-- Messaging starts after profile creation