Flutter Integration

Intro

The SDK can be integrated into a Flutter project.

How to start the SDK

  • The most important thing is to start the SDK as soon as possible in your app flow. The reason for that is when your app will send an URL request to your server before the SDK was started, the request will not include the SDK’s HTTP headers. As a result, HUMAN’s Enforcer could block the request and the SDK will not be able to present a challenge to the user. The best place to start the SDK is in the:
    • Application’s onCreate function on Android.
    • AppDelegate’s didFinishLaunchingWithOptions function on iOS.
  • You should start the SDK on the main thread.
  • Create a HumanManager object that will handle calls from Flutter side.
  • On Android, register the HumanManager in the FlutterEngine.
  • On iOS, setup the channel.

Here is an example of how it should be:

Android

Kotlin / MainApplication.kt:

1import android.app.Application
2
3class MainApplication: Application() {
4
5 override fun onCreate() {
6 super.onCreate()
7 HumanManager.start(this)
8 }
9}

Kotlin / HumanManager.kt:

1import android.app.Application
2import com.humansecurity.mobile_sdk.HumanSecurity
3import com.humansecurity.mobile_sdk.main.policy.HSPolicy
4import com.humansecurity.mobile_sdk.main.policy.HSAutomaticInterceptorType
5import com.humansecurity.mobile_sdk.main.HSBotDefenderChallengeResult
6import io.flutter.plugin.common.MethodCall
7import io.flutter.plugin.common.MethodChannel
8import org.json.JSONObject
9
10class HumanManager {
11
12 companion object {
13 fun start(application: Application) {
14 try {
15 val policy = HSPolicy()
16 policy.automaticInterceptorPolicy.interceptorType = HSAutomaticInterceptorType.NONE
17 HumanSecurity.start(application, "<APP_ID>", policy)
18 }
19 catch (exception: Exception) {
20 println("Exception: ${exception.message}")
21 }
22 }
23
24 fun handleEvent(call: MethodCall, result: MethodChannel.Result) {
25 if (call.method == "humanGetHeaders") {
26 val json = JSONObject((HumanSecurity.BD.headersForURLRequest("<APP_ID>") as Map<*, *>?))
27 result.success(json.toString())
28 }
29 else if (call.method == "humanHandleResponse") {
30 val handled = HumanSecurity.BD.handleResponse(call.arguments!! as String) { challengeResult: HSBotDefenderChallengeResult ->
31 result.success(if (challengeResult == HSBotDefenderChallengeResult.SOLVED) "solved" else "cancelled")
32 null
33 }
34 if (!handled) {
35 result.success("false")
36 }
37 }
38 else {
39 result.notImplemented()
40 }
41 }
42 }
43}

Kotlin / MainActivity.kt:

1import androidx.annotation.NonNull
2import io.flutter.embedding.android.FlutterActivity
3import io.flutter.embedding.engine.FlutterEngine
4import io.flutter.plugin.common.MethodChannel
5
6class MainActivity: FlutterActivity() {
7
8 override fun configureFlutterEngine(@NonNull flutterEngine: FlutterEngine) {
9 super.configureFlutterEngine(flutterEngine)
10 MethodChannel(flutterEngine.dartExecutor.binaryMessenger, "com.humansecurity/sdk").setMethodCallHandler {
11 call, result ->
12 HumanManager.handleEvent(call, result)
13 }
14 }
15}

iOS

Swift / HumanManager.swift:

