Developer Docs

Mobile SDK Architecture

Feature: Native Mobile Auth (OIDC) + Kotlin Multiplatform uCart SDK
Status: Scaffolding Complete โ€” Ready for Native Integration
Subgraphs: auth, ucart, gateway


Overview

The Ngwenya Mobile SDK provides native iOS and Android applications with the same uCart widget capabilities available on the web: product browsing, cart management, and authenticated checkout โ€” all powered by the federated GraphQL gateway.

The architecture is split into two layers:

  1. Mobile Auth Adapters โ€” Platform-specific OIDC authentication flows
  2. KMP Shared Module โ€” Cross-platform business logic in Kotlin Multiplatform
graph TD
    subgraph Native["Native App Layer"]
        iOS["iOS (Swift/SwiftUI)"]
        Android["Android (Kotlin/Compose)"]

        subgraph KMP["KMP Shared Module (commonMain)"]
            UCartApi["UCartApi"]
            UCartState["UCartState"]
            AuthAdapter["PlatformAuthAdapter"]
            Models["Models (@Serializable)"]
        end

        subgraph AuthLayer["TypeScript Auth Adapter Layer"]
            WebAdapter["WebAdapter (popup)"]
            NativeRedirect["NativeRedirect (PKCE+system)"]
            NativeWebView["NativeWebView (bridge)"]
        end
    end

    subgraph Gateway["Ngwenya Federated Gateway (:30000)"]
        auth["auth"]
        ucart["ucart"]
        malet["malet"]
        products["products"]
    end

    iOS --> KMP
    Android --> KMP
    KMP --> AuthLayer
    AuthLayer --> Gateway

Mobile Auth Adapters

Architecture

The auth system uses a platform-agnostic adapter interface (AuthAdapter) with three concrete implementations:

Adapter Platform Flow Security
WebAuthAdapter Web browsers OAuth popup + postMessage Same-origin validation
NativeRedirectAuthAdapter iOS / Android System browser + deep link redirect PKCE (S256)
NativeWebViewAuthAdapter Fallback Embedded WebView + JS bridge WebView interception
โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”    โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”    โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”    โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
โ”‚ Mobile Appโ”‚    โ”‚System Browserโ”‚    โ”‚ OIDC Providerโ”‚    โ”‚ Auth Serverโ”‚
โ”‚           โ”‚    โ”‚              โ”‚    โ”‚ (Google/Apple)โ”‚    โ”‚ (Ngwenya)  โ”‚
โ””โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”˜    โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜    โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜    โ””โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”˜
      โ”‚                 โ”‚                    โ”‚                   โ”‚
      โ”‚ buildAuthURL()  โ”‚                    โ”‚                   โ”‚
      โ”‚ (with PKCE)     โ”‚                    โ”‚                   โ”‚
      โ”œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ–บโ”‚                    โ”‚                   โ”‚
      โ”‚                 โ”‚  Authorization     โ”‚                   โ”‚
      โ”‚                 โ”‚  Request           โ”‚                   โ”‚
      โ”‚                 โ”œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ–บโ”‚                   โ”‚
      โ”‚                 โ”‚                    โ”‚                   โ”‚
      โ”‚                 โ”‚  User authenticatesโ”‚                   โ”‚
      โ”‚                 โ”‚ โ—„โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ค                   โ”‚
      โ”‚                 โ”‚                    โ”‚                   โ”‚
      โ”‚ Deep link       โ”‚  Redirect with     โ”‚                   โ”‚
      โ”‚ with code       โ”‚  auth code         โ”‚                   โ”‚
      โ”‚โ—„โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ค                    โ”‚                   โ”‚
      โ”‚                 โ”‚                    โ”‚                   โ”‚
      โ”‚ exchangeCode()  โ”‚                    โ”‚                   โ”‚
      โ”‚ (with verifier) โ”‚                    โ”‚                   โ”‚
      โ”œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ–บโ”‚                   โ”‚
      โ”‚                 โ”‚                    โ”‚  OIDC tokens      โ”‚
      โ”‚โ—„โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ค                   โ”‚
      โ”‚                 โ”‚                    โ”‚                   โ”‚
      โ”‚ Platform token  โ”‚                    โ”‚                   โ”‚
      โ”‚ exchange         โ”‚                    โ”‚                   โ”‚
      โ”œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ–บโ”‚
      โ”‚                 โ”‚                    โ”‚   Platform JWT     โ”‚
      โ”‚โ—„โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ค
      โ”‚                 โ”‚                    โ”‚                   โ”‚
      โ”‚ โœ… Authenticatedโ”‚                    โ”‚                   โ”‚

