Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package com.usercentrics.sdk.flutter

import android.app.Activity
import androidx.annotation.NonNull
import com.usercentrics.sdk.flutter.api.ConsentOrPayEventNotifier
import com.usercentrics.sdk.flutter.api.FlutterActivityProvider
import com.usercentrics.sdk.flutter.api.FlutterAssetsProvider
import com.usercentrics.sdk.flutter.api.FlutterMethodCallWrapper
Expand Down Expand Up @@ -33,6 +34,10 @@ class UsercentricsPlugin : FlutterPlugin,
private var activityBinding: ActivityPluginBinding? = null
private var flutterAssets: FlutterAssets? = null

private val consentOrPayEventNotifier = ConsentOrPayEventNotifier()
private var loginClickedEventChannel: EventChannel? = null
private var subscribeClickedEventChannel: EventChannel? = null

private val methods: Map<String, MethodBridge> by lazy {
listOf(
InitializeBridge(
Expand All @@ -42,7 +47,7 @@ class UsercentricsPlugin : FlutterPlugin,
ShowFirstLayerBridge(
assetsProvider = this,
activityProvider = this,
bannerProxy = UsercentricsBannerProxyImpl(this),
bannerProxy = UsercentricsBannerProxyImpl(this, consentOrPayEventNotifier),
),
ShowSecondLayerBridge(
assetsProvider = this,
Expand Down Expand Up @@ -73,7 +78,9 @@ class UsercentricsPlugin : FlutterPlugin,
GetGPPDataBridge(),
GetGPPStringBridge(),
SetGPPConsentBridge(),
GetDpsMetadataBridge()
GetDpsMetadataBridge(),
NotifyLoginSuccessBridge(),
NotifySubscribeSuccessBridge(),
).associateBy { it.name }
}

Expand Down Expand Up @@ -118,6 +125,28 @@ class UsercentricsPlugin : FlutterPlugin,
gppSectionChangeSubscription = null
}
})

loginClickedEventChannel = EventChannel(binding.binaryMessenger, "usercentrics/onLoginClicked")
loginClickedEventChannel?.setStreamHandler(object : EventChannel.StreamHandler {
override fun onListen(arguments: Any?, events: EventChannel.EventSink?) {
consentOrPayEventNotifier.loginClickedSink = events
}

override fun onCancel(arguments: Any?) {
consentOrPayEventNotifier.loginClickedSink = null
}
})

subscribeClickedEventChannel = EventChannel(binding.binaryMessenger, "usercentrics/onSubscribeClicked")
subscribeClickedEventChannel?.setStreamHandler(object : EventChannel.StreamHandler {
override fun onListen(arguments: Any?, events: EventChannel.EventSink?) {
consentOrPayEventNotifier.subscribeClickedSink = events
}

override fun onCancel(arguments: Any?) {
consentOrPayEventNotifier.subscribeClickedSink = null
}
})
}

override fun onDetachedFromEngine(@NonNull binding: FlutterPlugin.FlutterPluginBinding) {
Expand All @@ -127,6 +156,12 @@ class UsercentricsPlugin : FlutterPlugin,
gppSectionChangeSubscription = null
gppSectionChangeEventChannel?.setStreamHandler(null)
gppSectionChangeEventChannel = null
consentOrPayEventNotifier.loginClickedSink = null
consentOrPayEventNotifier.subscribeClickedSink = null
loginClickedEventChannel?.setStreamHandler(null)
loginClickedEventChannel = null
subscribeClickedEventChannel?.setStreamHandler(null)
subscribeClickedEventChannel = null
}