1import Foundation
2import Flutter
3import HUMAN
4
5class HumanManager {
6
7 static let shared = HumanManager()
8
9 func start() {
10 do {
11 let policy = HSPolicy()
12 policy.automaticInterceptorPolicy.interceptorType = .none
13
14 try HumanSecurity.start(appId: "<APP_ID>", policy: policy)
15 }
16 catch {
17 print("Error: \(error)")
18 }
19 }
20
21 func setupChannel(with controller: FlutterViewController) {
22 let humanChannel = FlutterMethodChannel(name: "com.humansecurity/sdk", binaryMessenger: controller.binaryMessenger)
23 humanChannel.setMethodCallHandler({(call: FlutterMethodCall, result: @escaping FlutterResult) -> Void in
24 if call.method == "humanGetHeaders" {
25 var json: String?
26 do {
27 let headers = HumanSecurity.BD.headersForURLRequest(forAppId: "<APP_ID>")
28 let data = try JSONSerialization.data(withJSONObject: headers)
29 json = String(data: data, encoding: .utf8)
30 }
31 catch {
32 print("Error: \(error)")
33 }
34 result(json)
35 }
36 else if call.method == "humanHandleResponse" {
37 if let response = call.arguments as? String, let data = response.data(using: .utf8), let httpURLResponse = HTTPURLResponse(url: URL(string: "https://example.com")!, statusCode: 403, httpVersion: nil, headerFields: nil) {
38 let handled = HumanSecurity.BD.handleResponse(response: httpURLResponse, data: data) { challengeResult in
39 result(challengeResult == .solved ? "solved" : "cancelled")
40 }
41 if handled {
42 return
43 }
44 }
45 result("false")
46 }
47 else {
48 result("")
49 }
50 })
51 }
52}

Swift / AppDelegate.swift:

1import UIKit
2import Flutter
3
4@UIApplicationMain
5@objc class AppDelegate: FlutterAppDelegate {
6
7 override func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
8 GeneratedPluginRegistrant.register(with: self)
9
10 HumanManager.shared.start()
11
12 let controller : FlutterViewController = window?.rootViewController as! FlutterViewController
13 HumanManager.shared.setupChannel(with: controller)
14
15 return super.application(application, didFinishLaunchingWithOptions: launchOptions)
16 }
17}

Let’s talk about what we have in the code here:

  1. We start the SDK as soon as possible and on the main thread.
  2. We create a HSPolicy instance. This object is used to configure the SDK’s behavior. Here, we set the HSAutomaticInterceptorPolicy/interceptorType property to HSAutomaticInterceptorType/none. This means that the Automatic Interception feature of the SDK is disabled. In short, this feature allows the SDK to manipulate URL requests and handle their responses by itself. However, this feature is not supported in Flutter.
  3. We call the HumanSecurity/start(appId:policy:) function of the SDK. We provide the following parameters:
    • The Application instance (Android only).
    • Your AppID.
    • The policy object that we configured.

Note: If your app communicates with several servers that have different AppID, you can call the HumanSecurity/start(appIds:policy:) function which allow you to pass an array of AppIDs. You should specify the relevant AppID for each API call in the SDK.

How to add HTTP headers and handle block response

In your Dart code, you should do the following:

  1. Crate a MethodChannel.
  2. Add The SDK’s HTTP headers to your URL requests.
  3. When a request is blocked, send the block response to the SDK.
  4. Handle the challenge’s result.

Here is an example of how it should be:

Dart:

1import 'package:flutter/material.dart';
2import 'package:flutter/services.dart';
3import '../settings/settings_view.dart';
4import 'dart:async';
5import 'dart:convert';
6import 'package:http/http.dart' as http;
7
8class MyView extends StatelessWidget {
9
10 static const humanPlatform = MethodChannel('com.humansecurity/sdk');
11
12 Future<void> sendUrlRequest() async {
13 try {
14 final String humanHeaders = await humanPlatform.invokeMethod('humanGetHeaders');
15 final Map<String, String> humanHeadersMap = Map.from(json.decode(humanHeaders));
16 var url = Uri.https('example.com', '/action');
17 var response = await http.get(url, headers: humanHeadersMap);
18 if (response.statusCode == 403) {
19 final String result = await humanPlatform.invokeMethod('humanHandleResponse', response.body);
20 if (result == 'solved') {
21 print("Challenge was solved");
22 } else if (result == 'false') {
23 print("Request was not blocked by HUMAN");
24 } else if (result == 'cancelled') {
25 print("Challenge was cancelled");
26 }
27 }
28 } on PlatformException catch (exception) {
29 print("Exception: '${exception.message}'");
30 }
31 }
32}