PKCE (Proof Key for Code Exchange)

All native auth flows use PKCE for public client security:

import { generatePKCEChallenge } from '$lib/ucart-widget/mobile-auth-adapter';

const pkce = await generatePKCEChallenge();
// pkce.codeVerifier  โ€” 64-char random string (kept secret)
// pkce.codeChallenge โ€” SHA-256 hash, base64url encoded (sent to provider)
// pkce.codeChallengeMethod โ€” always "S256"

Supported OIDC Providers

Provider Status Notes
Google โœ… Active Default provider for all platforms
Apple ๐Ÿ”ฒ Placeholder Required for iOS App Store submission

Redirect URIs

Environment URI
Development ngwenya://auth/callback
Production https://app.mallnline.io/auth/callback

Factory Function

import { createAuthAdapter } from '$lib/ucart-widget/mobile-auth-adapter';

// Auto-detect platform and use recommended strategy
const auth = createAuthAdapter({
    authUrl: 'https://auth.mallnline.io',
    platform: 'ios', // or 'android', 'web', 'react-native'
    redirectUri: 'https://app.mallnline.io/auth/callback',
    clientId: 'your-client-id',
    scopes: ['openid', 'email', 'profile']
});

KMP Shared Module

Architecture

The Kotlin Multiplatform module provides shared business logic that compiles to both iOS frameworks (Kotlin/Native) and Android libraries (Kotlin/JVM).

Source set structure:

  • commonMain โ€” Shared API client, state manager, models
  • iosMain โ€” Darwin HTTP engine (NSURLSession), Keychain adapter
  • androidMain โ€” OkHttp engine, EncryptedSharedPreferences adapter

Dependencies

Library Version Purpose
Ktor Client 3.0.3 HTTP networking
Kotlinx.serialization 1.7.3 JSON serialization
Kotlinx.coroutines 1.9.0 Async operations

Shared Models

All models use @Serializable for automatic JSON coding:

@Serializable
data class WidgetProduct(
    val id: String,
    val name: String,
    val description: String,
    val images: List<String>,
    val price: WidgetMoney,
    val compareAtPrice: WidgetMoney? = null,
    val totalInventory: Int,
    val availableForSale: Boolean,
    val variants: List<WidgetProductVariant>,
    val malet: MaletInfo
)

API Client

val api = UCartApi(
    baseUrl = "https://api.mallnline.io",
    getToken = { authAdapter.getToken() }
)

// Load product
val product = api.getProduct("prod_123")

// Add to cart
val cart = api.addToCart(AddToCartPayload(
    productId = "prod_123",
    maletId = "malet_1",
    quantity = 1
))

Reactive State

val state = UCartState(api, authAdapter)

// Observe state changes
state.stateFlow.collect { snapshot ->
    updateUI(
        product = snapshot.product,
        cart = snapshot.cart,
        isLoading = snapshot.isLoading
    )
}

// Trigger operations
state.loadProduct("prod_123")
state.addToCart("prod_123", "malet_1", quantity = 2)

iOS Distribution

Swift Package Manager (preferred):

// Package.swift dependency
.package(url: "https://github.com/mallnline/ngwenya-ucart-kmp", from: "0.1.0")

CocoaPods (fallback):

# Podfile
pod 'NgwenyaUCart', :path => '../kmp/shared'

File Reference

File Purpose
src/lib/ucart-widget/mobile-auth-types.ts Auth adapter interface + OIDC type definitions
src/lib/ucart-widget/mobile-auth-adapter.ts Web, NativeRedirect, NativeWebView adapters + PKCE utilities
src/lib/ucart-widget/auth.ts Original AuthManager (now implements AuthAdapter)
src/lib/ucart-widget/native/ios/NgwenyaAuth.swift iOS ASWebAuthenticationSession + Keychain
src/lib/ucart-widget/native/android/NgwenyaAuth.kt Android Custom Tabs + EncryptedSharedPreferences
src/lib/ucart-widget/kmp/shared/Models.kt @Serializable data models
src/lib/ucart-widget/kmp/shared/AuthAdapter.kt expect/actual auth adapter
src/lib/ucart-widget/kmp/shared/UCartApi.kt Ktor-based API client
src/lib/ucart-widget/kmp/shared/UCartState.kt StateFlow reactive state
src/lib/ucart-widget/kmp/build.gradle.kts Gradle KMM build config
src/lib/ucart-widget/kmp/README.md KMP module architecture spec
tests/mobileAuthAdapter.test.ts Auth adapter unit tests
tests/kmpModels.test.ts TypeScript โ†” Kotlin model parity tests
mobile/ios/Ngwenya/Services/NgwenyaViewModel.swift iOS @Observable ViewModel wrapping KMP state
mobile/android/.../ui/NgwenyaViewModel.kt Android Compose ViewModel wrapping KMP state
src/lib/utils/kmpScreenTypes.ts TypeScript parity types + Zod schemas
tests/kmpScreenTypes.test.ts Screen type parity tests (57 tests)

