This guide provides comprehensive documentation on how to use the Seam Components library, which offers both complete screen solutions and individual UI components for building credential management applications.
- Installation & Setup
- Quick Start - Complete Integration
- Initialization & Authentication
- Screen Components
- UI Components
- Theming and Customization
- Error Handling
- Component Relationships and Integration
Add the Seam Components library and required SDK modules to your app's build.gradle.kts (or build.gradle for Groovy):
Kotlin DSL (build.gradle.kts):
dependencies {
val seamVersion = "3.1.1" // Check for latest version
// Required: Seam Components UI library
implementation("co.seam:seam-phone-sdk-android-seamcomponents:$seamVersion")
// Required: Core SDK functionality
implementation("co.seam:seam-phone-sdk-android-core:$seamVersion")
// Optional: Add integration modules as needed based on your lock providers
implementation("co.seam:seam-phone-sdk-android-assaabloy:$seamVersion") // minSdk 28
implementation("co.seam:seam-phone-sdk-android-latch:$seamVersion") // minSdk 26
implementation("co.seam:seam-phone-sdk-android-saltoks:$seamVersion") // minSdk 24
implementation("co.seam:seam-phone-sdk-android-saltospace:$seamVersion") // minSdk 24
}Groovy (build.gradle):
dependencies {
def seamVersion = "3.1.1" // Check for latest version
// Required: Seam Components UI library
implementation "co.seam:seam-phone-sdk-android-seamcomponents:$seamVersion"
// Required: Core SDK functionality
implementation "co.seam:seam-phone-sdk-android-core:$seamVersion"
// Optional: Add integration modules as needed
implementation "co.seam:seam-phone-sdk-android-assaabloy:$seamVersion" // minSdk 28
implementation "co.seam:seam-phone-sdk-android-latch:$seamVersion" // minSdk 26
implementation "co.seam:seam-phone-sdk-android-saltoks:$seamVersion" // minSdk 24
implementation "co.seam:seam-phone-sdk-android-saltospace:$seamVersion" // minSdk 24
}Configure your settings.gradle.kts (or settings.gradle) to access Seam's GitHub Packages repository:
Kotlin DSL (settings.gradle.kts):
dependencyResolutionManagement {
repositories {
google()
mavenCentral()
maven {
name = "GitHubPackages"
url = uri("https://maven.pkg.github.com/seampkg/seam-mobile-sdk")
credentials {
username = getPropertyOrNull("seamUsername")
password = getPropertyOrNull("seamPat")
}
}
}
}
// Helper function to read from local.properties
fun getPropertyOrNull(propertyName: String): String? {
val propertiesFile = file("local.properties")
if (!propertiesFile.exists()) return null
val properties = java.util.Properties()
properties.load(propertiesFile.inputStream())
return properties.getProperty(propertyName)
}Create a local.properties file in your project root with your GitHub credentials:
# local.properties (DO NOT commit this file)
seamUsername=YOUR_GITHUB_USERNAME
seamPat=YOUR_SEAM_PROVIDED_PATNote: Contact Seam support to obtain a Personal Access Token (PAT) with
read:packagesscope.
Set your app's minSdk in build.gradle.kts based on the integration modules you're using:
android {
defaultConfig {
minSdk = 26 // or higher based on integration requirements:
// 24 for base SDK + Salto
// 26 for Latch integration
// 28 for Assa Abloy integration
}
}SeamAccessView is the main entry point composable for the Seam access management interface. This powerful component manages the entire user flow for accessing and managing credentials through the Seam SDK, making it the perfect choice for most applications.
What SeamAccessView handles for you:
- SDK Initialization: Automatically initializes the Seam SDK with your session token
- Navigation Management: Handles screen transitions and navigation state
- UI State Management: Manages loading, error handling, and different application states
- OTP Authorization: Automatically handles OTP verification flows when required
- Bluetooth Permissions: Guides users through Bluetooth setup when needed for credential operations
Screen Orchestration: SeamAccessView automatically navigates between these screens based on application state:
- Credentials List Screen: For viewing available keys with pull-to-refresh functionality
- OTP Authorization Screen: For completing authentication flows in a WebView
- Bluetooth Redirect Screen: For handling Bluetooth permission and setup
- Unlock Overlay: For interacting with individual credentials through a modal interface
For most use cases, you can use SeamAccessView as a complete solution:
@Composable
fun MyApp() {
SeamThemeProvider {
SeamAccessView(
clientSessionToken = "your-session-token-here"
)
}
}If you need more control over navigation or want to integrate with your existing navigation setup:
@Composable
fun MyApp() {
val navController = rememberNavController()
SeamThemeProvider {
SeamAccessView(
clientSessionToken = "your-session-token-here",
context = LocalContext.current,
navController = navController
)
}
}Parameters:
clientSessionToken: Required session token for SDK authentication and initializationcontext: Android context (defaults to current composition's local context)navController: Navigation controller for screen transitions (defaults to a new instance)
Client Session Tokens authenticate your mobile app with Seam's backend. These tokens should be generated by your backend server using Seam's API.
Ask Seam about how to obtain a CST. For more details see Seam's Client Session Token documentation.
If you need to store your CST do it securely using Android's encrypted storage:
class SecureTokenStorage(context: Context) {
private val masterKey = MasterKey.Builder(context)
.setKeyScheme(MasterKey.KeyScheme.AES256_GCM)
.build()
private val sharedPreferences = EncryptedSharedPreferences.create(
context,
"seam_secure_prefs",
masterKey,
EncryptedSharedPreferences.PrefKeyEncryptionScheme.AES256_SIV,
EncryptedSharedPreferences.PrefValueEncryptionScheme.AES256_GCM
)
fun saveToken(token: String) {
sharedPreferences.edit()
.putString("client_session_token", token)
.apply()
}
fun getToken(): String? {
return sharedPreferences.getString("client_session_token", null)
}
fun clearToken() {
sharedPreferences.edit()
.remove("client_session_token")
.apply()
}
}When using individual components instead of SeamAccessView, you must manually initialize the Seam SDK:
@Composable
fun MyCredentialsScreen(clientSessionToken: String) {
val context = LocalContext.current
LaunchedEffect(clientSessionToken) {
try {
// Initialize the SDK. This part is usually done in a ViewModel
SeamSDK.initialize(
context = context,
clientSessionToken = clientSessionToken
)
// Activate the SDK after initialization
SeamSDK.getInstance().activate()
} catch (error: SeamError) {
// handle error
}
}
SeamCredentialsView(
onNavigateToUnlock = { /* Handle navigation */ }
)
}When you need more granular control than SeamAccessView provides, you can use individual screen components to build custom flows while still benefiting from Seam's pre-built UI.
What it does: A composable screen that displays cards for user credentials (keys) with comprehensive state management and user interaction support.
Key Features:
- State Management: Automatically handles loading, success with data, and empty states
- Pull-to-Refresh: Built-in refresh functionality for updating credential data
- Error Handling: Displays appropriate user feedback for error states
- Navigation Integration: Provides callback for seamless navigation to unlock interfaces
- Internet Status: Shows connection status and handles offline scenarios
When to use: When you want a complete credentials listing screen but need to customize the navigation or integrate with your own view models and navigation system.
@Composable
fun CredentialsScreen() {
// viewModel is optional
val viewModel: KeysViewModel = viewModel()
// Remember to initialize the SDK before using the view. See previous section for details.
SeamCredentialsView(
viewModel = viewModel,
onNavigateToUnlock = { keyCard ->
// Navigate to unlock screen
println("Unlocking key: ${keyCard.name}")
}
)
}What it does: A modal bottom sheet composable that provides a complete user interface for unlocking credentials with phase-based state management.
Key Features:
- Modal Presentation: Displays as an overlay bottom sheet that doesn't disrupt the main UI
- Unlock Phases: Manages different states (idle, scanning, success, failed) with appropriate UI feedback
- Theme Integration: Supports customization through SeamUnlockCardStyle theming
- Automatic State Reset: Handles cleanup when dismissed or navigation occurs
- Error Recovery: Built-in error states with retry functionality
When to use: When you want to provide unlock functionality in a modal format, either triggered from your custom credential list or integrated into your own navigation flow.
@Composable
fun UnlockModal(keyCard: KeyCard) {
// The simplest way to use the unlock view is to call it on `onNavigateToUnlock`
// callback from `SeamCredentialsView`, as it already provides the keyCard
SeamUnlockCardView(
keyCard = keyCard,
onNavigateBack = { showUnlock = false }
)
}What it does: A full-screen dialog composable that displays an OTP (One-Time Password) verification interface using a WebView for interactive authentication flows.
Key Features:
- Full-Screen Presentation: Renders as a dialog that overlays the host app's UI completely
- WebView Integration: Handles OTP verification flows with JavaScript enabled for interactivity
- Custom Navigation: Includes a styled top bar with back navigation controls
- URL Handling: Loads the provided OTP URL and manages the verification process
- Dialog Management: Proper dismissal handling that integrates with your app's navigation
When to use: When your authentication flow requires OTP verification and you want a seamless, full-screen experience that doesn't disrupt your main app navigation.
@Composable
fun OtpScreen() {
SeamOtpView(
otpUrl = "https://example.com/otp-verification",
onNavigateBack = {
// Handle navigation back
}
)
}For maximum flexibility, Seam provides individual UI components that you can combine to build completely custom interfaces. These components are organized into categories based on their functionality and can be mixed and matched according to your needs.
Component Categories:
- Common Components: Essential UI elements like loading states, errors, and buttons
- Key Components: Components for displaying and managing credentials
- Unlock Components: Specialized components for the unlock process
- Bluetooth Components: Components for Bluetooth setup and permissions
Essential UI components for handling common application states and user interactions.
Display loading states with customizable text:
@Composable
fun MyLoadingScreen() {
LoadingContent(
title = "Loading your mobile keys..."
)
}
// With default loading text
@Composable
fun SimpleLoading() {
LoadingContent()
}Show error states with retry functionality:
@Composable
fun MyErrorScreen() {
ErrorContent(
errorState = SeamErrorState.InternetConnectionRequired,
onRetry = {
// Retry the failed operation
}
)
}A dismissible banner component that displays error messages at the top of the screen.
@Composable
fun MyScreen() {
var errorMessage by remember { mutableStateOf<String?>(null) }
Column {
ErrorBanner(
errorMessage = errorMessage,
onDismiss = { errorMessage = null }
)
// Your main content
Button(
onClick = { errorMessage = "Something went wrong!" }
) {
Text("Trigger Error")
}
}
}Seam provides three button variants:
@Composable
fun ButtonExamples() {
Column(
verticalArrangement = Arrangement.spacedBy(16.dp)
) {
// Primary button (full-width)
SeamPrimaryButton(
buttonText = "Unlock Door",
onClick = { /* Primary action */ }
)
// Secondary button (full-width with border)
SeamSecondaryButton(
buttonText = "Cancel",
onClick = { /* Secondary action */ }
)
// Custom button with full control
SeamButton(
text = "Custom Button",
backgroundColor = Color.Red,
textColor = Color.White,
borderColor = Color.DarkRed,
isFullWidth = false,
onClick = { /* Custom action */ }
)
}
}Components specifically designed for displaying and managing credential keys.
What it does: A composable that displays a stylized key card with gradient background and comprehensive credential information. This is the visual representation of your mobile keys.
Key Features:
- Rich Visual Design: Gradient backgrounds with customizable theming through SeamKeyCardStyle
- Hotel/Location Information: Displays room names, access codes, and checkout dates with proper formatting
- Brand Logo Support: Optional brand logo display (URL or resource-based) with fallback handling
- Multiple States: Supports loading, expired, and active states with appropriate visual indicators
- Interactive/Display Modes: Can be configured for interaction or display-only purposes
- Accessibility: Proper content descriptions and interaction feedback
Card States:
- Active: Normal interactive state with full functionality
- Loading: Shows loading indicator overlay when credential is being processed
- Expired: Displays error indicator and disables interaction for expired credentials
@Composable
fun KeyCardExample() {
val keyCard = KeyCard(
id = "key-1",
location = "Grand Hotel & Spa",
name = "1205",
checkoutDate = LocalDateTime.now().plusDays(2),
code = "1234"
)
KeyCardComponent(
keyCard = keyCard,
onPress = {
// Handle key card tap
println("Key card pressed: ${keyCard.name}")
}
)
}What it does: A composable that displays an empty state when no keys are available, providing user guidance and refresh functionality.
Key Features:
- Informative Design: Shows illustration, title, and subtitle to inform users about the empty state
- Refresh Action: Includes a refresh button to allow users to retry loading keys
- Consistent Styling: Integrates with Seam theme for consistent visual appearance
- User Guidance: Clear messaging about why no keys are shown and what users can do
When to use: Display this when credential loading results in no available keys, or when users need to refresh their credential list.
@Composable
fun EmptyStateExample() {
EmptyKeysState(
onRefresh = {
// Refresh keys
println("Refreshing keys...")
}
)
}Specialized components for building custom unlock interfaces. These components work together to create comprehensive unlock experiences, from user instructions to visual feedback during the unlock process.
Core Unlock Flow Components:
- CircleSpinner: Animated loading indicator
- UnlockButton: Main interaction button with phase-based states
- StatusMessage: Status and instruction text display
- UnlockHeader: Key card information display
- UnlockContent: Complete unlock interface (combines multiple components)
- UnlockError: Error state with retry functionality
- UnlockInstructions: Step-by-step user guidance
What it does: A customizable animated circular spinner composable with gradient effects and optional content support.
Key Features:
- Smooth Animation: Continuous 360-degree rotation with linear easing for smooth visual feedback
- Gradient Effects: Uses sweep gradient with customizable colors from SeamUnlockCardStyle theming
- Flexible Sizing: Configurable diameter and border width for different use cases
- Background Ring: Optional subtle background ring to enhance visual contrast
- Content Support: Can display any composable content in the center (text, icons, etc.)
- Theme Integration: Colors automatically adapt to your Seam theme configuration
Customization Options:
size: Spinner diameter in dp (default: 160)borderWidth: Stroke width in pixels (default: 16f)showBackgroundRing: Display background ring (default: false)content: Optional centered content
@Composable
fun SpinnerExamples() {
Column(
verticalArrangement = Arrangement.spacedBy(16.dp)
) {
// Basic spinner
CircleSpinner()
// Spinner with custom size and background ring
CircleSpinner(
size = 120,
borderWidth = 12f,
showBackgroundRing = true
)
// Spinner with centered content
CircleSpinner(
size = 160,
showBackgroundRing = true
) {
Text(
text = "Loading...",
color = MaterialTheme.colorScheme.primary
)
}
}
}What it does: A central unlock button that dynamically adapts its appearance and behavior based on the current unlock phase, providing visual feedback throughout the unlock process.
Key Features:
- Phase-Based States: Different visual representations for each unlock phase
- Theme Integration: Fully customizable through SeamUnlockCardStyle theming
- Smooth Transitions: Seamless visual transitions between states
- Accessibility: Proper content descriptions and interaction feedback
Visual States:
- IDLE: Gradient button with key icon, ready to initiate unlock
- SCANNING: Animated CircleSpinner around key icon during unlock attempt
- SUCCESS: Green background with checkmark icon indicating successful unlock
- FAILED: No visual representation (error handling is managed by other components)
Styling Options (via theming):
- Button gradient colors and key icon colors for different states
- Success color and icon customization
- Shadow and elevation effects
@Composable
fun UnlockButtonExample() {
var phase by remember { mutableStateOf(UnlockPhase.IDLE) }
Column(
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.spacedBy(16.dp)
) {
UnlockButton(
unlockPhase = phase,
onPress = {
when (phase) {
UnlockPhase.IDLE -> phase = UnlockPhase.SCANNING
UnlockPhase.SCANNING -> phase = UnlockPhase.SUCCESS
UnlockPhase.SUCCESS -> phase = UnlockPhase.IDLE
UnlockPhase.FAILED -> phase = UnlockPhase.IDLE
}
}
)
// Control buttons for demo
Row(
horizontalArrangement = Arrangement.spacedBy(8.dp)
) {
Button(onClick = { phase = UnlockPhase.IDLE }) { Text("Idle") }
Button(onClick = { phase = UnlockPhase.SCANNING }) { Text("Scanning") }
Button(onClick = { phase = UnlockPhase.SUCCESS }) { Text("Success") }
Button(onClick = { phase = UnlockPhase.FAILED }) { Text("Failed") }
}
}
}Display status information during unlock:
@Composable
fun StatusMessageExample() {
Column(
verticalArrangement = Arrangement.spacedBy(16.dp)
) {
StatusMessage(
statusText = "Tap to Unlock",
instructionText = null
)
StatusMessage(
statusText = "Connecting...",
instructionText = "Please hold your phone near the lock"
)
StatusMessage(
statusText = "Success!",
instructionText = "Door unlocked successfully"
)
}
}Display key card information in unlock interfaces:
@Composable
fun UnlockHeaderExample() {
val keyCard = KeyCard(
id = "key-1",
location = "Grand Hotel & Spa",
name = "Room 1205",
checkoutDate = LocalDateTime.now().plusDays(2),
code = "1234"
)
UnlockHeader(
keyCard = keyCard
)
}Main unlock interface content:
@Composable
fun UnlockContentExample() {
var phase by remember { mutableStateOf(UnlockPhase.IDLE) }
UnlockContent(
unlockPhase = phase,
onPressPrimaryButton = {
phase = when (phase) {
UnlockPhase.IDLE -> UnlockPhase.SCANNING
UnlockPhase.SCANNING -> UnlockPhase.SUCCESS
else -> UnlockPhase.IDLE
}
}
)
}Display error states with retry:
@Composable
fun UnlockErrorExample() {
UnlockError(
title = "Connection Failed",
description = "Unable to connect to the lock. Please check your connection and try again.",
onTryAgain = {
// Retry unlock operation
println("Retrying unlock...")
}
)
}Step-by-step unlock instructions:
@Composable
fun InstructionsExample() {
UnlockInstructions()
}Components for handling Bluetooth setup and permissions required for credential operations.
What it does: A full-screen composable that guides users to enable Bluetooth when it's required for key operations, providing clear instructions and system integration.
Key Features:
- Clear Guidance: Explains why Bluetooth is needed with informative title and description
- System Integration: Includes button that directly opens Android Bluetooth settings
- Flexible UX: Optional skip functionality for non-critical Bluetooth requirements
- Consistent Design: Follows Seam design patterns with proper spacing and typography
- Icon Support: Uses appropriate Bluetooth icon for visual clarity
When to use: Display this screen when Bluetooth is disabled but required for credential operations. Commonly used by SeamAccessView automatically, but can be used independently for custom flows.
Skip vs. Required Modes:
- With Skip: Allows users to proceed without Bluetooth (for non-critical features)
- Required Mode: Forces users to enable Bluetooth before proceeding (set
onSkipClickedto null)
@Composable
fun BluetoothSetup() {
BluetoothRedirectScreen(
onSkipClicked = {
// Handle skip action
println("User skipped Bluetooth setup")
}
)
}
// Without skip option
@Composable
fun BluetoothSetupRequired() {
BluetoothRedirectScreen(
onSkipClicked = null // Hide skip button
)
}Seam Mobile Components are fully customizable using Material 3 theming integrated with Seam's component-specific styling system. You can customize colors, typography, and detailed component styles to match your brand.
The simplest way to theme Seam components is to use SeamComponentsTheme with your Material 3 color schemes:
@Composable
fun MyApp() {
val lightColors = lightColorScheme(
primary = Color(0xFF1976D2),
onPrimary = Color.White,
secondary = Color(0xFF757575),
onSecondary = Color.White,
background = Color.White,
onBackground = Color(0xFF212121)
)
val darkColors = darkColorScheme(
primary = Color(0xFF90CAF9),
onPrimary = Color(0xFF003C7E),
secondary = Color(0xFF424242),
onSecondary = Color.White,
background = Color(0xFF121212),
onBackground = Color.White
)
val colorScheme = if (isSystemInDarkTheme()) darkColors else lightColors
SeamComponentsTheme(
colorScheme = colorScheme
) {
SeamAccessView(
clientSessionToken = "your-token"
)
}
}You can integrate custom typography with Material 3's typography system:
@Composable
fun MyThemedApp() {
val customTypography = Typography(
headlineLarge = TextStyle(
fontFamily = FontFamily.Serif,
fontWeight = FontWeight.Bold,
fontSize = 32.sp
),
bodyLarge = TextStyle(
fontFamily = FontFamily.SansSerif,
fontWeight = FontWeight.Normal,
fontSize = 16.sp
),
labelMedium = TextStyle(
fontFamily = FontFamily.Monospace,
fontWeight = FontWeight.Medium,
fontSize = 12.sp,
letterSpacing = 0.5.sp
)
)
val colorScheme = lightColorScheme(
primary = Color(0xFF6366F1),
onPrimary = Color.White
)
SeamComponentsTheme(
colorScheme = colorScheme,
typography = customTypography
) {
SeamAccessView(
clientSessionToken = "your-token"
)
}
}For detailed customization, you can use Seam's component-specific styling with the seamTheme parameter:
@Composable
fun BrandedSeamApp() {
val hotelBranding = SeamTheme(
keyCard = SeamKeyCardStyle(
backgroundGradient = listOf(
Color(0xFF1E3A8A), // Deep blue
Color(0xFF3B82F6) // Bright blue
),
accentColor = Color(0xFFEAB308), // Gold accent
cornerRadius = 16.dp,
textColor = Color.White,
shadowColor = Color(0x33000000),
shadowYOffset = 8.dp,
brandLogoUrl = "https://example.com/hotel-logo.png"
),
unlockCard = SeamUnlockCardStyle(
cardBackground = Color(0xFF1F2937),
headerBackground = Color(0xFF111827),
headerTitleColor = Color.White,
keyIconColorIdle = Color(0xFFEAB308),
keyIconColorActive = Color.White,
successColor = Color(0xFF10B981)
)
)
val colorScheme = lightColorScheme(
primary = Color(0xFF1E3A8A),
onPrimary = Color.White
)
SeamComponentsTheme(
seamTheme = hotelBranding,
colorScheme = colorScheme
) {
SeamAccessView(
clientSessionToken = "your-token"
)
}
}Complete customization options for key card appearance:
SeamKeyCardStyle(
// Brand Integration
brandLogoUrl = "https://example.com/logo.png", // Remote logo URL
brandLogoRes = R.drawable.brand_logo, // Local drawable resource
backgroundTextureRes = R.drawable.card_texture, // Background texture overlay
// Visual Design
backgroundGradient = listOf(Color.Blue, Color.Cyan), // Gradient colors (top to bottom)
accentColor = Color(0xFFFFD700), // Accent bar color
textColor = Color.White, // All text color on card
// Layout & Effects
cornerRadius = 20.dp, // Card corner radius
shadowColor = Color(0x44000000), // Shadow color
shadowYOffset = 6.dp, // Shadow vertical offset
// State Colors
errorColor = Color.Red // Error state indicator color
)Complete customization options for unlock interface:
SeamUnlockCardStyle(
// Container Backgrounds
cardBackground = Color(0xFF1F2937), // Main card background
headerBackground = Color(0xFF111827), // Header section background
// Header Text Colors
headerTitleColor = Color.White, // Main header text color
headerSubtitleColor = Color(0xFFD1D5DB), // Subtitle text color
// Key Icon Colors
keyIconColorIdle = Color(0xFFEAB308), // Key icon when idle
keyIconColorActive = Color.White, // Key icon when active
// Button Styling
keyButtonGradient = listOf( // Button gradient colors
Color(0xFF3B82F6), // Top color
Color(0xFF1E40AF) // Bottom color
),
keyButtonShadowColor = Color(0x33000000), // Button shadow color
keyButtonShadowRadius = 12.dp, // Button shadow blur radius
// Instructional Elements
instructionTextColor = Color(0xFFD1D5DB), // Instruction text color
bulletBackground = Color(0xFF3B82F6), // Bullet point background
bulletTextColor = Color.White, // Bullet point text color
// State Colors
successColor = Color(0xFF10B981), // Success state color
successIconColor = Color.White, // Success icon color
errorColor = Color(0xFFEF4444) // Error state color
)@Composable
fun HotelChainTheme() {
val hotelTheme = SeamTheme(
keyCard = SeamKeyCardStyle(
backgroundGradient = listOf(
Color(0xFF8B5A3C), // Warm brown
Color(0xFF6B4226) // Dark brown
),
accentColor = Color(0xFFD4AF37), // Gold
textColor = Color(0xFFF5F5DC), // Beige
cornerRadius = 12.dp,
brandLogoUrl = "https://hotel.com/logo.png",
shadowColor = Color(0x55000000),
shadowYOffset = 4.dp
),
unlockCard = SeamUnlockCardStyle(
cardBackground = Color(0xFF2D1810), // Dark brown
headerBackground = Color(0xFF1A0F08), // Very dark brown
keyIconColorIdle = Color(0xFFD4AF37), // Gold
keyButtonGradient = listOf(
Color(0xFFD4AF37), // Gold
Color(0xFFB8941F) // Dark gold
),
successColor = Color(0xFF22C55E)
)
)
SeamComponentsTheme(
seamTheme = hotelTheme,
colorScheme = lightColorScheme(primary = Color(0xFF8B5A3C))
) {
SeamAccessView(clientSessionToken = "token")
}
}@Composable
fun TechCorpTheme() {
val techTheme = SeamTheme(
keyCard = SeamKeyCardStyle(
backgroundGradient = listOf(
Color(0xFF0F172A), // Slate 900
Color(0xFF1E293B) // Slate 800
),
accentColor = Color(0xFF06B6D4), // Cyan
textColor = Color(0xFFF8FAFC), // Slate 50
cornerRadius = 8.dp, // Sharp corners
brandLogoRes = R.drawable.tech_logo,
shadowColor = Color(0x66000000),
shadowYOffset = 8.dp
),
unlockCard = SeamUnlockCardStyle(
cardBackground = Color(0xFF020617), // Slate 950
headerBackground = Color(0xFF0F172A), // Slate 900
keyIconColorIdle = Color(0xFF06B6D4), // Cyan
keyIconColorActive = Color(0xFF67E8F9), // Cyan 300
keyButtonGradient = listOf(
Color(0xFF0891B2), // Cyan 600
Color(0xFF0E7490) // Cyan 700
),
successColor = Color(0xFF10B981), // Emerald 500
bulletBackground = Color(0xFF06B6D4),
instructionTextColor = Color(0xFF94A3B8) // Slate 400
)
)
SeamComponentsTheme(
seamTheme = techTheme,
colorScheme = darkColorScheme(
primary = Color(0xFF06B6D4),
background = Color(0xFF020617)
)
) {
SeamAccessView(clientSessionToken = "token")
}
}Seam Components handle errors automatically when using SeamAccessView. For custom implementations using individual components, you'll need to handle errors through ViewModels.
sealed class SeamErrorState {
object InitializationRequired : SeamErrorState()
object ActivationRequired : SeamErrorState()
object IntegrationNotFound : SeamErrorState()
object AlreadyInitialized : SeamErrorState()
object DeactivationInProgress : SeamErrorState()
object InvalidCredentialId : SeamErrorState()
object InternetConnectionRequired : SeamErrorState()
object InvalidClientSessionToken : SeamErrorState()
object InvalidUnlockProximity : SeamErrorState()
object Unknown : SeamErrorState()
}When using SeamAccessView, errors are handled automatically with built-in UI:
@Composable
fun MyApp() {
SeamAccessView(
clientSessionToken = "seam_cst_..."
)
// Errors are automatically displayed with retry options
}When using individual components, implement error handling in your ViewModel:
class MySeamViewModel : ViewModel() {
private val _sdkState = MutableStateFlow<SeamSDKState>(SeamSDKState.Idle)
val sdkState: StateFlow<SeamSDKState> = _sdkState.asStateFlow()
sealed class SeamSDKState {
object Idle : SeamSDKState()
object Initializing : SeamSDKState()
object Initialized : SeamSDKState()
data class Error(val message: String) : SeamSDKState()
}
fun initializeSDK(context: Context, token: String) {
viewModelScope.launch {
_sdkState.value = SeamSDKState.Initializing
try {
SeamSDK.initialize(context, token)
SeamSDK.getInstance().activate()
_sdkState.value = SeamSDKState.Initialized
} catch (e: SeamError) {
_sdkState.value = SeamSDKState.Error(
when (e) {
is SeamError.InvalidClientSessionToken -> "Invalid token"
is SeamError.InternetConnectionRequired -> "No internet connection"
else -> e.message ?: "Unknown error"
}
)
}
}
}
}
// Usage in Composable
@Composable
fun CustomCredentialsScreen(viewModel: MySeamViewModel) {
val sdkState by viewModel.sdkState.collectAsState()
when (sdkState) {
is SeamSDKState.Error -> {
ErrorContent(
errorState = SeamErrorState.Unknown,
onRetry = {
viewModel.initializeSDK(context, token)
}
)
}
SeamSDKState.Initialized -> {
SeamCredentialsView(
onNavigateToUnlock = { /* Handle navigation */ }
)
}
else -> {
LoadingContent()
}
}
}class UnlockViewModel : ViewModel() {
private val _unlockErrors = MutableStateFlow<List<SeamError>>(emptyList())
val unlockErrors: StateFlow<List<SeamError>> = _unlockErrors.asStateFlow()
fun unlockCredential(credentialId: String) {
viewModelScope.launch {
try {
SeamSDK.getInstance().unlock(
credentialId = credentialId,
unlockProximity = UnlockProximity.TOUCH
)
} catch (e: SeamError) {
when (e) {
is SeamError.CredentialErrors -> {
_unlockErrors.value = e.errors
}
else -> {
_unlockErrors.value = listOf(e)
}
}
}
}
}
fun dismissError(index: Int) {
_unlockErrors.value = _unlockErrors.value.filterIndexed { i, _ -> i != index }
}
}You can use the built-in ErrorContent component or create custom error UI:
@Composable
fun MyScreen() {
val sdkState by seamViewModel.sdkState.collectAsState()
when (sdkState) {
is SeamSDKState.Error -> {
ErrorContent(
errorState = sdkState.errorState,
onRetry = {
// Retry initialization
seamViewModel.initializeSeamSDK(context, clientSessionToken)
}
)
}
// ... other states
}
}@Composable
fun CustomKeyList() {
val keys = listOf(
KeyCard(id = "1", location = "Hotel A", name = "Room 101", /* ... */),
KeyCard(id = "2", location = "Hotel B", name = "Room 202", /* ... */)
)
LazyColumn {
items(keys) { keyCard ->
KeyCardComponent(
keyCard = keyCard,
onPress = {
// Custom unlock logic
handleUnlock(keyCard)
},
modifier = Modifier.padding(horizontal = 16.dp, vertical = 8.dp)
)
}
}
}
private fun handleUnlock(keyCard: KeyCard) {
// Your custom unlock implementation
}@Composable
fun CustomUnlockFlow() {
var currentPhase by remember { mutableStateOf(UnlockPhase.IDLE) }
var showError by remember { mutableStateOf(false) }
Column(
modifier = Modifier.fillMaxSize().padding(16.dp),
horizontalAlignment = Alignment.CenterHorizontally
) {
// Header
Text(
text = "Unlock Room 1205",
style = MaterialTheme.typography.headlineMedium,
modifier = Modifier.padding(bottom = 24.dp)
)
// Main unlock area
when {
showError -> {
UnlockError(
title = "Unlock Failed",
description = "Please try again",
onTryAgain = {
showError = false
currentPhase = UnlockPhase.IDLE
}
)
}
else -> {
UnlockButton(
unlockPhase = currentPhase,
onPress = {
performUnlock { success ->
if (success) {
currentPhase = UnlockPhase.SUCCESS
} else {
showError = true
}
}
}
)
Spacer(modifier = Modifier.height(24.dp))
StatusMessage(
statusText = when (currentPhase) {
UnlockPhase.IDLE -> "Tap to Unlock"
UnlockPhase.SCANNING -> "Connecting..."
UnlockPhase.SUCCESS -> "Success!"
UnlockPhase.FAILED -> "Failed"
},
instructionText = when (currentPhase) {
UnlockPhase.SCANNING -> "Hold phone near the lock"
else -> null
}
)
if (currentPhase == UnlockPhase.IDLE) {
Spacer(modifier = Modifier.height(24.dp))
UnlockInstructions()
}
}
}
}
}
private fun performUnlock(callback: (Boolean) -> Unit) {
// Simulate unlock process
// In real implementation, this would interact with the Seam SDK
callback(true) // or false for failure
}@Composable
fun MyCredentialsScreen() {
val keysViewModel: KeysViewModel = viewModel()
val seamErrors by keysViewModel.seamErrors.collectAsState()
val credentialErrorState by keysViewModel.credentialErrorState.collectAsState()
Box {
// Handle credential-specific errors that replace the main content
when (credentialErrorState) {
is KeysErrorState.InternetConnectionRequired -> {
ErrorContent(
errorState = SeamErrorState.InternetConnectionRequired,
onRetry = { keysViewModel.refreshCredentials() }
)
}
else -> {
SeamCredentialsView(
viewModel = keysViewModel,
onNavigateToUnlock = { keyCard ->
// Handle navigation
}
)
// Display transient errors as dismissible banners on top
seamErrors.forEachIndexed { index, error ->
ErrorBanner(
error = error,
onDismiss = { keysViewModel.dismissError(index) }
)
}
}
}
}
}@Composable
fun ErrorHandlingExample() {
var errorState by remember { mutableStateOf<String?>(null) }
var isLoading by remember { mutableStateOf(false) }
Column {
when {
isLoading -> {
LoadingContent(title = "Processing request...")
}
errorState != null -> {
ErrorContent(
errorState = SeamErrorState.Unknown,
onRetry = {
errorState = null
// Retry operation
}
)
}
else -> {
// Normal content with optional error banners
Text("Your content here")
// Show dismissible error banners for non-critical errors
errorState?.let { error ->
ErrorBanner(
errorMessage = error,
onDismiss = { errorState = null }
)
}
}
}
}
}Understanding how components work together can help you build more effective custom interfaces:
Complete Flow Components:
SeamAccessView→ UsesSeamCredentialsView,SeamOtpView,SeamUnlockCardView, andBluetoothRedirectScreenSeamCredentialsView→ UsesKeyCardComponent,EmptyKeysState,LoadingContent, andErrorBannerSeamUnlockCardView→ UsesUnlockContent,UnlockHeader, andUnlockErrorUnlockContent→ UsesUnlockButton,StatusMessage, andUnlockInstructions
Standalone Utility Components:
- Error handling:
ErrorContent,ErrorBanner - Loading states:
LoadingContent,CircleSpinner - Buttons:
SeamButton,SeamPrimaryButton,SeamSecondaryButton
Whether you choose SeamAccessView for a complete out-of-the-box solution or mix and match individual components to build custom credential management interfaces, you now have all the information needed to integrate Seam's powerful mobile credential capabilities into your application.
For additional support:
- 📧 Email: support@seam.co
- 📖 Documentation: docs.seam.co
- 🐛 Issues: GitHub Issues