This comprehensive guide is designed to help new developers understand the Flipcash iOS codebase. It covers architecture, patterns, and implementation details across all major systems.
- Project Architecture & Structure
- Authentication & Session Management
- Multi-Currency System
- CodeScanner (2D Code System)
- Database & Persistence
- gRPC & Networking
- UI Architecture
- Intent & Action System
- Controllers & Business Logic
- Cryptography & Key Management
- Transaction System
- Testing Patterns
The project uses a hybrid architecture: Swift Package Manager (SPM) for business logic combined with an Xcode project wrapper for the iOS app.
code-ios-app/
├── Flipcash/ # Main iOS app (Xcode project)
├── FlipcashCore/ # Business logic (SPM)
├── FlipcashUI/ # UI components (SPM)
├── FlipcashAPI/ # gRPC proto definitions - Flipcash API (SPM)
├── FlipcashCoreAPI/ # gRPC proto definitions - Payments API (SPM)
├── CodeCurves/ # Ed25519 cryptography (SPM)
├── CodeScanner/ # C++ circular code scanning
├── CodeServices/ # Legacy shared services (DO NOT import in Flipcash)
└── Code.xcodeproj/ # Xcode project file
Flipcash App
│
┌───────────────┼───────────────┐
│ │ │
▼ ▼ ▼
FlipcashCore FlipcashUI CodeScanner
│ │
▼ │
┌─────┴─────┐ │
│ │ ▼
FlipcashAPI FlipcashCoreAPI
│ │
└─────┬─────┘
│
▼
CodeCurves
Critical Rule: Flipcash NEVER imports CodeServices directly. Use import FlipcashCore instead.
Flipcash/Core/
├── AppDelegate.swift # App lifecycle, window setup
├── Container.swift # Root DI container
├── ContainerScreen.swift # Root navigation (auth state routing)
├── Session/ # Auth, session management
│ ├── Session.swift # Main state object
│ ├── SessionAuthenticator.swift
│ └── AccountManager.swift # Keychain management
├── Controllers/ # Business logic
│ ├── Database/ # SQLite persistence
│ ├── HistoryController.swift
│ ├── RatesController.swift
│ └── PushController.swift
└── Screens/ # SwiftUI screens
├── Main/ # Authenticated screens
├── Onboarding/ # Login/registration
├── Onramp/ # Add cash flow
└── Settings/ # User settings
| Technology | Version | Purpose |
|---|---|---|
| Swift | 6.1 | Primary language |
| iOS Minimum | 17.0 | Deployment target |
| SwiftUI | Primary | UI framework |
| SQLite.swift | - | Database |
| grpc-swift | 1.22.0+ | Networking |
| CodeCurves | - | Ed25519 cryptography |
| OpenCV | 4.10.0 | Code scanning |
AuthenticationState:
├── .loggedOut → IntroScreen (mnemonic entry)
├── .migrating → LoadingView (app startup)
├── .pending → (transitional state)
└── .loggedIn(SessionContainer) → ScanScreen (main app)
User enters 12-word mnemonic
↓
Derive keypair: KeyPair(mnemonic, path: .primary())
↓
Create KeyAccount (mnemonic + derivedKey)
↓
Create AccountCluster (owner + timelock accounts)
↓
Server registration: flipClient.register(owner:)
↓
Store in Keychain via @SecureCodable
↓
Create SessionContainer:
├── Session (main state)
├── Database (per-user SQLite)
├── HistoryController
├── RatesController
├── PushController
└── WalletConnection
↓
state = .loggedIn(SessionContainer)
| Class | Location | Purpose |
|---|---|---|
SessionAuthenticator |
Session/SessionAuthenticator.swift |
Auth state machine |
Session |
Session/Session.swift |
Main app state (ObservableObject) |
AccountManager |
Session/AccountManager.swift |
Keychain persistence |
KeyAccount |
FlipcashCore/Solana/Keys/KeyAccount.swift |
Mnemonic + derived keys |
struct SessionContainer {
let session: Session
let database: Database
let walletConnection: WalletConnection
let ratesController: RatesController
let historyController: HistoryController
let pushController: PushController
let poolController: PoolController
let poolViewModel: PoolViewModel
let onrampViewModel: OnrampViewModel
}Quarks - Smallest unit of currency (like cents for dollars)
- Stored as
UInt64to avoid floating-point precision issues - USDC uses 6 decimals: 1 USDC = 1,000,000 quarks
- Custom tokens use 10 decimals: 1 token = 10,000,000,000 quarks
ExchangedFiat - Amount with exchange rate conversion
struct ExchangedFiat {
let underlying: Quarks // Always in USD
let converted: Quarks // Display currency (CAD, EUR, etc.)
let rate: Rate // FX rate used
let mint: PublicKey // Which token
}Rate - Foreign exchange rate
struct Rate {
var fx: Decimal // e.g., 1.4 for CAD
var currency: CurrencyCode
}The DiscreteBondingCurve calculates token pricing based on Total Value Locked (TVL):
- 100-token steps with constant price per step
- Precomputed lookup tables for efficiency
- Matches Solana program exactly (no floating-point drift)
// Buy tokens with USD
let estimation = curve.buy(usdcQuarks: amount, feeBps: 100, tvl: tvl)
// Sell tokens for USD
let estimation = curve.sell(tokenQuarks: amount, feeBps: 100, tvl: tvl)| File | Purpose |
|---|---|
Models/Quarks.swift |
Atomic currency unit |
Models/ExchangedFiat.swift |
Multi-currency wrapper |
Models/Rate.swift |
Exchange rate |
Models/DiscreteBondingCurve.swift |
Token pricing |
Controllers/RatesController.swift |
Rate management |
User in Canada enters $10 CAD:
1. Exchange rate: 1 CAD = 0.714 USD
2. Convert: $10 / 1.4 = $7.14 USD
3. Bonding curve: $7.14 USD = 714 tokens
4. Store as ExchangedFiat:
- underlying: $7.14 USD
- converted: $10.00 CAD
- rate: 1.4 CAD/USD
CodeScanner is a C++ library for scanning/encoding circular "Kik Codes":
Flipcash Swift Code
↓
KikCodes (Objective-C API)
↓
C++ Scanner (OpenCV 4.10.0)
↓
Reed-Solomon Error Correction
@interface KikCodes : NSObject
+ (NSData *)encode:(NSData *)data; // 20-byte → 35-byte
+ (NSData *)decode:(NSData *)data; // 35-byte → 20-byte
+ (nullable NSData *)scan:(NSData *)data
width:(NSInteger)width
height:(NSInteger)height
quality:(KikCodesScanQuality)quality;
@endByte 0: Type (1 byte) - Kind enum
Byte 1: Currency Code (1 byte)
Bytes 2-9: Fiat Amount (8 bytes) - UInt64 quarks
Bytes 10-19: Nonce (10 bytes) - random
// Scanning
if let data = KikCodes.scan(yPlaneData, width: width, height: height, quality: .best) {
let payload = KikCodes.decode(data)
let cashCode = try CashCode.Payload(data: payload)
}
// Encoding
let encoded = KikCodes.encode(payloadData)| File | Purpose |
|---|---|
CodeScanner/Code.h |
Objective-C public interface |
CodeScanner/src/scanner.cpp |
OpenCV scanning (~1000 lines) |
CodeScanner/src/kikcode_encoding.cpp |
Encoding/decoding logic |
Flipcash/Bill/CodeExtractor.swift |
Swift camera integration |
Flipcash/Bill/CashCode.Payload.swift |
Payload model |
class Database {
let reader: Connection // Read-only
let writer: Connection // Read-write, WAL mode
}Configuration:
- WAL (Write-Ahead Logging) for concurrency
- 10,000 page cache (~20-40MB)
- Foreign keys enabled
- 2-second busy timeout
- Per-user database:
flipcash-{publicKey}.sqlite
| Table | Primary Key | Purpose |
|---|---|---|
balance |
mint | User's token holdings |
mint |
mint | Token metadata |
rate |
currency | Exchange rates |
activity |
id | Transaction history |
cashLinkMetadata |
id | Gift card details |
pool |
id | Betting pools (deprecated) |
bet |
id | Individual bets (deprecated) |
// Updateable wrapper auto-refreshes on database changes
class Updateable<T>: ObservableObject {
@Published var value: T
init(_ valueBlock: @escaping () -> T) {
NotificationCenter.default.addObserver(
self, selector: #selector(handleDatabaseDidChange),
name: .databaseDidChange, object: nil
)
}
}
// Usage in Session
private lazy var updateableBalances = Updateable {
(try? database.getBalances()) ?? []
}// Write (uses writer connection)
try database.transaction {
try $0.insertBalance(quarks: amount, mint: mint, date: .now)
}
// Read (uses reader connection)
let balances = try database.getBalances()-
Client (Payments API) - Solana blockchain operations
- Host:
ocp.api.flipcash-infra.net:443 - Services: Account, Transaction, Currency, Messaging
- Host:
-
FlipClient (Flipcash API) - Backend services
- Host:
fc.api.flipcash-infra.net:443 - Services: Account, Activity, Profile, Push, IAP, Pool
- Host:
@MainActor
class FlipClient: ObservableObject {
internal let accountService: AccountService
internal let activityService: ActivityService
// ... more services
// Public methods exposed via extensions
public func login(owner: KeyPair) async throws -> UserID {
try await withCheckedThrowingContinuation { c in
accountService.login(owner: owner) { c.resume(with: $0) }
}
}
}All API calls include cryptographic authentication:
let request = Flipcash_Account_V1_LoginRequest.with {
$0.timestamp = .init(date: .now)
$0.auth = owner.authFor(message: $0) // Ed25519 signature
}Each service defines domain-specific errors:
enum ErrorSendEmailCode: Int, Error {
case ok
case denied
case rateLimited
case invalidEmailAddress
case unknown = -1
}Flipcash/Core/Screens/
├── Main/ # Authenticated screens
│ ├── ScanScreen.swift
│ ├── GiveScreen.swift
│ ├── BalanceScreen.swift
│ └── Operations/ # Async operations
├── Onboarding/ # Login flow
├── Onramp/ # Add cash flow
├── Settings/ # User settings
└── Pools/ # Betting (deprecated)
1. State-Driven (Root)
// ContainerScreen.swift
switch sessionAuthenticator.state {
case .loggedOut: IntroScreen()
case .migrating: LoadingView()
case .loggedIn(let container): ScanScreen()
}2. NavigationStack (Multi-step flows)
NavigationStack(path: $viewModel.path) {
// Root content
.navigationDestination(for: OnboardingPath.self) { destination in
// Destination views
}
}3. Sheet/Modal (Overlays)
.sheet(isPresented: $isShowingGive) {
GiveScreen(viewModel: giveViewModel)
}ViewModels are used for complex, multi-screen flows:
@MainActor
class GiveViewModel: ObservableObject {
@Published var enteredAmount: String = ""
@Published var actionState: ButtonState = .normal
let session: Session
func giveAction() { /* ... */ }
}Simple screens access Session/Controllers directly via @EnvironmentObject.
| Category | Components |
|---|---|
| Buttons | CodeButton, LargeButton, BorderedButton, CapsuleButton |
| Containers | Background, PartialSheet, BlurView, Row |
| Dialog | Dialog, DialogButton |
| Modifiers | .loading(), .if(), .badged() |
// Colors
Color.backgroundMain = Color(r: 0, g: 26, b: 12) // Dark green
Color.textMain = .white
Color.mainAccent = .white
// Fonts
Font.appDisplayLarge // 55pt bold
Font.appTextMedium // 16pt bold
Font.appTextBody // 16pt regularIntents model blockchain transactions as composable actions:
IntentType (protocol)
├── IntentTransfer
├── IntentSendCashLink
├── IntentReceiveCashLink
├── IntentWithdraw
└── IntentCreateAccount
↓
ActionGroup (ordered actions)
↓
ActionType (atomic operations)
├── ActionTransfer
├── ActionOpenAccount
├── ActionWithdraw
└── ActionFeePayment
Phase 1: Submit Actions
Client submits Intent → Server validates → Returns ServerParameters
Phase 2: Apply & Sign
Client applies parameters → Signs transactions → Sends signatures
Phase 3: Validation
Server validates signatures → Broadcasts to blockchain
// Supported routes
/login → Account switch
/c or /cash → Receive cash
/verify → Email verification
// Fragment parsing
#e=<entropy> → Base58-encoded mnemonic entropy
#p=<payload> → Payment payloadenum BillState {
case .visible(.pop) // Cash received (animates up)
case .visible(.slide) // Cash sent (slides in)
case .hidden(.slide) // Dismissed
}| Controller | Purpose |
|---|---|
HistoryController |
Transaction history sync |
RatesController |
Exchange rates, currency preferences |
PushController |
APNs/FCM setup |
NotificationController |
System lifecycle events |
PoolController |
Betting pools (deprecated) |
StoreController |
In-app purchases |
// RatesController - 55 second poll
private func registerPoller() {
poller = Poller(seconds: 55, fireImmediately: true) { [weak self] in
Task { try await self?.fetchExchangeRates() }
}
}
// Session - 10 second poll
poller = Poller(seconds: 10, fireImmediately: true) { [weak self] in
Task { await self?.poll() }
}- Balance management (reactive via
Updateable) - Cash operations (send/receive bills)
- Transaction limits validation
- Toast/dialog presentation
- Post-transaction sync
// UserDefaults via @Defaults wrapper
@Defaults(.entryCurrency)
static var entryCurrency: CurrencyCode?
// Keychain via @SecureCodable wrapper
@SecureCodable(.keyAccount)
private var currentKeyAccount: KeyAccount?Pure C implementation with Swift wrappers:
// Key generation
let keypair = KeyPair(seed: Seed32)
let keypair = KeyPair(mnemonic: phrase, path: .primary())
// Signing
let signature = keypair.sign(data)BIP39 Mnemonic (12/24 words)
↓
SLIP-0010 Derivation (m/44'/501'/0'/0')
↓
KeyPair (PublicKey + PrivateKey)
↓
AccountCluster (per-mint accounts)
| Type | Size | Purpose |
|---|---|---|
Seed32 |
32 bytes | Random entropy |
PublicKey |
32 bytes | Account address |
PrivateKey |
64 bytes | Signing key |
Signature |
64 bytes | Transaction signature |
Groups keys for each token mint:
struct AccountCluster {
let authority: DerivedKey // Owner's derived key
let timelock: TimelockDerivedAccounts
var authorityPublicKey: PublicKey
var vaultPublicKey: PublicKey
var depositPublicKey: PublicKey
}// SecureCodable encodes to JSON, stores in Keychain
@SecureCodable(.keyAccount)
private var currentKeyAccount: KeyAccount?
// Stored keys:
// - .keyAccount (current)
// - .historicalAccounts (all past accounts)
// - .currentUserAccount (synced)GiveScreen → GiveViewModel.giveAction()
↓
Session.hasSufficientFunds() [validation]
↓
Session.showCashBill()
↓
SendCashOperation:
├── Opens message stream
├── Sends mint info to receiver
├── Waits for signed destination
├── Validates signature
├── Calls client.transfer()
└── Polls for completion
↓
Session.updatePostTransaction()
ScanScreen [scans QR code]
↓
Session.receiveCash(payload)
↓
ScanCashOperation:
├── Listens for sender's mint info
├── Creates destination accounts
├── Sends signed destination
├── Polls for completion
└── Shows toast notification
struct Activity {
let id: PublicKey // Transaction ID
let state: State // pending, completed
let kind: Kind // gave, received, cashLink, etc.
let exchangedFiat: ExchangedFiat
let date: Date
}// Validate before send
session.hasSufficientFunds(for: amount) → SufficientFundsResult
session.hasLimitToSendFunds(for: amount) → Bool
// Limits refresh every 10 seconds
struct Limits {
let sendLimits: [SendLimit] // Per currency
let depositLimit: DepositLimit?
}import Testing
@Suite("Session Tests")
struct SessionTests {
@Test
static func testSufficientFunds_ExactMatch() {
let balance = Quarks(quarks: 1_000_000, currencyCode: .usd, decimals: 6)
#expect(balance.quarks == 1_000_000)
}
}// Mocks defined as static properties
extension Session {
static let mock = Session(
container: .mock,
historyController: .mock,
ratesController: .mock,
database: .mock,
keyAccount: .mock,
// ...
)
}xcodebuild test -scheme Flipcash \
-destination 'platform=iOS Simulator,name=iPhone 16'| Area | Coverage |
|---|---|
| Currency/Exchange | Excellent |
| Bonding Curve | Excellent |
| Session Logic | Good |
| ViewModels | Moderate |
| UI/Integration | Limited |
- Use Swift Testing (
import Testing), not XCTest - Name:
testFeature_Scenario_ExpectedResult - Use
#expect()with descriptive messages - Mark UI tests with
@MainActor - Use
.mockproperties for dependencies
# Build
xcodebuild build -scheme Flipcash -destination 'generic/platform=iOS'
# Test
xcodebuild test -scheme Flipcash -destination 'platform=iOS Simulator,name=iPhone 16'
# Clean
xcodebuild clean -scheme FlipcashPublicKey.usdc // Main USDC mint
PublicKey.usdc.mintDecimals // 6 decimals
BondingCurve.startPrice // $0.01
BondingCurve.endPrice // $1,000,000
BondingCurve.maxSupply // 21,000,000 tokens- Never import CodeServices in Flipcash - Use FlipcashCore
- Use Swift Testing - Not XCTest
- Use exhaustive switch - Not
if casefor enums - Don't modify generated files - Proto files are auto-generated
- Pools feature is deprecated - Don't work on it
| Pitfall | Solution |
|---|---|
| Importing CodeServices | Use import FlipcashCore |
| Using XCTest | Use Swift Testing |
Using if case for enums |
Use exhaustive switch |
| Modifying proto files | Update service files instead |
| Adding unnecessary abstractions | Keep it simple |
- Read
CLAUDE.mdfor coding guidelines - Understand Container/SessionContainer DI pattern
- Explore
Session.swift- the main state hub - Run tests to verify setup works
- Build and run on simulator
- Study one screen end-to-end (e.g., GiveScreen)
This document was generated from comprehensive codebase analysis. For the latest guidelines, refer to CLAUDE.md.