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:
- Mobile Auth Adapters โ Platform-specific OIDC authentication flows
- 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 |
Auth Flow: Native Redirect (Recommended)
โโโโโโโโโโโโโ โโโโโโโโโโโโโโโโ โโโโโโโโโโโโโโโโ โโโโโโโโโโโโโโ
โ 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 |
|---|---|---|
| โ 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, modelsiosMainโ Darwin HTTP engine (NSURLSession), Keychain adapterandroidMainโ 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
- PKCE is mandatory for all native auth flows (prevents authorization code interception)
- System browser preferred over WebView (prevents credential harvesting)
- Tokens stored in platform-secure storage (iOS Keychain / Android EncryptedSharedPreferences)
- Token exchange โ OIDC tokens are exchanged for Ngwenya platform JWTs server-side
- No client secrets โ native apps are public clients (PKCE replaces client_secret)
Related Documentation
- Mobile Push Infrastructure โ In-house APNs/FCM push dispatch, native app shells, and WebView bridge
- iOS Development Setup โ Xcode project creation, signing, and simulator/device testing
- Android Development Setup โ Android Studio, Gradle, Firebase, and emulator setup
- uCart Concurrency Resilience โ Cart atomic operations
- MFA and Passkeys โ Additional auth factors
- Gateway Rate Limiting โ Mobile API rate limits
- REST API โ Public API endpoints consumed by the widget
- Developer Docs Overview โ Back to the main documentation index.