Understanding the block response

  1. The HUMAN’s Enforcer, when it decides to block a request, returns a JSON string in the response’s body. The HTTP status code is 403. Here is an example of the response:

    1{
    2 "vid": "928d7ab3-9cf1-11ee-a624-b802520f369f",
    3 "uuid": "fd01e6d6-9cf2-11ee-808c-acde48001122",
    4 "page": "PCFET0NUWVBFIGh0bWw+CjxodG1sIGxhbmc9ImVuIj4KPGhlYWQ+CiAgICA8bWV0YSBjaGFyc2V0PSJ1dGYtOCI+CiAgICA8bWV0YSBuYW1lPSJ2aWV3cG9ydCIgY29udGVudD0id2lkdGg9ZGV2aWNlLXdpZHRoLCBpbml0aWFsLXNjYWxlPTEiPgogICAgPHRpdGxlPkFjY2VzcyB0byB0aGlzIHBhZ2UgaGFzIGJlZW4gZGVuaWVkLjwvdGl0bGU+CiAgICA8bGluayBocmVmPSJodHRwczovL2ZvbnRzLmdvb2dsZWFwaXMuY29tL2Nzcz9mYW1pbHk9T3BlbitTYW5zOjMwMCIgcmVsPSJzdHlsZXNoZWV0Ij4KICAgIDxzdHlsZT4KICAgICAgICBodG1sLCBib2R5IHsKICAgICAgICAgICAgbWFyZ2luOiAwOwogICAgICAgICAgICBwYWRkaW5nOiAwOwogICAgICAgICAgICBmb250LWZhbWlseTogJ09wZW4gU2FucycsIHNhbnMtc2VyaWY7CiAgICAgICAgICAgIGNvbG9yOiAjMDAwOwogICAgICAgIH0KCiAgICAgICAgYSB7CiAgICAgICAgICAgIGNvbG9yOiAjYzVjNWM1OwogICAgICAgICAgICB0ZXh0LWRlY29yYXRpb246IG5vbmU7CiAgICAgICAgfQoKICAgICAgICAuY29udGFpbmVyIHsKICAgICAgICAgICAgYWxpZ24taXRlbXM6IGNlbnRlcjsKICAgICAgICAgICAgZGlzcGxheTogZmxleDsKICAgICAgICAgICAgZmxleDogMTsKICAgICAgICAgICAganVzdGlmeS1jb250ZW50OiBzcGFjZS1iZXR3ZWVuOwogICAgICAgICAgICBmbGV4LWRpcmVjdGlvbjogY29sdW1uOwogICAgICAgICAgICBoZWlnaHQ6IDEwMCU7CiAgICAgICAgfQoKICAgICAgICAuY29udGFpbmVyID4gZGl2IHsKICAgICAgICAgICAgd2lkdGg6IDEwMCU7CiAgICAgICAgICAgIGRpc3BsYXk6IGZsZXg7CiAgICAgICAgICAgIGp1c3RpZnktY29udGVudDogY2VudGVyOwogICAgICAgIH0KCiAgICAgICAgLmNvbnRhaW5lciA+IGRpdiA+IGRpdiB7CiAgICAgICAgICAgIGRpc3BsYXk6IGZsZXg7CiAgICAgICAgICAgIHdpZHRoOiA4MCU7CiAgICAgICAgfQoKICAgICAgICAuY3VzdG9tZXItbG9nby13cmFwcGVyIHsKICAgICAgICAgICAgcGFkZGluZy10b3A6IDJyZW07CiAgICAgICAgICAgIGZsZXgtZ3JvdzogMDsKICAgICAgICAgICAgYmFja2dyb3VuZC1jb2xvcjogI2ZmZjsKICAgICAgICAgICAgdmlzaWJpbGl0eTogaGlkZGVuOwogICAgICAgIH0KCiAgICAgICAgLmN1c3RvbWVyLWxvZ28gewogICAgICAgICAgICBib3JkZXItYm90dG9tOiAxcHggc29saWQgIzAwMDsKICAgICAgICB9CgogICAgICAgIC5jdXN0b21lci1sb2dvID4gaW1nIHsKICAgICAgICAgICAgcGFkZGluZy1ib3R0b206IDFyZW07CiAgICAgICAgICAgIG1heC1oZWlnaHQ6IDUwcHg7CiAgICAgICAgICAgIG1heC13aWR0aDogMTAwJTsKICAgICAgICB9CgogICAgICAgIC5wYWdlLXRpdGxlLXdyYXBwZXIgewogICAgICAgICAgICBmbGV4LWdyb3c6IDI7CiAgICAgICAgfQoKICAgICAgICAucGFnZS10aXRsZSB7CiAgICAgICAgICAgIGZsZXgtZGlyZWN0aW9uOiBjb2x1bW4tcmV2ZXJzZTsKICAgICAgICB9CgogICAgICAgIC5jb250ZW50LXdyYXBwZXIgewogICAgICAgICAgICBmbGV4LWdyb3c6IDU7CiAgICAgICAgfQoKICAgICAgICAuY29udGVudCB7CiAgICAgICAgICAgIGZsZXgtZGlyZWN0aW9uOiBjb2x1bW47CiAgICAgICAgfQoKICAgICAgICAucGFnZS1mb290ZXItd3JhcHBlciB7CiAgICAgICAgICAgIGFsaWduLWl0ZW1zOiBjZW50ZXI7CiAgICAgICAgICAgIGZsZXgtZ3JvdzogMC4yOwogICAgICAgICAgICBiYWNrZ3JvdW5kLWNvbG9yOiAjMDAwOwogICAgICAgICAgICBjb2xvcjogI2M1YzVjNTsKICAgICAgICAgICAgZm9udC1zaXplOiA3MCU7CiAgICAgICAgfQoKICAgICAgICBAbWVkaWEgKG1pbi13aWR0aDogNzY4cHgpIHsKICAgICAgICAgICAgaHRtbCwgYm9keSB7CiAgICAgICAgICAgICAgICBoZWlnaHQ6IDEwMCU7CiAgICAgICAgICAgIH0KICAgICAgICB9CiAgICA8L3N0eWxlPgogICAgPCEtLSBDdXN0b20gQ1NTIC0tPgo8L2hlYWQ+Cgo8Ym9keT4KPHNlY3Rpb24gY2xhc3M9ImNvbnRhaW5lciI+CiAgICA8ZGl2IGNsYXNzPSJjdXN0b21lci1sb2dvLXdyYXBwZXIiPgogICAgICAgIDxkaXYgY2xhc3M9ImN1c3RvbWVyLWxvZ28iPgogICAgICAgICAgICA8aW1nIHNyYz0iIiBhbHQ9IkxvZ28iLz4KICAgICAgICA8L2Rpdj4KICAgIDwvZGl2PgogICAgPGRpdiBjbGFzcz0icGFnZS10aXRsZS13cmFwcGVyIj4KICAgICAgICA8ZGl2IGNsYXNzPSJwYWdlLXRpdGxlIj4KICAgICAgICAgICAgPGgxPlBsZWFzZSB2ZXJpZnkgeW91IGFyZSBhIGh1bWFuPC9oMT4KICAgICAgICA8L2Rpdj4KICAgIDwvZGl2PgogICAgPGRpdiBjbGFzcz0iY29udGVudC13cmFwcGVyIj4KICAgICAgICA8ZGl2IGNsYXNzPSJjb250ZW50Ij4KICAgICAgICAgICAgPGRpdiBpZD0icHgtY2FwdGNoYSI+CiAgICAgICAgICAgIDwvZGl2PgogICAgICAgICAgICA8cD4KICAgICAgICAgICAgICAgIEFjY2VzcyB0byB0aGlzIHBhZ2UgaGFzIGJlZW4gZGVuaWVkIGJlY2F1c2Ugd2UgYmVsaWV2ZSB5b3UgYXJlIHVzaW5nIGF1dG9tYXRpb24gdG9vbHMgdG8gYnJvd3NlIHRoZQogICAgICAgICAgICAgICAgd2Vic2l0ZS4KICAgICAgICAgICAgPC9wPgogICAgICAgICAgICA8cD4KICAgICAgICAgICAgICAgIFRoaXMgbWF5IGhhcHBlbiBhcyBhIHJlc3VsdCBvZiB0aGUgZm9sbG93aW5nOgogICAgICAgICAgICA8L3A+CiAgICAgICAgICAgIDx1bD4KICAgICAgICAgICAgICAgIDxsaT4KICAgICAgICAgICAgICAgICAgICBKYXZhc2NyaXB0IGlzIGRpc2FibGVkIG9yIGJsb2NrZWQgYnkgYW4gZXh0ZW5zaW9uIChhZCBibG9ja2VycyBmb3IgZXhhbXBsZSkKICAgICAgICAgICAgICAgIDwvbGk+CiAgICAgICAgICAgICAgICA8bGk+CiAgICAgICAgICAgICAgICAgICAgWW91ciBicm93c2VyIGRvZXMgbm90IHN1cHBvcnQgY29va2llcwogICAgICAgICAgICAgICAgPC9saT4KICAgICAgICAgICAgPC91bD4KICAgICAgICAgICAgPHA+CiAgICAgICAgICAgICAgICBQbGVhc2UgbWFrZSBzdXJlIHRoYXQgSmF2YXNjcmlwdCBhbmQgY29va2llcyBhcmUgZW5hYmxlZCBvbiB5b3VyIGJyb3dzZXIgYW5kIHRoYXQgeW91IGFyZSBub3QgYmxvY2tpbmcKICAgICAgICAgICAgICAgIHRoZW0gZnJvbSBsb2FkaW5nLgogICAgICAgICAgICA8L3A+CiAgICAgICAgICAgIDxwPgogICAgICAgICAgICAgICAgUmVmZXJlbmNlIElEOiAjZmQwMWU2ZDYtOWNmMi0xMWVlLTgwOGMtYWNkZTQ4MDAxMTIyCiAgICAgICAgICAgIDwvcD4KICAgICAgICA8L2Rpdj4KICAgIDwvZGl2PgogICAgPGRpdiBjbGFzcz0icGFnZS1mb290ZXItd3JhcHBlciI+CiAgICAgICAgPGRpdiBjbGFzcz0icGFnZS1mb290ZXIiPgogICAgICAgICAgICA8cD4KICAgICAgICAgICAgICAgIFBvd2VyZWQgYnkKICAgICAgICAgICAgICAgIDxhIGhyZWY9Imh0dHBzOi8vd3d3LnBlcmltZXRlcnguY29tL3doeXdhc2libG9ja2VkIj5QZXJpbWV0ZXJYPC9hPgogICAgICAgICAgICAgICAgLCBJbmMuCiAgICAgICAgICAgIDwvcD4KICAgICAgICA8L2Rpdj4KICAgIDwvZGl2Pgo8L3NlY3Rpb24+CjwhLS0gUHggLS0+CjxzY3JpcHQ+CiAgICB3aW5kb3cuX3B4QXBwSWQgPSAnUFhqOXk0UThFbSc7CiAgICB3aW5kb3cuX3B4SnNDbGllbnRTcmMgPSAnLy9jbGllbnQucGVyaW1ldGVyeC5uZXQvUFhqOXk0UThFbS9tYWluLm1pbi5qcyc7CiAgICB3aW5kb3cuX3B4Rmlyc3RQYXJ0eUVuYWJsZWQgPSBmYWxzZTsKICAgIHdpbmRvdy5fcHhWaWQgPSAnOTI4ZDdhYjMtOWNmMS0xMWVlLWE2MjQtYjgwMjUyMGYzNjlmJzsKICAgIHdpbmRvdy5fcHhVdWlkID0gJ2ZkMDFlNmQ2LTljZjItMTFlZS04MDhjLWFjZGU0ODAwMTEyMic7CiAgICB3aW5kb3cuX3B4SG9zdFVybCA9ICcvL2NvbGxlY3Rvci1QWGo5eTRROEVtLnBlcmltZXRlcngubmV0JzsKPC9zY3JpcHQ+CjwhLS0gQ2FwdGNoYSAtLT4KPHNjcmlwdD4KICAgIHZhciBzID0gZG9jdW1lbnQuY3JlYXRlRWxlbWVudCgnc2NyaXB0Jyk7CiAgICBzLnNyYyA9ICcvL2NhcHRjaGEucGVyaW1ldGVyeC5uZXQvUFhqOXk0UThFbS9jYXB0Y2hhLmpzP2E9YyZtPTEmdT1mZDAxZTZkNi05Y2YyLTExZWUtODA4Yy1hY2RlNDgwMDExMjImdj05MjhkN2FiMy05Y2YxLTExZWUtYTYyNC1iODAyNTIwZjM2OWYnOwogICAgdmFyIHAgPSBkb2N1bWVudC5nZXRFbGVtZW50c0J5VGFnTmFtZSgnaGVhZCcpWzBdOwogICAgcC5pbnNlcnRCZWZvcmUocywgbnVsbCk7CiAgICBpZiAoZmFsc2UpIHsKICAgICAgICBzLm9uZXJyb3IgPSBmdW5jdGlvbiAoKSB7CiAgICAgICAgICAgIHMgPSBkb2N1bWVudC5jcmVhdGVFbGVtZW50KCdzY3JpcHQnKTsKICAgICAgICAgICAgdmFyIHN1ZmZpeEluZGV4ID0gJy8vY2FwdGNoYS5wZXJpbWV0ZXJ4Lm5ldC9QWGo5eTRROEVtL2NhcHRjaGEuanM/YT1jJm09MSZ1PWZkMDFlNmQ2LTljZjItMTFlZS04MDhjLWFjZGU0ODAwMTEyMiZ2PTkyOGQ3YWIzLTljZjEtMTFlZS1hNjI0LWI4MDI1MjBmMzY5ZicuaW5kZXhPZignY2FwdGNoYS5qcycpOwogICAgICAgICAgICB2YXIgdGVtcGVyZWRCbG9ja1NjcmlwdCA9ICcvL2NhcHRjaGEucGVyaW1ldGVyeC5uZXQvUFhqOXk0UThFbS9jYXB0Y2hhLmpzP2E9YyZtPTEmdT1mZDAxZTZkNi05Y2YyLTExZWUtODA4Yy1hY2RlNDgwMDExMjImdj05MjhkN2FiMy05Y2YxLTExZWUtYTYyNC1iODAyNTIwZjM2OWYnLnN1YnN0cmluZyhzdWZmaXhJbmRleCk7CiAgICAgICAgICAgIHMuc3JjID0gJy8vY2FwdGNoYS5weC1jZG4ubmV0L1BYajl5NFE4RW0vJyArIHRlbXBlcmVkQmxvY2tTY3JpcHQ7CiAgICAgICAgICAgIHAucGFyZW50Tm9kZS5pbnNlcnRCZWZvcmUocywgcCk7CiAgICAgICAgfTsKICAgIH0KPC9zY3JpcHQ+CjwhLS0gQ3VzdG9tIFNjcmlwdCAtLT4KPC9ib2R5Pgo8L2h0bWw+Cg==",
    5 "appId": "PXj9y4Q8Em",
    6 "action": "captcha",
    7 "collectorUrl": "https://collector-pxj9y4q8em.perimeterx.net"
    8}

  2. The JSON contains metadata for the SDK.

  3. Your app should pass the whole JSON to the SDK via the HSBotDefender/handleResponse(response:data:callback:) function. Otherwise, the SDK won’t present a challenge to the user.