iOS Bitwards SDK
The Bitwards SDK has a minimum deployment target of iOS 15, and it uses Swift 5. Implement the following steps to use the Bitwards SDK in your application.
1. Handle Bluetooth permissions as required by Apple
SDK uses Bluetooth to detect and connect to resources (locks, cabinet locks and readers). When users open your app, they are shown an iOS-provided screen where they can grant permission for your app to use Bluetooth. See Apple documentation for latest information on how to ask for Bluetooth permission. Add these properties and values in yout app's info.plist:
com.apple.security.device.bluetooth: YES/TRUE
Privacy - Bluetooth Always Usage Description: A message that tells people why the app needs access to Bluetooth
2. Configure SDK
You must configure the SDK before accessing it for the first time. Typically you would do this right after your app has launched. Pass a BwEnvironment struct with valid values to BitwardsService.configure function. If BwEnvironment properties are are not valid, the SDK will throw an BitwardsServiceError.invalidSDKConfiguration error.
Example:
let environment = BwEnvironment(
baseURL: "https://myserver.com/api",
OAuthURL: "https://myOAuthServer.com/api",
OAuthAppVariant: "MYAPPVARIANT",
errorLoggingAllowed: true,
errorLoggingLevel: .errorLogsAndSessionLog,
errorLoggingURL: "https://myErrorLogServer.com",
errorLoggingAPIKey: "myErrorLogAPIKey"
)
try BitwardsService.configure(environment: environment)
baseURL (required): URL to Bitwards Service API in your server.
OAuthURL (optional): URL to OAuth login server, if you have one.
OAuthAppVariant (required if you provide OAuthURL): OAuth app variant name.
errorLoggingAllowed (required): Tells SDK if it is allowed to send error reports or session logs to your error logging server. Error logs and session logs help investigate issues if end users experience problems. Recommended value is true.
errorLoggingLevel (required):
.errorLogs: only error logs are sent (in case of an error)
.errorLogsAndSessionLog: also session logs are sent each time user app session ends. Session logs contain even more information that help track possible issues.
errorLoggingURL (required): URL to your server API that handles error logs and session logs.
errorLoggingAPIKey (required): API key to your server API that handles error logs and session logs.
After successful configuration, you can access the SDK as a singleton object like:
let sdk = BitwardsService.shared
3. Log in user
Almost all SDK functionality requires a user to be logged in.
To check if user is logged in use:
var userIsLoggedIn: Bool { get }
If already logged in, you must log out the user first:
func logout() async
To log in with username and password use:
func login(with userName: String, and password: String, synchronize: Bool) async throws
To log in with OAuth credentials, get login details first:
func OAuthLoginDetails() async throws -> [BwOAuthLoginDetail]
Then using information in BwOAuthLoginDetail, create a BwOAuth2Credential, and login:
func login(with credential: BwOAuth2Credential, synchronize: Bool) async throws
4. Subscribe to SDK publishers
Bitwards SDK uses Swift Combine Framework to publish the following information. Your app should subscribe to these publishers and handle the values as needed in your application.
-
var SDKErrorPublisher: AnyPublisher<BitwardsServiceError, Never> { get }Publishes errors that do not result from direct call to a SDK function by parent app. These could happen when the SDK fails to synchronize with the service backend automatically or something else goes wrong. -
var isLoggedInPublisher: AnyPublisher<Bool, Never> { get }Publishes user login status. Emitted when user is logged in with success and when user is logged out.
Publishes user login status. Emitted when user is logged in with success and when user is logged out.
-
var userPublisher: AnyPublisher<BwUser?, Never> { get }Publishes user's information. BwUser is a struct that contains information about logged in user, user's accounts, locations, roles etc. -
var radioIsAvailablePublisher: AnyPublisher<Bool, Never> { get }Tells if Bluetooth is available for the SDK to use. Device Bluetooth must be on and user must give give your app permission to use Bluetooth. -
jsx var isScanningPublisher: AnyPublisher<Bool, Never> { get }Publishes Bluetooth scanning status. Tells whether the SDK is currently scanning with Bluetooth. Scanning starts automatically when app launches or user returns to app. -
jsx var resourcesPublisher: AnyPublisher<[BwResource], Never> { get }Publishes all user's resources — locks, cabinet locks and readers. New values are published very frequently as resource Bluetooth (BLE) signal levels change continuously. It is a good idea to throttle this subscriber to one or two updates per second, for example. Nearby resources have a non-nil rssi value. Rssi values range from -127 to 0 with stronger values closer to zero. To get nearby resources that have strong enough signal for opening, filter out all that have a low or nil rssi value. These are the recommended treshold rssi levels:
Bluetooth detected resource: -80.0
Ready for opening: -68.0
Ready for proximity opening (put phone near lock to open): -53.0
5. Synchronize with the backend when needed
The SDK synchronizes with the Bitwards backend service automatically when app is launched and when user returns to your app. Synchronize loads up to date information of user's accounts, locations, resources, and access rights. You can also force a synchronization in response to UI refresh button tap, for example. To synchronize, call:
func synchronize() async throws
6. Open locks
To open a lock, use a resource (BwResource) emitted by resourcesPublisher. It should have high enough rssi value for opening (-68 or higher). Pass the BwResource to open function. If user has valid access right to that resource, lock should open with BwEventStatus success.
func open(resource: BwResource) async -> BwEventStatus
You can use the following extension to find if user has access rights to a particular resource:
extension BwResource {
var hasValidAccessRights: Bool {
guard let resourceInfo = resourceInfo as? BwMobileAccessResource else { return true }
return resourceInfo.accessRights.contains(where: { $0.isValid })
}
}
The SDK Interface
Here is the full BitwardsServiceInterface and BitwardsServiceError public interface:
public protocol BitwardsServiceInterface {
/// Publisher for errors that do not result from direct call to a SDK function by parent app.
/// These could happen when the SDK fails to synchronize with the service backend
/// automatically, automatic OAuth token refresh fails etc. Your app should implement
/// a subscriber for this publisher to handle possible errors.
var SDKErrorPublisher: AnyPublisher<BitwardsServiceError, Never> { get }
//MARK: user login state
/// Publisher for user login status. Triggered when user is logged in with success and
/// when user is logged out.
/// - Returns: AnyPublisher<Bool, Never>
var isLoggedInPublisher: AnyPublisher<Bool, Never> { get }
/// Boolean value to check if user is currently logged in.
var userIsLoggedIn: Bool { get }
/// Login user into the SDK service with username and password.
/// - Parameters:
/// - userName: valid userName for Bitwards service
/// - password: valid password for Bitwards service
/// - synchronize: determines if the SDK should synchronize with the service backend after login. Default value is true.
/// - Throws: BitwardsServiceError
func login(with userName: String, and password: String, synchronize: Bool) async throws
/// Login user into the SDK service with OAuth credentials.
/// - Parameters:
/// - credential: see BwOAuth2Credential documentation
/// - synchronize: determines if the SDK should synchronize with the service backend after login. Default value is true.
/// - Throws: BitwardsServiceError
func login(with credential: BwOAuth2Credential, synchronize: Bool) async throws
/// Log user out from the SDK service. All user's data will be removed from the device local database.
/// In the next login the local database will be re-created (for the same user or different user).
/// - Throws: BitwardsServiceError
func logout() async
/// Function returns a list of OAuth server details. Aquire details for OAuth servers using this function,
/// and then use the details to login with OAuth.
/// - Returns: [BwOAuthLoginDetail]
/// - Throws: BitwardsServiceError
func OAuthLoginDetails() async throws -> [BwOAuthLoginDetail]
//MARK: user account
/// Publisher for BwUser. Any changes on user datamodel causes this to trigger. BwUser is a struct that contains
/// information about user, user's accounts, locations, roles etc.
var userPublisher: AnyPublisher<BwUser?, Never> { get }
/// A function to update user's information on the SDK service.
/// - Parameter user: See BwUser documentation
/// - Throws: BitwardsServiceError
func update(user: BwUser) async throws
/// A function to change current user's password. Users can change their password while staying logged in the application.
/// - Parameters:
/// - oldPassword: old password as String
/// - newPassword: new password as String
/// - Throws: BitwardsServiceError
func changePassword(oldPassword: String, newPassword: String) async throws
/// A function to request an email with a link to resume forgotten password. User must login again after password change.
/// - Parameters:
/// - email: user email
/// - Throws: BitwardsServiceError
func forgotPassword(email: String) async throws
//MARK: Bluetooth resources and scanning
/// Publisher for all current user's resources (locks, cabinet locks and readers). New values are published very
/// frequently as resource Bluetooth (BLE) signal levels change continuously. You app should subscribe to this
/// publisher to have a list of resources that can currently be connected to (open lock, reserve a cabinet lock,
/// or release a cabinet lock).
/// - Returns: AnyPublisher<[BwResource], Never>
var resourcesPublisher: AnyPublisher<[BwResource], Never> { get }
/// Publisher for Bluetooth scanning status. Tells whether the SDK is currently scanning with Bluetooth.
/// - Returns: AnyPublisher<Bool, Never>
var isScanningPublisher: AnyPublisher<Bool, Never> { get }
/// Publisher for Bluetooth availability on the user's device. The device must have Bluetooth on and the user must allow
/// your app to use Bluetooth. Otherwise radioIsAvailablePublisher emits false.
/// - Returns: AnyPublisher<Bool, Never>
var radioIsAvailablePublisher: AnyPublisher<Bool, Never> { get }
/// A function to start Bluetooth scanning in case it has been stopped by using stopScanning(). Bluetooth scanning
/// starts automatically when app launches or user returns to the app (if not stopped by calling stopScanning()).
func startScanning()
/// A function to stop Bluetooth scanning. The SDK service remembers scanning status across app launches.
/// If scanning has been stopped, you can start it again with startScanning()
func stopScanning()
//MARK: synchronize with backend
/// A function to synchronize the SDK local database on device with the SDK service on the backend.
/// The SDK synchronizes automatically on app launch, and when user returns to the app. Your app
/// can call this function in response to a refresh button tap on UI, for example.
/// - Throws: BitwardsServiceError
func synchronize() async throws
/// A function to synchronize only locations of cabinet locks that are available for the current user.
/// - Throws: BitwardsServiceError
func synchronizeAnyUserLocations() async throws
//MARK: opening resources
/// A function to open a mobile access resource.
/// - Parameter resource: the resource to be opened
/// - Returns: BwEventStatus, see documentation of that
func open(resource: BwResource) async -> BwEventStatus
/// A function to reserve a cabinet lock resource for the current user.
/// - Parameter anyUserResource: the resource to reserve
/// - Returns: BwEventStatus, see documentation of that
func reserve(anyUserResource: BwResource) async -> BwEventStatus
/// A function to release a cabinet lock resource that the current user has reserved previously.
/// - Parameter anyUserResource: the resource to release
/// - Returns: BwEventStatus, see documentation of that
func release(anyUserResource: BwResource) async -> BwEventStatus
//MARK: debug logging
/// Controls whether SDK error logging is allowed at any level or not.
/// If false: nothing is sent
/// If true: logs are sent according to value of errorLoggingLevel
var errorLoggingAllowed: Bool { get set }
/// Defines at what level error logs are sent when errorLoggingAllowed is set true. Initial value must be
/// passed when configuring the SDK. You can read or change the value using this property. Error logs
/// are sent only when something goes wrong. Session logs are sent every time user leaves the app and
/// they contain more information about app use on technical level (helpfull for debugging and development).
/// .errorLogs: only error logs are sent.
/// .errorLogsAndSessionLog: both error logs and sesion logs are sent.
var errorLoggingLevel: BwErrorLoggingLevel { get set }
}
public enum BitwardsServiceError: Error {
/// Error when configuring the SDK with BwEnvironment struct. Reason can be:
/// - baseURL is empty or not valid
/// - OAuthURL is provided but not valid
/// - OAuthAppVariant is empty or not valid. If OAuthURL is provided, you should also provide a valid OAuthAppVariant.
case invalidSDKConfiguration
/// Unspecified internal error in SDK
case sdkInternalError
/// Login with username and password failed.
case loginFailed
/// OAuth login failed.
case OAuthLoginFailed
/// User is already logged in when trying to login with username and password or with OAuth login.
case alreadyLoggedIn
/// Failed to get OAuth login details from the service backend.
case getOAuthLoginDetailsFailed
/// Failed to refresh user's OAuth token.
case OAuthRefreshFailed
/// User is not logged in when trying to call any service backend API endpoint.
case notLoggedIn
/// User is logged in but unauthorized when trying to call any service backend API endpoint.
case userUnAuthorized
/// Failed to synchronize SDK with the service backend.
case synchronizeFailed
/// Failed to synchronize SDK with the service backend.
case synchronizeLocationsFailed
/// Service backend operation such as update user data or change password failed.
case serviceBackendOperationFailed
/// Resource's mac address is not valid when trying open, book, or release a resource.
case invalidMacAddress
/// User has no token or token is not valid when trying to access a resource.
case noValidToken
/// Communication with a resource ended because of timeout.
case timeout
/// Resource is not reachable via Bluetooth when trying to book, open or release it.
case resourceUnreachable
/// Trying to use open function with a cabinet lock type resource (use book or release with cabinet locks).
case wrongResourceType
/// Mobile access resource opening failed because user has no valid access right.
case noValidAccessRight
/// Mobile access resource opening failed because user is blocked by schedule.
case blockedBySchedule
/// Mobile access resource opening failed because user is blocked by schedule expetion day.
case blockedByExceptionDay
/// Failed to book a cabinet lock type resource.
case resourceBookingFailed
/// Failed to release a cabinet lock type resource.
case resourceReleaseFailed
/// Network communication failed in a cabinet lock type resource.
case resourceNetworkIssue
}
SDK Types
BwUser
public struct BwUser {
public var userId: String
public var firstname: String?
public var lastname: String?
public var street: String?
public var zipCode: String?
public var city: String?
public var country: String?
public var email: String?
public var phoneNumber: String?
public let accounts: [BwAccount]
public let roles: [BwRole]
}
#### BwAccount
public struct BwAccount: Hashable {
public let id: Int
public let name: String
public let additionalInfo: String?
public let organization: String?
public let role: BwRole
public let address: BwAddress?
public let contact: BwContact?
public let locations: [BwLocation]
// MARK: - Hashable
public func hash(into hasher: inout Hasher) {
hasher.combine(id)
}
public static func == (lhs: BwAccount, rhs: BwAccount) -> Bool { lhs.id == rhs.id }
}
#### BwLocation
public struct BwLocation: Hashable {
public let id: Int
public let types: [BwLocationType]
public let name: String?
public let latitude: Double?
public let longitude: Double?
public let address: BwAddress?
public let contact: BwContact?
// MARK: - Hashable
public static func == (lhs: BwLocation, rhs: BwLocation) -> Bool {
lhs.id == rhs.id &&
lhs.types == rhs.types
}
public func hash(into hasher: inout Hasher) {
hasher.combine(id)
hasher.combine(types)
}
}
#### BwAccessRight
public struct BwAccessRight {
public let validFrom: Date
public let validTo: Date?
public let rules: [BwRule]?
public let exceptionDays: [BwExceptionDay]?
public var isValid: Bool {
let now = Date()
guard validFrom < now && validTo ?? Date.distantFuture > now else { return false }
guard rules?.contains(where: { !$0.isBlocking }) ?? true else { return false }
guard exceptionDays?.contains(where: { $0.isBlocking }) ?? false else { return true }
return false
}
}
#### BwRule
public struct BwRule {
public let days: Set<BwWeekday>
public let fromHour: Int
public let fromMinute: Int
public let fromSecond: Int
public let toHour: Int
public let toMinute: Int
public let toSecond: Int
public var isBlocking: Bool {
let now = Date()
let weekDay = Calendar.current.component(.weekday, from: now)
guard days.map({ $0.rawValue }).contains(weekDay) else { return true }
var fromDateComponents = Calendar.current.dateComponents([.year, .month, .day], from: now)
fromDateComponents.hour = fromHour
fromDateComponents.minute = fromMinute
guard Calendar.current.date(from: fromDateComponents) ?? Date.distantFuture < now else { return true }
var toDateComponents = Calendar.current.dateComponents([.year, .month, .day], from: now)
toDateComponents.hour = toHour
toDateComponents.minute = toMinute
return !(Calendar.current.date(from: toDateComponents) ?? Date.distantPast > now)
}
}
#### BwExceptionDay
public struct BwExceptionDay {
public let name: String?
public let from: Date
public let to: Date?
var isBlocking: Bool {
guard Calendar.current.isDateInToday(from) else { return false }
let now = Date()
return !(from < now && now < to ?? Calendar.current.startOfDay(for: now).advanced(by: 60*60*24 - 1))
}
}
#### BwResource
public struct BwResource: Hashable {
public private(set) var rssi: Double?
public let type: BwResourceType
public internal(set) var resourceInfo: BwResourceInfo
// MARK: -internal api
public mutating func set(radioStrengthTo rssi: Double?) {
self.rssi = rssi
}
// MARK: - Hashable
public static func == (lhs: BwResource, rhs: BwResource) -> Bool {
lhs.resourceInfo.macAddress == rhs.resourceInfo.macAddress
}
public func hash(into hasher: inout Hasher) {
hasher.combine(resourceInfo.macAddress)
}
}
#### BwResourceInfo
public protocol BwResourceInfo {
var macAddress: String { get }
var id: String { get }
var description: String? { get }
var info: String? { get }
var doorId: String? { get }
var floor: String? { get }
}
#### BwEventStatus
public enum BwEventStatus {
case success
case failure(BwEventFailureType?)
}
extension BwEventStatus {
public enum BwEventFailureType: String {
case timeout
case noValidAccessRight
case blockedByRule
case blockedByExceptionDay
case noValidToken
case unreachableResource
case deniedByRegularSchedule
case deniedByExceptionSchedule
case handleInWrongPosition
case operationFailedBatteryCritical
case tokenBlacklisted
case wrongTimeWindow
case anyUserReservationFailed
case anyUserReleaseFailed
case anyUserNetworkIssue
}
}