override fun onAttachedToActivity(activityBinding: ActivityPluginBinding) {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
package com.usercentrics.sdk.flutter.api

import io.flutter.plugin.common.EventChannel

/**
* Holds the EventSinks for the Consent-or-Pay 1st-layer click events (onLoginClicked/
* onSubscribeClicked). These fire independently of showFirstLayer's own completion callback
* (the banner stays open), so they're modeled as persistent EventChannels rather than as part
* of showFirstLayer's result, mirroring the existing onGppSectionChange EventChannel.
*/
internal class ConsentOrPayEventNotifier {
var loginClickedSink: EventChannel.EventSink? = null
var subscribeClickedSink: EventChannel.EventSink? = null

fun onLoginClicked(url: String?) {
loginClickedSink?.success(url)
}

fun onSubscribeClicked(url: String?) {
subscribeClickedSink?.success(url)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -18,14 +18,21 @@ internal interface UsercentricsBannerProxy {

internal class UsercentricsBannerProxyImpl(
private val activityProvider: FlutterActivityProvider,
// Optional: only Consent-or-Pay first-layer screens ever invoke these, so a plain first-layer
// call with no such buttons configured never touches this at all.
private val consentOrPayEventNotifier: ConsentOrPayEventNotifier? = null,
) : UsercentricsBannerProxy {

override fun showFirstLayer(
bannerSettings: BannerSettings?,
callback: (UsercentricsConsentUserResponse?) -> Unit,
) {
val context = activityProvider.provide() ?: return
UsercentricsBanner(context, bannerSettings).showFirstLayer(callback)
UsercentricsBanner(context, bannerSettings).showFirstLayer(
callback = callback,
onLoginClicked = { url -> consentOrPayEventNotifier?.onLoginClicked(url) },
onSubscribeClicked = { url -> consentOrPayEventNotifier?.onSubscribeClicked(url) },
)
}

override fun showSecondLayer(
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
package com.usercentrics.sdk.flutter.bridge

import com.usercentrics.sdk.flutter.api.FlutterMethodCall
import com.usercentrics.sdk.flutter.api.FlutterResult
import com.usercentrics.sdk.flutter.api.UsercentricsProxy
import com.usercentrics.sdk.flutter.api.UsercentricsProxySingleton

internal class NotifyLoginSuccessBridge(
private val usercentrics: UsercentricsProxy = UsercentricsProxySingleton
) : MethodBridge {

companion object {
private const val notifyLoginSuccessErrorCode =
"usercentrics_flutter_notifyLoginSuccess_error"
}

override val name: String
get() = "notifyLoginSuccess"

override fun invoke(call: FlutterMethodCall, result: FlutterResult) {
assert(name == call.method)
usercentrics.instance.notifyLoginSuccess(
onSuccess = {
result.success(null)
},
onError = {
result.error(
notifyLoginSuccessErrorCode,
it.message,
it
)
},
)
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
package com.usercentrics.sdk.flutter.bridge

import com.usercentrics.sdk.flutter.api.FlutterMethodCall
import com.usercentrics.sdk.flutter.api.FlutterResult
import com.usercentrics.sdk.flutter.api.UsercentricsProxy
import com.usercentrics.sdk.flutter.api.UsercentricsProxySingleton

internal class NotifySubscribeSuccessBridge(
private val usercentrics: UsercentricsProxy = UsercentricsProxySingleton
) : MethodBridge {

companion object {
private const val notifySubscribeSuccessErrorCode =
"usercentrics_flutter_notifySubscribeSuccess_error"
}

override val name: String
get() = "notifySubscribeSuccess"

override fun invoke(call: FlutterMethodCall, result: FlutterResult) {
assert(name == call.method)
usercentrics.instance.notifySubscribeSuccess(
onSuccess = {
result.success(null)
},
onError = {
result.error(
notifySubscribeSuccessErrorCode,
it.message,
it
)
},
)
}
}
9 changes: 9 additions & 0 deletions example/lib/main.dart
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,15 @@ class HomePageState extends State<HomePage> {
consentMediation: _kMediationTestEnabled,
);

// Consent-or-Pay: notify the SDK once the host app confirms a successful login/subscription
// so it can clear stored TCF consent data. The banner is not dismissed automatically.
Usercentrics.onLoginClicked.listen((url) async {
await Usercentrics.notifyLoginSuccess();
});
Usercentrics.onSubscribeClicked.listen((url) async {
await Usercentrics.notifySubscribeSuccess();
});

final status = await Usercentrics.status;

setState(() {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import Flutter

/// Holds the EventSinks for the Consent-or-Pay 1st-layer click events (onLoginClicked/
/// onSubscribeClicked). These fire independently of showFirstLayer's own completion handler
/// (the banner stays open), so they're modeled as persistent EventChannels rather than as part
/// of showFirstLayer's result, mirroring the existing onGppSectionChange EventChannel.
class ConsentOrPayEventNotifier {
var loginClickedSink: FlutterEventSink?
var subscribeClickedSink: FlutterEventSink?

func onLoginClicked(_ url: String?) {
loginClickedSink?(url)
}

func onSubscribeClicked(_ url: String?) {
subscribeClickedSink?(url)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -12,9 +12,21 @@ protocol UsercentricsBannerProxyProtocol {

struct UsercentricsBannerProxy: UsercentricsBannerProxyProtocol {

// Optional: only Consent-or-Pay first-layer screens ever invoke these, so a plain first-layer
// call with no such buttons configured never touches this at all.
let consentOrPayEventNotifier: ConsentOrPayEventNotifier?

init(consentOrPayEventNotifier: ConsentOrPayEventNotifier? = nil) {
self.consentOrPayEventNotifier = consentOrPayEventNotifier
}

func showFirstLayer(bannerSettings: BannerSettings?,
completionHandler: @escaping (UsercentricsConsentUserResponse) -> Void) {
UsercentricsBanner(bannerSettings: bannerSettings).showFirstLayer { response in
UsercentricsBanner(bannerSettings: bannerSettings).showFirstLayer(onLoginClicked: { [weak consentOrPayEventNotifier] url in
consentOrPayEventNotifier?.onLoginClicked(url)
}, onSubscribeClicked: { [weak consentOrPayEventNotifier] url in
consentOrPayEventNotifier?.onSubscribeClicked(url)
}) { response in
completionHandler(response)
}
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import Flutter

class LoginClickedStreamHandler: NSObject, FlutterStreamHandler {

private let notifier: ConsentOrPayEventNotifier

init(notifier: ConsentOrPayEventNotifier) {
self.notifier = notifier
}

func onListen(withArguments arguments: Any?, eventSink events: @escaping FlutterEventSink) -> FlutterError? {
notifier.loginClickedSink = events
return nil
}

func onCancel(withArguments arguments: Any?) -> FlutterError? {
notifier.loginClickedSink = nil
return nil
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import Flutter
import Foundation

struct NotifyLoginSuccessBridge : MethodBridge {

let name: String = "notifyLoginSuccess"
let usercentrics: UsercentricsProxyProtocol

func invoke(_ call: FlutterMethodCall, _ result: @escaping FlutterResult) {
assert(call.method == name)

usercentrics.shared.notifyLoginSuccess(onSuccess: {
result(nil)
}, onError: { error in
result(FlutterError(code: "usercentrics_flutter_notifyLoginSuccess_error",
message: error.localizedDescription,
details: nil))
})
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import Flutter
import Foundation

struct NotifySubscribeSuccessBridge : MethodBridge {

let name: String = "notifySubscribeSuccess"
let usercentrics: UsercentricsProxyProtocol

func invoke(_ call: FlutterMethodCall, _ result: @escaping FlutterResult) {
assert(call.method == name)

usercentrics.shared.notifySubscribeSuccess(onSuccess: {
result(nil)
}, onError: { error in
result(FlutterError(code: "usercentrics_flutter_notifySubscribeSuccess_error",
message: error.localizedDescription,
details: nil))
})
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import Flutter

class SubscribeClickedStreamHandler: NSObject, FlutterStreamHandler {

private let notifier: ConsentOrPayEventNotifier

init(notifier: ConsentOrPayEventNotifier) {
self.notifier = notifier
}

func onListen(withArguments arguments: Any?, eventSink events: @escaping FlutterEventSink) -> FlutterError? {
notifier.subscribeClickedSink = events
return nil
}

func onCancel(withArguments arguments: Any?) -> FlutterError? {
notifier.subscribeClickedSink = nil
return nil
}
}
Loading
Loading