Screen Data Integration

Added 2026-05-03 โ€” KMP shared module now provides data for all 5 native tabs.

Architecture

The KMP shared module uses GraphQL queries against the same federated gateway as the SvelteKit web frontend. No REST wrappers needed โ€” the UCartApi sends raw GraphQL operations via POST /graphql.

graph LR
    subgraph Native["Native UI (SwiftUI / Compose)"]
        ViewModel["ViewModel"]
    end

    subgraph KMP["KMP Shared Module"]
        UCartState["UCartState\nloadFeed() ยท loadOrders()\nloadNotifs() ยท loadConvs()"]
        UCartApi["UCartApi\ngraphql() โ†’ POST /graphql"]
    end

    Gateway["Ngwenya Federated Gateway"]

    ViewModel -- "StateFlow" --> UCartState
    UCartState --> UCartApi
    UCartApi -- "GraphQL" --> Gateway

Extended KMP API Methods

Method GraphQL Operation Screen
getFeed(maletIds?, limit, offset) forYouFeed Lobby
getOrders() orders Murchases
getOrder(id) order(id) Murchases
getNotifications(first?, after?) myNotifications Notifications
getUnreadNotificationCount() unreadNotificationCount Notifications
markNotificationRead(id) markNotificationRead Notifications
markAllNotificationsRead() markAllNotificationsRead Notifications
getConversations(first?, after?) myConversations uChat
getMessages(conversationId, first?, after?) conversationMessages uChat
createConversation(participants, maletId?, convType) createConversation uChat
sendMessage(conversationId, body, msgType) sendMessage uChat
deleteMessage(messageId) deleteMessage uChat
markConversationRead(conversationId, messageId) markAsRead uChat
leaveConversation(conversationId) leaveConversation uChat
searchDocs(query, source?, limit, offset) searchDocs Help / Search

ViewModel Pattern

iOS (SwiftUI):

@Observable
class NgwenyaViewModel {
    var feedItems: [FeedItem] = []
    var orders: [OrderItem] = []
    // ...
    func loadFeed() { /* KMP: state.loadFeed { items in ... } */ }
}

// Injected via .environment() in NgwenyaApp.swift
ContentView().environment(viewModel)

Android (Compose):

class NgwenyaViewModel : ViewModel() {
    data class ScreenState(
        val feedItems: List<FeedItem> = emptyList(),
        val orders: List<OrderItem> = emptyList(),
        // ...
    )
    private val _state = MutableStateFlow(ScreenState())
    val state: StateFlow<ScreenState> = _state.asStateFlow()

    fun loadFeed() { /* KMP: uCartState.loadFeed { items -> ... } */ }
}

// Created via viewModel() in MainActivity
val viewModel: NgwenyaViewModel = viewModel()

Bridge Data Contract

The webโ†”native bridge supports screen data exchange:

Bridge Action Direction Purpose
requestScreenData Native โ†’ Web Request data for a screen
screenDataResponse Web โ†’ Native Respond with serialized data
navigateToConversation Web โ†’ Native Open a uChat conversation

TypeScript parity types in kmpScreenTypes.ts with Zod schemas enable validation of data shapes exchanged between platforms.


Security Considerations

  1. PKCE is mandatory for all native auth flows (prevents authorization code interception)
  2. System browser preferred over WebView (prevents credential harvesting)
  3. Tokens stored in platform-secure storage (iOS Keychain / Android EncryptedSharedPreferences)
  4. Token exchange โ€” OIDC tokens are exchanged for Ngwenya platform JWTs server-side
  5. No client secrets โ€” native apps are public clients (PKCE replaces client_secret)