SDK Callbacks
SDK callbacks are essential for updating your app's UI based on SDK operations. This guide documents all available callbacks, their lifecycle, and how to use them effectively.
Overview
Callbacks provide real-time notifications about the progress of dock operations. They are invoked during NFC scanning (which reads the dock's metadata), BLE connection, key transmission, and dock state changes. All callbacks are guaranteed to be invoked on the main/UI thread, making it safe to update UI directly.
Callback Lifecycle
The typical callback sequence for an unlock operation is:
onNFCScanCompleted → onBleConnecting → onBleConnected → onKeySent →
onDockUnlocked → onDockChainRemoved → [onDockChainInserted] → [onDockSecured] →
onBleDisconnected
For lock operations, the sequence includes onDockChainInserted and onDockSecured before disconnection.
Available Callbacks
- Android
- iOS
- Flutter
MOSADockSDKCallback Interface
The Android SDK uses the MOSADockSDKCallback interface. Implement this interface to receive callbacks:
interface MOSADockSDKCallback {
fun onNFCScanCompleted(mosaTag: MosaTag)
fun onBleConnecting()
fun onBleConnected()
fun onBleDisConnected()
fun onKeySent()
fun onDockUnlocked()
fun onDockChainRemoved()
fun onDockChainInserted()
fun onDockSecured()
fun onError(error: MosaError)
}
MosaDockDelegate Protocol
The iOS SDK uses the MosaDockDelegate protocol. Conform to this protocol to receive callbacks:
public protocol MosaDockDelegate: AnyObject {
func onNFCScanCompleted(_ tag: MosaTag)
func onBleConnecting()
func onBleConnected()
func onBleDisconnected()
func onKeySent()
func onDockUnlocked()
func onDockChainRemoved()
func onDockChainInserted()
func onDockSecured()
func onError(_ error: MosaError)
}
DockCallbackEvent Stream
The Flutter SDK uses event streams. Subscribe to dockCallbackEvents to receive callbacks:
sdk.dockCallbackEvents.listen((DockCallbackEvent event) {
// Handle event
});
Callback Reference
onNFCScanCompleted
When: NFC scan successfully reads the dock's metadata from the dock
Parameters:
- Android:
mosaTag: MosaTag- Contains dock metadata including dock MAC address and tag data - iOS:
tag: MosaTag- Contains dock metadata including dock MAC address and tag data - Flutter:
DockCallbackEventwithtype: nfcScanCompletedanddatacontaining dock metadata
Use Case: Update UI to show "Dock found" or extract MAC address for display. The NFC scan reads the dock's metadata and returns it as a MosaTag object in this callback.
Example:
- Android
- iOS
- Flutter
override fun onNFCScanCompleted(mosaTag: MosaTag) {
runOnUiThread {
statusText.text = "Dock found: ${mosaTag.macAddress}"
// MAC address is now available for BLE connection
}
}
func onNFCScanCompleted(_ tag: MosaTag) {
DispatchQueue.main.async {
statusLabel.text = "Dock found: \(tag.macAddress)"
// MAC address is now available for BLE connection
}
}
sdk.dockCallbackEvents.listen((event) {
if (event.type == DockCallbackType.nfcScanCompleted) {
final macAddress = event.data?['macAddress'];
setState(() {
statusText = 'Dock found: $macAddress';
});
}
});
onBleConnecting
When: BLE connection attempt has started
Parameters: None
Use Case: Update UI to show "Connecting to dock..."
Example:
- Android
- iOS
- Flutter
override fun onBleConnecting() {
runOnUiThread {
statusText.text = "Connecting to dock..."
progressBar.visibility = View.VISIBLE
}
}
func onBleConnecting() {
DispatchQueue.main.async {
statusLabel.text = "Connecting to dock..."
progressIndicator.startAnimating()
}
}
if (event.type == DockCallbackType.bleConnecting) {
setState(() {
statusText = 'Connecting to dock...';
isLoading = true;
});
}
onBleConnected
When: BLE connection is successfully established
Parameters: None
Use Case: Update UI to show "Connected" and enable dock operations
Example:
- Android
- iOS
- Flutter
override fun onBleConnected() {
runOnUiThread {
statusText.text = "Connected to dock"
connectionIndicator.setColorFilter(Color.GREEN)
}
}
func onBleConnected() {
DispatchQueue.main.async {
statusLabel.text = "Connected to dock"
connectionIndicator.tintColor = .systemGreen
}
}
if (event.type == DockCallbackType.bleConnected) {
setState(() {
statusText = 'Connected to dock';
isConnected = true;
});
}
onBleDisConnected
When: BLE connection is lost or closed
Parameters: None
Use Case: Update UI to show disconnected state, hide connection indicators
Example:
- Android
- iOS
- Flutter
override fun onBleDisConnected() {
runOnUiThread {
statusText.text = "Disconnected from dock"
connectionIndicator.setColorFilter(Color.GRAY)
}
}
func onBleDisconnected() {
DispatchQueue.main.async {
statusLabel.text = "Disconnected from dock"
connectionIndicator.tintColor = .systemGray
}
}
if (event.type == DockCallbackType.bleDisconnected) {
setState(() {
statusText = 'Disconnected from dock';
isConnected = false;
});
}
onKeySent
When: Unlock key has been successfully transmitted to the dock via BLE
Parameters: None
Use Case: Update UI to show "Key sent, waiting for dock response..."
Example:
- Android
- iOS
- Flutter
override fun onKeySent() {
runOnUiThread {
statusText.text = "Key sent, unlocking dock..."
}
}
func onKeySent() {
DispatchQueue.main.async {
statusLabel.text = "Key sent, unlocking dock..."
}
}
if (event.type == DockCallbackType.keySent) {
setState(() {
statusText = 'Key sent, unlocking dock...';
});
}
onDockUnlocked
When: Dock has successfully unlocked
Parameters: None
Use Case: Update UI to show "Dock unlocked" and indicate user can remove bike
Example:
- Android
- iOS
- Flutter
override fun onDockUnlocked() {
runOnUiThread {
statusText.text = "Dock unlocked!"
unlockIndicator.setColorFilter(Color.GREEN)
// User can now remove bike
}
}
func onDockUnlocked() {
DispatchQueue.main.async {
statusLabel.text = "Dock unlocked!"
unlockIndicator.tintColor = .systemGreen
// User can now remove bike
}
}
if (event.type == DockCallbackType.dockUnlocked) {
setState(() {
statusText = 'Dock unlocked!';
isUnlocked = true;
// User can now remove bike
});
}
onDockChainRemoved
When: Chain has been removed from the dock (bike can be taken)
Parameters: None
Use Case: Update UI to show "Chain removed, bike ready" or start rental timer
Example:
- Android
- iOS
- Flutter
override fun onDockChainRemoved() {
runOnUiThread {
statusText.text = "Chain removed - bike ready!"
// For bike sharing: start rental timer
// For bike parking: bike is now parked
}
}
func onDockChainRemoved() {
DispatchQueue.main.async {
statusLabel.text = "Chain removed - bike ready!"
// For bike sharing: start rental timer
// For bike parking: bike is now parked
}
}
if (event.type == DockCallbackType.dockChainRemoved) {
setState(() {
statusText = 'Chain removed - bike ready!';
// For bike sharing: start rental timer
// For bike parking: bike is now parked
});
}
onDockChainInserted
When: Chain has been inserted back into the dock (bike is locked)
Parameters: None
Use Case: Update UI to show "Chain inserted, securing dock..." or stop rental timer
Example:
- Android
- iOS
- Flutter
override fun onDockChainInserted() {
runOnUiThread {
statusText.text = "Chain inserted, securing dock..."
// For bike sharing: stop rental timer
// For bike parking: bike is being secured
}
}
func onDockChainInserted() {
DispatchQueue.main.async {
statusLabel.text = "Chain inserted, securing dock..."
// For bike sharing: stop rental timer
// For bike parking: bike is being secured
}
}
if (event.type == DockCallbackType.dockChainInserted) {
setState(() {
statusText = 'Chain inserted, securing dock...';
// For bike sharing: stop rental timer
// For bike parking: bike is being secured
});
}
onDockSecured
When: Dock has been fully secured/locked
Parameters: None
Use Case: Update UI to show "Dock secured" and indicate operation complete
Example:
- Android
- iOS
- Flutter
override fun onDockSecured() {
runOnUiThread {
statusText.text = "Dock secured - operation complete!"
progressBar.visibility = View.GONE
// Operation complete, can navigate away or show success message
}
}
func onDockSecured() {
DispatchQueue.main.async {
statusLabel.text = "Dock secured - operation complete!"
progressIndicator.stopAnimating()
// Operation complete, can navigate away or show success message
}
}
if (event.type == DockCallbackType.dockSecured) {
setState(() {
statusText = 'Dock secured - operation complete!';
isLoading = false;
// Operation complete, can navigate away or show success message
});
}
onError
When: An error occurs during any SDK operation
Parameters:
- Android:
error: MosaError- Structured error with type, message, and metadata - iOS:
error: MosaError- Structured error with type, message, and metadata - Flutter:
DockCallbackEventwithtype: erroranderrorfield containing error information
Use Case: Display error message to user, handle retry logic, log error for debugging
Example:
- Android
- iOS
- Flutter
override fun onError(error: MosaError) {
runOnUiThread {
when (error.errorType) {
MosaErrorType.NFC_DISABLED -> {
showError("Please enable NFC in settings")
// Navigate to NFC settings
}
MosaErrorType.BLE_DISABLED -> {
showError("Please enable Bluetooth in settings")
// Navigate to Bluetooth settings
}
MosaErrorType.DOCK_TIMEOUT -> {
showError("Dock operation timed out. Please try again.")
// Show retry button
}
else -> {
showError("Error: ${error.message}")
}
}
progressBar.visibility = View.GONE
}
}
func onError(_ error: MosaError) {
DispatchQueue.main.async {
switch error.errorType {
case .nfcDisabled:
showError("Please enable NFC in settings")
// Navigate to NFC settings
case .bleDisabled:
showError("Please enable Bluetooth in settings")
// Navigate to Bluetooth settings
case .dockTimeout:
showError("Dock operation timed out. Please try again.")
// Show retry button
default:
showError("Error: \(error.message)")
}
progressIndicator.stopAnimating()
}
}
if (event.type == DockCallbackType.error) {
final error = event.error;
if (error is Map<String, dynamic>) {
final errorType = error['errorType'];
final message = error['message'];
setState(() {
isLoading = false;
});
switch (errorType) {
case 'nfc_disabled':
showError('Please enable NFC in settings');
break;
case 'ble_disabled':
showError('Please enable Bluetooth in settings');
break;
case 'dock_timeout':
showError('Dock operation timed out. Please try again.');
break;
default:
showError('Error: $message');
}
}
}
Complete Implementation Example
Here's a complete example showing how to implement callbacks for a dock unlock operation:
- Android
- iOS
- Flutter
class MainActivity : AppCompatActivity(), MOSADockSDKCallback {
private lateinit var sdk: MOSADockSDK
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
sdk = MOSADockSDK.builder()
.setActivity(this)
.setApiKey("your-api-key")
.setUserId("user-123")
.build()
}
private fun unlockBike() {
lifecycleScope.launch {
val unlockKey = sdk.requestUnlockKey(
type = MOSADockUnlockKeyType.BikeUnlock,
areaId = "area-123"
)
if (unlockKey != null) {
sdk.unlockSmartLock(
unlockKey = unlockKey,
gfaLocationType = MOSADockLocationType.PARKING,
callback = this@MainActivity
)
}
}
}
override fun onNFCScanCompleted(mosaTag: MosaTag) {
runOnUiThread {
statusText.text = "Dock found: ${mosaTag.macAddress}"
}
}
override fun onBleConnecting() {
runOnUiThread {
statusText.text = "Connecting..."
progressBar.visibility = View.VISIBLE
}
}
override fun onBleConnected() {
runOnUiThread {
statusText.text = "Connected"
}
}
override fun onKeySent() {
runOnUiThread {
statusText.text = "Key sent, unlocking..."
}
}
override fun onDockUnlocked() {
runOnUiThread {
statusText.text = "Dock unlocked!"
}
}
override fun onDockChainRemoved() {
runOnUiThread {
statusText.text = "Chain removed - bike ready!"
progressBar.visibility = View.GONE
}
}
override fun onBleDisConnected() {
runOnUiThread {
statusText.text = "Disconnected"
}
}
override fun onError(error: MosaError) {
runOnUiThread {
statusText.text = "Error: ${error.message}"
progressBar.visibility = View.GONE
showErrorDialog(error)
}
}
// Implement other callback methods...
override fun onDockChainInserted() {}
override fun onDockSecured() {}
}
class ViewController: UIViewController, MosaDockDelegate {
var sdk: MosaDockSDK!
override func viewDidLoad() {
super.viewDidLoad()
MosaDockSDK.initialize(
with: self,
apiKey: "your-api-key",
userId: "user-123"
)
sdk = MosaDockSDK.shared
sdk.setDelegate(self)
}
func unlockBike() {
Task {
do {
let unlockKey = try await sdk.requestUnlockKey(
type: .bikeUnlock,
areaId: "area-123",
userId: "user-123"
)
try await sdk.unlockSmartLock(
with: unlockKey,
location: .parking,
delegate: self
)
} catch {
showError("Failed: \(error)")
}
}
}
func onNFCScanCompleted(_ tag: MosaTag) {
DispatchQueue.main.async {
statusLabel.text = "Dock found: \(tag.macAddress)"
}
}
func onBleConnecting() {
DispatchQueue.main.async {
statusLabel.text = "Connecting..."
progressIndicator.startAnimating()
}
}
func onBleConnected() {
DispatchQueue.main.async {
statusLabel.text = "Connected"
}
}
func onKeySent() {
DispatchQueue.main.async {
statusLabel.text = "Key sent, unlocking..."
}
}
func onDockUnlocked() {
DispatchQueue.main.async {
statusLabel.text = "Dock unlocked!"
}
}
func onDockChainRemoved() {
DispatchQueue.main.async {
statusLabel.text = "Chain removed - bike ready!"
progressIndicator.stopAnimating()
}
}
func onBleDisconnected() {
DispatchQueue.main.async {
statusLabel.text = "Disconnected"
}
}
func onError(_ error: MosaError) {
DispatchQueue.main.async {
statusLabel.text = "Error: \(error.message)"
progressIndicator.stopAnimating()
showErrorDialog(error)
}
}
// Implement other callback methods...
func onDockChainInserted() {}
func onDockSecured() {}
}
class UnlockScreen extends StatefulWidget {
@override
_UnlockScreenState createState() => _UnlockScreenState();
}
class _UnlockScreenState extends State<UnlockScreen> {
final mosaDockSdk = MosaDockSdk();
StreamSubscription<DockCallbackEvent>? _callbackSubscription;
String statusText = 'Ready';
bool isLoading = false;
@override
void initState() {
super.initState();
_setupCallbacks();
}
void _setupCallbacks() {
// IMPORTANT: Subscribe to callbacks BEFORE initializing SDK
_callbackSubscription = mosaDockSdk.dockCallbackEvents.listen((event) {
setState(() {
switch (event.type) {
case DockCallbackType.nfcScanCompleted:
final macAddress = event.data?['macAddress'];
statusText = 'Dock found: $macAddress';
break;
case DockCallbackType.bleConnecting:
statusText = 'Connecting...';
isLoading = true;
break;
case DockCallbackType.bleConnected:
statusText = 'Connected';
break;
case DockCallbackType.keySent:
statusText = 'Key sent, unlocking...';
break;
case DockCallbackType.dockUnlocked:
statusText = 'Dock unlocked!';
break;
case DockCallbackType.dockChainRemoved:
statusText = 'Chain removed - bike ready!';
isLoading = false;
break;
case DockCallbackType.bleDisconnected:
statusText = 'Disconnected';
break;
case DockCallbackType.error:
statusText = 'Error: ${event.message ?? "Unknown error"}';
isLoading = false;
_handleError(event.error);
break;
default:
break;
}
});
});
// Now initialize SDK
mosaDockSdk.initialize(
apiKey: 'your-api-key',
userId: 'user-123',
);
}
Future<void> unlockBike() async {
try {
final unlockKey = await mosaDockSdk.requestUnlockKey(
type: MosaDockUnlockKeyType.bikeUnlock,
areaId: 'area-123',
userId: 'user-123',
);
await mosaDockSdk.handleClientUnlock(
unlockKey: unlockKey,
locationType: MosaDockLocationType.parking.value,
);
} catch (e) {
setState(() {
statusText = 'Error: $e';
isLoading = false;
});
}
}
void _handleError(dynamic error) {
// Handle error based on error type
if (error is Map<String, dynamic>) {
final errorType = error['errorType'];
// Show appropriate error message
}
}
@override
void dispose() {
_callbackSubscription?.cancel();
super.dispose();
}
@override
Widget build(BuildContext context) {
return Scaffold(
body: Column(
children: [
Text(statusText),
if (isLoading) CircularProgressIndicator(),
ElevatedButton(
onPressed: unlockBike,
child: Text('Unlock Bike'),
),
],
),
);
}
}