iOS Mobile SDK
Introduction
Arkose Labs' mobile SDK lets you wrap our solution with iOS native function calls. This guarantees seamless integration of your mobile apps with Arkose's full interactive challenges on detection and enforcement, and does so without the extended wait times for separate mobile solutions.
This page covers the Mobile SDK for iOS. If you are developing in Android, see the Mobile SDK for Android page.
The Arkose Mobile SDK for iOS v1:
-
Wraps Arkose's Advanced Enforcement Challenge in native iOS “web views”.
-
Has 1-to-1 feature availability between web and mobile solutions.
-
Integrates with your apps through native functions.
-
Handles errors through callback events.
-
Complies with Arkose Internal Security guidelines.
-
Complies with Apple App Store guidelines for ease of integration.
-
Is fully compatible with new API releases.
-
Supports minimum version iOS 13.0.
Mobile SDK High Level Design
Mobile SDK Builds Availability
The Arkose Labs Mobile SDKs are available via the Mobile SDK’s Support page. Please talk with your CSM (Customer Success Manager) about your intended usage and to request access.
Compatibility
Device | Minimum OS Version | Target OS Versions |
---|---|---|
iPhone | iOS 13+ | iOS 13, 14, 15, 16, 17, 18 |
iPad | iPadOS 13+ | iPadOS 13, 14, 15, 16, 17, 18 |
Mac | macOS 12+ | macOS 12, 13, 14, 15 |
Vision OS | Vision OS 1.3+ | Vision OS 1.3, 2.0 |
All existing detection and challenge features on our web solution are also available on the Mobile SDKs. All new ones are automatically added; you don't need to update your application every time we have a new release of our Web platform.
Security
The Arkose Labs Mobile SDKs are Arkose Labs Security reviewed and comply with Apple App Store guidelines.
Performance
We created the Arkose Labs Mobile SDKs with stability and performance in mind. Their use has no significant impact on the host application’s performance.
Installation
Follow these steps to set up Arkose Labs Mobile SDK for iOS in Xcode in your host application. This applies to both our detection and enforcement components.
Prerequisites
-
A host iOS application. You must be able to build and run this application.
-
For the full end-to-end Arkose setup, you must also complete the standard Arkose Server-Side setup instructions.
Integration Steps
Add ArkoseLabsKit to the Project
-
In Xcode, open your Host application.
-
From the Project Navigator area, select the folder with your project’s name.
-
From the project’s
xcodeproj
details, select Target. -
To use the dynamic framework:
- Scroll to Frameworks, Libraries, and Embedded Content and drag and drop
ALSDK.XCFramework
into this section. - In the Project Navigator, find the
Frameworks
folder and verifyArkoseLabsKit
is inside it. - Click on Build Phases to display that tab. Find the Link Binary with Libraries section and confirm it contains the
ArkoseLabsKit.xcframework
.
- Scroll to Frameworks, Libraries, and Embedded Content and drag and drop
-
To use the static library:
- Create a New Folder in your project called
ArkoseLabsKitStatic
. - Drag and drop all files into the new folder from the SDK build
dist
folder. - Scroll to Frameworks, Libraries, and Embedded Content and drag and drop:
- for iOS Device Support add
ArkoseLabsKitStaticiOS.a
file in the section - for iOS Simulator Support add
ArkoseLabsKitStaticiOSSim.a
file in the section - for Mac Support add
ArkoseLabsKitStaticMac.a
file in the section
- for iOS Device Support add
- In the Copy Bundle Resources section, add
ArkoseLabsKitResource.Bundle
- Create a New Folder in your project called
-
Perform a Clean task.
-
Perform a Build.
Initialize the SDK
To integrate Arkose Bot Manager with the enforce challenge, follow the steps outlined below:
- Import the
ArkoseLabsKit
orArkoseLabsKitStatic
module before invoking any API from the SDK:
// For dynamic framework
import ArkoseLabsKit
// For static framework
//import ArkoseLabsKitStatic
-
Initialize the SDK as soon as the application launches with the
ArkoseConfig
object that contains all configuration parameters. We recommend using theUIApplicationDelegate
didFinishLaunchingWithOptions
notification to do the initialization. A sample initialization code is shown below:ArkoseManager.initialize( with: ArkoseConfig.Builder(withAPIKey: <SPECIFY_YOUR_API_KEY>) .with(language: "en") .build() )
A complete example of didFinishLaunchingWithOptions implementation is below:
// For dynamic framework import ArkoseLabsKit // For static framework //import ArkoseLabsKitStatic import SwiftUI class AppDelegate: NSObject, UIApplicationDelegate { func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey : Any]? = nil) -> Bool { ArkoseManager.initialize( with: ArkoseConfig.Builder(withAPIKey: <SPECIFY_YOUR_API_KEY>) .with(language: "en") //optional .with(clientAPIRetryCount: 0) //optional .with(styleTheme: "") //optional .build() ) return true } }
Integrate into a SwiftUI Aplication
- To run Arkose Bot Manager and display the Enforcement Challenge in a SwiftUI
View
, integrateArkoseChallengeView
into the content of your custom view.
ArkoseChallengeView(isPresented: $isPresented, delegate: self, cancelButtonTitle: "Cancel")
See Appendix: SwiftUI Content View below for a complete example.
Integrate into a UIKit Application
- To run Arkose Bot Manager and display the Enforcement Challenge from a UIKit
ViewController
, invokeArkoseManager.showEnforcementChallenge
from an action method.
ArkoseManager.showEnforcementChallenge(
parent: self,
delegate: self,
cancelButtonTitle: "Cancel")
See the Appendix: UIKit View Controller below for a complete example.
Receiving Notifications
To receive notifications about various events triggered by Enforcement Challenge, implement the ArkoseChallengeDelegate
protocol on the SwiftUI View or the UIViewController class and pass the instance as the delegate
parameter above. To simplify the implementation, ArkoseChallengeDelegate
has a default implementation, so implement only the necessary methods for your desired functionality. The most commonly implemented protocol methods are onCompleted
, onError
, and onFailed
to complete the necessary action from the application.
Build the revised project
- Perform a Clean.
- Perform a Build.
Run and test the application
- Run your modified iOS application.
- If running Arkose Bot Manager:
- On the integrated screen, confirm you now see an Arkose Labs Enforcement Challenge.b. Verify the challenge.
- On successful verification, the
onCompleted
event returns atoken
as part of the response JSON object.
- Send the
token
to your back-end server for verification.
Update Configuration in the SDK
To update configuration of the SDK any time before the enforcement challenge is called, follow the steps outlined below:
In SwiftUI Application
- To run Arkose Bot Manager and display the Enforcement Challenge in a SwiftUI
View
, integrateArkoseChallengeView
into the content of your custom view.See Appendix: SwiftUI Content View below for a complete example.ArkoseChallengeView(isPresented: $isPresented, delegate: self, cancelButtonTitle: "Cancel", resetButtonTitle: "Reset", config: ArkoseConfig.Builder( withAPIKey: <SPECIFY_YOUR_API_KEY>) .with(language: "en") //optional .with(clientAPIRetryCount: 0) //optional .with(styleTheme: "") //optional .build())
In a UIKit Application
- To run Arkose Bot Manager and display the Enforcement Challenge from a UIKit
ViewController
, invokeArkoseManager.showEnforcementChallenge
from an action method.ArkoseManager.update(with: ArkoseConfig.Builder( withAPIKey: <SPECIFY_YOUR_API_KEY>) .with(language: "fr") .build() )
See the Appendix: UIKit View Controller below for a complete example.
For API Version 2.1.0 and Above
ArkoseConfig.Builder
Arkose Labs Enforcement Challenge configuration parameters can be updated using ArkoseConfig.Builder class. The public methods to specify these parameters are listed below:
public init(withAPIKey apiKey: String)
Initialize the Builder instance with the public key of your accountpublic func with(apiBaseUrl: String) -> Builder
Base URL of Arkose Labs EC platform as supplied by Arkose Labs.public func with(blob: String) -> Builder
Specify any encrypted data blobs to share with Arkose Bot Manager. It is optional.public func with(language: String) -> Builder
Specify any language setting for the Enforcement Challenge, the default value is en. It is optional.public func with(userAgent: String) -> Builder
Specify any userAgent setting for ease of testing forced Enforcement Challenge for a session, the default value is inbuilt webview’s userAgent. It is optional. Please talk with your CSM (Customer Success Manager) about your intended usage and request backend configuration.public func with(clientAPIRetryCount: Int) -> Builder
Specify the number of retries when network issues are triggered when the client app tries to connect to the apiBaseUrl.
The retry only works foronError
events with error codes below. When the configured number of retries is exhausted, the last error code is returned.- Challenge load error.
- API_REQUEST_ERROR
- API_REQUEST_TIMEOUT
Default: 0. It is optional.
public func with(styleTheme: String) -> Builder
Specify style theme name for Enforcement Challenge. It is optional.public func build() -> ArkoseConfig
Builds and returns an instance of ArkoseConfig
ArkoseConfig
An instance of ArkoseConfig
contains all the configuration parameters, use ArkoseConfig.Builder
to class to construct an instance of ArkoseConfig
.
ArkoseManager
ArkoseManager class provides public methods to integrate the application with Arkose Bot Manager.
-
public static func initialize (with configuration: ArkoseConfig)
Initialize Arkose Bot Manager SDK with the configuration parameters. -
public static func update (with configuration: ArkoseConfig)
Update Arkose Bot Manager SDK with the configuration parameters. -
public static func showEnforcementChallenge(parent: UIViewController, delegate: ArkoseChallengeDelegate, cancelButtonTitle: String? = "Cancel", resetButtonTitle: String? = nil, withActivity: Bool? = nil)
-
Display the Enforcement Challenge View and invokes the
ArkoseChallengeDelegate
methods to notify the result.parent
: An instance of UIViewController where the Enforcement Challenge View is displayeddelegate
: An instance of ArkoseChallengeDelegate to receive event notificationscancelButtonTitle
: A localized string for the title of the Cancel button, the default value is"Cancel"
.
If this parameter is set tonil
, the Cancel button is not shown in the view.resetButtonTitle
: A localize string for the title of the Reset button, the default value isnil
.
If this parameter is set tonil
, the Reset button is not shown in the view.withActivity
: A bool to control enablement of loading Spinner animation, the default value istrue
.
If this parameter is set tonil
, the loading Spinner animation is shown in the view.public static var logLevel: LogLevel
Set the log level for the SDK. All messages logged by theArkoseLabsKit
framework will have the[ArkoseLabsKit]
text for easy identification. Valid LogLevel values are:info
,warn
,error
.
ArkoseChallengeView
ArkoseChallengeView
is the SwiftUI View component to integrate into the contents of the application View.
public init(isPresented: Binding<Bool>,
delegate: ArkoseChallengeDelegate,
cancelButtonTitle: String? = nil,
resetButtonTitle: String? = nil,
withActivity: Bool? = nil,
config: ArkoseConfig? = nil)
isPresented
: A boolean value to control the visibility of the Enforcement Challenge Viewdelegate
: An instance ofArkoseChallengeDelegate
to receive event notificationscancelButtonTitle
: A localized string for the title of the Cancel button, the default value is"Cancel"
.
If this parameter is set tonil
, the Cancel button is not shown in the view.resetButtonTitle
: A localized string for the title of the Reset button, the default value isnil
.
If this parameter is set tonil
, the Reset button is not shown in the view.withActivity
: A bool to control enablement of loading Spinner animation, the default value is true.
If this parameter is set tonil
, the loading Spinner animation is shown in the view.config
: An ArkoseConfig created using it’s builder, the default isnil
.
if this parameter is set tonil
, the configuration is not updated with latest configuration
LogLevel
LogLevel
is an enumeration to control the logs generated by the framework. The valid values are:
info
: Log messages with severity level of info and abovewarn
: Log messages with severity level of warn and aboveerror
: Log messages with severity level of error only
ArkoseChallengeDelegate
ArkoseChallengeDelegate is a protocol to be implemented by the application and passed in showEnforcementChallenge function call to receive notification about different events during Enforcement Challenge View lifecycle.
func onReady()
: onReady callback is invoked when the Enforcement Challenge or detection is ready.func onShow():
onShow callback is invoked when the Enforcement Challenge is running and our detection component is analyzing the user intent. The function is also invoked when an Enforcement Challenge or detection is re-displayed (e.g. if the user closes the EC or detection view and tries to continue). Applicable to enforcement only.func onShown()
: onShown callback is invoked when the Enforcement Challenge is displayed for the first time. Applicable to enforcement only.func onSuppress():
onSuppress callback is invoked when either an Enforcement Challenge is suppressed (i.e. A session was classified as not requiring a challenge) or detection is running and is analyzing the user intent.func onHide()
:onHide callback is invoked when the EC or detection view is hidden. For example, this happens after an EC or detection is completed or if the user clicks the close button.func onReset()
:onReset callback is invoked after the Enforcement Challenge is reset. Typically occurs after a challenge has been successfully answered. Applicable to enforcement only.func onCompleted(response: [String: Any?])
: onCompleted callback is invoked when a session is classified as not needing a challenge or a detection has been successfully completed.
A Response Object is passed to this function.func onError(response: [String: Any?])
: onError callback is invoked when an error occurs when loading the challenge or detection.
A Response Object is passed to this function.func onWarning(response: [String: Any?])
: onWarning callback is invoked when an issue occurs which needs to be shared with the app as a warning, based on which App can take custom actions, when loading the challenge or detection.
A Response Object is passed to this function.func onFailed(response: [String: Any?])
: onFailed callback is invoked when a challenge has failed (the user has failed the challenge multiple times and is not allowed to continue the session).
A Response Object is passed to this function. Applicable to enforcement only.
For API Version 2.0.1
Arkose Labs Enforcement Challenge configuration parameters can be updated using ArkoseConfig.Builder
class. The public methods to specify these parameters are listed below:
public init(withAPIKey apiKey: String)
Initialize the Builder instance with the public key of your account.public func with(apiBaseUrl: String) -> Builder
Base URL of Arkose Labs EC platform as supplied by Arkose Labs.public func with(blob: String) -> Builder
Specify any encrypted data blobs to share with Arkose Bot Manager. It is optional.public func with(language: String) -> Builder
Specify any language setting for the Enforcement Challenge, the default value is en. It is optional.public func build() -> ArkoseConfig
Builds and returns an instance of ArkoseConfig.
ArkoseConfig
An instance of ArkoseConfig contains all the configuration parameters, use ArkoseConfig.Builder
class to construct an instance of ArkoseConfig.
ArkoseManager
ArkoseManager class provides public methods to integrate the application with the Arkose Bot Manager.
-
public static func initialize(with configuration: ArkoseConfig)
Initialize Arkose Bot Manager SDK with the configuration parameters. -
public static func update(with configuration: ArkoseConfig)
Update Arkose Bot Manager SDK with the configuration parameters. -
public static func showEnforcementChallenge(parent: UIViewController, delegate: ArkoseChallengeDelegate, cancelButtonTitle: String? = "Cancel", resetButtonTitle: String? = nil)
-
Display the Enforcement Challenge View and invokes the ArkoseChallengeDelegate methods to notify the result.
- parent: An instance of
UIViewController
where the Enforcement Challenge View is displayed. - delegate: An instance of
ArkoseChallengeDelegate
to receive event notifications. - cancelButtonTitle: A localized string for the title of the
Cancel
button, the default value is "Cancel". If this parameter is set to nil, theCancel
button is not shown in the view. - resetButtonTitle: A localize string for the title of the
Reset
button, the default value is nil. If this parameter is set to nil, theReset
button is not shown in the view.
- parent: An instance of
-
public static var logLevel: LogLevel
:
Set the log level for the SDK. All messages logged by theArkoseLabsKit
framework will have the [ArkoseLabsKit] text for easy identification. Valid LogLevel values are:info
,warn
,error
ArkoseChallengeView
ArkoseChallengeView is the SwiftUI View component to integrate into the contents of the application view.
public init(isPresented: Binding<Bool>,
delegate: ArkoseChallengeDelegate,
cancelButtonTitle: String? = "Cancel",
resetButtonTitle: String? = nil)
config: ArkoseCongig? = nil)
isPresented: A boolean value to control the visibility of the Enforcement Challenge View
delegate: An instance of ArkoseChallengeDelegate
to receive event notifications
cancelButtonTitle: A localized string for the title of the Cancel
button, the default value is "Cancel".
If this parameter is set to nil, the Cancel button is not shown in the view.
resetButtonTitle: A localize string for the title of the Reset
button, the default value is nil.
If this parameter is set to nil, the Reset button is not shown in the view.
config: An ArkoseConfig
created using it’s builder, the default is nil.
If this parameter is set to nil, the configuration is not updated with latest configuration.
Log Level
LogLevel is an enumeration to control the logs generated by the framework. The valid values are:
info:
Log messages with severity level of info and abovewarn:
Log messages with severity level of warn and aboveerror:
Log messages with severity level of error only
ArkoseChallengeDelegate
ArkoseChallengeDelegate is a protocol to be implemented by the application and passed in showEnforcementChallenge
function call to receive notification about different events during Enforcement Challenge View lifecycle.
func onReady()
: onReady callback is invoked when the Enforcement Challenge or detection is ready.func onShow():
onShow callback is invoked when the Enforcement Challenge is running and our detection component is analyzing the user intent. The function is also invoked when an Enforcement Challenge or detection is re-displayed (e.g. if the user closes the EC or detection view and tries to continue). Applicable to enforcement only.func onShown()
: onShown callback is invoked when the Enforcement Challenge is displayed for the first time. Applicable to enforcement only.func onSuppress():
onSuppress callback is invoked when either an Enforcement Challenge is suppressed (i.e. A session was classified as not requiring a challenge) or detection is running and our detection component is analyzing the user intent.func onHide()
:onHide callback is invoked when the EC or detection view is hidden. For example, this happens after an EC or detection is completed or if the user clicks the close button.func onReset()
:onReset callback is invoked after the Enforcement Challenge is reset. Typically occurs after a challenge has been successfully answered. Applicable to enforcement only.func onCompleted(response: [String: Any?])
: onCompleted callback is invoked when a session is classified as not needing a challenge or a detection has been successfully completed.func onError(response: [String: Any?])
: onError callback is invoked when an error occurs when loading the challenge or detection.
A Response Object is passed to this function.func onWarning(response: [String: Any?])
: onWarning callback is invoked when an issue occurs which needs to be shared with the app as a warning, based on which App can take custom actions, when loading the challenge or detection.
A Response Object is passed to this function.func onFailed(response: [String: Any?])
: onFailed callback is invoked when a challenge has failed (the user has failed the challenge multiple times and is not allowed to continue the session).
A Response Object is passed to this function. Applicable to enforcement only.
For API Version 1.0
Note: Version 1.0 APIs are deprecated starting with ArkoseLabsKit framework 2.0.0, but fully backward compatible with old APIs. But we recommend upgrading to the simplified implementation in 2.0.0 and above.
ALWebView
ALWebView
Configuration Object | Type | Description |
---|---|---|
ALWebView | Public method | Method used to start Arkose Bot Manager. Before this method is called, the model object must be initialized with these three additional configuration parameters detailed in this table. - webEventDelegate (should be assigned to “receiving component”)- apiLang - apiBlob |
WebEventDelegate | Public delegate component | Lets clients receive callbacks from Arkose Bot Manager. |
apiLang | String | Applies only to our enforcement component. Language setting for the Enforcement Challenge. Default: "en" |
apiBlob | String | Optional. Mainly used to share any client encrypted data blobs with Arkose Bot Manager. Default: "" |
apiKey | String | Optional. The API_KEY added in Config.plist can be overridden with this value if needed.Default: "" |
resetSession | Public method | Optional method to reset the current session for Arkose Bot Manager. This creates a new session. |
WebEventDelete
Protocol
WebEventDelete
ProtocolEvents Callback | Type | Description Event | Applicable Component |
---|---|---|---|
onReady | Event | Receives the onReady event callback from the API server.Invoked when the Enforcement or detection component is ready. The Enforcement or detection cannot be triggered before this event. You may want to disable the UI you are protecting until this event has been triggered. | Detection Enforcement |
onShow | Event | Listener function invoked when the Enforcement is running and our detection component is analyzing the user intent. The function is also invoked when an Enforcement Challenge or detection is re-displayed (e.g. if the user closes the EC or detection view and tries to continue). Note that the close button only appears when in Lightbox mode. | Detection Enforcement |
onShown | Event | Function only invoked the when the Enforcement Challenge is displayed for the first time. | Enforcement |
onSuppress | Event | Listener function invoked when either an Enforcement Challenge is suppressed (i.e. A session was classified as not requiring a challenge) or detection is running and our detection component is analyzing the user intent. | Detection Enforcement |
onHide | Event | Listener function invoked when the EC or detection view is hidden. For example, this happens after an EC or detection is completed or if the user clicks the close button. Note that the close button only appears when in Lightbox mode. | Detection Enforcement |
onReset | Event | Function invoked after the Enforcement resets. Typically occurs after a challenge has been successfully answered. | Enforcement |
onCompleted | Event | Listener function invoked when a session is classified as not needing a challenge or a detection has been successfully completed. A Response Object is passed to this function. | Detection Enforcement |
onError | Event | Function invoked when an error occurs when loading the challenge or detection. A Response Object is passed to this function. | Detection Enforcement |
onWarning | Event | Function invoked when an issue occurs which needs to be shared with the app as a warning, based on which App can take custom actions, when loading the challenge or detection. | Detection Enforcement |
onFailed | Event | Function invoked when a challenge has failed (the user has failed the challenge multiple times and is not allowed to continue the session). A Response Object is passed to this function. | Enforcement |
onResize | Event | Function invoked on a resizing event which provides the new width and height values of the EC due to an SDK call. A Response Object is passed to this function. | Enforcement |
Configuration Parameters
You can change the following configuration parameters by specifying their values in the config.plist
file.
Configuration Parameters | Type | Description |
---|---|---|
API_URL_BASE | String | Base URL of Arkose Bot Manager as supplied by Arkose Labs. |
API_KEY | String | Public key for your account. |
API_FILE | String | Name of JavaScript file of Arkose Bot Manager as supplied by Arkose Labs. |
Config.plist
Example
Config.plist
Example<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>VERIFICATION_URL</key>
<string></string>
<key>API_KEY</key>
<string>11111111-1111-1111-1111-111111111111</string>
<key>API_URL_BASE</key>
<string>https://client-api.arkoselabs.com/v2</string>
<key>API_FILE</key>
<string>api.js</string>
</dict>
</plist>
Appendix
Appendix: SwiftUI Content View
import SwiftUI
import ArkoseLabsKit
struct ContentView: View, ArkoseChallengeDelegate {
func onCompleted(response: [String: Any?]) {
print("onComplete received: \(response)")
isPresented = false
}
func onError(response: [String: Any?]) {
print("onError received: \(response)")
isPresented = false
}
func onFailed(response: [String: Any?]) {
print("onFailed received: \(response)")
isPresented = false
}
@State var isPresented = false
@State var username : String = ""
@State var password : String = ""
var body: some View {
ZStack {
VStack {
Text("SwiftUI App")
.padding()
.font(.largeTitle)
.foregroundColor(Color.black)
TextField("Username", text: $username)
.font(.title3)
.disableAutocorrection(true)
.autocapitalization(.none)
.padding()
SecureField("Password", text: $password)
.font(.title3)
.disableAutocorrection(true)
.autocapitalization(.none)
.padding()
Button("Login") {
self.isPresented = true
}
ArkoseChallengeView(isPresented: $isPresented,
delegate: self,
cancelButtonTitle: "Cancel",
resetButtonTitle: "Reset",
config:
ArkoseConfig.Builder(
withAPIKey: "11111111-1111-1111-1111-111111111111")
.with(language: "fr")
.build())
}
}
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
Appendix: UIKit View Controller
import UIKit
import ArkoseLabsKit
class ViewController: UIViewController, ArkoseChallengeDelegate {
func onCompleted(response: [String: Any?]) {
print("onComplete received: \(response)")
}
func onError(response: [String: Any?]) {
print("onError received: \(response)")
}
func onFailed(response: [String: Any?]) {
print("onFailed received: \(response)")
}
@IBAction func Login(_ sender: Any) {
ArkoseManager.update(with: ArkoseConfig.Builder(withAPIKey: "11111111-1111-1111-1111-111111111111")
.with(language: "fr")
.build()
)
ArkoseManager.showEnforcementChallenge(
parent: self,
delegate: self,
cancelButtonTitle: "Cancel")
}
}
Updated 19 days ago