diff --git a/android/src/main/kotlin/com/fingerprintjs/flutter/fpjs_pro/fpjs_pro_plugin/FpjsProPlugin.kt b/android/src/main/kotlin/com/fingerprintjs/flutter/fpjs_pro/fpjs_pro_plugin/FpjsProPlugin.kt index 333704e..6a6927f 100644 --- a/android/src/main/kotlin/com/fingerprintjs/flutter/fpjs_pro/fpjs_pro_plugin/FpjsProPlugin.kt +++ b/android/src/main/kotlin/com/fingerprintjs/flutter/fpjs_pro/fpjs_pro_plugin/FpjsProPlugin.kt @@ -24,6 +24,7 @@ import com.fingerprintjs.android.fpjs_pro.UnsupportedVersion import com.fingerprintjs.android.fpjs_pro.InstallationMethodRestricted import com.fingerprintjs.android.fpjs_pro.ResponseCannotBeParsed import com.fingerprintjs.android.fpjs_pro.NetworkError +import com.fingerprintjs.android.fpjs_pro.ClientTimeout import com.fingerprintjs.android.fpjs_pro.UnknownError import io.flutter.embedding.engine.plugins.FlutterPlugin @@ -70,7 +71,8 @@ class FpjsProPlugin: FlutterPlugin, MethodCallHandler { GET_VISITOR_ID -> { val tags = call.argument>("tags") ?: emptyMap() val linkedId = call.argument("linkedId") ?: "" - getVisitorId(linkedId, tags, { visitorId -> + val timeoutMillis = call.argument("timeoutMs") + getVisitorId(timeoutMillis, linkedId, tags, { visitorId -> result.success(visitorId) }, { errorCode, errorMessage -> result.error(errorCode, errorMessage, null) @@ -79,7 +81,8 @@ class FpjsProPlugin: FlutterPlugin, MethodCallHandler { GET_VISITOR_DATA -> { val tags = call.argument>("tags") ?: emptyMap() val linkedId = call.argument("linkedId") ?: "" - getVisitorData(linkedId, tags, { getVisitorData -> + val timeoutMillis = call.argument("timeoutMs") + getVisitorData(timeoutMillis, linkedId, tags, { getVisitorData -> result.success(getVisitorData) }, { errorCode, errorMessage -> result.error(errorCode, errorMessage, null) @@ -110,31 +113,53 @@ class FpjsProPlugin: FlutterPlugin, MethodCallHandler { } private fun getVisitorId( + timeoutMillis: Int?, linkedId: String, tags: Map, listener: (String) -> Unit, errorListener: (String, String) -> (Unit) ) { - fpjsClient.getVisitorId( - tags, - linkedId, - listener = {result -> listener(result.visitorId)}, - errorListener = { error -> errorListener(getErrorCode(error), error.description.toString())} - ) + if (timeoutMillis != null) { + fpjsClient.getVisitorId( + timeoutMillis, + tags, + linkedId, + listener = { result -> listener(result.visitorId) }, + errorListener = { error -> errorListener(getErrorCode(error), error.description.toString()) } + ) + } else { + fpjsClient.getVisitorId( + tags, + linkedId, + listener = { result -> listener(result.visitorId) }, + errorListener = { error -> errorListener(getErrorCode(error), error.description.toString()) } + ) + } } private fun getVisitorData( + timeoutMillis: Int?, linkedId: String, tags: Map, listener: (List) -> Unit, errorListener: (String, String) -> (Unit) ) { - fpjsClient.getVisitorId( - tags, - linkedId, - listener = {result -> listener(listOf(result.requestId, result.confidenceScore.score, result.asJson, result.sealedResult ?: ""))}, - errorListener = { error -> errorListener(getErrorCode(error), error.description.toString())} - ) + if (timeoutMillis != null) { + fpjsClient.getVisitorId( + timeoutMillis, + tags, + linkedId, + listener = {result -> listener(listOf(result.requestId, result.confidenceScore.score, result.asJson, result.sealedResult ?: ""))}, + errorListener = { error -> errorListener(getErrorCode(error), error.description.toString())} + ) + } else { + fpjsClient.getVisitorId( + tags, + linkedId, + listener = {result -> listener(listOf(result.requestId, result.confidenceScore.score, result.asJson, result.sealedResult ?: ""))}, + errorListener = { error -> errorListener(getErrorCode(error), error.description.toString())} + ) + } } } @@ -170,6 +195,7 @@ private fun getErrorCode(error: Error): String { is InstallationMethodRestricted -> "InstallationMethodRestricted" is ResponseCannotBeParsed -> "ResponseCannotBeParsed" is NetworkError -> "NetworkError" + is ClientTimeout -> "ClientTimeout" else -> "UnknownError" } return errorType diff --git a/example/lib/main.dart b/example/lib/main.dart index eace1ac..242c2cb 100644 --- a/example/lib/main.dart +++ b/example/lib/main.dart @@ -136,6 +136,13 @@ class _MyAppState extends State { FpjsProPlugin.getVisitorId(linkedId: 'checkIdWithTag', tags: tags), () async => FpjsProPlugin.getVisitorData( linkedId: 'checkDataWithTag', tags: tags), + () async => FpjsProPlugin.getVisitorId(timeoutMs: 5000), + () async => FpjsProPlugin.getVisitorData(timeoutMs: 5000), + ]; + + var timeoutChecks = [ + () async => FpjsProPlugin.getVisitorId(timeoutMs: 5), + () async => FpjsProPlugin.getVisitorData(timeoutMs: 5) ]; for (var check in checks) { @@ -144,6 +151,16 @@ class _MyAppState extends State { _checksResult += '.'; }); } + for (var check in timeoutChecks) { + try { + await check(); + throw Exception('Expected timeout error'); + } on FingerprintProError catch (e) { + setState(() { + _checksResult += '!'; + }); + } + } setState(() { _checksResult = 'Success!'; }); diff --git a/ios/Classes/FPJSError+Flutter.swift b/ios/Classes/FPJSError+Flutter.swift index 98245fa..9bb2352 100644 --- a/ios/Classes/FPJSError+Flutter.swift +++ b/ios/Classes/FPJSError+Flutter.swift @@ -19,6 +19,8 @@ extension FPJSError { return ("JsonParsingError", jsonParsingError.localizedDescription) case .invalidResponseType: return ("InvalidResponseType", description) + case .clientTimeout: + return ("ClientTimeout", description) case .unknownError: fallthrough @unknown default: diff --git a/ios/Classes/SwiftFpjsProPlugin.swift b/ios/Classes/SwiftFpjsProPlugin.swift index 6592ada..a8477c2 100644 --- a/ios/Classes/SwiftFpjsProPlugin.swift +++ b/ios/Classes/SwiftFpjsProPlugin.swift @@ -30,10 +30,10 @@ public class SwiftFpjsProPlugin: NSObject, FlutterPlugin { } } else if (call.method == "getVisitorId") { let metadata = prepareMetadata(args["linkedId"] as? String, tags: args["tags"]) - getVisitorId(metadata, result) + getVisitorId(metadata, result, args["timeoutMs"] as? Double) } else if (call.method == "getVisitorData") { let metadata = prepareMetadata(args["linkedId"] as? String, tags: args["tags"]) - getVisitorData(metadata, result) + getVisitorData(metadata, result, args["timeoutMs"] as? Double) } } @@ -79,13 +79,13 @@ public class SwiftFpjsProPlugin: NSObject, FlutterPlugin { fpjsClient = FingerprintProFactory.getInstance(configuration) } - private func getVisitorId(_ metadata: Metadata?, _ result: @escaping FlutterResult) { + private func getVisitorId(_ metadata: Metadata?, _ result: @escaping FlutterResult, _ timeout: Double? = nil) { guard let client = fpjsClient else { result(FlutterError.init(code: "undefinedFpClient", message: "You need to call init method first", details: nil)) return } - client.getVisitorId(metadata) { visitorIdResult in + let completionHandler: FingerprintPro.VisitorIdBlock = { visitorIdResult in switch visitorIdResult { case .success(let visitorId): result(visitorId) @@ -93,15 +93,21 @@ public class SwiftFpjsProPlugin: NSObject, FlutterPlugin { self.processNativeLibraryError(error, result: result) } } + + if let timeout = timeout { + client.getVisitorId(metadata, timeout: timeout / 1000, completion: completionHandler) + } else { + client.getVisitorId(metadata, completion: completionHandler) + } } - private func getVisitorData(_ metadata: Metadata?, _ result: @escaping FlutterResult) { + private func getVisitorData(_ metadata: Metadata?, _ result: @escaping FlutterResult, _ timeout: Double? = nil) { guard let client = fpjsClient else { result(FlutterError(code: "undefinedFpClient", message: "You need to call init method first", details: nil)) return } - client.getVisitorIdResponse(metadata) { visitorIdResponseResult in + let completionHandler: FingerprintPro.VisitorIdResponseBlock = { visitorIdResponseResult in switch visitorIdResponseResult { case .success(let visitorDataResponse): result([ @@ -114,6 +120,12 @@ public class SwiftFpjsProPlugin: NSObject, FlutterPlugin { self.processNativeLibraryError(error, result: result) } } + + if let timeout = timeout { + client.getVisitorIdResponse(metadata, timeout: timeout / 1000, completion: completionHandler) + } else { + client.getVisitorIdResponse(metadata, completion: completionHandler) + } } private func processNativeLibraryError(_ error: FPJSError, result: @escaping FlutterResult) { diff --git a/lib/error.dart b/lib/error.dart index 30092c4..6e0e193 100644 --- a/lib/error.dart +++ b/lib/error.dart @@ -158,6 +158,11 @@ class UnknownError extends FingerprintProError { UnknownError(String? message) : super('UnknownError', message); } +/// ClientTimeout error +class ClientTimeoutError extends FingerprintProError { + ClientTimeoutError(String? message) : super('ClientTimeoutError', message); +} + /// Casts error from generic platform type to FingerprintProError FingerprintProError unwrapError(PlatformException error) { switch (error.code) { @@ -238,6 +243,9 @@ FingerprintProError unwrapError(PlatformException error) { return CspBlockError(error.message); case 'IntegrationFailureError': return IntegrationFailureError(error.message); + case 'ClientTimeout': + case 'ClientTimeoutError': + return ClientTimeoutError(error.message); default: return UnknownError(error.message); } diff --git a/lib/fpjs_pro_plugin.dart b/lib/fpjs_pro_plugin.dart index 8b7c92e..86578ac 100644 --- a/lib/fpjs_pro_plugin.dart +++ b/lib/fpjs_pro_plugin.dart @@ -44,19 +44,20 @@ class FpjsProPlugin { } /// Returns the visitorId generated by the native Fingerprint Pro client - /// Support [tags](https://dev.fingerprint.com/docs/quick-start-guide#tagging-your-requests) - /// Support [linkedId](https://dev.fingerprint.com/docs/quick-start-guide#tagging-your-requests) + /// Support [tags](https://dev.fingerprint.com/reference/get-function#tag) + /// Support [linkedId](https://dev.fingerprint.com/reference/get-function#linkedid) + /// Support [timeoutMs](https://dev.fingerprint.com/reference/get-function#timeout) /// Throws a [FingerprintProError] if identification request fails for any reason static Future getVisitorId( - {Map? tags, String? linkedId}) async { + {Map? tags, String? linkedId, int? timeoutMs}) async { if (!_isInitialized) { throw Exception( 'You need to initialize the FPJS Client first by calling the "initFpjs" method'); } try { - final String? visitorId = await _channel - .invokeMethod('getVisitorId', {'linkedId': linkedId, 'tags': tags}); + final String? visitorId = await _channel.invokeMethod('getVisitorId', + {'linkedId': linkedId, 'tags': tags, 'timeoutMs': timeoutMs}); return visitorId; } on PlatformException catch (exception) { throw unwrapError(exception); @@ -64,19 +65,20 @@ class FpjsProPlugin { } /// Returns the visitor data generated by the native Fingerprint Pro client - /// Support [tags](https://dev.fingerprint.com/docs/quick-start-guide#tagging-your-requests) - /// Support [linkedId](https://dev.fingerprint.com/docs/quick-start-guide#tagging-your-requests) + /// Support [tags](https://dev.fingerprint.com/reference/get-function#tag) + /// Support [linkedId](https://dev.fingerprint.com/reference/get-function#linkedid) + /// Support [timeoutMs](https://dev.fingerprint.com/reference/get-function#timeout) /// Throws a [FingerprintProError] if identification request fails for any reason static Future getVisitorData( - {Map? tags, String? linkedId}) async { + {Map? tags, String? linkedId, int? timeoutMs}) async { if (!_isInitialized) { throw Exception( 'You need to initialize the FPJS Client first by calling the "initFpjs" method'); } try { - final visitorDataTuple = await _channel - .invokeMethod('getVisitorData', {'linkedId': linkedId, 'tags': tags}); + final visitorDataTuple = await _channel.invokeMethod('getVisitorData', + {'linkedId': linkedId, 'tags': tags, 'timeoutMs': timeoutMs}); final String requestId = visitorDataTuple[0]; final num confidence = visitorDataTuple[1]; diff --git a/lib/fpjs_pro_plugin_web.dart b/lib/fpjs_pro_plugin_web.dart index 360f416..c408eba 100644 --- a/lib/fpjs_pro_plugin_web.dart +++ b/lib/fpjs_pro_plugin_web.dart @@ -42,11 +42,13 @@ class FpjsProPluginWeb { case 'getVisitorId': return getVisitorId( linkedId: call.arguments['linkedId'], - tags: getTags(call.arguments['tags'])); + tags: getTags(call.arguments['tags']), + timeoutMs: call.arguments['timeoutMs']); case 'getVisitorData': return getVisitorData( linkedId: call.arguments['linkedId'], - tags: getTags(call.arguments['tags'])); + tags: getTags(call.arguments['tags']), + timeoutMs: call.arguments['timeoutMs']); default: throw PlatformException( code: 'Unimplemented', @@ -96,8 +98,10 @@ class FpjsProPluginWeb { /// Returns the visitorId generated by the native Fingerprint Pro client /// Support [tags](https://dev.fingerprint.com/docs/quick-start-guide#tagging-your-requests) /// Support [linkedId](https://dev.fingerprint.com/docs/quick-start-guide#tagging-your-requests) + /// Support [timeoutMs](https://dev.fingerprint.com/reference/get-function#timeout) /// Throws a [FingerprintProError] if identification request fails for any reason - static Future getVisitorId({Object? tags, String? linkedId}) async { + static Future getVisitorId( + {Object? tags, String? linkedId, int? timeoutMs}) async { if (!_isInitialized) { throw Exception( 'You need to initialize the FPJS Client first by calling the "initFpjs" method'); @@ -105,8 +109,8 @@ class FpjsProPluginWeb { try { FingerprintJSAgent fp = await (_fpPromise as Future); - var result = await promiseToFuture( - fp.get(FingerprintJSGetOptions(linkedId: linkedId, tag: tags))); + var result = await promiseToFuture(fp.get(FingerprintJSGetOptions( + linkedId: linkedId, tag: tags, timeout: timeoutMs))); return result.visitorId; } catch (e) { if (e is WebException) { @@ -120,9 +124,10 @@ class FpjsProPluginWeb { /// Returns the visitor data generated by the native Fingerprint Pro client /// Support [tags](https://dev.fingerprint.com/docs/quick-start-guide#tagging-your-requests) /// Support [linkedId](https://dev.fingerprint.com/docs/quick-start-guide#tagging-your-requests) + /// Support [timeoutMs](https://dev.fingerprint.com/reference/get-function#timeout) /// Throws a [FingerprintProError] if identification request fails for any reason static Future> getVisitorData( - {Object? tags, String? linkedId}) async { + {Object? tags, String? linkedId, int? timeoutMs}) async { if (!_isInitialized) { throw Exception( 'You need to initialize the FPJS Client first by calling the "initFpjs" method'); @@ -130,7 +135,10 @@ class FpjsProPluginWeb { try { FingerprintJSAgent fp = await (_fpPromise as Future); final getOptions = FingerprintJSGetOptions( - linkedId: linkedId, tag: tags, extendedResult: _isExtendedResult); + linkedId: linkedId, + tag: tags, + timeout: timeoutMs, + extendedResult: _isExtendedResult); final IdentificationResult result = await promiseToFuture(fp.get(getOptions)); diff --git a/lib/js_agent_interop.dart b/lib/js_agent_interop.dart index 2c0c9c5..bc1996a 100644 --- a/lib/js_agent_interop.dart +++ b/lib/js_agent_interop.dart @@ -251,8 +251,16 @@ class FingerprintJSGetOptions { /// Adds details about the visitor to the result external bool extendedResult; + /// Controls client-side timeout. Client timeout controls total time (both client-side and server-side) that any + /// identification event is allowed to run. It doesn't include time when the page is in background (not visible). + /// The value is in milliseconds. + external int? get timeout; + external factory FingerprintJSGetOptions( - {Object? tag, String? linkedId, bool extendedResult = false}); + {Object? tag, + String? linkedId, + int? timeout, + bool extendedResult = false}); } /// Interop for JS Agent exceptions diff --git a/lib/web_error.dart b/lib/web_error.dart index 407cf98..c7790bc 100644 --- a/lib/web_error.dart +++ b/lib/web_error.dart @@ -29,7 +29,7 @@ FingerprintProError unwrapWebError(WebException error) { return FailedError(message); } if (message == FingerprintJS.ERROR_CLIENT_TIMEOUT) { - return RequestTimeoutError(message); + return ClientTimeoutError(message); } if (message == FingerprintJS.ERROR_SERVER_TIMEOUT) { return RequestTimeoutError(message); diff --git a/test/fpjs_pro_plugin_test.dart b/test/fpjs_pro_plugin_test.dart index 7c639dd..0f17448 100644 --- a/test/fpjs_pro_plugin_test.dart +++ b/test/fpjs_pro_plugin_test.dart @@ -57,6 +57,12 @@ void main() { .setMockMethodCallHandler(channel, null); }); + test('should return visitor id when called with timeout', () async { + await FpjsProPlugin.initFpjs(testApiKey); + final result = await FpjsProPlugin.getVisitorId(timeoutMs: 1000); + expect(result, testVisitorId); + }); + test('should return visitor id when called without tags', () async { await FpjsProPlugin.initFpjs(testApiKey); final result = await FpjsProPlugin.getVisitorId(); @@ -93,13 +99,19 @@ void main() { .setMockMethodCallHandler(channel, null); }); - test('should return visitor id when called without tags', () async { + test('should return visitor data when called with timeout', () async { + await FpjsProPlugin.initFpjs(testApiKey); + final result = await FpjsProPlugin.getVisitorData(timeoutMs: 1000); + expect(result.toJson(), getVisitorDataResponse); + }); + + test('should return visitor data when called without tags', () async { await FpjsProPlugin.initFpjs(testApiKey); final result = await FpjsProPlugin.getVisitorData(); expect(result.toJson(), getVisitorDataResponse); }); - test('should return visitor id when called with tags', () async { + test('should return visitor data when called with tags', () async { await FpjsProPlugin.initFpjs(testApiKey); final result = await FpjsProPlugin.getVisitorData( tags: {'sessionId': DateTime.now().millisecondsSinceEpoch});